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
--- 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) {