Bug 1283453 - Add network throttling to emulation actor. r=tromey draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Fri, 07 Oct 2016 14:55:16 -0500
changeset 423913 bcecc0fc250b191caeb91fc0cb433107e063f401
parent 422256 49fe455cac957808ed4a5d1685c3a1938dac1d31
child 423914 713a62cd453274ea769ff780f3ba28d0b5c24e1e
child 424472 223e4d6e8993cc6181d728caf13f7b9671756f8a
push id32022
push userbmo:jryans@gmail.com
push dateTue, 11 Oct 2016 21:33:47 +0000
reviewerstromey
bugs1283453
milestone52.0a1
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) {