Bug 1349559 - Introduce UI for network throttling in the Net panel; r=jryans draft
authorJan Odvarko <odvarko@gmail.com>
Tue, 01 May 2018 11:07:18 +0200
changeset 790081 10aa7eacd8cd8ebdf01e650bc3ccf0c9bb3d586b
parent 790006 7ef8450810693ab08e79ab0d4702de6f479e678c
push id108421
push userjodvarko@mozilla.com
push dateTue, 01 May 2018 09:08:20 +0000
reviewersjryans
bugs1349559
milestone61.0a1
Bug 1349559 - Introduce UI for network throttling in the Net panel; r=jryans MozReview-Commit-ID: Ad0ABzf0YuY
devtools/client/locales/en-US/network-throttling.properties
devtools/client/locales/en-US/responsive.properties
devtools/client/netmonitor/src/assets/styles/Toolbar.css
devtools/client/netmonitor/src/components/Toolbar.js
devtools/client/netmonitor/src/connector/firefox-connector.js
devtools/client/netmonitor/src/connector/index.js
devtools/client/netmonitor/src/create-store.js
devtools/client/netmonitor/src/middleware/moz.build
devtools/client/netmonitor/src/middleware/throttling.js
devtools/client/netmonitor/src/reducers/index.js
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/GlobalToolbar.js
devtools/client/responsive.html/components/NetworkThrottlingSelector.js
devtools/client/responsive.html/components/moz.build
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_network_throttling.js
devtools/client/responsive.html/test/unit/test_change_network_throttling.js
devtools/client/shared/components/moz.build
devtools/client/shared/components/throttling/NetworkThrottlingSelector.js
devtools/client/shared/components/throttling/actions.js
devtools/client/shared/components/throttling/moz.build
devtools/client/shared/components/throttling/profiles.js
devtools/client/shared/components/throttling/reducer.js
devtools/client/shared/components/throttling/types.js
devtools/client/shared/moz.build
devtools/client/shared/network-throttling-profiles.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/locales/en-US/network-throttling.properties
@@ -0,0 +1,18 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the NetworkThrottlingSelector
+# component used to throttle network bandwidth.
+#
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# 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/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -62,22 +62,16 @@ responsive.screenshotGeneratedFilename=S
 # 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.noContainerTabs): Message displayed in the tab's
 # notification box if a user tries to open Responsive Design Mode in a
 # container tab.
 responsive.noContainerTabs=Responsive Design Mode is currently unavailable in container tabs.
 
-# 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
-
 # LOCALIZATION NOTE (responsive.changeDevicePixelRatio): tooltip for the
 # device pixel ratio dropdown when is enabled.
 responsive.changeDevicePixelRatio=Change device pixel ratio of the viewport
 
 # LOCALIZATION NOTE (responsive.devicePixelRatio.auto): tooltip for the device pixel ratio
 # dropdown when it is disabled because a device is selected.
 # The argument (%1$S) is the selected device (e.g. iPhone 6) that set
 # automatically the device pixel ratio value.
--- a/devtools/client/netmonitor/src/assets/styles/Toolbar.css
+++ b/devtools/client/netmonitor/src/assets/styles/Toolbar.css
@@ -89,16 +89,41 @@
 
 .devtools-checkbox-label {
   margin-inline-start: 10px;
   margin-inline-end: 3px;
   white-space: nowrap;
   margin-top: 1px;
 }
 
+/* Throttling Button */
+
+#global-network-throttling-selector:not(:hover) {
+  background-color: transparent;
+}
+
+#global-network-throttling-selector:hover {
+  background-color: var(--toolbarbutton-background);
+}
+
+#global-network-throttling-selector {
+  width: 92px;
+  padding-right: 12px;
+  background-image: var(--drop-down-icon-url);
+  background-position: right 6px;
+  background-repeat: no-repeat;
+  fill: var(--theme-toolbar-photon-icon-color);
+}
+
+/* Make sure the Throttle button icon is vertically centered on Mac */
+:root[platform="mac"] #global-network-throttling-selector {
+  height: 17px;
+  background-position-y: 5px;
+}
+
 /* Search box */
 
 .devtools-searchbox {
   height: 100%;
 }
 
 .devtools-plaininput:focus {
   border: 1px solid var(--blue-50);
--- a/devtools/client/netmonitor/src/components/Toolbar.js
+++ b/devtools/client/netmonitor/src/components/Toolbar.js
@@ -44,16 +44,21 @@ const ENABLE_PERSISTENT_LOGS_LABEL =
   L10N.getStr("netmonitor.toolbar.enablePersistentLogs.label");
 const DISABLE_CACHE_TOOLTIP = L10N.getStr("netmonitor.toolbar.disableCache.tooltip");
 const DISABLE_CACHE_LABEL = L10N.getStr("netmonitor.toolbar.disableCache.label");
 
 // Menu
 loader.lazyRequireGetter(this, "showMenu", "devtools/client/netmonitor/src/utils/menu", true);
 loader.lazyRequireGetter(this, "HarMenuUtils", "devtools/client/netmonitor/src/har/har-menu-utils", true);
 
+// Throttling
+const Types = require("devtools/client/shared/components/throttling/types");
+const NetworkThrottlingSelector = createFactory(require("devtools/client/shared/components/throttling/NetworkThrottlingSelector"));
+const { changeNetworkThrottling } = require("devtools/client/shared/components/throttling/actions");
+
 /**
  * Network monitor toolbar component.
  *
  * Toolbar contains a set of useful tools to control network requests
  * as well as set of filters for filtering the content.
  */
 class Toolbar extends Component {
   static get propTypes() {
@@ -75,21 +80,25 @@ class Toolbar extends Component {
       browserCacheDisabled: PropTypes.bool.isRequired,
       toggleRequestFilterType: PropTypes.func.isRequired,
       filteredRequests: PropTypes.array.isRequired,
       // Set to true if there is enough horizontal space
       // and the toolbar needs just one row.
       singleRow: PropTypes.bool.isRequired,
       // Callback for opening split console.
       openSplitConsole: PropTypes.func,
+      networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
+      // Executed when throttling changes (through toolbar button).
+      onChangeNetworkThrottling: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
+
     this.autocompleteProvider = this.autocompleteProvider.bind(this);
     this.onSearchBoxFocus = this.onSearchBoxFocus.bind(this);
     this.toggleRequestFilterType = this.toggleRequestFilterType.bind(this);
     this.updatePersistentLogsEnabled = this.updatePersistentLogsEnabled.bind(this);
     this.updateBrowserCacheDisabled = this.updateBrowserCacheDisabled.bind(this);
   }
 
   componentDidMount() {
@@ -100,16 +109,17 @@ class Toolbar extends Component {
   }
 
   shouldComponentUpdate(nextProps) {
     return this.props.persistentLogsEnabled !== nextProps.persistentLogsEnabled
     || this.props.browserCacheDisabled !== nextProps.browserCacheDisabled
     || this.props.recording !== nextProps.recording
     || this.props.singleRow !== nextProps.singleRow
     || !Object.is(this.props.requestFilterTypes, nextProps.requestFilterTypes)
+    || this.props.networkThrottling !== nextProps.networkThrottling
 
     // Filtered requests are useful only when searchbox is focused
     || !!(this.refs.searchbox && this.refs.searchbox.focused);
   }
 
   componentWillUnmount() {
     Services.prefs.removeObserver(DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF,
                                   this.updatePersistentLogsEnabled);
@@ -257,16 +267,32 @@ class Toolbar extends Component {
           onChange: toggleBrowserCache,
         }),
         DISABLE_CACHE_LABEL,
       )
     );
   }
 
   /**
+   * Render network throttling selector button.
+   */
+  renderThrottlingSelector() {
+    let {
+      networkThrottling,
+      onChangeNetworkThrottling,
+    } = this.props;
+
+    return NetworkThrottlingSelector({
+      className: "devtools-button",
+      networkThrottling,
+      onChangeNetworkThrottling,
+    });
+  }
+
+  /**
    * Render drop down button with HAR related actions.
    */
   renderHarButton() {
     return button({
       id: "devtools-har-button",
       title: TOOLBAR_HAR_BUTTON,
       className: "devtools-button devtools-har-button",
       onClick: evt => {
@@ -357,31 +383,33 @@ class Toolbar extends Component {
           this.renderSeparator(),
           this.renderToggleRecordingButton(recording, toggleRecording),
           this.renderSeparator(),
           this.renderFilterButtons(requestFilterTypes),
           this.renderSeparator(),
           this.renderPersistlogCheckbox(persistentLogsEnabled, togglePersistentLogs),
           this.renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache),
           this.renderSeparator(),
+          this.renderThrottlingSelector(),
           this.renderHarButton(),
         )
       )
     ) : (
       span({ className: "devtools-toolbar devtools-toolbar-container" },
         span({ className: "devtools-toolbar-group devtools-toolbar-two-rows-1" },
           this.renderClearButton(clearRequests),
           this.renderSeparator(),
           this.renderFilterBox(setRequestFilterText),
           this.renderSeparator(),
           this.renderToggleRecordingButton(recording, toggleRecording),
           this.renderSeparator(),
           this.renderPersistlogCheckbox(persistentLogsEnabled, togglePersistentLogs),
           this.renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache),
           this.renderSeparator(),
+          this.renderThrottlingSelector(),
           this.renderHarButton(),
         ),
         span({ className: "devtools-toolbar-group devtools-toolbar-two-rows-2" },
           this.renderFilterButtons(requestFilterTypes)
         )
       )
     );
   }
@@ -390,20 +418,23 @@ class Toolbar extends Component {
 module.exports = connect(
   (state) => ({
     browserCacheDisabled: state.ui.browserCacheDisabled,
     displayedRequests: getDisplayedRequests(state),
     filteredRequests: getTypeFilteredRequests(state),
     persistentLogsEnabled: state.ui.persistentLogsEnabled,
     recording: getRecordingState(state),
     requestFilterTypes: state.filters.requestFilterTypes,
+    networkThrottling: state.networkThrottling,
   }),
   (dispatch) => ({
     clearRequests: () => dispatch(Actions.clearRequests()),
     disableBrowserCache: (disabled) => dispatch(Actions.disableBrowserCache(disabled)),
     enablePersistentLogs: (enabled) => dispatch(Actions.enablePersistentLogs(enabled)),
     setRequestFilterText: (text) => dispatch(Actions.setRequestFilterText(text)),
     toggleBrowserCache: () => dispatch(Actions.toggleBrowserCache()),
     toggleRecording: () => dispatch(Actions.toggleRecording()),
     togglePersistentLogs: () => dispatch(Actions.togglePersistentLogs()),
     toggleRequestFilterType: (type) => dispatch(Actions.toggleRequestFilterType(type)),
+    onChangeNetworkThrottling: (enabled, profile) =>
+      dispatch(changeNetworkThrottling(enabled, profile)),
   }),
 )(Toolbar);
--- a/devtools/client/netmonitor/src/connector/firefox-connector.js
+++ b/devtools/client/netmonitor/src/connector/firefox-connector.js
@@ -7,16 +7,23 @@
 const Services = require("Services");
 const { ACTIVITY_TYPE, EVENTS } = require("../constants");
 const FirefoxDataProvider = require("./firefox-data-provider");
 const { getDisplayedTimingMarker } = require("../selectors/index");
 
 // To be removed once FF60 is deprecated
 loader.lazyRequireGetter(this, "TimelineFront", "devtools/shared/fronts/timeline", true);
 
+// Network throttling
+loader.lazyRequireGetter(this, "throttlingProfiles", "devtools/client/shared/components/throttling/profiles");
+loader.lazyRequireGetter(this, "EmulationFront", "devtools/shared/fronts/emulation", true);
+
+/**
+ * Connector to Firefox backend.
+ */
 class FirefoxConnector {
   constructor() {
     // Public methods
     this.connect = this.connect.bind(this);
     this.disconnect = this.disconnect.bind(this);
     this.willNavigate = this.willNavigate.bind(this);
     this.navigate = this.navigate.bind(this);
     this.displayCachedEvents = this.displayCachedEvents.bind(this);
@@ -24,16 +31,17 @@ class FirefoxConnector {
     this.onDocEvent = this.onDocEvent.bind(this);
     this.sendHTTPRequest = this.sendHTTPRequest.bind(this);
     this.setPreferences = this.setPreferences.bind(this);
     this.triggerActivity = this.triggerActivity.bind(this);
     this.getTabTarget = this.getTabTarget.bind(this);
     this.viewSourceInDebugger = this.viewSourceInDebugger.bind(this);
     this.requestData = this.requestData.bind(this);
     this.getTimingMarker = this.getTimingMarker.bind(this);
+    this.updateNetworkThrottling = this.updateNetworkThrottling.bind(this);
 
     // Internals
     this.getLongString = this.getLongString.bind(this);
     this.getNetworkRequest = this.getNetworkRequest.bind(this);
   }
 
   /**
    * Connect to the backend.
@@ -54,41 +62,51 @@ class FirefoxConnector {
     this.webConsoleClient = this.tabTarget.activeConsole;
 
     this.dataProvider = new FirefoxDataProvider({
       webConsoleClient: this.webConsoleClient,
       actions: this.actions,
       owner: this.owner,
     });
 
+    // Register all listeners
     await this.addListeners();
 
     // Listener for `will-navigate` event is (un)registered outside
     // of the `addListeners` and `removeListeners` methods since
     // these are used to pause/resume the connector.
     // Paused network panel should be automatically resumed when page
     // reload, so `will-navigate` listener needs to be there all the time.
     if (this.tabTarget) {
       this.tabTarget.on("will-navigate", this.willNavigate);
       this.tabTarget.on("navigate", this.navigate);
+
+      // Initialize Emulation front for network throttling.
+      const { tab } = await this.tabTarget.client.getTab();
+      this.emulationFront = EmulationFront(this.tabTarget.client, tab);
     }
 
     // Displaying cache events is only intended for the UI panel.
     if (this.actions) {
       this.displayCachedEvents();
     }
   }
 
   async disconnect() {
     if (this.actions) {
       this.actions.batchReset();
     }
 
     await this.removeListeners();
 
+    if (this.emulationFront) {
+      this.emulationFront.destroy();
+      this.emulationFront = null;
+    }
+
     if (this.tabTarget) {
       this.tabTarget.off("will-navigate", this.willNavigate);
       this.tabTarget.off("navigate", this.navigate);
       this.tabTarget = null;
     }
 
     this.webConsoleClient = null;
     this.dataProvider = null;
@@ -407,16 +425,30 @@ class FirefoxConnector {
     if (!this.getState) {
       return -1;
     }
 
     let state = this.getState();
     return getDisplayedTimingMarker(state, name);
   }
 
+  async updateNetworkThrottling(enabled, profile) {
+    if (!enabled) {
+      await this.emulationFront.clearNetworkThrottling();
+    } else {
+      const data = throttlingProfiles.find(({ id }) => id == profile);
+      const { download, upload, latency } = data;
+      await this.emulationFront.setNetworkThrottling({
+        downloadThroughput: download,
+        uploadThroughput: upload,
+        latency,
+      });
+    }
+  }
+
   /**
    * Fire events for the owner object.
    */
   emit(type, data) {
     if (this.owner) {
       this.owner.emit(type, data);
     }
   }
--- a/devtools/client/netmonitor/src/connector/index.js
+++ b/devtools/client/netmonitor/src/connector/index.js
@@ -22,16 +22,17 @@ class Connector {
     this.getNetworkRequest = this.getNetworkRequest.bind(this);
     this.getTabTarget = this.getTabTarget.bind(this);
     this.sendHTTPRequest = this.sendHTTPRequest.bind(this);
     this.setPreferences = this.setPreferences.bind(this);
     this.triggerActivity = this.triggerActivity.bind(this);
     this.viewSourceInDebugger = this.viewSourceInDebugger.bind(this);
     this.requestData = this.requestData.bind(this);
     this.getTimingMarker = this.getTimingMarker.bind(this);
+    this.updateNetworkThrottling = this.updateNetworkThrottling.bind(this);
   }
 
   // Connect/Disconnect API
 
   async connect(connection, actions, getState) {
     if (!connection || !connection.tab) {
       return;
     }
@@ -109,11 +110,15 @@ class Connector {
 
   requestData() {
     return this.connector.requestData(...arguments);
   }
 
   getTimingMarker() {
     return this.connector.getTimingMarker(...arguments);
   }
+
+  updateNetworkThrottling() {
+    return this.connector.updateNetworkThrottling(...arguments);
+  }
 }
 
 module.exports.Connector = Connector;
--- a/devtools/client/netmonitor/src/create-store.js
+++ b/devtools/client/netmonitor/src/create-store.js
@@ -7,16 +7,17 @@
 const Services = require("Services");
 const { applyMiddleware, createStore } = require("devtools/client/shared/vendor/redux");
 
 // Middleware
 const batching = require("./middleware/batching");
 const prefs = require("./middleware/prefs");
 const thunk = require("./middleware/thunk");
 const recording = require("./middleware/recording");
+const throttling = require("./middleware/throttling");
 
 // Reducers
 const rootReducer = require("./reducers/index");
 const { FilterTypes, Filters } = require("./reducers/filters");
 const { Requests } = require("./reducers/requests");
 const { Sort } = require("./reducers/sort");
 const { TimingMarkers } = require("./reducers/timing-markers");
 const { UI, Columns } = require("./reducers/ui");
@@ -38,17 +39,18 @@ function configureStore(connector) {
     }),
   };
 
   // Prepare middleware.
   let middleware = applyMiddleware(
     thunk,
     prefs,
     batching,
-    recording(connector)
+    recording(connector),
+    throttling(connector),
   );
 
   return createStore(rootReducer, initialState, middleware);
 }
 
 // Helpers
 
 /**
--- a/devtools/client/netmonitor/src/middleware/moz.build
+++ b/devtools/client/netmonitor/src/middleware/moz.build
@@ -1,10 +1,11 @@
 # 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(
     'batching.js',
     'prefs.js',
     'recording.js',
+    'throttling.js',
     'thunk.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/middleware/throttling.js
@@ -0,0 +1,26 @@
+/* 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("devtools/client/shared/components/throttling/actions");
+
+/**
+ * Network throttling middleware is responsible for
+ * updating/syncing currently connected backend
+ * according to user actions.
+ */
+function throttlingMiddleware(connector) {
+  return store => next => action => {
+    const res = next(action);
+    if (action.type === CHANGE_NETWORK_THROTTLING) {
+      connector.updateNetworkThrottling(action.enabled, action.profile);
+    }
+    return res;
+  };
+}
+
+module.exports = throttlingMiddleware;
--- a/devtools/client/netmonitor/src/reducers/index.js
+++ b/devtools/client/netmonitor/src/reducers/index.js
@@ -6,18 +6,20 @@
 
 const { combineReducers } = require("devtools/client/shared/vendor/redux");
 const batchingReducer = require("./batching");
 const { requestsReducer } = require("./requests");
 const { sortReducer } = require("./sort");
 const { filters } = require("./filters");
 const { timingMarkers } = require("./timing-markers");
 const { ui } = require("./ui");
+const networkThrottling = require("devtools/client/shared/components/throttling/reducer");
 
 module.exports = batchingReducer(
   combineReducers({
     requests: requestsReducer,
     sort: sortReducer,
     filters,
     timingMarkers,
     ui,
+    networkThrottling,
   })
 );
--- a/devtools/client/responsive.html/actions/index.js
+++ b/devtools/client/responsive.html/actions/index.js
@@ -5,16 +5,20 @@
 "use strict";
 
 // This file lists all of the actions available in responsive design.  This
 // central list of constants makes it easy to see all possible action names at
 // a glance.  Please add a comment with each new action type.
 
 const { createEnum } = require("devtools/client/shared/enum");
 
+const {
+  CHANGE_NETWORK_THROTTLING,
+} = require("devtools/client/shared/components/throttling/actions");
+
 createEnum([
 
   // Add a new device.
   "ADD_DEVICE",
 
   // Add a new device type.
   "ADD_DEVICE_TYPE",
 
@@ -29,17 +33,17 @@ createEnum([
   "CHANGE_LOCATION",
 
   // The pixel ratio of the display has changed. This may be triggered by the user
   // when changing the monitor resolution, or when the window is dragged to a different
   // display with a different pixel ratio.
   "CHANGE_DISPLAY_PIXEL_RATIO",
 
   // Change the network throttling profile.
-  "CHANGE_NETWORK_THROTTLING",
+  CHANGE_NETWORK_THROTTLING,
 
   // The pixel ratio of the viewport has changed. This may be triggered by the user
   // when changing the device displayed in the viewport, or when a pixel ratio is
   // selected from the device pixel ratio dropdown.
   "CHANGE_PIXEL_RATIO",
 
   // Change one of the reload conditions.
   "CHANGE_RELOAD_CONDITION",
--- a/devtools/client/responsive.html/actions/moz.build
+++ b/devtools/client/responsive.html/actions/moz.build
@@ -4,14 +4,13 @@
 # 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',
     'display-pixel-ratio.js',
     'index.js',
     'location.js',
-    'network-throttling.js',
     'reload-conditions.js',
     'screenshot.js',
     'touch-simulation.js',
     'viewports.js',
 )
--- a/devtools/client/responsive.html/app.js
+++ b/devtools/client/responsive.html/app.js
@@ -13,17 +13,17 @@ const { connect } = require("devtools/cl
 
 const {
   addCustomDevice,
   removeCustomDevice,
   updateDeviceDisplayed,
   updateDeviceModal,
   updatePreferredDevices,
 } = require("./actions/devices");
-const { changeNetworkThrottling } = require("./actions/network-throttling");
+const { changeNetworkThrottling } = require("devtools/client/shared/components/throttling/actions");
 const { changeReloadCondition } = require("./actions/reload-conditions");
 const { takeScreenshot } = require("./actions/screenshot");
 const { changeTouchSimulation } = require("./actions/touch-simulation");
 const {
   changeDevice,
   changePixelRatio,
   removeDeviceAssociation,
   resizeViewport,
@@ -87,17 +87,17 @@ class App extends Component {
     }, "*");
     this.props.dispatch(changeDevice(id, device.name, deviceType));
     this.props.dispatch(changeTouchSimulation(device.touch));
     this.props.dispatch(changePixelRatio(id, device.pixelRatio));
   }
 
   onChangeNetworkThrottling(enabled, profile) {
     window.postMessage({
-      type: "change-network-throtting",
+      type: "change-network-throttling",
       enabled,
       profile,
     }, "*");
     this.props.dispatch(changeNetworkThrottling(enabled, profile));
   }
 
   onChangePixelRatio(pixelRatio) {
     window.postMessage({
--- a/devtools/client/responsive.html/components/GlobalToolbar.js
+++ b/devtools/client/responsive.html/components/GlobalToolbar.js
@@ -6,17 +6,17 @@
 
 const { PureComponent, createFactory } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 
 const { getStr } = require("../utils/l10n");
 const Types = require("../types");
 const DevicePixelRatioSelector = createFactory(require("./DevicePixelRatioSelector"));
-const NetworkThrottlingSelector = createFactory(require("./NetworkThrottlingSelector"));
+const NetworkThrottlingSelector = createFactory(require("devtools/client/shared/components/throttling/NetworkThrottlingSelector"));
 const ReloadConditions = createFactory(require("./ReloadConditions"));
 
 class GlobalToolbar extends PureComponent {
   static get propTypes() {
     return {
       devices: PropTypes.shape(Types.devices).isRequired,
       displayPixelRatio: Types.pixelRatio.value.isRequired,
       networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
--- a/devtools/client/responsive.html/components/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -6,17 +6,16 @@
 
 DevToolsModules(
     'Browser.js',
     'DeviceAdder.js',
     'DeviceModal.js',
     'DevicePixelRatioSelector.js',
     'DeviceSelector.js',
     'GlobalToolbar.js',
-    'NetworkThrottlingSelector.js',
     'ReloadConditions.js',
     'ResizableViewport.js',
     'ToggleMenu.js',
     'Viewport.js',
     'ViewportDimension.js',
     'Viewports.js',
     'ViewportToolbar.js',
 )
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -8,17 +8,17 @@ const { Ci } = require("chrome");
 const promise = require("promise");
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/event-emitter");
 
 const TOOL_URL = "chrome://devtools/content/responsive.html/index.xhtml";
 
 loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/debugger-client", true);
 loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
-loader.lazyRequireGetter(this, "throttlingProfiles", "devtools/client/shared/network-throttling-profiles");
+loader.lazyRequireGetter(this, "throttlingProfiles", "devtools/client/shared/components/throttling/profiles");
 loader.lazyRequireGetter(this, "swapToInnerBrowser", "devtools/client/responsive.html/browser/swap", true);
 loader.lazyRequireGetter(this, "startup", "devtools/client/responsive.html/utils/window", true);
 loader.lazyRequireGetter(this, "message", "devtools/client/responsive.html/utils/message");
 loader.lazyRequireGetter(this, "showNotification", "devtools/client/responsive.html/utils/notification", true);
 loader.lazyRequireGetter(this, "l10n", "devtools/client/responsive.html/utils/l10n");
 loader.lazyRequireGetter(this, "EmulationFront", "devtools/shared/fronts/emulation", true);
 loader.lazyRequireGetter(this, "PriorityLevels", "devtools/client/shared/components/NotificationBox", true);
 loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
@@ -511,17 +511,17 @@ ResponsiveUI.prototype = {
     if (event.origin !== "chrome://devtools") {
       return;
     }
 
     switch (event.data.type) {
       case "change-device":
         this.onChangeDevice(event);
         break;
-      case "change-network-throtting":
+      case "change-network-throttling":
         this.onChangeNetworkThrottling(event);
         break;
       case "change-pixel-ratio":
         this.onChangePixelRatio(event);
         break;
       case "change-touch-simulation":
         this.onChangeTouchSimulation(event);
         break;
--- a/devtools/client/responsive.html/reducers.js
+++ b/devtools/client/responsive.html/reducers.js
@@ -2,13 +2,13 @@
  * 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.displayPixelRatio = require("./reducers/display-pixel-ratio");
 exports.location = require("./reducers/location");
-exports.networkThrottling = require("./reducers/network-throttling");
+exports.networkThrottling = require("devtools/client/shared/components/throttling/reducer");
 exports.reloadConditions = require("./reducers/reload-conditions");
 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
@@ -3,14 +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',
     'display-pixel-ratio.js',
     'location.js',
-    'network-throttling.js',
     'reload-conditions.js',
     'screenshot.js',
     'touch-simulation.js',
     'viewports.js',
 )
--- a/devtools/client/responsive.html/test/browser/browser_network_throttling.js
+++ b/devtools/client/responsive.html/test/browser/browser_network_throttling.js
@@ -1,14 +1,14 @@
 /* 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");
+const throttlingProfiles = require("devtools/client/shared/components/throttling/profiles");
 
 // Tests changing network throttling
 const TEST_URL = "data:text/html;charset=utf-8,Network throttling test";
 
 addRDMTask(TEST_URL, async function({ ui, manager }) {
   let { store } = ui.toolWindow;
 
   // Wait until the viewport has been added
--- a/devtools/client/responsive.html/test/unit/test_change_network_throttling.js
+++ b/devtools/client/responsive.html/test/unit/test_change_network_throttling.js
@@ -2,17 +2,17 @@
    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");
+} = require("devtools/client/shared/components/throttling/actions");
 
 add_task(async function() {
   let store = Store();
   const { getState, dispatch } = store;
 
   ok(!getState().networkThrottling.enabled,
     "Network throttling is disabled by default.");
   equal(getState().networkThrottling.profile, "",
--- a/devtools/client/shared/components/moz.build
+++ b/devtools/client/shared/components/moz.build
@@ -3,17 +3,18 @@
 # 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/.
 
 DIRS += [
     'reps',
     'splitter',
     'tabs',
-    'tree'
+    'throttling',
+    'tree',
 ]
 
 DevToolsModules(
     'AutoCompletePopup.js',
     'Frame.js',
     'HSplitBox.js',
     'NotificationBox.css',
     'NotificationBox.js',
rename from devtools/client/responsive.html/components/NetworkThrottlingSelector.js
rename to devtools/client/shared/components/throttling/NetworkThrottlingSelector.js
--- a/devtools/client/responsive.html/components/NetworkThrottlingSelector.js
+++ b/devtools/client/shared/components/throttling/NetworkThrottlingSelector.js
@@ -3,71 +3,87 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { PureComponent } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 
-const Types = require("../types");
-const { getStr } = require("../utils/l10n");
-const throttlingProfiles = require("devtools/client/shared/network-throttling-profiles");
+const Types = require("./types");
+const throttlingProfiles = require("./profiles");
 
+// Localization
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper(
+  "devtools/client/locales/network-throttling.properties");
+
+/**
+ * This component represents selector button that can be used
+ * to throttle network bandwidth.
+ */
 class NetworkThrottlingSelector extends PureComponent {
   static get propTypes() {
     return {
+      className: PropTypes.string,
       networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
       onChangeNetworkThrottling: PropTypes.func.isRequired,
     };
   }
 
+  static get defaultProps() {
+    return {
+      className: "",
+    };
+  }
+
   constructor(props) {
     super(props);
     this.onSelectChange = this.onSelectChange.bind(this);
   }
 
   onSelectChange({ target }) {
     let {
       onChangeNetworkThrottling,
     } = this.props;
 
-    if (target.value == getStr("responsive.noThrottling")) {
+    if (target.value == L10N.getStr("responsive.noThrottling")) {
       onChangeNetworkThrottling(false, "");
       return;
     }
 
     for (let profile of throttlingProfiles) {
       if (profile.id === target.value) {
         onChangeNetworkThrottling(true, profile.id);
         return;
       }
     }
   }
 
   render() {
     let {
+      className,
       networkThrottling,
     } = this.props;
 
-    let selectClass = "toolbar-dropdown";
+    let selectClass = className + " toolbar-dropdown";
     let selectedProfile;
     if (networkThrottling.enabled) {
       selectClass += " selected";
       selectedProfile = networkThrottling.profile;
     } else {
-      selectedProfile = getStr("responsive.noThrottling");
+      selectedProfile = L10N.getStr("responsive.noThrottling");
     }
 
     let listContent = [
       dom.option(
         {
           key: "disabled",
         },
-        getStr("responsive.noThrottling")
+        L10N.getStr("responsive.noThrottling")
       ),
       dom.option(
         {
           key: "divider",
           className: "divider",
           disabled: true,
         }
       ),
rename from devtools/client/responsive.html/actions/network-throttling.js
rename to devtools/client/shared/components/throttling/actions.js
--- a/devtools/client/responsive.html/actions/network-throttling.js
+++ b/devtools/client/shared/components/throttling/actions.js
@@ -1,21 +1,22 @@
 /* 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");
+const actionTypes = {
+  CHANGE_NETWORK_THROTTLING: "CHANGE_NETWORK_THROTTLING",
+};
+
+function changeNetworkThrottling(enabled, profile) {
+  return {
+    type: actionTypes.CHANGE_NETWORK_THROTTLING,
+    enabled,
+    profile,
+  };
+}
 
 module.exports = {
-
-  changeNetworkThrottling(enabled, profile) {
-    return {
-      type: CHANGE_NETWORK_THROTTLING,
-      enabled,
-      profile,
-    };
-  },
-
+  ...actionTypes,
+  changeNetworkThrottling,
 };
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/throttling/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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(
+    'actions.js',
+    'NetworkThrottlingSelector.js',
+    'profiles.js',
+    'reducer.js',
+    'types.js',
+)
rename from devtools/client/shared/network-throttling-profiles.js
rename to devtools/client/shared/components/throttling/profiles.js
rename from devtools/client/responsive.html/reducers/network-throttling.js
rename to devtools/client/shared/components/throttling/reducer.js
--- a/devtools/client/responsive.html/reducers/network-throttling.js
+++ b/devtools/client/shared/components/throttling/reducer.js
@@ -1,33 +1,29 @@
 /* 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");
+} = require("./actions");
 
-const INITIAL_NETWORK_THROTTLING = {
+const INITIAL_STATE = {
   enabled: false,
   profile: "",
 };
 
-let reducers = {
-
-  [CHANGE_NETWORK_THROTTLING](throttling, { enabled, profile }) {
-    return {
-      enabled,
-      profile,
-    };
-  },
+function throttlingReducer(state = INITIAL_STATE, action) {
+  switch (action.type) {
+    case CHANGE_NETWORK_THROTTLING: {
+      return {
+        enabled: action.enabled,
+        profile: action.profile
+      };
+    }
+    default:
+      return state;
+  }
+}
 
-};
-
-module.exports = function(throttling = INITIAL_NETWORK_THROTTLING, action) {
-  let reducer = reducers[action.type];
-  if (!reducer) {
-    return throttling;
-  }
-  return reducer(throttling, action);
-};
+module.exports = throttlingReducer;
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/throttling/types.js
@@ -0,0 +1,17 @@
+/* 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 PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+/**
+ * Network throttling state.
+ */
+exports.networkThrottling = {
+  // Whether or not network throttling is enabled
+  enabled: PropTypes.bool,
+  // Name of the selected throttling profile
+  profile: PropTypes.string,
+};
--- a/devtools/client/shared/moz.build
+++ b/devtools/client/shared/moz.build
@@ -33,17 +33,16 @@ DevToolsModules(
     'enum.js',
     'file-saver.js',
     'getjson.js',
     'inplace-editor.js',
     'key-shortcuts.js',
     'keycodes.js',
     'link.js',
     'natural-sort.js',
-    'network-throttling-profiles.js',
     'node-attribute-parser.js',
     'options-view.js',
     'output-parser.js',
     'poller.js',
     'prefs.js',
     'react-utils.js',
     'scroll.js',
     'source-utils.js',