Bug 1283453 - Add network throttling to emulation actor. r=tromey
authorJ. Ryan Stinnett <jryans@gmail.com>
Fri, 07 Oct 2016 14:55:16 -0500
changeset 317873 9f289545ab7d9f3a3b5b18452818df1e3e44f382
parent 317872 44726da7a2869614b04e2193f17f6060bdbf4ceb
child 317874 56b16d2eaa77c4639f624f1b8eb01da70b5f0227
push id33170
push usercbook@mozilla.com
push dateFri, 14 Oct 2016 10:37:07 +0000
treeherderautoland@0d101ebfd95c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstromey
bugs1283453
milestone52.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1283453 - Add network throttling to emulation actor. r=tromey Expose network throttling via the emulation actor, similar to other platform features that RDM alters. This simplifies the client side since we can avoid thinking about console clients, etc. MozReview-Commit-ID: 3CNnJl6Ude8
devtools/server/actors/emulation.js
devtools/server/actors/webconsole.js
devtools/shared/specs/emulation.js
devtools/shared/webconsole/network-monitor.js
--- a/devtools/server/actors/emulation.js
+++ b/devtools/server/actors/emulation.js
@@ -4,33 +4,193 @@
 
 "use strict";
 
 const { Ci } = require("chrome");
 const protocol = require("devtools/shared/protocol");
 const { emulationSpec } = require("devtools/shared/specs/emulation");
 const { SimulatorCore } = require("devtools/shared/touch/simulator-core");
 
+/**
+ * This actor overrides various browser features to simulate different environments to
+ * test how pages perform under various conditions.
+ *
+ * The design below, which saves the previous value of each property before setting, is
+ * needed because it's possible to have multiple copies of this actor for a single page.
+ * When some instance of this actor changes a property, we want it to be able to restore
+ * that property to the way it was found before the change.
+ *
+ * A subtle aspect of the code below is that all get* methods must return non-undefined
+ * values, so that the absence of a previous value can be distinguished from the value for
+ * "no override" for each of the properties.
+ */
 let EmulationActor = protocol.ActorClassWithSpec(emulationSpec, {
+
   initialize(conn, tabActor) {
     protocol.Actor.prototype.initialize.call(this, conn);
+    this.tabActor = tabActor;
     this.docShell = tabActor.docShell;
     this.simulatorCore = new SimulatorCore(tabActor.chromeEventHandler);
   },
 
+  disconnect() {
+    this.destroy();
+  },
+
+  destroy() {
+    this.clearDPPXOverride();
+    this.clearNetworkThrottling();
+    this.clearTouchEventsOverride();
+    this.clearUserAgentOverride();
+    this.tabActor = null;
+    this.docShell = null;
+    this.simulatorCore = null;
+    protocol.Actor.prototype.destroy.call(this);
+  },
+
+  /**
+   * Retrieve the console actor for this tab.  This allows us to expose network throttling
+   * as part of emulation settings, even though it's internally connected to the network
+   * monitor, which for historical reasons is part of the console actor.
+   */
+  get _consoleActor() {
+    if (this.tabActor.exited) {
+      return null;
+    }
+    let form = this.tabActor.form();
+    return this.conn._getOrCreateActor(form.consoleActor);
+  },
+
+  /* DPPX override */
+
+  _previousDPPXOverride: undefined,
+
+  setDPPXOverride(dppx) {
+    if (this.getDPPXOverride() === dppx) {
+      return false;
+    }
+
+    if (this._previousDPPXOverride === undefined) {
+      this._previousDPPXOverride = this.getDPPXOverride();
+    }
+
+    this.docShell.contentViewer.overrideDPPX = dppx;
+
+    return true;
+  },
+
+  getDPPXOverride() {
+    return this.docShell.contentViewer.overrideDPPX;
+  },
+
+  clearDPPXOverride() {
+    if (this._previousDPPXOverride !== undefined) {
+      return this.setDPPXOverride(this._previousDPPXOverride);
+    }
+
+    return false;
+  },
+
+  /* Network Throttling */
+
+  _previousNetworkThrottling: undefined,
+
+  /**
+   * Transform the RDP format into the internal format and then set network throttling.
+   */
+  setNetworkThrottling({ downloadThroughput, uploadThroughput, latency }) {
+    let throttleData = {
+      roundTripTimeMean: latency,
+      roundTripTimeMax: latency,
+      downloadBPSMean: downloadThroughput,
+      downloadBPSMax: downloadThroughput,
+      uploadBPSMean: uploadThroughput,
+      uploadBPSMax: uploadThroughput,
+    };
+    return this._setNetworkThrottling(throttleData);
+  },
+
+  _setNetworkThrottling(throttleData) {
+    let current = this._getNetworkThrottling();
+    // Check if they are both objects or both null
+    let match = throttleData == current;
+    // If both objects, check all entries
+    if (match && current && throttleData) {
+      match = Object.entries(current).every(([ k, v ]) => {
+        return throttleData[k] === v;
+      });
+    }
+    if (match) {
+      return false;
+    }
+
+    if (this._previousNetworkThrottling === undefined) {
+      this._previousNetworkThrottling = current;
+    }
+
+    let consoleActor = this._consoleActor;
+    if (!consoleActor) {
+      return false;
+    }
+    consoleActor.onStartListeners({
+      listeners: [ "NetworkActivity" ],
+    });
+    consoleActor.onSetPreferences({
+      preferences: {
+        "NetworkMonitor.throttleData": throttleData,
+      }
+    });
+    return true;
+  },
+
+  /**
+   * Get network throttling and then transform the internal format into the RDP format.
+   */
+  getNetworkThrottling() {
+    let throttleData = this._getNetworkThrottling();
+    if (!throttleData) {
+      return null;
+    }
+    let { downloadBPSMax, uploadBPSMax, roundTripTimeMax } = throttleData;
+    return {
+      downloadThroughput: downloadBPSMax,
+      uploadThroughput: uploadBPSMax,
+      latency: roundTripTimeMax,
+    };
+  },
+
+  _getNetworkThrottling() {
+    let consoleActor = this._consoleActor;
+    if (!consoleActor) {
+      return null;
+    }
+    let prefs = consoleActor.onGetPreferences({
+      preferences: [ "NetworkMonitor.throttleData" ],
+    });
+    return prefs.preferences["NetworkMonitor.throttleData"] || null;
+  },
+
+  clearNetworkThrottling() {
+    if (this._previousNetworkThrottling !== undefined) {
+      return this._setNetworkThrottling(this._previousNetworkThrottling);
+    }
+
+    return false;
+  },
+
   /* Touch events override */
 
-  _previousTouchEventsOverride: null,
+  _previousTouchEventsOverride: undefined,
 
   setTouchEventsOverride(flag) {
-    if (this.docShell.touchEventsOverride == flag) {
+    if (this.getTouchEventsOverride() == flag) {
       return false;
     }
-    if (this._previousTouchEventsOverride === null) {
-      this._previousTouchEventsOverride = this.docShell.touchEventsOverride;
+    if (this._previousTouchEventsOverride === undefined) {
+      this._previousTouchEventsOverride = this.getTouchEventsOverride();
     }
 
     // Start or stop the touch simulator depending on the override flag
     if (flag == Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED) {
       this.simulatorCore.start();
     } else {
       this.simulatorCore.stop();
     }
@@ -39,87 +199,43 @@ let EmulationActor = protocol.ActorClass
     return true;
   },
 
   getTouchEventsOverride() {
     return this.docShell.touchEventsOverride;
   },
 
   clearTouchEventsOverride() {
-    if (this._previousTouchEventsOverride !== null) {
+    if (this._previousTouchEventsOverride !== undefined) {
       return this.setTouchEventsOverride(this._previousTouchEventsOverride);
     }
     return false;
   },
 
   /* User agent override */
 
-  _previousUserAgentOverride: null,
+  _previousUserAgentOverride: undefined,
 
   setUserAgentOverride(userAgent) {
-    if (this.docShell.customUserAgent == userAgent) {
+    if (this.getUserAgentOverride() == userAgent) {
       return false;
     }
-    if (this._previousUserAgentOverride === null) {
-      this._previousUserAgentOverride = this.docShell.customUserAgent;
+    if (this._previousUserAgentOverride === undefined) {
+      this._previousUserAgentOverride = this.getUserAgentOverride();
     }
     this.docShell.customUserAgent = userAgent;
     return true;
   },
 
   getUserAgentOverride() {
     return this.docShell.customUserAgent;
   },
 
   clearUserAgentOverride() {
-    if (this._previousUserAgentOverride !== null) {
+    if (this._previousUserAgentOverride !== undefined) {
       return this.setUserAgentOverride(this._previousUserAgentOverride);
     }
     return false;
   },
 
-  /* DPPX override */
-
-  _previousDPPXOverride: null,
-
-  setDPPXOverride(dppx) {
-    let { contentViewer } = this.docShell;
-
-    if (contentViewer.overrideDPPX === dppx) {
-      return false;
-    }
-
-    if (this._previousDPPXOverride === null) {
-      this._previousDPPXOverride = contentViewer.overrideDPPX;
-    }
-
-    contentViewer.overrideDPPX = dppx;
-
-    return true;
-  },
-
-  getDPPXOverride() {
-    return this.docShell.contentViewer.overrideDPPX;
-  },
-
-  clearDPPXOverride() {
-    if (this._previousDPPXOverride !== null) {
-      return this.setDPPXOverride(this._previousDPPXOverride);
-    }
-
-    return false;
-  },
-
-  disconnect() {
-    this.destroy();
-  },
-
-  destroy() {
-    this.clearTouchEventsOverride();
-    this.clearUserAgentOverride();
-    this.clearDPPXOverride();
-    this.docShell = null;
-    this.simulatorCore = null;
-    protocol.Actor.prototype.destroy.call(this);
-  },
 });
 
 exports.EmulationActor = EmulationActor;
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -1054,17 +1054,17 @@ WebConsoleActor.prototype =
    *        The request message - which preferences need to be retrieved.
    * @return object
    *         The response message - a { key: value } object map.
    */
   onGetPreferences: function WCA_onGetPreferences(aRequest)
   {
     let prefs = Object.create(null);
     for (let key of aRequest.preferences) {
-      prefs[key] = !!this._prefs[key];
+      prefs[key] = this._prefs[key];
     }
     return { preferences: prefs };
   },
 
   /**
    * The "setPreferences" request handler.
    *
    * @param object aRequest
--- a/devtools/shared/specs/emulation.js
+++ b/devtools/shared/specs/emulation.js
@@ -4,16 +4,62 @@
 "use strict";
 
 const { Arg, RetVal, generateActorSpec } = require("devtools/shared/protocol");
 
 const emulationSpec = generateActorSpec({
   typeName: "emulation",
 
   methods: {
+    setDPPXOverride: {
+      request: {
+        dppx: Arg(0, "number")
+      },
+      response: {
+        valueChanged: RetVal("boolean")
+      }
+    },
+
+    getDPPXOverride: {
+      request: {},
+      response: {
+        dppx: RetVal("number")
+      }
+    },
+
+    clearDPPXOverride: {
+      request: {},
+      response: {
+        valueChanged: RetVal("boolean")
+      }
+    },
+
+    setNetworkThrottling: {
+      request: {
+        options: Arg(0, "json")
+      },
+      response: {
+        valueChanged: RetVal("boolean")
+      }
+    },
+
+    getNetworkThrottling: {
+      request: {},
+      response: {
+        state: RetVal("json")
+      }
+    },
+
+    clearNetworkThrottling: {
+      request: {},
+      response: {
+        valueChanged: RetVal("boolean")
+      }
+    },
+
     setTouchEventsOverride: {
       request: {
         flag: Arg(0, "number")
       },
       response: {
         valueChanged: RetVal("boolean")
       }
     },
@@ -49,35 +95,12 @@ const emulationSpec = generateActorSpec(
     },
 
     clearUserAgentOverride: {
       request: {},
       response: {
         valueChanged: RetVal("boolean")
       }
     },
-
-    setDPPXOverride: {
-      request: {
-        dppx: Arg(0, "number")
-      },
-      response: {
-        valueChanged: RetVal("boolean")
-      }
-    },
-
-    getDPPXOverride: {
-      request: {},
-      response: {
-        dppx: RetVal("number")
-      }
-    },
-
-    clearDPPXOverride: {
-      request: {},
-      response: {
-        valueChanged: RetVal("boolean")
-      }
-    },
   }
 });
 
 exports.emulationSpec = emulationSpec;
--- a/devtools/shared/webconsole/network-monitor.js
+++ b/devtools/shared/webconsole/network-monitor.js
@@ -640,17 +640,17 @@ function NetworkMonitor(filters, owner) 
   this.owner = owner;
   this.openRequests = {};
   this.openResponses = {};
   this._httpResponseExaminer =
     DevToolsUtils.makeInfallible(this._httpResponseExaminer).bind(this);
   this._httpModifyExaminer =
     DevToolsUtils.makeInfallible(this._httpModifyExaminer).bind(this);
   this._serviceWorkerRequest = this._serviceWorkerRequest.bind(this);
-  this.throttleData = null;
+  this._throttleData = null;
   this._throttler = null;
 }
 
 exports.NetworkMonitor = NetworkMonitor;
 
 NetworkMonitor.prototype = {
   filters: null,
 
@@ -718,16 +718,26 @@ NetworkMonitor.prototype = {
                                "http-on-modify-request", false);
     }
     // In child processes, only watch for service worker requests
     // everything else only happens in the parent process
     Services.obs.addObserver(this._serviceWorkerRequest,
                              "service-worker-synthesized-response", false);
   },
 
+  get throttleData() {
+    return this._throttleData;
+  },
+
+  set throttleData(value) {
+    this._throttleData = value;
+    // Clear out any existing throttlers
+    this._throttler = null;
+  },
+
   _getThrottler: function () {
     if (this.throttleData !== null && this._throttler === null) {
       this._throttler = new NetworkThrottleManager(this.throttleData);
     }
     return this._throttler;
   },
 
   _serviceWorkerRequest: function (subject, topic, data) {