Bug 1283453 - Add network throttling UI to RDM. r=gl draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Fri, 07 Oct 2016 14:58:46 -0500
changeset 424472 223e4d6e8993cc6181d728caf13f7b9671756f8a
parent 423913 bcecc0fc250b191caeb91fc0cb433107e063f401
child 533681 7ec5f76f55c29c6475a9576aa84cddcbdf4e8287
push id32160
push userbmo:jryans@gmail.com
push dateWed, 12 Oct 2016 20:12:21 +0000
reviewersgl
bugs1283453
milestone52.0a1
Bug 1283453 - Add network throttling UI to RDM. r=gl MozReview-Commit-ID: 9MYLXuhlT2F
devtools/client/locales/en-US/responsive.properties
devtools/client/responsive.html/actions/index.js
devtools/client/responsive.html/actions/moz.build
devtools/client/responsive.html/actions/network-throttling.js
devtools/client/responsive.html/app.js
devtools/client/responsive.html/components/global-toolbar.js
devtools/client/responsive.html/components/moz.build
devtools/client/responsive.html/components/network-throttling-selector.js
devtools/client/responsive.html/index.css
devtools/client/responsive.html/manager.js
devtools/client/responsive.html/reducers.js
devtools/client/responsive.html/reducers/moz.build
devtools/client/responsive.html/reducers/network-throttling.js
devtools/client/responsive.html/test/browser/browser.ini
devtools/client/responsive.html/test/browser/browser_network_throttling.js
devtools/client/responsive.html/test/browser/head.js
devtools/client/responsive.html/test/unit/test_change_network_throttling.js
devtools/client/responsive.html/test/unit/xpcshell.ini
devtools/client/responsive.html/types.js
devtools/client/shared/moz.build
devtools/client/shared/network-throttling-profiles.js
--- a/devtools/client/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -53,8 +53,14 @@ responsive.screenshot=Take a screenshot 
 # The first argument (%1$S) is the date string in yyyy-mm-dd format and the
 # second argument (%2$S) is the time string in HH.MM.SS format.
 responsive.screenshotGeneratedFilename=Screen Shot %1$S at %2$S
 
 # LOCALIZATION NOTE (responsive.remoteOnly): Message displayed in the tab's
 # notification box if a user tries to open Responsive Design Mode in a
 # non-remote tab.
 responsive.remoteOnly=Responsive Design Mode is only available for remote browser tabs, such as those used for web content in multi-process Firefox.
+
+# LOCALIZATION NOTE (responsive.noThrottling): UI option in a menu to configure
+# network throttling.  This option is the default and disables throttling so you
+# just have normal network conditions.  There is not very much room in the UI
+# so a short string would be best if possible.
+responsive.noThrottling=No throttling
--- a/devtools/client/responsive.html/actions/index.js
+++ b/devtools/client/responsive.html/actions/index.js
@@ -19,43 +19,46 @@ createEnum([
   "ADD_DEVICE_TYPE",
 
   // Add an additional viewport to display the document.
   "ADD_VIEWPORT",
 
   // Change the device displayed in the viewport.
   "CHANGE_DEVICE",
 
-  // The location of the page has changed.  This may be triggered by the user
+  // Change the location of the page.  This may be triggered by the user
   // directly entering a new URL, navigating with links, etc.
   "CHANGE_LOCATION",
 
+  // Change the network throttling profile.
+  "CHANGE_NETWORK_THROTTLING",
+
+  // Indicates that the device list is being loaded
+  "LOAD_DEVICE_LIST_START",
+
+  // Indicates that the device list loading action threw an error
+  "LOAD_DEVICE_LIST_ERROR",
+
+  // Indicates that the device list has been loaded successfully
+  "LOAD_DEVICE_LIST_END",
+
   // Resize the viewport.
   "RESIZE_VIEWPORT",
 
   // Rotate the viewport.
   "ROTATE_VIEWPORT",
 
   // Take a screenshot of the viewport.
   "TAKE_SCREENSHOT_START",
 
   // Indicates when the screenshot action ends.
   "TAKE_SCREENSHOT_END",
 
   // Update the device display state in the device selector.
   "UPDATE_DEVICE_DISPLAYED",
 
-  // Indicates that the device list is being loaded
-  "LOAD_DEVICE_LIST_START",
-
-  // Indicates that the device list loading action threw an error
-  "LOAD_DEVICE_LIST_ERROR",
-
-  // Indicates that the device list has been loaded successfully
-  "LOAD_DEVICE_LIST_END",
-
   // Update the device modal open state.
   "UPDATE_DEVICE_MODAL_OPEN",
 
   // Update the touch simulation enabled state.
   "UPDATE_TOUCH_SIMULATION_ENABLED",
 
 ], module.exports);
--- a/devtools/client/responsive.html/actions/moz.build
+++ b/devtools/client/responsive.html/actions/moz.build
@@ -3,12 +3,13 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'devices.js',
     'index.js',
     'location.js',
+    'network-throttling.js',
     'screenshot.js',
     'touch-simulation.js',
     'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/actions/network-throttling.js
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  CHANGE_NETWORK_THROTTLING,
+} = require("./index");
+
+module.exports = {
+
+  changeNetworkThrottling(enabled, profile) {
+    return {
+      type: CHANGE_NETWORK_THROTTLING,
+      enabled,
+      profile,
+    };
+  },
+
+};
--- a/devtools/client/responsive.html/app.js
+++ b/devtools/client/responsive.html/app.js
@@ -10,43 +10,54 @@ const { createClass, createFactory, Prop
   require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 const {
   updateDeviceDisplayed,
   updateDeviceModalOpen,
   updatePreferredDevices,
 } = require("./actions/devices");
+const { changeNetworkThrottling } = require("./actions/network-throttling");
+const { takeScreenshot } = require("./actions/screenshot");
+const { updateTouchSimulationEnabled } = require("./actions/touch-simulation");
 const {
   changeDevice,
   resizeViewport,
   rotateViewport
 } = require("./actions/viewports");
-const { takeScreenshot } = require("./actions/screenshot");
-const { updateTouchSimulationEnabled } = require("./actions/touch-simulation");
 const DeviceModal = createFactory(require("./components/device-modal"));
 const GlobalToolbar = createFactory(require("./components/global-toolbar"));
 const Viewports = createFactory(require("./components/viewports"));
 const Types = require("./types");
 
 let App = createClass({
   displayName: "App",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
+    networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
     touchSimulation: PropTypes.shape(Types.touchSimulation).isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
   },
 
   onBrowserMounted() {
     window.postMessage({ type: "browser-mounted" }, "*");
   },
 
+  onChangeNetworkThrottling(enabled, profile) {
+    window.postMessage({
+      type: "change-network-throtting",
+      enabled,
+      profile,
+    }, "*");
+    this.props.dispatch(changeNetworkThrottling(enabled, profile));
+  },
+
   onChangeViewportDevice(id, device) {
     window.postMessage({
       type: "change-viewport-device",
       device,
     }, "*");
     this.props.dispatch(changeDevice(id, device.name));
     this.props.dispatch(updateTouchSimulationEnabled(device.touch));
   },
@@ -95,23 +106,25 @@ let App = createClass({
 
     this.props.dispatch(updateTouchSimulationEnabled(isEnabled));
   },
 
   render() {
     let {
       devices,
       location,
+      networkThrottling,
       screenshot,
       touchSimulation,
       viewports,
     } = this.props;
 
     let {
       onBrowserMounted,
+      onChangeNetworkThrottling,
       onChangeViewportDevice,
       onContentResize,
       onDeviceListUpdate,
       onExit,
       onResizeViewport,
       onRotateViewport,
       onScreenshot,
       onUpdateDeviceDisplayed,
@@ -119,18 +132,20 @@ let App = createClass({
       onUpdateTouchSimulation,
     } = this;
 
     return dom.div(
       {
         id: "app",
       },
       GlobalToolbar({
+        networkThrottling,
         screenshot,
         touchSimulation,
+        onChangeNetworkThrottling,
         onExit,
         onScreenshot,
         onUpdateTouchSimulation,
       }),
       Viewports({
         devices,
         location,
         screenshot,
--- a/devtools/client/responsive.html/components/global-toolbar.js
+++ b/devtools/client/responsive.html/components/global-toolbar.js
@@ -1,36 +1,42 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const { DOM: dom, createClass, createFactory, PropTypes, addons } =
+  require("devtools/client/shared/vendor/react");
+
 const { getStr } = require("../utils/l10n");
-const { DOM: dom, createClass, PropTypes, addons } =
-  require("devtools/client/shared/vendor/react");
 const Types = require("../types");
+const NetworkThrottlingSelector = createFactory(require("./network-throttling-selector"));
 
 module.exports = createClass({
   displayName: "GlobalToolbar",
 
   propTypes: {
+    networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
     touchSimulation: PropTypes.shape(Types.touchSimulation).isRequired,
+    onChangeNetworkThrottling: PropTypes.func.isRequired,
     onExit: PropTypes.func.isRequired,
     onScreenshot: PropTypes.func.isRequired,
     onUpdateTouchSimulation: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   render() {
     let {
+      networkThrottling,
       screenshot,
       touchSimulation,
+      onChangeNetworkThrottling,
       onExit,
       onScreenshot,
       onUpdateTouchSimulation
     } = this.props;
 
     let touchButtonClass = "toolbar-button devtools-button";
     if (touchSimulation.enabled) {
       touchButtonClass += " active";
@@ -40,17 +46,22 @@ module.exports = createClass({
       {
         id: "global-toolbar",
         className: "container",
       },
       dom.span(
         {
           className: "title",
         },
-        getStr("responsive.title")),
+        getStr("responsive.title")
+      ),
+      NetworkThrottlingSelector({
+        networkThrottling,
+        onChangeNetworkThrottling,
+      }),
       dom.button({
         id: "global-touch-simulation-button",
         className: touchButtonClass,
         title: (touchSimulation.enabled ?
           getStr("responsive.disableTouch") : getStr("responsive.enableTouch")),
         onClick: () => onUpdateTouchSimulation(!touchSimulation.enabled),
       }),
       dom.button({
--- a/devtools/client/responsive.html/components/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -4,14 +4,15 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'browser.js',
     'device-modal.js',
     'device-selector.js',
     'global-toolbar.js',
+    'network-throttling-selector.js',
     'resizable-viewport.js',
     'viewport-dimension.js',
     'viewport-toolbar.js',
     'viewport.js',
     'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/components/network-throttling-selector.js
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { DOM: dom, createClass, PropTypes, addons } =
+  require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+const { getStr } = require("../utils/l10n");
+const throttlingProfiles = require("devtools/client/shared/network-throttling-profiles");
+
+module.exports = createClass({
+
+  displayName: "NetworkThrottlingSelector",
+
+  propTypes: {
+    networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
+    onChangeNetworkThrottling: PropTypes.func.isRequired,
+  },
+
+  mixins: [ addons.PureRenderMixin ],
+
+  onSelectChange({ target }) {
+    let {
+      onChangeNetworkThrottling,
+    } = this.props;
+
+    if (target.value == getStr("responsive.noThrottling")) {
+      onChangeNetworkThrottling(false, "");
+      return;
+    }
+
+    for (let profile of throttlingProfiles) {
+      if (profile.id === target.value) {
+        onChangeNetworkThrottling(true, profile.id);
+        return;
+      }
+    }
+  },
+
+  render() {
+    let {
+      networkThrottling,
+    } = this.props;
+
+    let selectClass = "";
+    let selectedProfile;
+    if (networkThrottling.enabled) {
+      selectClass += " selected";
+      selectedProfile = networkThrottling.profile;
+    } else {
+      selectedProfile = getStr("responsive.noThrottling");
+    }
+
+    let listContent = [
+      dom.option(
+        {
+          key: "disabled",
+        },
+        getStr("responsive.noThrottling")
+      ),
+      dom.option(
+        {
+          key: "divider",
+          className: "divider",
+          disabled: true,
+        }
+      ),
+      throttlingProfiles.map(profile => {
+        return dom.option(
+          {
+            key: profile.id,
+          },
+          profile.id
+        );
+      }),
+    ];
+
+    return dom.select(
+      {
+        id: "global-network-throttling-selector",
+        className: selectClass,
+        value: selectedProfile,
+        onChange: this.onSelectChange,
+      },
+      ...listContent
+    );
+  },
+
+});
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -50,17 +50,17 @@ html, body {
   flex-direction: column;
   padding-top: 15px;
   padding-bottom: 1%;
   position: relative;
   height: 100%;
 }
 
 /**
- * Common style for containers and toolbar buttons
+ * Common styles for shared components
  */
 
 .container {
   background-color: var(--theme-toolbar-background);
   border: 1px solid var(--theme-splitter-color);
 }
 
 .toolbar-button {
@@ -72,16 +72,67 @@ html, body {
   min-height: initial;
   align-self: center;
 }
 
 .toolbar-button:active::before {
   filter: url("chrome://devtools/skin/images/filters.svg#checked-icon-state");
 }
 
+select {
+  -moz-appearance: none;
+  background-color: var(--theme-toolbar-background);
+  background-image: var(--viewport-selection-arrow);
+  background-position: 100% 50%;
+  background-repeat: no-repeat;
+  background-size: 7px;
+  border: none;
+  color: var(--viewport-color);
+  padding: 0 8px;
+  text-align: center;
+  text-overflow: ellipsis;
+  font-size: 11px;
+}
+
+select.selected {
+  background-image: var(--viewport-selection-arrow-selected);
+  color: var(--viewport-active-color);
+}
+
+select:hover {
+  background-image: var(--viewport-selection-arrow-hovered);
+  color: var(--viewport-hover-color);
+}
+
+/* This is (believed to be?) separate from the identical select.selected rule
+   set so that it overrides select:hover because of file ordering once the
+   select is focused.  It's unclear whether the visual effect that results here
+   is intentional and desired. */
+select:focus {
+  background-image: var(--viewport-selection-arrow-selected);
+  color: var(--viewport-active-color);
+}
+
+select > option {
+  text-align: left;
+  padding: 5px 10px;
+}
+
+select > option,
+select > option:hover {
+  color: var(--viewport-active-color);
+}
+
+select > option.divider {
+  border-top: 1px solid var(--theme-splitter-color);
+  height: 0px;
+  padding: 0;
+  font-size: 0px;
+}
+
 /**
  * Global Toolbar
  */
 
 #global-toolbar {
   color: var(--theme-body-color-alt);
   border-radius: 2px;
   box-shadow: var(--rdm-box-shadow);
@@ -126,16 +177,21 @@ html, body {
   margin: -6px 0 0 -6px;
 }
 
 #global-screenshot-button:disabled {
   filter: url("chrome://devtools/skin/images/filters.svg#checked-icon-state");
   opacity: 1 !important;
 }
 
+#global-network-throttling-selector {
+  height: 15px;
+  padding-left: 0;
+  width: 103px;
+}
 
 #viewports {
   /* Make sure left-most viewport is visible when there's horizontal overflow.
      That is, when the horizontal space become smaller than the viewports and a
      scrollbar appears, then the first viewport will still be visible */
   position: sticky;
   left: 0;
   /* Individual viewports are inline elements, make sure they stay on a single
@@ -167,60 +223,16 @@ html, body {
   border-width: 0;
   border-bottom-width: 1px;
   display: flex;
   flex-direction: row;
   justify-content: center;
   height: 18px;
 }
 
-.viewport-device-selector {
-  -moz-appearance: none;
-  background-color: var(--theme-toolbar-background);
-  background-image: var(--viewport-selection-arrow);
-  background-position: 100% 52%;
-  background-repeat: no-repeat;
-  background-size: 7px;
-  border: none;
-  color: var(--viewport-color);
-  height: 100%;
-  padding: 0 8px 0 8px;
-  text-align: center;
-  text-overflow: ellipsis;
-  width: 150px;
-  font-size: 11px;
-  width: -moz-fit-content;
-}
-
-.viewport-device-selector.selected {
-  background-image: var(--viewport-selection-arrow-selected);
-  color: var(--viewport-active-color);
-}
-
-.viewport-device-selector:hover {
-  background-image: var(--viewport-selection-arrow-hovered);
-  color: var(--viewport-hover-color);
-}
-
-.viewport-device-selector:focus {
-  background-image: var(--viewport-selection-arrow-selected);
-  color: var(--viewport-active-color);
-}
-
-.viewport-device-selector > option {
-  text-align: left;
-  padding: 5px 10px;
-}
-
-.viewport-device-selector > option,
-.viewport-device-selector > option:hover,
-.viewport-device-selector:hover > option:hover {
-  color: var(--viewport-active-color);
-}
-
 .viewport-rotate-button {
   position: absolute;
   right: 0;
 }
 
 .viewport-rotate-button::before {
   background-image: url("./images/rotate-viewport.svg");
 }
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -9,25 +9,25 @@ const promise = require("promise");
 const { Task } = require("devtools/shared/task");
 const EventEmitter = require("devtools/shared/event-emitter");
 const { getOwnerWindow } = require("sdk/tabs/utils");
 const { startup } = require("sdk/window/helpers");
 const message = require("./utils/message");
 const { swapToInnerBrowser } = require("./browser/swap");
 const { EmulationFront } = require("devtools/shared/fronts/emulation");
 const { getStr } = require("./utils/l10n");
-const { TargetFactory } = require("devtools/client/framework/target");
-const { gDevTools } = require("devtools/client/framework/devtools");
 
 const TOOL_URL = "chrome://devtools/content/responsive.html/index.xhtml";
 
-loader.lazyRequireGetter(this, "DebuggerClient",
-                         "devtools/shared/client/main", true);
-loader.lazyRequireGetter(this, "DebuggerServer",
-                         "devtools/server/main", true);
+loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
+loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "throttlingProfiles",
+  "devtools/client/shared/network-throttling-profiles");
 
 /**
  * ResponsiveUIManager is the external API for the browser UI, etc. to use when
  * opening and closing the responsive UI.
  *
  * While the HTML UI is in an experimental stage, the older ResponsiveUIManager
  * from devtools/client/responsivedesign/responsivedesign.jsm delegates to this
  * object when the pref "devtools.responsive.html.enabled" is true.
@@ -357,32 +357,31 @@ ResponsiveUI.prototype = {
       yield this.inited;
     }
 
     this.tab.removeEventListener("TabClose", this);
     this.browserWindow.removeEventListener("unload", this);
     this.toolWindow.removeEventListener("message", this);
 
     if (!isTabClosing) {
-      // Stop the touch event simulator if it was running
-      yield this.emulationFront.clearTouchEventsOverride();
-
       // Notify the inner browser to stop the frame script
       yield message.request(this.toolWindow, "stop-frame-script");
     }
 
     // Destroy local state
     let swap = this.swap;
     this.browserWindow = null;
     this.tab = null;
     this.inited = null;
     this.toolWindow = null;
     this.swap = null;
 
-    // Close the debugger client used to speak with emulation actor
+    // Close the debugger client used to speak with emulation actor.
+    // The actor handles clearing any overrides itself, so it's not necessary to clear
+    // anything on shutdown client side.
     let clientClosed = this.client.close();
     if (!isTabClosing) {
       yield clientClosed;
     }
     this.client = this.emulationFront = null;
 
     if (!isWindowClosing) {
       // Undo the swap and return the content back to a normal tab
@@ -417,74 +416,113 @@ ResponsiveUI.prototype = {
         ResponsiveUIManager.closeIfNeeded(browserWindow, tab, {
           reason: event.type,
         });
         break;
     }
   },
 
   handleMessage(event) {
-    let { browserWindow, tab } = this;
-
     if (event.origin !== "chrome://devtools") {
       return;
     }
 
     switch (event.data.type) {
+      case "change-network-throtting":
+        this.onChangeNetworkThrottling(event);
+        break;
       case "change-viewport-device":
-        let { userAgent, pixelRatio, touch } = event.data.device;
-        this.updateUserAgent(userAgent);
-        this.updateDPPX(pixelRatio);
-        this.updateTouchSimulation(touch);
+        this.onChangeViewportDevice(event);
         break;
       case "content-resize":
-        let { width, height } = event.data;
-        this.emit("content-resize", {
-          width,
-          height,
-        });
+        this.onContentResize(event);
         break;
       case "exit":
-        ResponsiveUIManager.closeIfNeeded(browserWindow, tab);
+        this.onExit();
         break;
       case "update-touch-simulation":
-        let { enabled } = event.data;
-        this.updateTouchSimulation(enabled);
+        this.onUpdateTouchSimulation(event);
         break;
     }
   },
 
-  updateTouchSimulation: Task.async(function* (enabled) {
-    if (enabled) {
-      let reloadNeeded = yield this.emulationFront.setTouchEventsOverride(
-        Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED
-      );
-      if (reloadNeeded) {
-        this.getViewportBrowser().reload();
-      }
-    } else {
-      this.emulationFront.clearTouchEventsOverride();
+  onChangeNetworkThrottling: Task.async(function* (event) {
+    let { enabled, profile } = event.data;
+    yield this.updateNetworkThrottling(enabled, profile);
+    // Used by tests
+    this.emit("network-throttling-changed");
+  }),
+
+  onChangeViewportDevice(event) {
+    let { userAgent, pixelRatio, touch } = event.data.device;
+    this.updateUserAgent(userAgent);
+    this.updateDPPX(pixelRatio);
+    this.updateTouchSimulation(touch);
+  },
+
+  onContentResize(event) {
+    let { width, height } = event.data;
+    this.emit("content-resize", {
+      width,
+      height,
+    });
+  },
+
+  onExit() {
+    let { browserWindow, tab } = this;
+    ResponsiveUIManager.closeIfNeeded(browserWindow, tab);
+  },
+
+  onUpdateTouchSimulation(event) {
+    let { enabled } = event.data;
+    this.updateTouchSimulation(enabled);
+  },
+
+  updateDPPX(dppx) {
+    if (!dppx) {
+      this.emulationFront.clearDPPXOverride();
+      return;
     }
+    this.emulationFront.setDPPXOverride(dppx);
+  },
+
+  updateNetworkThrottling: Task.async(function* (enabled, profile) {
+    if (!enabled) {
+      yield this.emulationFront.clearNetworkThrottling();
+      return;
+    }
+    let data = throttlingProfiles.find(({ id }) => id == profile);
+    let { download, upload, latency } = data;
+    yield this.emulationFront.setNetworkThrottling({
+      downloadThroughput: download,
+      uploadThroughput: upload,
+      latency,
+    });
   }),
 
   updateUserAgent(userAgent) {
-    if (userAgent) {
-      this.emulationFront.setUserAgentOverride(userAgent);
-    } else {
+    if (!userAgent) {
       this.emulationFront.clearUserAgentOverride();
+      return;
     }
+    this.emulationFront.setUserAgentOverride(userAgent);
   },
 
-  updateDPPX(dppx) {
-    if (dppx) {
-      this.emulationFront.setDPPXOverride(dppx);
-    } else {
-      this.emulationFront.clearDPPXOverride();
+  updateTouchSimulation: Task.async(function* (enabled) {
+    if (!enabled) {
+      yield this.emulationFront.clearTouchEventsOverride();
+      return;
     }
-  },
+    let reloadNeeded = yield this.emulationFront.setTouchEventsOverride(
+      Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED
+    );
+    if (reloadNeeded) {
+      this.getViewportBrowser().reload();
+    }
+  }),
 
   /**
    * Helper for tests. Assumes a single viewport for now.
    */
   getViewportSize() {
     return this.toolWindow.getViewportSize();
   },
 
--- a/devtools/client/responsive.html/reducers.js
+++ b/devtools/client/responsive.html/reducers.js
@@ -1,11 +1,12 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 exports.devices = require("./reducers/devices");
 exports.location = require("./reducers/location");
+exports.networkThrottling = require("./reducers/network-throttling");
 exports.screenshot = require("./reducers/screenshot");
 exports.touchSimulation = require("./reducers/touch-simulation");
 exports.viewports = require("./reducers/viewports");
--- a/devtools/client/responsive.html/reducers/moz.build
+++ b/devtools/client/responsive.html/reducers/moz.build
@@ -2,12 +2,13 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'devices.js',
     'location.js',
+    'network-throttling.js',
     'screenshot.js',
     'touch-simulation.js',
     'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/network-throttling.js
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  CHANGE_NETWORK_THROTTLING,
+} = require("../actions/index");
+
+const INITIAL_NETWORK_THROTTLING = {
+  enabled: false,
+  profile: "",
+};
+
+let reducers = {
+
+  [CHANGE_NETWORK_THROTTLING](throttling, { enabled, profile }) {
+    return {
+      enabled,
+      profile,
+    };
+  },
+
+};
+
+module.exports = function (throttling = INITIAL_NETWORK_THROTTLING, action) {
+  let reducer = reducers[action.type];
+  if (!reducer) {
+    return throttling;
+  }
+  return reducer(throttling, action);
+};
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -22,16 +22,17 @@ support-files =
 [browser_device_modal_submit.js]
 [browser_device_width.js]
 [browser_exit_button.js]
 [browser_frame_script_active.js]
 [browser_menu_item_01.js]
 [browser_menu_item_02.js]
 [browser_mouse_resize.js]
 [browser_navigation.js]
+[browser_network_throttling.js]
 [browser_page_state.js]
 [browser_permission_doorhanger.js]
 [browser_resize_cmd.js]
 [browser_screenshot_button.js]
 [browser_shutdown_close_sync.js]
 [browser_toolbox_computed_view.js]
 [browser_toolbox_rule_view.js]
 [browser_toolbox_swap_browsers.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_network_throttling.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const throttlingProfiles = require("devtools/client/shared/network-throttling-profiles");
+
+// Tests changing network throttling
+const TEST_URL = "data:text/html;charset=utf-8,Network throttling test";
+
+addRDMTask(TEST_URL, function* ({ ui, manager }) {
+  let { store } = ui.toolWindow;
+
+  // Wait until the viewport has been added
+  yield waitUntilState(store, state => state.viewports.length == 1);
+
+  // Test defaults
+  testNetworkThrottlingSelectorLabel(ui, "No throttling");
+  yield testNetworkThrottlingState(ui, null);
+
+  // Test a fast profile
+  yield testThrottlingProfile(ui, "Wi-Fi");
+
+  // Test a slower profile
+  yield testThrottlingProfile(ui, "Regular 3G");
+
+  // Test switching back to no throttling
+  let changed = once(ui, "network-throttling-changed");
+  yield switchNetworkThrottling(ui, "No throttling");
+  yield changed;
+  testNetworkThrottlingSelectorLabel(ui, "No throttling");
+  yield testNetworkThrottlingState(ui, null);
+});
+
+function testNetworkThrottlingSelectorLabel(ui, expected) {
+  let selector = "#global-network-throttling-selector";
+  let select = ui.toolWindow.document.querySelector(selector);
+  is(select.selectedOptions[0].textContent, expected,
+    `Select label should be changed to ${expected}`);
+}
+
+var testNetworkThrottlingState = Task.async(function* (ui, expected) {
+  let state = yield ui.emulationFront.getNetworkThrottling();
+  Assert.deepEqual(state, expected, "Network throttling state should be " +
+                                    JSON.stringify(expected, null, 2));
+});
+
+var testThrottlingProfile = Task.async(function* (ui, profile) {
+  let changed = once(ui, "network-throttling-changed");
+  yield switchNetworkThrottling(ui, profile);
+  yield changed;
+  testNetworkThrottlingSelectorLabel(ui, profile);
+  let data = throttlingProfiles.find(({ id }) => id == profile);
+  let { download, upload, latency } = data;
+  yield testNetworkThrottlingState(ui, {
+    downloadThroughput: download,
+    uploadThroughput: upload,
+    latency,
+  });
+});
--- a/devtools/client/responsive.html/test/browser/head.js
+++ b/devtools/client/responsive.html/test/browser/head.js
@@ -225,19 +225,18 @@ function openDeviceModal(ui) {
     ui.toolWindow);
   EventUtils.synthesizeMouseAtCenter(editDeviceOption, {type: "mouseup"},
     ui.toolWindow);
 
   ok(modal.classList.contains("opened") && !modal.classList.contains("closed"),
     "The device modal is displayed.");
 }
 
-function switchDevice({ toolWindow }, value) {
+function switchSelector({ toolWindow }, selector, value) {
   return new Promise(resolve => {
-    let selector = ".viewport-device-selector";
     let select = toolWindow.document.querySelector(selector);
     isnot(select, null, `selector "${selector}" should match an existing element.`);
 
     let option = [...select.options].find(o => o.value === String(value));
     isnot(option, undefined, `value "${value}" should match an existing option.`);
 
     let event = new toolWindow.UIEvent("change", {
       view: toolWindow,
@@ -251,16 +250,24 @@ function switchDevice({ toolWindow }, va
       resolve();
     }, { once: true });
 
     select.value = value;
     select.dispatchEvent(event);
   });
 }
 
+function switchDevice(ui, value) {
+  return switchSelector(ui, ".viewport-device-selector", value);
+}
+
+function switchNetworkThrottling(ui, value) {
+  return switchSelector(ui, "#global-network-throttling-selector", value);
+}
+
 function getSessionHistory(browser) {
   return ContentTask.spawn(browser, {}, function* () {
     /* eslint-disable no-undef */
     let { interfaces: Ci } = Components;
     let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
     let sessionHistory = webNav.sessionHistory;
     let result = {
       index: sessionHistory.index,
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_change_network_throttling.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test changing the network throttling state
+
+const {
+  changeNetworkThrottling,
+} = require("devtools/client/responsive.html/actions/network-throttling");
+
+add_task(function* () {
+  let store = Store();
+  const { getState, dispatch } = store;
+
+  ok(!getState().networkThrottling.enabled,
+    "Network throttling is disabled by default.");
+  equal(getState().networkThrottling.profile, "",
+    "Network throttling profile is empty by default.");
+
+  dispatch(changeNetworkThrottling(true, "Bob"));
+
+  ok(getState().networkThrottling.enabled,
+    "Network throttling is enabled.");
+  equal(getState().networkThrottling.profile, "Bob",
+    "Network throttling profile is set.");
+});
--- a/devtools/client/responsive.html/test/unit/xpcshell.ini
+++ b/devtools/client/responsive.html/test/unit/xpcshell.ini
@@ -3,13 +3,14 @@ tags = devtools
 head = head.js ../../../framework/test/shared-redux-head.js
 tail =
 firefox-appdir = browser
 
 [test_add_device.js]
 [test_add_device_type.js]
 [test_add_viewport.js]
 [test_change_location.js]
+[test_change_network_throttling.js]
 [test_change_viewport_device.js]
 [test_resize_viewport.js]
 [test_rotate_viewport.js]
 [test_update_device_displayed.js]
 [test_update_touch_simulation_enabled.js]
--- a/devtools/client/responsive.html/types.js
+++ b/devtools/client/responsive.html/types.js
@@ -90,37 +90,50 @@ exports.devices = {
  */
 exports.location = PropTypes.string;
 
 /**
  * The progression of the screenshot
  */
 exports.screenshot = {
 
-  isCapturing: PropTypes.bool.isRequired,
+  isCapturing: PropTypes.bool,
 
 };
 
 /**
  * Touch simulation.
  */
 exports.touchSimulation = {
 
-  // Whether or not the touch simulation is enabled
-  enabled: PropTypes.bool.isRequired,
+  // Whether or not touch simulation is enabled
+  enabled: PropTypes.bool,
+
+};
+
+/**
+ * Network throttling.
+ */
+exports.networkThrottling = {
+
+  // Whether or not network throttling is enabled
+  enabled: PropTypes.bool,
+
+  // Name of the selected throttling profile
+  profile: PropTypes.string,
 
 };
 
 /**
  * A single viewport displaying a document.
  */
 exports.viewport = {
 
   // The id of the viewport
-  id: PropTypes.number.isRequired,
+  id: PropTypes.number,
 
   // The currently selected device applied to the viewport.
   device: PropTypes.string,
 
   // The width of the viewport
   width: PropTypes.number,
 
   // The height of the viewport
--- a/devtools/client/shared/moz.build
+++ b/devtools/client/shared/moz.build
@@ -31,16 +31,17 @@ DevToolsModules(
     'file-watcher-worker.js',
     'file-watcher.js',
     'frame-script-utils.js',
     'getjson.js',
     'inplace-editor.js',
     'Jsbeautify.jsm',
     'key-shortcuts.js',
     'keycodes.js',
+    'network-throttling-profiles.js',
     'node-attribute-parser.js',
     'options-view.js',
     'output-parser.js',
     'poller.js',
     'prefs.js',
     'scroll.js',
     'source-utils.js',
     'SplitView.jsm',
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/network-throttling-profiles.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const K = 1024;
+const M = 1024 * 1024;
+const Bps = 1 / 8;
+const KBps = K * Bps;
+const MBps = M * Bps;
+
+/**
+ * Predefined network throttling profiles.
+ * Speeds are in bytes per second.  Latency is in ms.
+ */
+/* eslint-disable key-spacing */
+module.exports = [
+  {
+    id:          "GPRS",
+    download:    50 * KBps,
+    upload:      20 * KBps,
+    latency:     500,
+  },
+  {
+    id:          "Regular 2G",
+    download:    250 * KBps,
+    upload:      50 * KBps,
+    latency:     300,
+  },
+  {
+    id:          "Good 2G",
+    download:    450 * KBps,
+    upload:      150 * KBps,
+    latency:     150,
+  },
+  {
+    id:          "Regular 3G",
+    download:    750 * KBps,
+    upload:      250 * KBps,
+    latency:     100,
+  },
+  {
+    id:          "Good 3G",
+    download:    1.5 * MBps,
+    upload:      750 * KBps,
+    latency:     40,
+  },
+  {
+    id:          "Regular 4G / LTE",
+    download:    4 * MBps,
+    upload:      3 * MBps,
+    latency:     20,
+  },
+  {
+    id:          "DSL",
+    download:    2 * MBps,
+    upload:      1 * MBps,
+    latency:     5,
+  },
+  {
+    id:          "Wi-Fi",
+    download:    30 * MBps,
+    upload:      15 * MBps,
+    latency:     2,
+  },
+];
+/* eslint-enable key-spacing */