Bug 1244227 - properly report throttled network timing; r?Honza draft
authorTom Tromey <tom@tromey.com>
Thu, 04 Feb 2016 06:40:25 -0700
changeset 358400 84380104cd7815437be22ac3839898586f89caa7
parent 358399 a64983fa015281350f3d71897d4198fd70f8beb7
child 358401 e583352fc50ab73a48c38974cd60453d379a2a4a
push id16997
push userbmo:ttromey@mozilla.com
push dateMon, 02 May 2016 19:31:32 +0000
reviewersHonza
bugs1244227
milestone49.0a1
Bug 1244227 - properly report throttled network timing; r?Honza MozReview-Commit-ID: BCJLSRGS0vE
devtools/shared/webconsole/network-monitor.js
devtools/shared/webconsole/throttle.js
--- a/devtools/shared/webconsole/network-monitor.js
+++ b/devtools/shared/webconsole/network-monitor.js
@@ -333,18 +333,16 @@ NetworkResponseListener.prototype = {
 
     this.receivedData = "";
 
     this.httpActivity.owner.addResponseContent(
       response,
       this.httpActivity.discardResponseBody
     );
 
-    this.httpActivity.channel = null;
-    this.httpActivity.owner = null;
     this.httpActivity = null;
     this.sink = null;
     this.inputStream = null;
     this.converter = null;
     this.request = null;
     this.owner = null;
   },
 
@@ -458,16 +456,23 @@ NetworkMonitor.prototype = {
     0x804b000b: "STATUS_RESOLVED",
     0x804b0007: "STATUS_CONNECTING_TO",
     0x804b0004: "STATUS_CONNECTED_TO",
     0x804b0005: "STATUS_SENDING_TO",
     0x804b000a: "STATUS_WAITING_FOR",
     0x804b0006: "STATUS_RECEIVING_FROM"
   },
 
+  httpDownloadActivities: [
+    gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_START,
+    gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER,
+    gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE,
+    gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE
+  ],
+
   // Network response bodies are piped through a buffer of the given size (in
   // bytes).
   responsePipeSegmentSize: null,
 
   owner: null,
 
   /**
    * Whether to save the bodies of network requests and responses.
@@ -643,16 +648,54 @@ NetworkMonitor.prototype = {
       let channel = subject.QueryInterface(Ci.nsIHttpChannel);
       if (this._matchRequest(channel)) {
         throttler.manageUpload(channel);
       }
     }
   },
 
   /**
+   * A helper function for observeActivity.  This does whatever work
+   * is required by a particular http activity event.  Arguments are
+   * the same as for observeActivity.
+   */
+  _dispatchActivity: function (httpActivity, channel, activityType,
+                               activitySubtype, timestamp, extraSizeData,
+                               extraStringData) {
+    let transCodes = this.httpTransactionCodes;
+
+    // Store the time information for this activity subtype.
+    if (activitySubtype in transCodes) {
+      let stage = transCodes[activitySubtype];
+      if (stage in httpActivity.timings) {
+        httpActivity.timings[stage].last = timestamp;
+      } else {
+        httpActivity.timings[stage] = {
+          first: timestamp,
+          last: timestamp,
+        };
+      }
+    }
+
+    switch (activitySubtype) {
+      case gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT:
+        this._onRequestBodySent(httpActivity);
+        break;
+      case gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER:
+        this._onResponseHeader(httpActivity, extraStringData);
+        break;
+      case gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE:
+        this._onTransactionClose(httpActivity);
+        break;
+      default:
+        break;
+    }
+  },
+
+  /**
    * Begin observing HTTP traffic that originates inside the current tab.
    *
    * @see https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIHttpActivityObserver
    *
    * @param nsIHttpChannel channel
    * @param number activityType
    * @param number activitySubtype
    * @param number timestamp
@@ -691,43 +734,30 @@ NetworkMonitor.prototype = {
         break;
       }
     }
 
     if (!httpActivity) {
       return;
     }
 
-    let transCodes = this.httpTransactionCodes;
-
-    // Store the time information for this activity subtype.
-    if (activitySubtype in transCodes) {
-      let stage = transCodes[activitySubtype];
-      if (stage in httpActivity.timings) {
-        httpActivity.timings[stage].last = timestamp;
-      } else {
-        httpActivity.timings[stage] = {
-          first: timestamp,
-          last: timestamp,
-        };
-      }
-    }
-
-    switch (activitySubtype) {
-      case gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT:
-        this._onRequestBodySent(httpActivity);
-        break;
-      case gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER:
-        this._onResponseHeader(httpActivity, extraStringData);
-        break;
-      case gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE:
-        this._onTransactionClose(httpActivity);
-        break;
-      default:
-        break;
+    // If we're throttling, we must not report events as they arrive
+    // from platform, but instead let the throttler emit the events
+    // after some time has elapsed.
+    if (httpActivity.downloadThrottle &&
+        this.httpDownloadActivities.indexOf(activitySubtype) >= 0) {
+      let callback = this._dispatchActivity.bind(this);
+      httpActivity.downloadThrottle
+        .addActivityCallback(callback, httpActivity, channel, activityType,
+                             activitySubtype, timestamp, extraSizeData,
+                             extraStringData);
+    } else {
+      this._dispatchActivity(httpActivity, channel, activityType,
+                             activitySubtype, timestamp, extraSizeData,
+                             extraStringData);
     }
   }),
 
   /**
    * Check if a given network request should be logged by this network monitor
    * instance based on the current filters.
    *
    * @private
--- a/devtools/shared/webconsole/throttle.js
+++ b/devtools/shared/webconsole/throttle.js
@@ -8,16 +8,20 @@
 
 const {CC, Ci, Cu, Cc} = require("chrome");
 
 const ArrayBufferInputStream = CC("@mozilla.org/io/arraybuffer-input-stream;1",
                                   "nsIArrayBufferInputStream");
 const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
                              "nsIBinaryInputStream", "setInputStream");
 
+loader.lazyServiceGetter(this, "gActivityDistributor",
+                         "@mozilla.org/network/http-activity-distributor;1",
+                         "nsIHttpActivityDistributor");
+
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 const {setTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {});
 
 /**
  * Construct a new nsIStreamListener that buffers data and provides a
  * method to notify another listener when data is available.  This is
  * used to throttle network data on a per-channel basis.
  *
@@ -27,16 +31,18 @@ const {setTimeout} = Cu.import("resource
  * @param {NetworkThrottleQueue} queue the NetworkThrottleQueue to
  *        which status changes should be reported
  */
 function NetworkThrottleListener(queue) {
   this.queue = queue;
   this.pendingData = [];
   this.pendingException = null;
   this.offset = 0;
+  this.responseStarted = false;
+  this.activities = {};
 }
 
 NetworkThrottleListener.prototype = {
   QueryInterface:
     XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInterfaceRequestor,
                            Ci.nsISupports]),
 
   /**
@@ -128,26 +134,94 @@ NetworkThrottleListener.prototype = {
     if (bytesPermitted === count) {
       this.pendingData.shift();
       done = true;
     } else {
       this.pendingData[0].count -= bytesPermitted;
     }
 
     this.offset += bytesPermitted;
+    // Maybe our state has changed enough to emit an event.
+    this.maybeEmitEvents();
+
     return {length: bytesPermitted, done};
   },
 
   /**
    * Return the number of pending data requests available for this
    * listener.
    */
   pendingCount: function () {
     return this.pendingData.length;
   },
+
+  /**
+   * This is called when an http activity event is delivered.  This
+   * object delays the event until the appropriate moment.
+   */
+  addActivityCallback: function (callback, httpActivity, channel, activityType,
+                                 activitySubtype, timestamp, extraSizeData,
+                                 extraStringData) {
+    let datum = {callback, httpActivity, channel, activityType,
+                 activitySubtype, extraSizeData,
+                 extraStringData};
+    this.activities[activitySubtype] = datum;
+
+    if (activitySubtype ===
+        gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE) {
+      this.totalSize = extraSizeData;
+    }
+
+    this.maybeEmitEvents();
+  },
+
+  /**
+   * This is called for a download throttler when the latency timeout
+   * has ended.
+   */
+  responseStart: function () {
+    this.responseStarted = true;
+    this.maybeEmitEvents();
+  },
+
+  /**
+   * Check our internal state and emit any http activity events as
+   * needed.  Note that we wait until both our internal state has
+   * changed and we've received the real http activity event from
+   * platform.  This approach ensures we can both pass on the correct
+   * data from the original event, and update the reported time to be
+   * consistent with the delay we're introducing.
+   */
+  maybeEmitEvents: function () {
+    if (this.responseStarted) {
+      this.maybeEmit(gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_START);
+      this.maybeEmit(gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER);
+    }
+
+    if (this.totalSize !== undefined && this.offset >= this.totalSize) {
+      this.maybeEmit(gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE);
+      this.maybeEmit(gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE);
+    }
+  },
+
+  /**
+   * Emit an event for |code|, if the appropriate entry in
+   * |activities| is defined.
+   */
+  maybeEmit: function (code) {
+    if (this.activities[code] !== undefined) {
+      let {callback, httpActivity, channel, activityType,
+           activitySubtype, extraSizeData,
+           extraStringData} = this.activities[code];
+      let now = Date.now() * 1000;
+      callback(httpActivity, channel, activityType, activitySubtype,
+               now, extraSizeData, extraStringData);
+      this.activities[code] = undefined;
+    }
+  },
 };
 
 /**
  * Construct a new queue that can be used to throttle the network for
  * a group of related network requests.
  *
  * meanBPS {Number} Mean bytes per second.
  * maxBPS {Number} Maximum bytes per second.
@@ -176,16 +250,17 @@ NetworkThrottleQueue.prototype = {
   },
 
   /**
    * A helper function that lets the indicating listener start sending
    * data.  This is called after the initial round trip time for the
    * listener has elapsed.
    */
   allowDataFrom: function (throttleListener) {
+    throttleListener.responseStart();
     this.pendingRequests.delete(throttleListener);
     const count = throttleListener.pendingCount();
     for (let i = 0; i < count; ++i) {
       this.downloadQueue.push(throttleListener);
     }
     this.pump();
   },