Merge autoland to m-c, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Wed, 30 Nov 2016 19:17:54 -0800
changeset 324849 a183d5c5a8f79b09be159329992ef958a55f23ed
parent 324803 27449c736a04eec8c9033d70697747fce3a3c13d (current diff)
parent 324848 928ed9aab0b9a46c728758fd1f7959fa975f9350 (diff)
child 324855 cd4cdcc9ad6c45dad8b8d8c0d40e459db2bca8a1
push id31018
push userphilringnalda@gmail.com
push dateThu, 01 Dec 2016 03:18:13 +0000
treeherdermozilla-central@a183d5c5a8f7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge autoland to m-c, a=merge MozReview-Commit-ID: ZxgsQWdyOP
devtools/server/actors/utils/webconsole-worker-utils.js
layout/base/nsPresShell.cpp
layout/base/nsPresShell.h
layout/generic/nsBRFrame.cpp
layout/generic/nsViewportFrame.cpp
layout/generic/nsViewportFrame.h
mobile/android/config/mozconfigs/android-api-15-gradle/nightly
mobile/android/config/mozconfigs/android-api-15-gradle/nightly-artifact
mobile/android/config/mozconfigs/android-api-15/nightly-artifact
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -5396,17 +5396,18 @@
       <method name="observe">
         <parameter name="aSubject"/>
         <parameter name="aTopic"/>
         <parameter name="aData"/>
         <body><![CDATA[
           switch (aTopic) {
             case "nsPref:changed":
               // This is the only pref observed.
-              let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled");
+              let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled")
+                                        && !PrivateBrowsingUtils.isWindowPrivate(window);
 
               const newTab = document.getElementById("new-tab-button");
               const newTab2 = document.getAnonymousElementByAttribute(this, "anonid", "tabs-newtab-button")
 
               if (containersEnabled) {
                 for (let parent of [newTab, newTab2]) {
                   if (!parent)
                     continue;
--- a/browser/components/contextualidentity/test/browser/browser_newtabButton.js
+++ b/browser/components/contextualidentity/test/browser/browser_newtabButton.js
@@ -28,8 +28,23 @@ add_task(function* test() {
     EventUtils.synthesizeMouseAtCenter(contextIdItem, {});
 
     let tab = yield waitForTabPromise;
 
     is(tab.getAttribute('usercontextid'), i, `New tab has UCI equal ${i}`);
     yield BrowserTestUtils.removeTab(tab);
   }
 });
+
+
+add_task(function* test_private_mode() {
+  let privateWindow = yield BrowserTestUtils.openNewBrowserWindow({private: true});
+  let privateDocument = privateWindow.document;
+  let {tabContainer} = privateWindow.gBrowser;
+  let newTab = privateDocument.getAnonymousElementByAttribute(tabContainer, "anonid", "tabs-newtab-button");
+  let newTab2 = privateDocument.getElementById("new-tab-button");
+  // Check to ensure we are talking about the right button
+  ok(!!newTab.clientWidth, "new tab button should not be hidden");
+  ok(!newTab2.clientWidth, "overflow new tab button should be hidden");
+  let popup = privateDocument.getAnonymousElementByAttribute(newTab, "anonid", "newtab-popup");
+  ok(!popup, "new tab should not have a popup");
+  yield BrowserTestUtils.closeWindow(privateWindow);
+});
--- a/browser/locales/Makefile.in
+++ b/browser/locales/Makefile.in
@@ -82,18 +82,16 @@ DIST_SUBDIRS = $(DIST_SUBDIR)
 include $(topsrcdir)/config/rules.mk
 
 include $(topsrcdir)/toolkit/locales/l10n.mk
 
 $(list-json): $(call mkdir_deps,$(SEARCHPLUGINS_PATH)) $(if $(IS_LANGUAGE_REPACK),FORCE)
 	$(call py_action,generate_searchjson,$(srcdir)/search/list.json $(AB_CD) $(list-json))
 searchplugins:: $(list-json)
 
-$(STAGEDIST): $(DIST)/branding
-
 $(DIST)/branding:
 	$(NSINSTALL) -D $@
 
 DEFINES += -DBOOKMARKS_INCLUDE_DIR=$(dir $(call MERGE_FILE,profile/bookmarks.inc))
 
 libs-%:
 	$(NSINSTALL) -D $(DIST)/install
 	@$(MAKE) -C ../../toolkit/locales libs-$* XPI_ROOT_APPID='$(XPI_ROOT_APPID)'
@@ -116,35 +114,25 @@ repackage-win32-installer: $(call ESCAPE
 	  AB_CD=$(AB_CD) \
 	  MOZ_PKG_FORMAT=SFX7Z \
 	  ZIP_IN='$(WIN32_INSTALLER_IN)' \
 	  ZIP_OUT='$(WIN32_INSTALLER_OUT)' \
 	  SFX_HEADER='$(PWD)/../installer/windows/l10ngen/7zSD.sfx \
 	              $(topsrcdir)/browser/installer/windows/app.tag'
 
 ifeq (WINNT,$(OS_ARCH))
-repackage-win32-installer-%: $(STAGEDIST)
+repackage-win32-installer-%: unpack
 	@$(MAKE) repackage-win32-installer AB_CD=$* WIN32_INSTALLER_IN='$(WIN32_INSTALLER_IN)'
 
 repackage-zip-%: repackage-win32-installer-%
 else
 repackage-win32-installer-%: ;
 endif
 
 
-clobber-zip:
-	$(RM) $(STAGEDIST)/chrome/$(AB_CD).jar \
-	  $(STAGEDIST)/chrome/$(AB_CD).manifest \
-	  $(STAGEDIST)/$(PREF_DIR)/firefox-l10n.js
-	$(RM) -rf  $(STAGEDIST)/dictionaries \
-	  $(STAGEDIST)/hyphenation \
-	  $(STAGEDIST)/defaults/profile \
-	  $(STAGEDIST)/chrome/$(AB_CD)
-
-
 langpack: langpack-$(AB_CD)
 
 # This is a generic target that will make a langpack, repack ZIP (+tarball)
 # builds, and repack an installer if applicable. It is called from the
 # tinderbox scripts. Alter it with caution.
 
 installers-%: clobber-% langpack-% repackage-win32-installer-% repackage-zip-%
 	@echo 'repackaging done'
--- a/build/compare-mozconfig/compare-mozconfigs-wrapper.py
+++ b/build/compare-mozconfig/compare-mozconfigs-wrapper.py
@@ -47,22 +47,20 @@ class TestCompareMozconfigs(unittest.Tes
             release_mozconfig_path = path.join(browser_dir, 'config/mozconfigs', platform, 'release')
             nightly_mozconfig_path = path.join(browser_dir, 'config/mozconfigs', platform, 'nightly')
 
             log.info("Comparing beta against nightly mozconfigs")
             ret_code = subprocess.call([python_exe, script_path, '--whitelist',
                                         whitelist_path, '--no-download',
                                         platform + ',' + beta_mozconfig_path +
                                         ',' + nightly_mozconfig_path])
+            self.assertEqual(0, ret_code)
 
-            if ret_code > 0:
-                return ret_code
 
             log.info("Comparing release against nightly mozconfigs")
             ret_code = subprocess.call([python_exe, script_path, '--whitelist',
                                         whitelist_path, '--no-download',
                                         platform + ',' + release_mozconfig_path +
                                         ',' + nightly_mozconfig_path])
-
             self.assertEqual(0, ret_code)
 
 if __name__ == '__main__':
     mozunit.main()
--- a/build/unix/mozconfig.asan
+++ b/build/unix/mozconfig.asan
@@ -18,10 +18,11 @@ ac_add_options --enable-address-sanitize
 
 # Mandatory options required for ASan builds (both on Linux and Mac)
 export MOZ_DEBUG_SYMBOLS=1
 ac_add_options --enable-debug-symbols
 ac_add_options --disable-install-strip
 ac_add_options --disable-jemalloc
 ac_add_options --disable-crashreporter
 ac_add_options --disable-elf-hack
+ac_add_options --disable-profiling
 
 . "$topsrcdir/build/unix/mozconfig.stdcxx"
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -612,29 +612,42 @@ Inspector.prototype = {
    */
   addSidebarTab: function (id, title, panel, selected) {
     this.sidebar.addTab(id, title, panel, selected);
   },
 
   /**
    * Method to check whether the document is a HTML document and
    * pickColorFromPage method is available or not.
-   * Returns a boolean value
+   *
+   * @return {Boolean} true if the eyedropper highlighter is supported by the current
+   *         document.
    */
   supportsEyeDropper: Task.async(function* () {
-    let isInHTMLDocument = this.selection.nodeFront &&
-                            this.selection.nodeFront.isInHTMLDocument;
-    let pickColorAvailable = false;
     try {
-      pickColorAvailable = yield this.target
-                                      .actorHasMethod("inspector", "pickColorFromPage");
+      let hasSupportsHighlighters =
+        yield this.target.actorHasMethod("inspector", "supportsHighlighters");
+      let hasPickColorFromPage =
+        yield this.target.actorHasMethod("inspector", "pickColorFromPage");
+
+      let supportsHighlighters;
+      if (hasSupportsHighlighters) {
+        supportsHighlighters = yield this.inspector.supportsHighlighters();
+      } else {
+        // If the actor does not provide the supportsHighlighter method, fallback to
+        // check if the selected node's document is a HTML document.
+        let { nodeFront } = this.selection;
+        supportsHighlighters = nodeFront && nodeFront.isInHTMLDocument;
+      }
+
+      return supportsHighlighters && hasPickColorFromPage;
     } catch (e) {
       console.error(e);
+      return false;
     }
-    return isInHTMLDocument && pickColorAvailable;
   }),
 
   setupToolbar: Task.async(function* () {
     this.teardownToolbar();
 
     // Setup the sidebar toggle button.
     let SidebarToggle = this.React.createFactory(this.browserRequire(
       "devtools/client/shared/components/sidebar-toggle"));
@@ -651,16 +664,23 @@ Inspector.prototype = {
 
     // Setup the add-node button.
     this.addNode = this.addNode.bind(this);
     this.addNodeButton = this.panelDoc.getElementById("inspector-element-add-button");
     this.addNodeButton.addEventListener("click", this.addNode);
 
     // Setup the eye-dropper icon if we're in an HTML document and we have actor support.
     let canShowEyeDropper = yield this.supportsEyeDropper();
+
+    // Bail out if the inspector was destroyed in the meantime and panelDoc is no longer
+    // available.
+    if (!this.panelDoc) {
+      return;
+    }
+
     if (canShowEyeDropper) {
       this.onEyeDropperDone = this.onEyeDropperDone.bind(this);
       this.onEyeDropperButtonClicked = this.onEyeDropperButtonClicked.bind(this);
       this.eyeDropperButton = this.panelDoc
                                     .getElementById("inspector-eyedropper-toggle");
       this.eyeDropperButton.disabled = false;
       this.eyeDropperButton.title = INSPECTOR_L10N.getStr("inspector.eyedropper.label");
       this.eyeDropperButton.addEventListener("click", this.onEyeDropperButtonClicked);
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -33,16 +33,17 @@ support-files =
   doc_inspector_search.html
   doc_inspector_search-reserved.html
   doc_inspector_search-suggestions.html
   doc_inspector_search-svg.html
   doc_inspector_select-last-selected-01.html
   doc_inspector_select-last-selected-02.html
   doc_inspector_svg.svg
   head.js
+  img_browser_inspector_highlighter-eyedropper-image.png
   shared-head.js
   !/devtools/client/commandline/test/helpers.js
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/shared/test/test-actor.js
   !/devtools/client/shared/test/test-actor-registry.js
 
 [browser_inspector_addNode_01.js]
 [browser_inspector_addNode_02.js]
@@ -73,16 +74,17 @@ skip-if = os == "mac" # Full keyboard na
 [browser_inspector_highlighter-cssgrid_01.js]
 [browser_inspector_highlighter-csstransform_01.js]
 [browser_inspector_highlighter-csstransform_02.js]
 [browser_inspector_highlighter-embed.js]
 [browser_inspector_highlighter-eyedropper-clipboard.js]
 subsuite = clipboard
 [browser_inspector_highlighter-eyedropper-csp.js]
 [browser_inspector_highlighter-eyedropper-events.js]
+[browser_inspector_highlighter-eyedropper-image.js]
 [browser_inspector_highlighter-eyedropper-label.js]
 [browser_inspector_highlighter-eyedropper-show-hide.js]
 [browser_inspector_highlighter-eyedropper-xul.js]
 [browser_inspector_highlighter-geometry_01.js]
 [browser_inspector_highlighter-geometry_02.js]
 [browser_inspector_highlighter-geometry_03.js]
 [browser_inspector_highlighter-geometry_04.js]
 [browser_inspector_highlighter-geometry_05.js]
--- a/devtools/client/inspector/test/browser_inspector_gcli-inspect-command.js
+++ b/devtools/client/inspector/test/browser_inspector_gcli-inspect-command.js
@@ -1,14 +1,21 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 /* eslint key-spacing: 0 */
+/* import-globals-from ../../commandline/test/helpers.js */
+
 "use strict";
 
+// Import the GCLI test helper
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/commandline/test/helpers.js",
+  this);
+
 // Testing that the gcli 'inspect' command works as it should.
 
 const TEST_URI = URL_ROOT + "doc_inspector_gcli-inspect-command.html";
 
 add_task(function* () {
   return helpers.addTabWithToolbar(TEST_URI, Task.async(function* (options) {
     let {inspector} = yield openInspector();
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-image.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the eyedropper icon in the toolbar is enabled when viewing an image.
+
+const TEST_URL = URL_ROOT + "img_browser_inspector_highlighter-eyedropper-image.png";
+
+add_task(function* () {
+  let {inspector} = yield openInspectorForURL(TEST_URL);
+  info("Check the inspector toolbar when viewing an image");
+  let button = inspector.panelDoc.querySelector("#inspector-eyedropper-toggle");
+  ok(!button.disabled, "The button is enabled in the toolbar");
+});
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -1,34 +1,28 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* 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/. */
 /* eslint no-unused-vars: [2, {"vars": "local"}] */
 /* import-globals-from ../../framework/test/shared-head.js */
-/* import-globals-from ../../commandline/test/helpers.js */
 /* import-globals-from ../../shared/test/test-actor-registry.js */
 /* import-globals-from ../../inspector/test/shared-head.js */
 "use strict";
 
 // Load the shared-head file first.
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
   this);
 
 // Services.prefs.setBoolPref("devtools.debugger.log", true);
 // SimpleTest.registerCleanupFunction(() => {
 //   Services.prefs.clearUserPref("devtools.debugger.log");
 // });
 
-// Import the GCLI test helper
-Services.scriptloader.loadSubScript(
-  "chrome://mochitests/content/browser/devtools/client/commandline/test/helpers.js",
-  this);
-
 // Import helpers registering the test-actor in remote targets
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js",
   this);
 
 // Import helpers for the inspector that are also shared with others
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a0aef4f93bb102939de9d78d92a8ee64e9ba3df3
GIT binary patch
literal 522
zc%17D@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU_9^X;uum9_x7%%V1t2(^T80l
ze@8?71V4$frx^V{+`0L!?w>u<bw6|68ATcx9`Xqs;M~A?h~Wl<+k*8CMhq+p42&EB
zehmyP8sG1;4H?b}?hkpt?;m{b&AeeT1J{RBf<SKp#W@GwWfXGi?GLDxPMDn-+Xswc
P22WQ%mvv4FO$-bGi|mr%
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -9,18 +9,17 @@ devtools.jar:
     content/shared/widgets/widgets.css (shared/widgets/widgets.css)
     content/shared/widgets/VariablesView.xul (shared/widgets/VariablesView.xul)
     content/projecteditor/chrome/content/projecteditor.xul (projecteditor/chrome/content/projecteditor.xul)
     content/projecteditor/lib/helpers/readdir.js (projecteditor/lib/helpers/readdir.js)
     content/projecteditor/chrome/content/projecteditor-loader.xul (projecteditor/chrome/content/projecteditor-loader.xul)
     content/projecteditor/chrome/content/projecteditor-test.xul (projecteditor/chrome/content/projecteditor-test.xul)
     content/projecteditor/chrome/content/projecteditor-loader.js (projecteditor/chrome/content/projecteditor-loader.js)
     content/netmonitor/netmonitor.xul (netmonitor/netmonitor.xul)
-    content/netmonitor/netmonitor-controller.js (netmonitor/netmonitor-controller.js)
-    content/netmonitor/netmonitor-view.js (netmonitor/netmonitor-view.js)
+    content/netmonitor/netmonitor.js (netmonitor/netmonitor.js)
     content/webconsole/webconsole.xul (webconsole/webconsole.xul)
 *   content/scratchpad/scratchpad.xul (scratchpad/scratchpad.xul)
     content/scratchpad/scratchpad.js (scratchpad/scratchpad.js)
     content/shared/splitview.css (shared/splitview.css)
     content/shared/theme-switching.js (shared/theme-switching.js)
     content/shared/frame-script-utils.js (shared/frame-script-utils.js)
     content/styleeditor/styleeditor.xul (styleeditor/styleeditor.xul)
     content/storage/storage.xul (storage/storage.xul)
--- a/devtools/client/locales/en-US/storage.properties
+++ b/devtools/client/locales/en-US/storage.properties
@@ -48,18 +48,20 @@ table.headers.localStorage.name=Key
 table.headers.localStorage.value=Value
 
 table.headers.sessionStorage.name=Key
 table.headers.sessionStorage.value=Value
 
 table.headers.Cache.url=URL
 table.headers.Cache.status=Status
 
+table.headers.indexedDB.uniqueKey=Unique key
 table.headers.indexedDB.name=Key
 table.headers.indexedDB.db=Database Name
+table.headers.indexedDB.storage=Storage
 table.headers.indexedDB.objectStore=Object Store Name
 table.headers.indexedDB.value=Value
 table.headers.indexedDB.origin=Origin
 table.headers.indexedDB.version=Version
 table.headers.indexedDB.objectStores=Object Stores
 table.headers.indexedDB.keyPath2=Key Path
 table.headers.indexedDB.autoIncrement=Auto Increment
 table.headers.indexedDB.indexes=Indexes
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/actions/batching.js
@@ -0,0 +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 {
+  BATCH_ACTIONS,
+  BATCH_ENABLE,
+  BATCH_RESET,
+} = require("../constants");
+
+/**
+ * Process multiple actions at once as part of one dispatch, and produce only one
+ * state update at the end. This action is not processed by any reducer, but by a
+ * special store enhancer.
+ */
+function batchActions(actions) {
+  return {
+    type: BATCH_ACTIONS,
+    actions
+  };
+}
+
+function batchEnable(enabled) {
+  return {
+    type: BATCH_ENABLE,
+    enabled
+  };
+}
+
+function batchReset() {
+  return {
+    type: BATCH_RESET,
+  };
+}
+
+module.exports = {
+  batchActions,
+  batchEnable,
+  batchReset,
+};
--- a/devtools/client/netmonitor/actions/filters.js
+++ b/devtools/client/netmonitor/actions/filters.js
@@ -37,22 +37,22 @@ function enableFilterTypeOnly(filter) {
     type: ENABLE_FILTER_TYPE_ONLY,
     filter,
   };
 }
 
 /**
  * Set filter text.
  *
- * @param {string} url - A filter text is going to be set
+ * @param {string} text - A filter text is going to be set
  */
-function setFilterText(url) {
+function setFilterText(text) {
   return {
     type: SET_FILTER_TEXT,
-    url,
+    text,
   };
 }
 
 module.exports = {
   toggleFilterType,
   enableFilterTypeOnly,
   setFilterText,
 };
--- a/devtools/client/netmonitor/actions/index.js
+++ b/devtools/client/netmonitor/actions/index.js
@@ -1,11 +1,23 @@
 /* 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 batching = require("./batching");
 const filters = require("./filters");
 const requests = require("./requests");
+const selection = require("./selection");
+const sort = require("./sort");
+const timingMarkers = require("./timing-markers");
 const ui = require("./ui");
 
-module.exports = Object.assign({}, filters, requests, ui);
+Object.assign(exports,
+  batching,
+  filters,
+  requests,
+  selection,
+  sort,
+  timingMarkers,
+  ui
+);
--- a/devtools/client/netmonitor/actions/moz.build
+++ b/devtools/client/netmonitor/actions/moz.build
@@ -1,10 +1,14 @@
 # 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',
     'filters.js',
     'index.js',
     'requests.js',
+    'selection.js',
+    'sort.js',
+    'timing-markers.js',
     'ui.js',
 )
--- a/devtools/client/netmonitor/actions/requests.js
+++ b/devtools/client/netmonitor/actions/requests.js
@@ -1,25 +1,65 @@
 /* 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 {
-  UPDATE_REQUESTS,
+  ADD_REQUEST,
+  UPDATE_REQUEST,
+  CLONE_SELECTED_REQUEST,
+  REMOVE_SELECTED_CUSTOM_REQUEST,
+  CLEAR_REQUESTS,
 } = require("../constants");
 
+function addRequest(id, data, batch) {
+  return {
+    type: ADD_REQUEST,
+    id,
+    data,
+    meta: { batch },
+  };
+}
+
+function updateRequest(id, data, batch) {
+  return {
+    type: UPDATE_REQUEST,
+    id,
+    data,
+    meta: { batch },
+  };
+}
+
 /**
- * Update request items
- *
- * @param {array} requests - visible request items
+ * Clone the currently selected request, set the "isCustom" attribute.
+ * Used by the "Edit and Resend" feature.
  */
-function updateRequests(items) {
+function cloneSelectedRequest() {
   return {
-    type: UPDATE_REQUESTS,
-    items,
+    type: CLONE_SELECTED_REQUEST
+  };
+}
+
+/**
+ * Remove a request from the list. Supports removing only cloned requests with a
+ * "isCustom" attribute. Other requests never need to be removed.
+ */
+function removeSelectedCustomRequest() {
+  return {
+    type: REMOVE_SELECTED_CUSTOM_REQUEST
+  };
+}
+
+function clearRequests() {
+  return {
+    type: CLEAR_REQUESTS
   };
 }
 
 module.exports = {
-  updateRequests,
+  addRequest,
+  updateRequest,
+  cloneSelectedRequest,
+  removeSelectedCustomRequest,
+  clearRequests,
 };
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/actions/selection.js
@@ -0,0 +1,67 @@
+/* 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 { getDisplayedRequests } = require("../selectors/index");
+const { SELECT_REQUEST, PRESELECT_REQUEST } = require("../constants");
+
+/**
+ * When a new request with a given id is added in future, select it immediately.
+ * Used by the "Edit and Resend" feature, where we know in advance the ID of the
+ * request, at a time when it wasn't sent yet.
+ */
+function preselectRequest(id) {
+  return {
+    type: PRESELECT_REQUEST,
+    id
+  };
+}
+
+/**
+ * Select request with a given id.
+ */
+function selectRequest(id) {
+  return {
+    type: SELECT_REQUEST,
+    id
+  };
+}
+
+const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
+
+/**
+ * Move the selection up to down according to the "delta" parameter. Possible values:
+ * - Number: positive or negative, move up or down by specified distance
+ * - "PAGE_UP" | "PAGE_DOWN" (String): page up or page down
+ * - +Infinity | -Infinity: move to the start or end of the list
+ */
+function selectDelta(delta) {
+  return (dispatch, getState) => {
+    const state = getState();
+    const requests = getDisplayedRequests(state);
+
+    if (requests.isEmpty()) {
+      return;
+    }
+
+    const selIndex = requests.findIndex(r => r.id === state.requests.selectedId);
+
+    if (delta === "PAGE_DOWN") {
+      delta = Math.ceil(requests.size / PAGE_SIZE_ITEM_COUNT_RATIO);
+    } else if (delta === "PAGE_UP") {
+      delta = -Math.ceil(requests.size / PAGE_SIZE_ITEM_COUNT_RATIO);
+    }
+
+    const newIndex = Math.min(Math.max(0, selIndex + delta), requests.size - 1);
+    const newItem = requests.get(newIndex);
+    dispatch(selectRequest(newItem.id));
+  };
+}
+
+module.exports = {
+  preselectRequest,
+  selectRequest,
+  selectDelta,
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/actions/sort.js
@@ -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/. */
+
+"use strict";
+
+const { SORT_BY } = require("../constants");
+
+function sortBy(sortType) {
+  return {
+    type: SORT_BY,
+    sortType
+  };
+}
+
+module.exports = {
+  sortBy
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/actions/timing-markers.js
@@ -0,0 +1,19 @@
+/* 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 { ADD_TIMING_MARKER, CLEAR_TIMING_MARKERS } = require("../constants");
+
+exports.addTimingMarker = (marker) => {
+  return {
+    type: ADD_TIMING_MARKER,
+    marker
+  };
+};
+
+exports.clearTimingMarkers = () => {
+  return {
+    type: CLEAR_TIMING_MARKERS
+  };
+};
--- a/devtools/client/netmonitor/actions/ui.js
+++ b/devtools/client/netmonitor/actions/ui.js
@@ -1,17 +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 {
   OPEN_SIDEBAR,
-  TOGGLE_SIDEBAR,
+  WATERFALL_RESIZE,
 } = require("../constants");
 
 /**
  * Change sidebar open state.
  *
  * @param {boolean} open - open state
  */
 function openSidebar(open) {
@@ -20,17 +20,26 @@ function openSidebar(open) {
     open,
   };
 }
 
 /**
  * Toggle sidebar open state.
  */
 function toggleSidebar() {
+  return (dispatch, getState) => dispatch(openSidebar(!getState().ui.sidebarOpen));
+}
+
+/**
+ * Waterfall width has changed (likely on window resize). Update the UI.
+ */
+function resizeWaterfall(width) {
   return {
-    type: TOGGLE_SIDEBAR,
+    type: WATERFALL_RESIZE,
+    width
   };
 }
 
 module.exports = {
   openSidebar,
   toggleSidebar,
+  resizeWaterfall,
 };
--- a/devtools/client/netmonitor/components/clear-button.js
+++ b/devtools/client/netmonitor/components/clear-button.js
@@ -1,29 +1,32 @@
 /* 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/. */
 
-/* globals NetMonitorView */
-
 "use strict";
 
 const { DOM } = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { L10N } = require("../l10n");
+const Actions = require("../actions/index");
 
 const { button } = DOM;
 
 /*
  * Clear button component
  * A type of tool button is responsible for cleaning network requests.
  */
-function ClearButton() {
+function ClearButton({ onClick }) {
   return button({
     id: "requests-menu-clear-button",
     className: "devtools-button devtools-clear-icon",
     title: L10N.getStr("netmonitor.toolbar.clear"),
-    onClick: () => {
-      NetMonitorView.RequestsMenu.clear();
-    },
+    onClick,
   });
 }
 
-module.exports = ClearButton;
+module.exports = connect(
+  undefined,
+  dispatch => ({
+    onClick: () => dispatch(Actions.clearRequests())
+  })
+)(ClearButton);
--- a/devtools/client/netmonitor/components/moz.build
+++ b/devtools/client/netmonitor/components/moz.build
@@ -1,12 +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/.
 
 DevToolsModules(
     'clear-button.js',
     'filter-buttons.js',
+    'request-list-content.js',
+    'request-list-empty.js',
+    'request-list-header.js',
+    'request-list-item.js',
+    'request-list-tooltip.js',
+    'request-list.js',
     'search-box.js',
     'summary-button.js',
     'toggle-button.js',
     'toolbar.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-content.js
@@ -0,0 +1,255 @@
+/* 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/. */
+/* globals NetMonitorView */
+
+"use strict";
+
+const { Task } = require("devtools/shared/task");
+const { createClass, createFactory, DOM } = require("devtools/client/shared/vendor/react");
+const { div } = DOM;
+const Actions = require("../actions/index");
+const RequestListItem = createFactory(require("./request-list-item"));
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { setTooltipImageContent,
+        setTooltipStackTraceContent } = require("./request-list-tooltip");
+const { getDisplayedRequests,
+        getWaterfallScale } = require("../selectors/index");
+const { KeyCodes } = require("devtools/client/shared/keycodes");
+
+// tooltip show/hide delay in ms
+const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
+
+/**
+ * Renders the actual contents of the request list.
+ */
+const RequestListContent = createClass({
+  displayName: "RequestListContent",
+
+  componentDidMount() {
+    // Set the CSS variables for waterfall scaling
+    this.setScalingStyles();
+
+    // Install event handler for displaying a tooltip
+    this.props.tooltip.startTogglingOnHover(this.refs.contentEl, this.onHover, {
+      toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
+      interactive: true
+    });
+
+    // Install event handler to hide the tooltip on scroll
+    this.refs.contentEl.addEventListener("scroll", this.onScroll, true);
+  },
+
+  componentWillUpdate() {
+    // Check if the list is scrolled to bottom, before UI update
+    this.shouldScrollBottom = this.isScrolledToBottom();
+  },
+
+  componentDidUpdate(prevProps) {
+    // Update the CSS variables for waterfall scaling after props change
+    this.setScalingStyles();
+
+    // Keep the list scrolled to bottom if a new row was added
+    if (this.shouldScrollBottom) {
+      let node = this.refs.contentEl;
+      node.scrollTop = node.scrollHeight;
+    }
+  },
+
+  componentWillUnmount() {
+    this.refs.contentEl.removeEventListener("scroll", this.onScroll, true);
+
+    // Uninstall the tooltip event handler
+    this.props.tooltip.stopTogglingOnHover();
+  },
+
+  /**
+   * Set the CSS variables for waterfall scaling. If React supported setting CSS
+   * variables as part of the "style" property of a DOM element, we would use that.
+   *
+   * However, React doesn't support this, so we need to use a hack and update the
+   * DOM element directly: https://github.com/facebook/react/issues/6411
+   */
+  setScalingStyles(prevProps) {
+    const { scale } = this.props;
+    if (scale == this.currentScale) {
+      return;
+    }
+
+    this.currentScale = scale;
+
+    const { style } = this.refs.contentEl;
+    style.removeProperty("--timings-scale");
+    style.removeProperty("--timings-rev-scale");
+    style.setProperty("--timings-scale", scale);
+    style.setProperty("--timings-rev-scale", 1 / scale);
+  },
+
+  isScrolledToBottom() {
+    const { contentEl } = this.refs;
+    const lastChildEl = contentEl.lastElementChild;
+
+    if (!lastChildEl) {
+      return false;
+    }
+
+    let lastChildRect = lastChildEl.getBoundingClientRect();
+    let contentRect = contentEl.getBoundingClientRect();
+
+    return (lastChildRect.height + lastChildRect.top) <= contentRect.bottom;
+  },
+
+  /**
+   * The predicate used when deciding whether a popup should be shown
+   * over a request item or not.
+   *
+   * @param nsIDOMNode target
+   *        The element node currently being hovered.
+   * @param object tooltip
+   *        The current tooltip instance.
+   * @return {Promise}
+   */
+  onHover: Task.async(function* (target, tooltip) {
+    let itemEl = target.closest(".request-list-item");
+    if (!itemEl) {
+      return false;
+    }
+    let itemId = itemEl.dataset.id;
+    if (!itemId) {
+      return false;
+    }
+    let requestItem = this.props.displayedRequests.find(r => r.id == itemId);
+    if (!requestItem) {
+      return false;
+    }
+
+    if (requestItem.responseContent && target.closest(".requests-menu-icon-and-file")) {
+      return setTooltipImageContent(tooltip, itemEl, requestItem);
+    } else if (requestItem.cause && target.closest(".requests-menu-cause-stack")) {
+      return setTooltipStackTraceContent(tooltip, requestItem);
+    }
+
+    return false;
+  }),
+
+  /**
+   * Scroll listener for the requests menu view.
+   */
+  onScroll() {
+    this.props.tooltip.hide();
+  },
+
+  /**
+   * Handler for keyboard events. For arrow up/down, page up/down, home/end,
+   * move the selection up or down.
+   */
+  onKeyDown(e) {
+    let delta;
+
+    switch (e.keyCode) {
+      case KeyCodes.DOM_VK_UP:
+      case KeyCodes.DOM_VK_LEFT:
+        delta = -1;
+        break;
+      case KeyCodes.DOM_VK_DOWN:
+      case KeyCodes.DOM_VK_RIGHT:
+        delta = +1;
+        break;
+      case KeyCodes.DOM_VK_PAGE_UP:
+        delta = "PAGE_UP";
+        break;
+      case KeyCodes.DOM_VK_PAGE_DOWN:
+        delta = "PAGE_DOWN";
+        break;
+      case KeyCodes.DOM_VK_HOME:
+        delta = -Infinity;
+        break;
+      case KeyCodes.DOM_VK_END:
+        delta = +Infinity;
+        break;
+    }
+
+    if (delta) {
+      // Prevent scrolling when pressing navigation keys.
+      e.preventDefault();
+      e.stopPropagation();
+      this.props.onSelectDelta(delta);
+    }
+  },
+
+  /**
+   * If selection has just changed (by keyboard navigation), don't keep the list
+   * scrolled to bottom, but allow scrolling up with the selection.
+   */
+  onFocusedNodeChange() {
+    this.shouldScrollBottom = false;
+  },
+
+  /**
+   * If a focused item was unmounted, transfer the focus to the container element.
+   */
+  onFocusedNodeUnmount() {
+    if (this.refs.contentEl) {
+      this.refs.contentEl.focus();
+    }
+  },
+
+  render() {
+    const { selectedRequestId,
+            displayedRequests,
+            firstRequestStartedMillis,
+            onItemMouseDown,
+            onItemContextMenu,
+            onSecurityIconClick } = this.props;
+
+    return div(
+      {
+        ref: "contentEl",
+        className: "requests-menu-contents",
+        tabIndex: 0,
+        onKeyDown: this.onKeyDown,
+      },
+      displayedRequests.map((item, index) => RequestListItem({
+        key: item.id,
+        item,
+        index,
+        isSelected: item.id === selectedRequestId,
+        firstRequestStartedMillis,
+        onMouseDown: e => onItemMouseDown(e, item.id),
+        onContextMenu: e => onItemContextMenu(e, item.id),
+        onSecurityIconClick: e => onSecurityIconClick(e, item),
+        onFocusedNodeChange: this.onFocusedNodeChange,
+        onFocusedNodeUnmount: this.onFocusedNodeUnmount,
+      }))
+    );
+  },
+});
+
+module.exports = connect(
+  state => ({
+    displayedRequests: getDisplayedRequests(state),
+    selectedRequestId: state.requests.selectedId,
+    scale: getWaterfallScale(state),
+    firstRequestStartedMillis: state.requests.firstStartedMillis,
+    tooltip: NetMonitorView.RequestsMenu.tooltip,
+  }),
+  dispatch => ({
+    onItemMouseDown: (e, item) => dispatch(Actions.selectRequest(item)),
+    onItemContextMenu: (e, item) => {
+      e.preventDefault();
+      NetMonitorView.RequestsMenu.contextMenu.open(e);
+    },
+    onSelectDelta: (delta) => dispatch(Actions.selectDelta(delta)),
+    /**
+     * A handler that opens the security tab in the details view if secure or
+     * broken security indicator is clicked.
+     */
+    onSecurityIconClick: (e, item) => {
+      const { securityState } = item;
+      if (securityState && securityState !== "insecure") {
+        // Choose the security tab.
+        NetMonitorView.NetworkDetails.widget.selectedIndex = 5;
+      }
+    },
+  })
+)(RequestListContent);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-empty.js
@@ -0,0 +1,65 @@
+/* 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/. */
+/* globals NetMonitorView */
+
+"use strict";
+
+const { createClass, PropTypes, DOM } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../l10n");
+const { div, span, button } = DOM;
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+/**
+ * UI displayed when the request list is empty. Contains instructions on reloading
+ * the page and on triggering performance analysis of the page.
+ */
+const RequestListEmptyNotice = createClass({
+  displayName: "RequestListEmptyNotice",
+
+  propTypes: {
+    onReloadClick: PropTypes.func.isRequired,
+    onPerfClick: PropTypes.func.isRequired,
+  },
+
+  render() {
+    return div(
+      {
+        id: "requests-menu-empty-notice",
+        className: "request-list-empty-notice",
+      },
+      div({ id: "notice-reload-message" },
+        span(null, L10N.getStr("netmonitor.reloadNotice1")),
+        button(
+          {
+            id: "requests-menu-reload-notice-button",
+            className: "devtools-toolbarbutton",
+            "data-standalone": true,
+            onClick: this.props.onReloadClick,
+          },
+          L10N.getStr("netmonitor.reloadNotice2")
+        ),
+        span(null, L10N.getStr("netmonitor.reloadNotice3"))
+      ),
+      div({ id: "notice-perf-message" },
+        span(null, L10N.getStr("netmonitor.perfNotice1")),
+        button({
+          id: "requests-menu-perf-notice-button",
+          title: L10N.getStr("netmonitor.perfNotice3"),
+          className: "devtools-button",
+          "data-standalone": true,
+          onClick: this.props.onPerfClick,
+        }),
+        span(null, L10N.getStr("netmonitor.perfNotice2"))
+      )
+    );
+  }
+});
+
+module.exports = connect(
+  undefined,
+  dispatch => ({
+    onPerfClick: e => NetMonitorView.toggleFrontendMode(),
+    onReloadClick: e => NetMonitorView.reloadPage(),
+  })
+)(RequestListEmptyNotice);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-header.js
@@ -0,0 +1,197 @@
+/* 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/. */
+
+/* globals document */
+
+"use strict";
+
+const { createClass, PropTypes, DOM } = require("devtools/client/shared/vendor/react");
+const { div, button } = DOM;
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { L10N } = require("../l10n");
+const { getWaterfallScale } = require("../selectors/index");
+const Actions = require("../actions/index");
+const WaterfallBackground = require("../waterfall-background");
+
+// ms
+const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5;
+// px
+const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60;
+
+const REQUEST_TIME_DECIMALS = 2;
+
+const HEADERS = [
+  { name: "status", label: "status3" },
+  { name: "method" },
+  { name: "file", boxName: "icon-and-file" },
+  { name: "domain", boxName: "security-and-domain" },
+  { name: "cause" },
+  { name: "type" },
+  { name: "transferred" },
+  { name: "size" },
+  { name: "waterfall" }
+];
+
+/**
+ * Render the request list header with sorting arrows for columns.
+ * Displays tick marks in the waterfall column header.
+ * Also draws the waterfall background canvas and updates it when needed.
+ */
+const RequestListHeader = createClass({
+  displayName: "RequestListHeader",
+
+  propTypes: {
+    sort: PropTypes.object,
+    scale: PropTypes.number,
+    waterfallWidth: PropTypes.number,
+    onHeaderClick: PropTypes.func.isRequired,
+  },
+
+  componentDidMount() {
+    this.background = new WaterfallBackground(document);
+    this.background.draw(this.props);
+  },
+
+  componentDidUpdate() {
+    this.background.draw(this.props);
+  },
+
+  componentWillUnmount() {
+    this.background.destroy();
+    this.background = null;
+  },
+
+  render() {
+    const { sort, scale, waterfallWidth, onHeaderClick } = this.props;
+
+    return div(
+      { id: "requests-menu-toolbar", className: "devtools-toolbar" },
+      div({ id: "toolbar-labels" },
+        HEADERS.map(header => {
+          const name = header.name;
+          const boxName = header.boxName || name;
+          const label = L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
+
+          let sorted, sortedTitle;
+          const active = sort.type == name ? true : undefined;
+          if (active) {
+            sorted = sort.ascending ? "ascending" : "descending";
+            sortedTitle = L10N.getStr(sort.ascending
+              ? "networkMenu.sortedAsc"
+              : "networkMenu.sortedDesc");
+          }
+
+          return div(
+            {
+              id: `requests-menu-${boxName}-header-box`,
+              key: name,
+              className: `requests-menu-header requests-menu-${boxName}`,
+              // Used to style the next column.
+              "data-active": active,
+            },
+            button(
+              {
+                id: `requests-menu-${name}-button`,
+                className: `requests-menu-header-button requests-menu-${name}`,
+                "data-sorted": sorted,
+                title: sortedTitle,
+                onClick: () => onHeaderClick(name),
+              },
+              name == "waterfall" ? WaterfallLabel(waterfallWidth, scale, label)
+                                  : div({ className: "button-text" }, label),
+              div({ className: "button-icon" })
+            )
+          );
+        })
+      )
+    );
+  }
+});
+
+/**
+ * Build the waterfall header - timing tick marks with the right spacing
+ */
+function waterfallDivisionLabels(waterfallWidth, scale) {
+  let labels = [];
+
+  // Build new millisecond tick labels...
+  let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE;
+  let scaledStep = scale * timingStep;
+
+  // Ignore any divisions that would end up being too close to each other.
+  while (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) {
+    scaledStep *= 2;
+  }
+
+  // Insert one label for each division on the current scale.
+  for (let x = 0; x < waterfallWidth; x += scaledStep) {
+    let millisecondTime = x / scale;
+
+    let normalizedTime = millisecondTime;
+    let divisionScale = "millisecond";
+
+    // If the division is greater than 1 minute.
+    if (normalizedTime > 60000) {
+      normalizedTime /= 60000;
+      divisionScale = "minute";
+    } else if (normalizedTime > 1000) {
+      // If the division is greater than 1 second.
+      normalizedTime /= 1000;
+      divisionScale = "second";
+    }
+
+    // Showing too many decimals is bad UX.
+    if (divisionScale == "millisecond") {
+      normalizedTime |= 0;
+    } else {
+      normalizedTime = L10N.numberWithDecimals(normalizedTime, REQUEST_TIME_DECIMALS);
+    }
+
+    let width = (x + scaledStep | 0) - (x | 0);
+    // Adjust the first marker for the borders
+    if (x == 0) {
+      width -= 2;
+    }
+    // Last marker doesn't need a width specified at all
+    if (x + scaledStep >= waterfallWidth) {
+      width = undefined;
+    }
+
+    labels.push(div(
+      {
+        key: labels.length,
+        className: "requests-menu-timings-division",
+        "data-division-scale": divisionScale,
+        style: { width }
+      },
+      L10N.getFormatStr("networkMenu." + divisionScale, normalizedTime)
+    ));
+  }
+
+  return labels;
+}
+
+function WaterfallLabel(waterfallWidth, scale, label) {
+  let className = "button-text requests-menu-waterfall-label-wrapper";
+
+  if (scale != null) {
+    label = waterfallDivisionLabels(waterfallWidth, scale);
+    className += " requests-menu-waterfall-visible";
+  }
+
+  return div({ className }, label);
+}
+
+module.exports = connect(
+  state => ({
+    sort: state.sort,
+    scale: getWaterfallScale(state),
+    waterfallWidth: state.ui.waterfallWidth,
+    firstRequestStartedMillis: state.requests.firstStartedMillis,
+    timingMarkers: state.timingMarkers,
+  }),
+  dispatch => ({
+    onHeaderClick: type => dispatch(Actions.sortBy(type)),
+  })
+)(RequestListHeader);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-item.js
@@ -0,0 +1,346 @@
+/* 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 { createClass, PropTypes, DOM } = require("devtools/client/shared/vendor/react");
+const { div, span, img } = DOM;
+const { L10N } = require("../l10n");
+const { getFormattedSize } = require("../utils/format-utils");
+const { getAbbreviatedMimeType } = require("../request-utils");
+
+/**
+ * Render one row in the request list.
+ */
+const RequestListItem = createClass({
+  displayName: "RequestListItem",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+    index: PropTypes.number.isRequired,
+    isSelected: PropTypes.bool.isRequired,
+    firstRequestStartedMillis: PropTypes.number.isRequired,
+    onContextMenu: PropTypes.func.isRequired,
+    onMouseDown: PropTypes.func.isRequired,
+    onSecurityIconClick: PropTypes.func.isRequired,
+  },
+
+  componentDidMount() {
+    if (this.props.isSelected) {
+      this.refs.el.focus();
+    }
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !relevantPropsEqual(this.props.item, nextProps.item)
+      || this.props.index !== nextProps.index
+      || this.props.isSelected !== nextProps.isSelected
+      || this.props.firstRequestStartedMillis !== nextProps.firstRequestStartedMillis;
+  },
+
+  componentDidUpdate(prevProps) {
+    if (!prevProps.isSelected && this.props.isSelected) {
+      this.refs.el.focus();
+      if (this.props.onFocusedNodeChange) {
+        this.props.onFocusedNodeChange();
+      }
+    }
+  },
+
+  componentWillUnmount() {
+    // If this node is being destroyed and has focus, transfer the focus manually
+    // to the parent tree component. Otherwise, the focus will get lost and keyboard
+    // navigation in the tree will stop working. This is a workaround for a XUL bug.
+    // See bugs 1259228 and 1152441 for details.
+    // DE-XUL: Remove this hack once all usages are only in HTML documents.
+    if (this.props.isSelected) {
+      this.refs.el.blur();
+      if (this.props.onFocusedNodeUnmount) {
+        this.props.onFocusedNodeUnmount();
+      }
+    }
+  },
+
+  render() {
+    const {
+      item,
+      index,
+      isSelected,
+      firstRequestStartedMillis,
+      onContextMenu,
+      onMouseDown,
+      onSecurityIconClick
+    } = this.props;
+
+    let classList = [ "request-list-item" ];
+    if (isSelected) {
+      classList.push("selected");
+    }
+    classList.push(index % 2 ? "odd" : "even");
+
+    return div(
+      {
+        ref: "el",
+        className: classList.join(" "),
+        "data-id": item.id,
+        tabIndex: 0,
+        onContextMenu,
+        onMouseDown,
+      },
+      StatusColumn(item),
+      MethodColumn(item),
+      FileColumn(item),
+      DomainColumn(item, onSecurityIconClick),
+      CauseColumn(item),
+      TypeColumn(item),
+      TransferredSizeColumn(item),
+      ContentSizeColumn(item),
+      WaterfallColumn(item, firstRequestStartedMillis)
+    );
+  }
+});
+
+/**
+ * Used by shouldComponentUpdate: compare two items, and compare only properties
+ * relevant for rendering the RequestListItem. Other properties (like request and
+ * response headers, cookies, bodies) are ignored. These are very useful for the
+ * sidebar details, but not here.
+ */
+const RELEVANT_ITEM_PROPS = [
+  "status",
+  "statusText",
+  "fromCache",
+  "fromServiceWorker",
+  "method",
+  "url",
+  "responseContentDataUri",
+  "remoteAddress",
+  "securityState",
+  "cause",
+  "mimeType",
+  "contentSize",
+  "transferredSize",
+  "startedMillis",
+  "totalTime",
+  "eventTimings",
+];
+
+function relevantPropsEqual(item1, item2) {
+  return item1 === item2 || RELEVANT_ITEM_PROPS.every(p => item1[p] === item2[p]);
+}
+
+function StatusColumn(item) {
+  const { status, statusText, fromCache, fromServiceWorker } = item;
+
+  let code, title;
+
+  if (status) {
+    if (fromCache) {
+      code = "cached";
+    } else if (fromServiceWorker) {
+      code = "service worker";
+    } else {
+      code = status;
+    }
+
+    if (statusText) {
+      title = `${status} ${statusText}`;
+      if (fromCache) {
+        title += " (cached)";
+      }
+      if (fromServiceWorker) {
+        title += " (service worker)";
+      }
+    }
+  }
+
+  return div({ className: "requests-menu-subitem requests-menu-status", title },
+    div({ className: "requests-menu-status-icon", "data-code": code }),
+    span({ className: "subitem-label requests-menu-status-code" }, status)
+  );
+}
+
+function MethodColumn(item) {
+  const { method } = item;
+  return div({ className: "requests-menu-subitem requests-menu-method-box" },
+    span({ className: "subitem-label requests-menu-method" }, method)
+  );
+}
+
+function FileColumn(item) {
+  const { urlDetails, responseContentDataUri } = item;
+
+  return div({ className: "requests-menu-subitem requests-menu-icon-and-file" },
+    img({
+      className: "requests-menu-icon",
+      src: responseContentDataUri,
+      hidden: !responseContentDataUri,
+      "data-type": responseContentDataUri ? "thumbnail" : undefined
+    }),
+    div(
+      {
+        className: "subitem-label requests-menu-file",
+        title: urlDetails.unicodeUrl
+      },
+      urlDetails.baseNameWithQuery
+    )
+  );
+}
+
+function DomainColumn(item, onSecurityIconClick) {
+  const { urlDetails, remoteAddress, securityState } = item;
+
+  let iconClassList = [ "requests-security-state-icon" ];
+  let iconTitle;
+  if (urlDetails.isLocal) {
+    iconClassList.push("security-state-local");
+    iconTitle = L10N.getStr("netmonitor.security.state.secure");
+  } else if (securityState) {
+    iconClassList.push(`security-state-${securityState}`);
+    iconTitle = L10N.getStr(`netmonitor.security.state.${securityState}`);
+  }
+
+  let title = urlDetails.host + (remoteAddress ? ` (${remoteAddress})` : "");
+
+  return div(
+    { className: "requests-menu-subitem requests-menu-security-and-domain" },
+    div({
+      className: iconClassList.join(" "),
+      title: iconTitle,
+      onClick: onSecurityIconClick,
+    }),
+    span({ className: "subitem-label requests-menu-domain", title }, urlDetails.host)
+  );
+}
+
+function CauseColumn(item) {
+  const { cause } = item;
+
+  let causeType = "";
+  let causeUri = undefined;
+  let causeHasStack = false;
+
+  if (cause) {
+    causeType = cause.type;
+    causeUri = cause.loadingDocumentUri;
+    causeHasStack = cause.stacktrace && cause.stacktrace.length > 0;
+  }
+
+  return div(
+    { className: "requests-menu-subitem requests-menu-cause", title: causeUri },
+    span({ className: "requests-menu-cause-stack", hidden: !causeHasStack }, "JS"),
+    span({ className: "subitem-label" }, causeType)
+  );
+}
+
+const CONTENT_MIME_TYPE_ABBREVIATIONS = {
+  "ecmascript": "js",
+  "javascript": "js",
+  "x-javascript": "js"
+};
+
+function TypeColumn(item) {
+  const { mimeType } = item;
+  let abbrevType;
+  if (mimeType) {
+    abbrevType = getAbbreviatedMimeType(mimeType);
+    abbrevType = CONTENT_MIME_TYPE_ABBREVIATIONS[abbrevType] || abbrevType;
+  }
+
+  return div(
+    { className: "requests-menu-subitem requests-menu-type", title: mimeType },
+    span({ className: "subitem-label" }, abbrevType)
+  );
+}
+
+function TransferredSizeColumn(item) {
+  const { transferredSize, fromCache, fromServiceWorker } = item;
+
+  let text;
+  let className = "subitem-label";
+  if (fromCache) {
+    text = L10N.getStr("networkMenu.sizeCached");
+    className += " theme-comment";
+  } else if (fromServiceWorker) {
+    text = L10N.getStr("networkMenu.sizeServiceWorker");
+    className += " theme-comment";
+  } else if (typeof transferredSize == "number") {
+    text = getFormattedSize(transferredSize);
+  } else if (transferredSize === null) {
+    text = L10N.getStr("networkMenu.sizeUnavailable");
+  }
+
+  return div(
+    { className: "requests-menu-subitem requests-menu-transferred", title: text },
+    span({ className }, text)
+  );
+}
+
+function ContentSizeColumn(item) {
+  const { contentSize } = item;
+
+  let text;
+  if (typeof contentSize == "number") {
+    text = getFormattedSize(contentSize);
+  }
+
+  return div(
+    { className: "requests-menu-subitem subitem-label requests-menu-size", title: text },
+    span({ className: "subitem-label" }, text)
+  );
+}
+
+// List of properties of the timing info we want to create boxes for
+const TIMING_KEYS = ["blocked", "dns", "connect", "send", "wait", "receive"];
+
+function timingBoxes(item) {
+  const { eventTimings, totalTime, fromCache, fromServiceWorker } = item;
+  let boxes = [];
+
+  if (fromCache || fromServiceWorker) {
+    return boxes;
+  }
+
+  if (eventTimings) {
+    // Add a set of boxes representing timing information.
+    for (let key of TIMING_KEYS) {
+      let width = eventTimings.timings[key];
+
+      // Don't render anything if it surely won't be visible.
+      // One millisecond == one unscaled pixel.
+      if (width > 0) {
+        boxes.push(div({
+          key,
+          className: "requests-menu-timings-box " + key,
+          style: { width }
+        }));
+      }
+    }
+  }
+
+  if (typeof totalTime == "number") {
+    let text = L10N.getFormatStr("networkMenu.totalMS", totalTime);
+    boxes.push(div({
+      key: "total",
+      className: "requests-menu-timings-total",
+      title: text
+    }, text));
+  }
+
+  return boxes;
+}
+
+function WaterfallColumn(item, firstRequestStartedMillis) {
+  const startedDeltaMillis = item.startedMillis - firstRequestStartedMillis;
+  const paddingInlineStart = `${startedDeltaMillis}px`;
+
+  return div({ className: "requests-menu-subitem requests-menu-waterfall" },
+    div(
+      { className: "requests-menu-timings", style: { paddingInlineStart } },
+      timingBoxes(item)
+    )
+  );
+}
+
+module.exports = RequestListItem;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-tooltip.js
@@ -0,0 +1,107 @@
+/* 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/. */
+
+/* globals gNetwork, NetMonitorController */
+
+"use strict";
+
+const { Task } = require("devtools/shared/task");
+const { formDataURI } = require("../request-utils");
+const { WEBCONSOLE_L10N } = require("../l10n");
+const { setImageTooltip,
+        getImageDimensions } = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
+
+// px
+const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
+// px
+const REQUESTS_TOOLTIP_STACK_TRACE_WIDTH = 600;
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const setTooltipImageContent = Task.async(function* (tooltip, itemEl, requestItem) {
+  let { mimeType, text, encoding } = requestItem.responseContent.content;
+
+  if (!mimeType || !mimeType.includes("image/")) {
+    return false;
+  }
+
+  let string = yield gNetwork.getString(text);
+  let src = formDataURI(mimeType, encoding, string);
+  let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
+  let { naturalWidth, naturalHeight } = yield getImageDimensions(tooltip.doc, src);
+  let options = { maxDim, naturalWidth, naturalHeight };
+  setImageTooltip(tooltip, tooltip.doc, src, options);
+
+  return itemEl.querySelector(".requests-menu-icon");
+});
+
+const setTooltipStackTraceContent = Task.async(function* (tooltip, requestItem) {
+  let {stacktrace} = requestItem.cause;
+
+  if (!stacktrace || stacktrace.length == 0) {
+    return false;
+  }
+
+  let doc = tooltip.doc;
+  let el = doc.createElementNS(HTML_NS, "div");
+  el.className = "stack-trace-tooltip devtools-monospace";
+
+  for (let f of stacktrace) {
+    let { functionName, filename, lineNumber, columnNumber, asyncCause } = f;
+
+    if (asyncCause) {
+      // if there is asyncCause, append a "divider" row into the trace
+      let asyncFrameEl = doc.createElementNS(HTML_NS, "div");
+      asyncFrameEl.className = "stack-frame stack-frame-async";
+      asyncFrameEl.textContent =
+        WEBCONSOLE_L10N.getFormatStr("stacktrace.asyncStack", asyncCause);
+      el.appendChild(asyncFrameEl);
+    }
+
+    // Parse a source name in format "url -> url"
+    let sourceUrl = filename.split(" -> ").pop();
+
+    let frameEl = doc.createElementNS(HTML_NS, "div");
+    frameEl.className = "stack-frame stack-frame-call";
+
+    let funcEl = doc.createElementNS(HTML_NS, "span");
+    funcEl.className = "stack-frame-function-name";
+    funcEl.textContent =
+      functionName || WEBCONSOLE_L10N.getStr("stacktrace.anonymousFunction");
+    frameEl.appendChild(funcEl);
+
+    let sourceEl = doc.createElementNS(HTML_NS, "span");
+    sourceEl.className = "stack-frame-source-name";
+    frameEl.appendChild(sourceEl);
+
+    let sourceInnerEl = doc.createElementNS(HTML_NS, "span");
+    sourceInnerEl.className = "stack-frame-source-name-inner";
+    sourceEl.appendChild(sourceInnerEl);
+
+    sourceInnerEl.textContent = sourceUrl;
+    sourceInnerEl.title = sourceUrl;
+
+    let lineEl = doc.createElementNS(HTML_NS, "span");
+    lineEl.className = "stack-frame-line";
+    lineEl.textContent = `:${lineNumber}:${columnNumber}`;
+    sourceInnerEl.appendChild(lineEl);
+
+    frameEl.addEventListener("click", () => {
+      // hide the tooltip immediately, not after delay
+      tooltip.hide();
+      NetMonitorController.viewSourceInDebugger(filename, lineNumber);
+    }, false);
+
+    el.appendChild(frameEl);
+  }
+
+  tooltip.setContent(el, {width: REQUESTS_TOOLTIP_STACK_TRACE_WIDTH});
+
+  return true;
+});
+
+module.exports = {
+  setTooltipImageContent,
+  setTooltipStackTraceContent,
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list.js
@@ -0,0 +1,34 @@
+/* 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 { createFactory, PropTypes, DOM } = require("devtools/client/shared/vendor/react");
+const { div } = DOM;
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const RequestListHeader = createFactory(require("./request-list-header"));
+const RequestListEmptyNotice = createFactory(require("./request-list-empty"));
+const RequestListContent = createFactory(require("./request-list-content"));
+
+/**
+ * Renders the request list - header, empty text, the actual content with rows
+ */
+const RequestList = function ({ isEmpty }) {
+  return div({ className: "request-list-container" },
+    RequestListHeader(),
+    isEmpty ? RequestListEmptyNotice() : RequestListContent()
+  );
+};
+
+RequestList.displayName = "RequestList";
+
+RequestList.propTypes = {
+  isEmpty: PropTypes.bool.isRequired,
+};
+
+module.exports = connect(
+  state => ({
+    isEmpty: state.requests.requests.isEmpty()
+  })
+)(RequestList);
--- a/devtools/client/netmonitor/components/summary-button.js
+++ b/devtools/client/netmonitor/components/summary-button.js
@@ -9,32 +9,30 @@
 const {
   CONTENT_SIZE_DECIMALS,
   REQUEST_TIME_DECIMALS,
 } = require("../constants");
 const { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { PluralForm } = require("devtools/shared/plural-form");
 const { L10N } = require("../l10n");
-const { getSummary } = require("../selectors/index");
+const { getDisplayedRequestsSummary } = require("../selectors/index");
 
 const { button, span } = DOM;
 
 function SummaryButton({
   summary,
   triggerSummary,
 }) {
-  let { count, totalBytes, totalMillis } = summary;
+  let { count, bytes, millis } = summary;
   const text = (count === 0) ? L10N.getStr("networkMenu.empty") :
     PluralForm.get(count, L10N.getStr("networkMenu.summary"))
     .replace("#1", count)
-    .replace("#2", L10N.numberWithDecimals(totalBytes / 1024,
-      CONTENT_SIZE_DECIMALS))
-    .replace("#3", L10N.numberWithDecimals(totalMillis / 1000,
-      REQUEST_TIME_DECIMALS));
+    .replace("#2", L10N.numberWithDecimals(bytes / 1024, CONTENT_SIZE_DECIMALS))
+    .replace("#3", L10N.numberWithDecimals(millis / 1000, REQUEST_TIME_DECIMALS));
 
   return button({
     id: "requests-menu-network-summary-button",
     className: "devtools-button",
     title: count ? text : L10N.getStr("netmonitor.toolbar.perf"),
     onClick: triggerSummary,
   },
   span({ className: "summary-info-icon" }),
@@ -42,16 +40,16 @@ function SummaryButton({
 }
 
 SummaryButton.propTypes = {
   summary: PropTypes.object.isRequired,
 };
 
 module.exports = connect(
   (state) => ({
-    summary: getSummary(state),
+    summary: getDisplayedRequestsSummary(state),
   }),
   (dispatch) => ({
     triggerSummary: () => {
       NetMonitorView.toggleFrontendMode();
     },
   })
 )(SummaryButton);
--- a/devtools/client/netmonitor/components/toggle-button.js
+++ b/devtools/client/netmonitor/components/toggle-button.js
@@ -1,65 +1,51 @@
 /* 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/. */
 
-/* globals NetMonitorView */
-
 "use strict";
 
 const { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { L10N } = require("../l10n");
 const Actions = require("../actions/index");
+const { isSidebarToggleButtonDisabled } = require("../selectors/index");
 
 const { button } = DOM;
 
 function ToggleButton({
   disabled,
   open,
-  triggerSidebar,
+  onToggle,
 }) {
   let className = ["devtools-button"];
   if (!open) {
     className.push("pane-collapsed");
   }
 
   const title = open ? L10N.getStr("collapseDetailsPane") :
                        L10N.getStr("expandDetailsPane");
 
   return button({
     id: "details-pane-toggle",
     className: className.join(" "),
     title,
     disabled,
     tabIndex: "0",
-    onMouseDown: triggerSidebar,
+    onMouseDown: onToggle,
   });
 }
 
 ToggleButton.propTypes = {
   disabled: PropTypes.bool.isRequired,
-  triggerSidebar: PropTypes.func.isRequired,
+  onToggle: PropTypes.func.isRequired,
 };
 
 module.exports = connect(
   (state) => ({
-    disabled: state.requests.items.length === 0,
-    open: state.ui.sidebar.open,
+    disabled: isSidebarToggleButtonDisabled(state),
+    open: state.ui.sidebarOpen,
   }),
   (dispatch) => ({
-    triggerSidebar: () => {
-      dispatch(Actions.toggleSidebar());
-
-      let requestsMenu = NetMonitorView.RequestsMenu;
-      let selectedIndex = requestsMenu.selectedIndex;
-
-      // Make sure there's a selection if the button is pressed, to avoid
-      // showing an empty network details pane.
-      if (selectedIndex == -1 && requestsMenu.itemCount) {
-        requestsMenu.selectedIndex = 0;
-      } else {
-        requestsMenu.selectedIndex = -1;
-      }
-    },
+    onToggle: () => dispatch(Actions.toggleSidebar())
   })
 )(ToggleButton);
--- a/devtools/client/netmonitor/constants.js
+++ b/devtools/client/netmonitor/constants.js
@@ -6,17 +6,45 @@
 
 const general = {
   FREETEXT_FILTER_SEARCH_DELAY: 200,
   CONTENT_SIZE_DECIMALS: 2,
   REQUEST_TIME_DECIMALS: 2,
 };
 
 const actionTypes = {
+  BATCH_ACTIONS: "BATCH_ACTIONS",
+  BATCH_ENABLE: "BATCH_ENABLE",
+  ADD_TIMING_MARKER: "ADD_TIMING_MARKER",
+  CLEAR_TIMING_MARKERS: "CLEAR_TIMING_MARKERS",
+  ADD_REQUEST: "ADD_REQUEST",
+  UPDATE_REQUEST: "UPDATE_REQUEST",
+  CLEAR_REQUESTS: "CLEAR_REQUESTS",
+  CLONE_SELECTED_REQUEST: "CLONE_SELECTED_REQUEST",
+  REMOVE_SELECTED_CUSTOM_REQUEST: "REMOVE_SELECTED_CUSTOM_REQUEST",
+  SELECT_REQUEST: "SELECT_REQUEST",
+  PRESELECT_REQUEST: "PRESELECT_REQUEST",
+  SORT_BY: "SORT_BY",
   TOGGLE_FILTER_TYPE: "TOGGLE_FILTER_TYPE",
   ENABLE_FILTER_TYPE_ONLY: "ENABLE_FILTER_TYPE_ONLY",
   SET_FILTER_TEXT: "SET_FILTER_TEXT",
   OPEN_SIDEBAR: "OPEN_SIDEBAR",
-  TOGGLE_SIDEBAR: "TOGGLE_SIDEBAR",
-  UPDATE_REQUESTS: "UPDATE_REQUESTS",
+  WATERFALL_RESIZE: "WATERFALL_RESIZE",
 };
 
-module.exports = Object.assign({}, general, actionTypes);
+// Descriptions for what this frontend is currently doing.
+const ACTIVITY_TYPE = {
+  // Standing by and handling requests normally.
+  NONE: 0,
+
+  // Forcing the target to reload with cache enabled or disabled.
+  RELOAD: {
+    WITH_CACHE_ENABLED: 1,
+    WITH_CACHE_DISABLED: 2,
+    WITH_CACHE_DEFAULT: 3
+  },
+
+  // Enabling or disabling the cache without triggering a reload.
+  ENABLE_CACHE: 3,
+  DISABLE_CACHE: 4
+};
+
+module.exports = Object.assign({ ACTIVITY_TYPE }, general, actionTypes);
--- a/devtools/client/netmonitor/custom-request-view.js
+++ b/devtools/client/netmonitor/custom-request-view.js
@@ -2,22 +2,21 @@
  * 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/. */
 
 /* globals window, dumpn, gNetwork, $, EVENTS, NetMonitorView */
 
 "use strict";
 
 const { Task } = require("devtools/shared/task");
-const {
-  writeHeaderText,
-  getKeyWithEvent,
-  getUrlQuery,
-  parseQueryString,
-} = require("./request-utils");
+const { writeHeaderText,
+        getKeyWithEvent,
+        getUrlQuery,
+        parseQueryString } = require("./request-utils");
+const Actions = require("./actions/index");
 
 /**
  * Functions handling the custom request view.
  */
 function CustomRequestView() {
   dumpn("CustomRequestView was instantiated");
 }
 
@@ -71,47 +70,51 @@ CustomRequestView.prototype = {
   /**
    * Handle user input in the custom request form.
    *
    * @param object field
    *        the field that the user updated.
    */
   onUpdate: function (field) {
     let selectedItem = NetMonitorView.RequestsMenu.selectedItem;
+    let store = NetMonitorView.RequestsMenu.store;
     let value;
 
     switch (field) {
       case "method":
         value = $("#custom-method-value").value.trim();
-        selectedItem.attachment.method = value;
+        store.dispatch(Actions.updateRequest(selectedItem.id, { method: value }));
         break;
       case "url":
         value = $("#custom-url-value").value;
         this.updateCustomQuery(value);
-        selectedItem.attachment.url = value;
+        store.dispatch(Actions.updateRequest(selectedItem.id, { url: value }));
         break;
       case "query":
         let query = $("#custom-query-value").value;
         this.updateCustomUrl(query);
-        field = "url";
         value = $("#custom-url-value").value;
-        selectedItem.attachment.url = value;
+        store.dispatch(Actions.updateRequest(selectedItem.id, { url: value }));
         break;
       case "body":
         value = $("#custom-postdata-value").value;
-        selectedItem.attachment.requestPostData = { postData: { text: value } };
+        store.dispatch(Actions.updateRequest(selectedItem.id, {
+          requestPostData: {
+            postData: { text: value }
+          }
+        }));
         break;
       case "headers":
         let headersText = $("#custom-headers-value").value;
         value = parseHeadersText(headersText);
-        selectedItem.attachment.requestHeaders = { headers: value };
+        store.dispatch(Actions.updateRequest(selectedItem.id, {
+          requestHeaders: { headers: value }
+        }));
         break;
     }
-
-    NetMonitorView.RequestsMenu.updateMenuView(selectedItem, field, value);
   },
 
   /**
    * Update the query string field based on the url.
    *
    * @param object url
    *        The URL to extract query string from.
    */
@@ -156,17 +159,17 @@ CustomRequestView.prototype = {
 function parseHeadersText(text) {
   return parseRequestText(text, "\\S+?", ":");
 }
 
 /**
  * Parse readable text list of a query string.
  *
  * @param string text
- *        Text of query string represetation
+ *        Text of query string representation
  * @return array
  *         Array of query params {name, value}
  */
 function parseQueryText(text) {
   return parseRequestText(text, ".+?", "=");
 }
 
 /**
--- a/devtools/client/netmonitor/details-view.js
+++ b/devtools/client/netmonitor/details-view.js
@@ -1,20 +1,20 @@
 /* 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/. */
 
-/* import-globals-from ./netmonitor-controller.js */
 /* eslint-disable mozilla/reject-some-requires */
-/* globals dumpn, $, NetMonitorView, gNetwork */
+/* globals window, dumpn, $, NetMonitorView, gNetwork */
 
 "use strict";
 
 const promise = require("promise");
 const EventEmitter = require("devtools/shared/event-emitter");
+const Editor = require("devtools/client/sourceeditor/editor");
 const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
 const { Task } = require("devtools/shared/task");
 const { ToolSidebar } = require("devtools/client/framework/sidebar");
 const { VariablesView } = require("resource://devtools/client/shared/widgets/VariablesView.jsm");
 const { VariablesViewController } = require("resource://devtools/client/shared/widgets/VariablesViewController.jsm");
 const { EVENTS } = require("./events");
 const { L10N } = require("./l10n");
 const { Filters } = require("./filter-predicates");
@@ -262,20 +262,16 @@ DetailsView.prototype = {
         if (viewState.dirty[tab]) {
           // The request information was updated while the task was running.
           viewState.dirty[tab] = false;
           view.populate(viewState.latestData);
         } else {
           // Tab is selected but not dirty. We're done here.
           populated[tab] = true;
           window.emit(EVENTS.TAB_UPDATED);
-
-          if (NetMonitorController.isConnected()) {
-            NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible();
-          }
         }
       } else if (viewState.dirty[tab]) {
         // Tab is dirty but no longer selected. Don't refresh it now, it'll be
         // done if the tab is shown again.
         viewState.dirty[tab] = false;
       }
     }, e => console.error(e));
   },
@@ -323,17 +319,17 @@ DetailsView.prototype = {
       let code;
       if (data.fromCache) {
         code = "cached";
       } else if (data.fromServiceWorker) {
         code = "service worker";
       } else {
         code = data.status;
       }
-      $("#headers-summary-status-circle").setAttribute("code", code);
+      $("#headers-summary-status-circle").setAttribute("data-code", code);
       $("#headers-summary-status-value").setAttribute("value",
         data.status + " " + data.statusText);
       $("#headers-summary-status").removeAttribute("hidden");
     } else {
       $("#headers-summary-status").setAttribute("hidden", "true");
     }
 
     if (data.httpVersion) {
--- a/devtools/client/netmonitor/har/har-builder.js
+++ b/devtools/client/netmonitor/har/har-builder.js
@@ -58,19 +58,17 @@ HarBuilder.prototype = {
    */
   build: function () {
     this.promises = [];
 
     // Build basic structure for data.
     let log = this.buildLog();
 
     // Build entries.
-    let items = this._options.items;
-    for (let i = 0; i < items.length; i++) {
-      let file = items[i].attachment;
+    for (let file of this._options.items) {
       log.entries.push(this.buildEntry(log, file));
     }
 
     // Some data needs to be fetched from the backend during the
     // build process, so wait till all is done.
     let { resolve, promise } = defer();
     all(this.promises).then(results => resolve({ log: log }));
 
--- a/devtools/client/netmonitor/har/har-collector.js
+++ b/devtools/client/netmonitor/har/har-collector.js
@@ -196,19 +196,17 @@ HarCollector.prototype = {
       method: method,
       url: url,
       isXHR: isXHR
     };
 
     this.files.set(actor, file);
 
     // Mimic the Net panel data structure
-    this.items.push({
-      attachment: file
-    });
+    this.items.push(file);
   },
 
   onNetworkEventUpdate: function (type, packet) {
     let actor = packet.from;
 
     // Skip events from unknown actors (not in the list).
     // It can happen when there are zombie requests received after
     // the target is closed or multiple tabs are attached through
--- a/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js
+++ b/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js
@@ -11,33 +11,33 @@ add_task(function* () {
 });
 
 function* throttleUploadTest(actuallyThrottle) {
   let { tab, monitor } = yield initNetMonitor(
     HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
 
   info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")");
 
-  let { NetMonitorView } = monitor.panelWin;
+  let { NetMonitorController, NetMonitorView } = monitor.panelWin;
   let { RequestsMenu } = NetMonitorView;
 
   const size = 4096;
   const uploadSize = actuallyThrottle ? size / 3 : 0;
 
   const request = {
     "NetworkMonitor.throttleData": {
       latencyMean: 0,
       latencyMax: 0,
       downloadBPSMean: 200000,
       downloadBPSMax: 200000,
       uploadBPSMean: uploadSize,
       uploadBPSMax: uploadSize,
     },
   };
-  let client = monitor._controller.webConsoleClient;
+  let client = NetMonitorController.webConsoleClient;
 
   info("sending throttle request");
   let deferred = promise.defer();
   client.setPreferences(request, response => {
     deferred.resolve(response);
   });
   yield deferred.promise;
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/middleware/batching.js
@@ -0,0 +1,132 @@
+/* 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 { BATCH_ACTIONS, BATCH_ENABLE, BATCH_RESET } = require("../constants");
+
+// ms
+const REQUESTS_REFRESH_RATE = 50;
+
+/**
+ * Middleware that watches for actions with a "batch = true" value in their meta field.
+ * These actions are queued and dispatched as one batch after a timeout.
+ * Special actions that are handled by this middleware:
+ * - BATCH_ENABLE can be used to enable and disable the batching.
+ * - BATCH_RESET discards the actions that are currently in the queue.
+ */
+function batchingMiddleware(store) {
+  return next => {
+    let queuedActions = [];
+    let enabled = true;
+    let flushTask = null;
+
+    return action => {
+      if (action.type === BATCH_ENABLE) {
+        return setEnabled(action.enabled);
+      }
+
+      if (action.type === BATCH_RESET) {
+        return resetQueue();
+      }
+
+      if (action.meta && action.meta.batch) {
+        if (!enabled) {
+          next(action);
+          return Promise.resolve();
+        }
+
+        queuedActions.push(action);
+
+        if (!flushTask) {
+          flushTask = new DelayedTask(flushActions, REQUESTS_REFRESH_RATE);
+        }
+
+        return flushTask.promise;
+      }
+
+      return next(action);
+    };
+
+    function setEnabled(value) {
+      enabled = value;
+
+      // If disabling the batching, flush the actions that have been queued so far
+      if (!enabled && flushTask) {
+        flushTask.runNow();
+      }
+    }
+
+    function resetQueue() {
+      queuedActions = [];
+
+      if (flushTask) {
+        flushTask.cancel();
+        flushTask = null;
+      }
+    }
+
+    function flushActions() {
+      const actions = queuedActions;
+      queuedActions = [];
+
+      next({
+        type: BATCH_ACTIONS,
+        actions,
+      });
+
+      flushTask = null;
+    }
+  };
+}
+
+/**
+ * Create a delayed task that calls the specified task function after a delay.
+ */
+function DelayedTask(taskFn, delay) {
+  this._promise = new Promise((resolve, reject) => {
+    this.runTask = (cancel) => {
+      if (cancel) {
+        reject("Task cancelled");
+      } else {
+        taskFn();
+        resolve();
+      }
+      this.runTask = null;
+    };
+    this.timeout = setTimeout(this.runTask, delay);
+  }).catch(console.error);
+}
+
+DelayedTask.prototype = {
+  /**
+   * Return a promise that is resolved after the task is performed or canceled.
+   */
+  get promise() {
+    return this._promise;
+  },
+
+  /**
+   * Cancel the execution of the task.
+   */
+  cancel() {
+    clearTimeout(this.timeout);
+    if (this.runTask) {
+      this.runTask(true);
+    }
+  },
+
+  /**
+   * Execute the scheduled task immediately, without waiting for the timeout.
+   * Resolves the promise correctly.
+   */
+  runNow() {
+    clearTimeout(this.timeout);
+    if (this.runTask) {
+      this.runTask();
+    }
+  }
+};
+
+module.exports = batchingMiddleware;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/middleware/moz.build
@@ -0,0 +1,7 @@
+# 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',
+)
--- a/devtools/client/netmonitor/moz.build
+++ b/devtools/client/netmonitor/moz.build
@@ -1,32 +1,37 @@
 # 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 += [
     'actions',
     'components',
     'har',
+    'middleware',
     'reducers',
-    'selectors'
+    'selectors',
+    'utils',
 ]
 
 DevToolsModules(
     'constants.js',
     'custom-request-view.js',
     'details-view.js',
     'events.js',
     'filter-predicates.js',
     'l10n.js',
+    'netmonitor-controller.js',
+    'netmonitor-view.js',
     'panel.js',
     'performance-statistics-view.js',
     'prefs.js',
     'request-list-context-menu.js',
     'request-utils.js',
     'requests-menu-view.js',
     'sidebar-view.js',
     'sort-predicates.js',
     'store.js',
     'toolbar-view.js',
+    'waterfall-background.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/netmonitor/netmonitor-controller.js
+++ b/devtools/client/netmonitor/netmonitor-controller.js
@@ -1,65 +1,40 @@
 /* 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/. */
 
 /* eslint-disable mozilla/reject-some-requires */
-/* globals window, document, NetMonitorView, gStore, Actions */
-/* exported loader */
+/* globals window, NetMonitorView, gStore, dumpn */
 
 "use strict";
 
-var { utils: Cu } = Components;
-
-// Descriptions for what this frontend is currently doing.
-const ACTIVITY_TYPE = {
-  // Standing by and handling requests normally.
-  NONE: 0,
-
-  // Forcing the target to reload with cache enabled or disabled.
-  RELOAD: {
-    WITH_CACHE_ENABLED: 1,
-    WITH_CACHE_DISABLED: 2,
-    WITH_CACHE_DEFAULT: 3
-  },
-
-  // Enabling or disabling the cache without triggering a reload.
-  ENABLE_CACHE: 3,
-  DISABLE_CACHE: 4
-};
-
-var BrowserLoaderModule = {};
-Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
-var { loader, require } = BrowserLoaderModule.BrowserLoader({
-  baseURI: "resource://devtools/client/netmonitor/",
-  window
-});
-
 const promise = require("promise");
 const Services = require("Services");
 const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
 const EventEmitter = require("devtools/shared/event-emitter");
 const Editor = require("devtools/client/sourceeditor/editor");
 const {TimelineFront} = require("devtools/shared/fronts/timeline");
 const {Task} = require("devtools/shared/task");
-const {Prefs} = require("./prefs");
-const {EVENTS} = require("./events");
+const { ACTIVITY_TYPE } = require("./constants");
+const { EVENTS } = require("./events");
+const { configureStore } = require("./store");
 const Actions = require("./actions/index");
+const { getDisplayedRequestById } = require("./selectors/index");
+const { Prefs } = require("./prefs");
 
-XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
-XPCOMUtils.defineConstant(this, "ACTIVITY_TYPE", ACTIVITY_TYPE);
-XPCOMUtils.defineConstant(this, "Editor", Editor);
-XPCOMUtils.defineConstant(this, "Prefs", Prefs);
-
-XPCOMUtils.defineLazyModuleGetter(this, "Chart",
+XPCOMUtils.defineConstant(window, "EVENTS", EVENTS);
+XPCOMUtils.defineConstant(window, "ACTIVITY_TYPE", ACTIVITY_TYPE);
+XPCOMUtils.defineConstant(window, "Editor", Editor);
+XPCOMUtils.defineConstant(window, "Prefs", Prefs);
+XPCOMUtils.defineLazyModuleGetter(window, "Chart",
   "resource://devtools/client/shared/widgets/Chart.jsm");
 
-XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
-  "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
+// Initialize the global Redux store
+window.gStore = configureStore();
 
 /**
  * Object defining the network monitor controller components.
  */
 var NetMonitorController = {
   /**
    * Initializes the view and connects the monitor client.
    *
@@ -86,16 +61,17 @@ var NetMonitorController = {
    *         A promise that is resolved when the monitor finishes shutdown.
    */
   shutdownNetMonitor: Task.async(function* () {
     if (this._shutdown) {
       return this._shutdown.promise;
     }
     this._shutdown = promise.defer();
     {
+      gStore.dispatch(Actions.batchReset());
       NetMonitorView.destroy();
       this.TargetEventsHandler.disconnect();
       this.NetworkEventsHandler.disconnect();
       yield this.disconnect();
     }
     this._shutdown.resolve();
     return undefined;
   }),
@@ -282,29 +258,28 @@ var NetMonitorController = {
    *         A promise resolved once the task finishes.
    */
   inspectRequest: function (requestId) {
     // Look for the request in the existing ones or wait for it to appear, if
     // the network monitor is still loading.
     let deferred = promise.defer();
     let request = null;
     let inspector = function () {
-      let predicate = i => i.value === requestId;
-      request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate);
+      request = getDisplayedRequestById(gStore.getState(), requestId);
       if (!request) {
         // Reset filters so that the request is visible.
         gStore.dispatch(Actions.toggleFilterType("all"));
-        request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate);
+        request = getDisplayedRequestById(gStore.getState(), requestId);
       }
 
       // If the request was found, select it. Otherwise this function will be
       // called again once new requests arrive.
       if (request) {
         window.off(EVENTS.REQUEST_ADDED, inspector);
-        NetMonitorView.RequestsMenu.selectedItem = request;
+        gStore.dispatch(Actions.selectRequest(request.id));
         deferred.resolve();
       }
     };
 
     inspector();
     if (!request) {
       window.on(EVENTS.REQUEST_ADDED, inspector);
     }
@@ -393,24 +368,24 @@ TargetEventsHandler.prototype = {
    *        Packet received from the server.
    */
   _onTabNavigated: function (type, packet) {
     switch (type) {
       case "will-navigate": {
         // Reset UI.
         if (!Services.prefs.getBoolPref("devtools.webconsole.persistlog")) {
           NetMonitorView.RequestsMenu.reset();
-          NetMonitorView.Sidebar.toggle(false);
+        } else {
+          // If the log is persistent, just clear all accumulated timing markers.
+          gStore.dispatch(Actions.clearTimingMarkers());
         }
         // Switch to the default network traffic inspector view.
         if (NetMonitorController.getCurrentActivity() == ACTIVITY_TYPE.NONE) {
           NetMonitorView.showNetworkInspectorView();
         }
-        // Clear any accumulated markers.
-        NetMonitorController.NetworkEventsHandler.clearMarkers();
 
         window.emit(EVENTS.TARGET_WILL_NAVIGATE);
         break;
       }
       case "navigate": {
         window.emit(EVENTS.TARGET_DID_NAVIGATE);
         break;
       }
@@ -424,18 +399,16 @@ TargetEventsHandler.prototype = {
     NetMonitorController.shutdownNetMonitor();
   }
 };
 
 /**
  * Functions handling target network events.
  */
 function NetworkEventsHandler() {
-  this._markers = [];
-
   this._onNetworkEvent = this._onNetworkEvent.bind(this);
   this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
   this._onDocLoadingMarker = this._onDocLoadingMarker.bind(this);
   this._onRequestHeaders = this._onRequestHeaders.bind(this);
   this._onRequestCookies = this._onRequestCookies.bind(this);
   this._onRequestPostData = this._onRequestPostData.bind(this);
   this._onResponseHeaders = this._onResponseHeaders.bind(this);
   this._onResponseCookies = this._onResponseCookies.bind(this);
@@ -451,29 +424,16 @@ NetworkEventsHandler.prototype = {
   get webConsoleClient() {
     return NetMonitorController.webConsoleClient;
   },
 
   get timelineFront() {
     return NetMonitorController.timelineFront;
   },
 
-  get firstDocumentDOMContentLoadedTimestamp() {
-    let marker = this._markers.filter(e => {
-      return e.name == "document::DOMContentLoaded";
-    })[0];
-
-    return marker ? marker.unixTime / 1000 : -1;
-  },
-
-  get firstDocumentLoadTimestamp() {
-    let marker = this._markers.filter(e => e.name == "document::Load")[0];
-    return marker ? marker.unixTime / 1000 : -1;
-  },
-
   /**
    * Connect to the current target client.
    */
   connect: function () {
     dumpn("NetworkEventsHandler is connecting...");
     this.webConsoleClient.on("networkEvent", this._onNetworkEvent);
     this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate);
 
@@ -520,17 +480,17 @@ NetworkEventsHandler.prototype = {
   },
 
   /**
    * The "DOMContentLoaded" and "Load" events sent by the timeline actor.
    * @param object marker
    */
   _onDocLoadingMarker: function (marker) {
     window.emit(EVENTS.TIMELINE_EVENT, marker);
-    this._markers.push(marker);
+    gStore.dispatch(Actions.addTimingMarker(marker));
   },
 
   /**
    * The "networkEvent" message type handler.
    *
    * @param string type
    *        Message type.
    * @param object networkInfo
@@ -542,18 +502,17 @@ NetworkEventsHandler.prototype = {
       request: { method, url },
       isXHR,
       cause,
       fromCache,
       fromServiceWorker
     } = networkInfo;
 
     NetMonitorView.RequestsMenu.addRequest(
-      actor, startedDateTime, method, url, isXHR, cause, fromCache,
-        fromServiceWorker
+      actor, {startedDateTime, method, url, isXHR, cause, fromCache, fromServiceWorker}
     );
     window.emit(EVENTS.NETWORK_EVENT, actor);
   },
 
   /**
    * The "networkEventUpdate" message type handler.
    *
    * @param string type
@@ -632,155 +591,139 @@ NetworkEventsHandler.prototype = {
    * Handles additional information received for a "requestHeaders" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onRequestHeaders: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       requestHeaders: response
-    }, () => {
+    }).then(() => {
       window.emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "requestCookies" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onRequestCookies: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       requestCookies: response
-    }, () => {
+    }).then(() => {
       window.emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "requestPostData" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onRequestPostData: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       requestPostData: response
-    }, () => {
+    }).then(() => {
       window.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "securityInfo" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onSecurityInfo: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       securityInfo: response.securityInfo
-    }, () => {
+    }).then(() => {
       window.emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "responseHeaders" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onResponseHeaders: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       responseHeaders: response
-    }, () => {
+    }).then(() => {
       window.emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "responseCookies" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onResponseCookies: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       responseCookies: response
-    }, () => {
+    }).then(() => {
       window.emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "responseContent" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onResponseContent: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       responseContent: response
-    }, () => {
+    }).then(() => {
       window.emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "eventTimings" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onEventTimings: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       eventTimings: response
-    }, () => {
+    }).then(() => {
       window.emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
     });
   },
 
   /**
-   * Clears all accumulated markers.
-   */
-  clearMarkers: function () {
-    this._markers.length = 0;
-  },
-
-  /**
    * Fetches the full text of a LongString.
    *
    * @param object | string stringGrip
    *        The long string grip containing the corresponding actor.
    *        If you pass in a plain string (by accident or because you're lazy),
    *        then a promise of the same string is simply returned.
    * @return object Promise
    *         A promise that is resolved when the full string contents
    *         are available, or rejected if something goes wrong.
    */
   getString: function (stringGrip) {
     return this.webConsoleClient.getString(stringGrip);
   }
 };
 
 /**
- * Returns true if this is document is in RTL mode.
- * @return boolean
- */
-XPCOMUtils.defineLazyGetter(window, "isRTL", function () {
-  return window.getComputedStyle(document.documentElement, null)
-    .direction == "rtl";
-});
-
-/**
  * Convenient way of emitting events from the panel window.
  */
-EventEmitter.decorate(this);
+EventEmitter.decorate(window);
 
 /**
  * Preliminary setup for the NetMonitorController object.
  */
 NetMonitorController.TargetEventsHandler = new TargetEventsHandler();
 NetMonitorController.NetworkEventsHandler = new NetworkEventsHandler();
 
 /**
@@ -790,19 +733,9 @@ Object.defineProperties(window, {
   "gNetwork": {
     get: function () {
       return NetMonitorController.NetworkEventsHandler;
     },
     configurable: true
   }
 });
 
-/**
- * Helper method for debugging.
- * @param string
- */
-function dumpn(str) {
-  if (wantLogging) {
-    dump("NET-FRONTEND: " + str + "\n");
-  }
-}
-
-var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+exports.NetMonitorController = NetMonitorController;
--- a/devtools/client/netmonitor/netmonitor-view.js
+++ b/devtools/client/netmonitor/netmonitor-view.js
@@ -1,31 +1,31 @@
 /* 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/. */
 
-/* import-globals-from ./netmonitor-controller.js */
 /* eslint-disable mozilla/reject-some-requires */
-/* globals Prefs, setInterval, setTimeout, clearInterval, clearTimeout, btoa */
-/* exported $, $all */
+/* globals $, gStore, NetMonitorController, dumpn */
 
 "use strict";
 
 const { testing: isTesting } = require("devtools/shared/flags");
+const promise = require("promise");
+const Editor = require("devtools/client/sourceeditor/editor");
+const { Task } = require("devtools/shared/task");
 const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
-const { configureStore } = require("./store");
 const { RequestsMenuView } = require("./requests-menu-view");
 const { CustomRequestView } = require("./custom-request-view");
 const { ToolbarView } = require("./toolbar-view");
 const { SidebarView } = require("./sidebar-view");
 const { DetailsView } = require("./details-view");
 const { PerformanceStatisticsView } = require("./performance-statistics-view");
-
-// Initialize the global redux variables
-var gStore = configureStore();
+const { ACTIVITY_TYPE } = require("./constants");
+const Actions = require("./actions/index");
+const { Prefs } = require("./prefs");
 
 // ms
 const WDA_DEFAULT_VERIFY_INTERVAL = 50;
 
 // Use longer timeout during testing as the tests need this process to succeed
 // and two seconds is quite short on slow debug builds. The timeout here should
 // be at least equal to the general mochitest timeout of 45 seconds so that this
 // never gets hit during testing.
@@ -75,22 +75,16 @@ var NetMonitorView = {
     dumpn("Initializing the NetMonitorView panes");
 
     this._body = $("#body");
     this._detailsPane = $("#details-pane");
 
     this._detailsPane.setAttribute("width", Prefs.networkDetailsWidth);
     this._detailsPane.setAttribute("height", Prefs.networkDetailsHeight);
     this.toggleDetailsPane({ visible: false });
-
-    // Disable the performance statistics mode.
-    if (!Prefs.statistics) {
-      $("#request-menu-context-perf").hidden = true;
-      $("#notice-perf-message").hidden = true;
-    }
   },
 
   /**
    * Destroys the UI for all the displayed panes.
    */
   _destroyPanes: Task.async(function* () {
     dumpn("Destroying the NetMonitorView panes");
 
@@ -164,17 +158,16 @@ var NetMonitorView = {
     }
   },
 
   /**
    * Switches to the "Inspector" frontend view mode.
    */
   showNetworkInspectorView: function () {
     this._body.selectedPanel = $("#network-inspector-view");
-    this.RequestsMenu._flushWaterfallViews(true);
   },
 
   /**
    * Switches to the "Statistics" frontend view mode.
    */
   showNetworkStatisticsView: function () {
     this._body.selectedPanel = $("#network-statistics-view");
 
@@ -187,26 +180,27 @@ var NetMonitorView = {
       yield controller.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
 
       try {
         // • The response headers and status code are required for determining
         // whether a response is "fresh" (cacheable).
         // • The response content size and request total time are necessary for
         // populating the statistics view.
         // • The response mime type is used for categorization.
-        yield whenDataAvailable(requestsView, [
+        yield whenDataAvailable(requestsView.store, [
           "responseHeaders", "status", "contentSize", "mimeType", "totalTime"
         ]);
       } catch (ex) {
         // Timed out while waiting for data. Continue with what we have.
         console.error(ex);
       }
 
-      statisticsView.createPrimedCacheChart(requestsView.items);
-      statisticsView.createEmptyCacheChart(requestsView.items);
+      const requests = requestsView.store.getState().requests.requests;
+      statisticsView.createPrimedCacheChart(requests);
+      statisticsView.createEmptyCacheChart(requests);
     });
   },
 
   reloadPage: function () {
     NetMonitorController.triggerActivity(
       ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT);
   },
 
@@ -237,56 +231,53 @@ var NetMonitorView = {
   },
 
   _body: null,
   _detailsPane: null,
   _editorPromises: new Map()
 };
 
 /**
- * DOM query helper.
- * TODO: Move it into "dom-utils.js" module and "require" it when needed.
- */
-var $ = (selector, target = document) => target.querySelector(selector);
-var $all = (selector, target = document) => target.querySelectorAll(selector);
-
-/**
  * Makes sure certain properties are available on all objects in a data store.
  *
- * @param array dataStore
- *        The request view object from which to fetch the item list.
+ * @param Store dataStore
+ *        A Redux store for which to check the availability of properties.
  * @param array mandatoryFields
  *        A list of strings representing properties of objects in dataStore.
  * @return object
  *         A promise resolved when all objects in dataStore contain the
  *         properties defined in mandatoryFields.
  */
-function whenDataAvailable(requestsView, mandatoryFields) {
-  let deferred = promise.defer();
+function whenDataAvailable(dataStore, mandatoryFields) {
+  return new Promise((resolve, reject) => {
+    let interval = setInterval(() => {
+      const { requests } = dataStore.getState().requests;
+      const allFieldsPresent = !requests.isEmpty() && requests.every(
+        item => mandatoryFields.every(
+          field => item.get(field) !== undefined
+        )
+      );
 
-  let interval = setInterval(() => {
-    const { attachments } = requestsView;
-    if (attachments.length > 0 && attachments.every(item => {
-      return mandatoryFields.every(field => field in item);
-    })) {
+      if (allFieldsPresent) {
+        clearInterval(interval);
+        clearTimeout(timer);
+        resolve();
+      }
+    }, WDA_DEFAULT_VERIFY_INTERVAL);
+
+    let timer = setTimeout(() => {
       clearInterval(interval);
-      clearTimeout(timer);
-      deferred.resolve();
-    }
-  }, WDA_DEFAULT_VERIFY_INTERVAL);
-
-  let timer = setTimeout(() => {
-    clearInterval(interval);
-    deferred.reject(new Error("Timed out while waiting for data"));
-  }, WDA_DEFAULT_GIVE_UP_TIMEOUT);
-
-  return deferred.promise;
+      reject(new Error("Timed out while waiting for data"));
+    }, WDA_DEFAULT_GIVE_UP_TIMEOUT);
+  });
 }
 
 /**
  * Preliminary setup for the NetMonitorView object.
  */
 NetMonitorView.Toolbar = new ToolbarView();
 NetMonitorView.Sidebar = new SidebarView();
 NetMonitorView.NetworkDetails = new DetailsView();
 NetMonitorView.RequestsMenu = new RequestsMenuView();
 NetMonitorView.CustomRequest = new CustomRequestView();
 NetMonitorView.PerformanceStatistics = new PerformanceStatisticsView();
+
+exports.NetMonitorView = NetMonitorView;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/netmonitor.js
@@ -0,0 +1,60 @@
+/* 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/. */
+
+/* globals window, document, NetMonitorController, NetMonitorView */
+/* exported Netmonitor, NetMonitorController, NetMonitorView, $, $all, dumpn */
+
+"use strict";
+
+const Cu = Components.utils;
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+
+function Netmonitor(toolbox) {
+  const { require } = BrowserLoader({
+    baseURI: "resource://devtools/client/netmonitor/",
+    window,
+    commonLibRequire: toolbox.browserRequire,
+  });
+
+  window.windowRequire = require;
+
+  const { NetMonitorController } = require("./netmonitor-controller.js");
+  const { NetMonitorView } = require("./netmonitor-view.js");
+
+  window.NetMonitorController = NetMonitorController;
+  window.NetMonitorView = NetMonitorView;
+
+  NetMonitorController._toolbox = toolbox;
+  NetMonitorController._target = toolbox.target;
+}
+
+Netmonitor.prototype = {
+  init() {
+    return window.NetMonitorController.startupNetMonitor();
+  },
+
+  destroy() {
+    return window.NetMonitorController.shutdownNetMonitor();
+  }
+};
+
+/**
+ * DOM query helper.
+ * TODO: Move it into "dom-utils.js" module and "require" it when needed.
+ */
+var $ = (selector, target = document) => target.querySelector(selector);
+var $all = (selector, target = document) => target.querySelectorAll(selector);
+
+/**
+ * Helper method for debugging.
+ * @param string
+ */
+function dumpn(str) {
+  if (wantLogging) {
+    dump("NET-FRONTEND: " + str + "\n");
+  }
+}
+
+var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
--- a/devtools/client/netmonitor/netmonitor.xul
+++ b/devtools/client/netmonitor/netmonitor.xul
@@ -7,212 +7,33 @@
 <?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/netmonitor.css" type="text/css"?>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml">
 
   <script type="application/javascript;version=1.8"
           src="chrome://devtools/content/shared/theme-switching.js"/>
-  <script type="text/javascript" src="netmonitor-controller.js"/>
-  <script type="text/javascript" src="netmonitor-view.js"/>
+  <script type="text/javascript" src="netmonitor.js"/>
 
   <deck id="body"
         class="theme-sidebar"
         flex="1"
         data-localization-bundle="devtools/client/locales/netmonitor.properties">
 
     <vbox id="network-inspector-view" flex="1">
       <html:div xmlns="http://www.w3.org/1999/xhtml"
                 id="react-toolbar-hook"/>
       <hbox id="network-table-and-sidebar"
             class="devtools-responsive-container"
             flex="1">
-        <vbox id="network-table" flex="1" class="devtools-main-content">
-          <toolbar id="requests-menu-toolbar"
-                   class="devtools-toolbar"
-                   align="center">
-            <hbox id="toolbar-labels" flex="1">
-              <hbox id="requests-menu-status-header-box"
-                    class="requests-menu-header requests-menu-status"
-                    align="center">
-                <button id="requests-menu-status-button"
-                        class="requests-menu-header-button requests-menu-status"
-                        data-key="status"
-                        data-localization="label=netmonitor.toolbar.status3"
-                        flex="1">
-                </button>
-              </hbox>
-              <hbox id="requests-menu-method-header-box"
-                    class="requests-menu-header requests-menu-method"
-                    align="center">
-                <button id="requests-menu-method-button"
-                        class="requests-menu-header-button requests-menu-method"
-                        data-key="method"
-                        data-localization="label=netmonitor.toolbar.method"
-                        crop="end"
-                        flex="1">
-                </button>
-              </hbox>
-              <hbox id="requests-menu-icon-and-file-header-box"
-                    class="requests-menu-header requests-menu-icon-and-file"
-                    align="center">
-                <button id="requests-menu-file-button"
-                        class="requests-menu-header-button requests-menu-file"
-                        data-key="file"
-                        data-localization="label=netmonitor.toolbar.file"
-                        crop="end"
-                        flex="1">
-                </button>
-              </hbox>
-              <hbox id="requests-menu-domain-header-box"
-                    class="requests-menu-header requests-menu-security-and-domain"
-                    align="center">
-                <button id="requests-menu-domain-button"
-                        class="requests-menu-header-button requests-menu-security-and-domain"
-                        data-key="domain"
-                        data-localization="label=netmonitor.toolbar.domain"
-                        crop="end"
-                        flex="1">
-                </button>
-              </hbox>
-              <hbox id="requests-menu-cause-header-box"
-                    class="requests-menu-header requests-menu-cause"
-                    align="center">
-                <button id="requests-menu-cause-button"
-                        class="requests-menu-header-button requests-menu-cause"
-                        data-key="cause"
-                        data-localization="label=netmonitor.toolbar.cause"
-                        crop="end"
-                        flex="1">
-                </button>
-              </hbox>
-              <hbox id="requests-menu-type-header-box"
-                    class="requests-menu-header requests-menu-type"
-                    align="center">
-                <button id="requests-menu-type-button"
-                        class="requests-menu-header-button requests-menu-type"
-                        data-key="type"
-                        data-localization="label=netmonitor.toolbar.type"
-                        crop="end"
-                        flex="1">
-                </button>
-              </hbox>
-              <hbox id="requests-menu-transferred-header-box"
-                    class="requests-menu-header requests-menu-transferred"
-                    align="center">
-                <button id="requests-menu-transferred-button"
-                        class="requests-menu-header-button requests-menu-transferred"
-                        data-key="transferred"
-                        data-localization="label=netmonitor.toolbar.transferred"
-                        crop="end"
-                        flex="1">
-                </button>
-              </hbox>
-              <hbox id="requests-menu-size-header-box"
-                    class="requests-menu-header requests-menu-size"
-                    align="center">
-                <button id="requests-menu-size-button"
-                        class="requests-menu-header-button requests-menu-size"
-                        data-key="size"
-                        data-localization="label=netmonitor.toolbar.size"
-                        crop="end"
-                        flex="1">
-                </button>
-              </hbox>
-              <hbox id="requests-menu-waterfall-header-box"
-                    class="requests-menu-header requests-menu-waterfall"
-                    align="center"
-                    flex="1">
-                <button id="requests-menu-waterfall-button"
-                        class="requests-menu-header-button requests-menu-waterfall"
-                        data-key="waterfall"
-                        pack="start"
-                        data-localization="label=netmonitor.toolbar.waterfall"
-                        flex="1">
-                  <image id="requests-menu-waterfall-image"/>
-                  <box id="requests-menu-waterfall-label-wrapper">
-                    <label id="requests-menu-waterfall-label"
-                           class="plain requests-menu-waterfall"
-                           data-localization="value=netmonitor.toolbar.waterfall"/>
-                  </box>
-                </button>
-              </hbox>
-            </hbox>
-          </toolbar>
-
-          <vbox id="requests-menu-empty-notice"
-                class="side-menu-widget-empty-text">
-            <hbox id="notice-reload-message" align="center">
-              <label data-localization="content=netmonitor.reloadNotice1"/>
-              <button id="requests-menu-reload-notice-button"
-                      class="devtools-toolbarbutton"
-                      standalone="true"
-                      data-localization="label=netmonitor.reloadNotice2"/>
-              <label data-localization="content=netmonitor.reloadNotice3"/>
-            </hbox>
-            <hbox id="notice-perf-message" align="center">
-              <label data-localization="content=netmonitor.perfNotice1"/>
-              <button id="requests-menu-perf-notice-button"
-                      class="devtools-toolbarbutton"
-                      standalone="true"
-                      data-localization="tooltiptext=netmonitor.perfNotice3"/>
-              <label data-localization="content=netmonitor.perfNotice2"/>
-            </hbox>
-          </vbox>
-
-          <vbox id="requests-menu-contents" flex="1">
-            <hbox id="requests-menu-item-template" hidden="true">
-              <hbox class="requests-menu-subitem requests-menu-status"
-                    align="center">
-                <box class="requests-menu-status-icon"/>
-                <label class="plain requests-menu-status-code"
-                       crop="end"/>
-              </hbox>
-              <hbox class="requests-menu-subitem requests-menu-method-box"
-                    align="center">
-                <label class="plain requests-menu-method"
-                       crop="end"
-                       flex="1"/>
-              </hbox>
-              <hbox class="requests-menu-subitem requests-menu-icon-and-file"
-                    align="center">
-                <image class="requests-menu-icon" hidden="true"/>
-                <label class="plain requests-menu-file"
-                       crop="end"
-                       flex="1"/>
-              </hbox>
-              <hbox class="requests-menu-subitem requests-menu-security-and-domain"
-                    align="center">
-                <image class="requests-security-state-icon" />
-                <label class="plain requests-menu-domain"
-                       crop="end"
-                       flex="1"/>
-              </hbox>
-              <hbox class="requests-menu-subitem requests-menu-cause" align="center">
-                <label class="requests-menu-cause-stack" value="JS" hidden="true"/>
-                <label class="plain requests-menu-cause-label" flex="1" crop="end"/>
-              </hbox>
-              <label class="plain requests-menu-subitem requests-menu-type"
-                     crop="end"/>
-              <label class="plain requests-menu-subitem requests-menu-transferred"
-                     crop="end"/>
-              <label class="plain requests-menu-subitem requests-menu-size"
-                     crop="end"/>
-              <hbox class="requests-menu-subitem requests-menu-waterfall"
-                    align="center"
-                    flex="1">
-                <hbox class="requests-menu-timings"
-                      align="center">
-                  <label class="plain requests-menu-timings-total"/>
-                </hbox>
-              </hbox>
-            </hbox>
-          </vbox>
-        </vbox>
+        <html:div xmlns="http://www.w3.org/1999/xhtml"
+                  id="network-table"
+                  class="devtools-main-content">
+        </html:div>
 
         <splitter id="network-inspector-view-splitter"
                   class="devtools-side-splitter"/>
 
         <deck id="details-pane"
               hidden="true">
           <vbox id="custom-pane"
                 class="tabpanel-content">
--- a/devtools/client/netmonitor/panel.js
+++ b/devtools/client/netmonitor/panel.js
@@ -9,20 +9,17 @@ const EventEmitter = require("devtools/s
 const { Task } = require("devtools/shared/task");
 const { localizeMarkup } = require("devtools/shared/l10n");
 
 function NetMonitorPanel(iframeWindow, toolbox) {
   this.panelWin = iframeWindow;
   this.panelDoc = iframeWindow.document;
   this._toolbox = toolbox;
 
-  this._view = this.panelWin.NetMonitorView;
-  this._controller = this.panelWin.NetMonitorController;
-  this._controller._target = this.target;
-  this._controller._toolbox = this._toolbox;
+  this._netmonitor = new iframeWindow.Netmonitor(toolbox);
 
   EventEmitter.decorate(this);
 }
 
 exports.NetMonitorPanel = NetMonitorPanel;
 
 NetMonitorPanel.prototype = {
   /**
@@ -41,17 +38,18 @@ NetMonitorPanel.prototype = {
     let deferred = promise.defer();
     this._opening = deferred.promise;
 
     // Local monitoring needs to make the target remote.
     if (!this.target.isRemote) {
       yield this.target.makeRemote();
     }
 
-    yield this._controller.startupNetMonitor();
+    yield this._netmonitor.init();
+
     this.isReady = true;
     this.emit("ready");
 
     deferred.resolve(this);
     return this._opening;
   }),
 
   // DevToolPanel API
@@ -62,15 +60,15 @@ NetMonitorPanel.prototype = {
 
   destroy: Task.async(function* () {
     if (this._destroying) {
       return this._destroying;
     }
     let deferred = promise.defer();
     this._destroying = deferred.promise;
 
-    yield this._controller.shutdownNetMonitor();
+    yield this._netmonitor.destroy();
     this.emit("destroyed");
 
     deferred.resolve();
     return this._destroying;
   })
 };
--- a/devtools/client/netmonitor/performance-statistics-view.js
+++ b/devtools/client/netmonitor/performance-statistics-view.js
@@ -1,22 +1,27 @@
 /* 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/. */
 
-/* import-globals-from ./netmonitor-controller.js */
-/* globals $ */
+/* eslint-disable mozilla/reject-some-requires */
+/* globals $, window, document, NetMonitorView */
 
 "use strict";
 
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
 const {PluralForm} = require("devtools/shared/plural-form");
 const {Filters} = require("./filter-predicates");
 const {L10N} = require("./l10n");
+const {EVENTS} = require("./events");
 const Actions = require("./actions/index");
 
+XPCOMUtils.defineLazyModuleGetter(this, "Chart",
+  "resource://devtools/client/shared/widgets/Chart.jsm");
+
 const REQUEST_TIME_DECIMALS = 2;
 const CONTENT_SIZE_DECIMALS = 2;
 
 // px
 const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200;
 
 /**
  * Functions handling the performance statistics view.
@@ -167,17 +172,17 @@ PerformanceStatisticsView.prototype = {
       cached: 0,
       count: 0,
       label: e,
       size: 0,
       time: 0
     }));
 
     for (let requestItem of items) {
-      let details = requestItem.attachment;
+      let details = requestItem;
       let type;
 
       if (Filters.html(details)) {
         // "html"
         type = 0;
       } else if (Filters.css(details)) {
         // "css"
         type = 1;
@@ -232,21 +237,18 @@ PerformanceStatisticsView.prototype = {
  */
 function responseIsFresh({ responseHeaders, status }) {
   // Check for a "304 Not Modified" status and response headers availability.
   if (status != 304 || !responseHeaders) {
     return false;
   }
 
   let list = responseHeaders.headers;
-  let cacheControl = list.filter(e => {
-    return e.name.toLowerCase() == "cache-control";
-  })[0];
-
-  let expires = list.filter(e => e.name.toLowerCase() == "expires")[0];
+  let cacheControl = list.find(e => e.name.toLowerCase() == "cache-control");
+  let expires = list.find(e => e.name.toLowerCase() == "expires");
 
   // Check the "Cache-Control" header for a maximum age value.
   if (cacheControl) {
     let maxAgeMatch =
       cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) ||
       cacheControl.value.match(/max-age\s*=\s*(\d+)/);
 
     if (maxAgeMatch && maxAgeMatch.pop() > 0) {
--- a/devtools/client/netmonitor/prefs.js
+++ b/devtools/client/netmonitor/prefs.js
@@ -8,11 +8,10 @@ const {PrefsHelper} = require("devtools/
 
 /**
  * Shortcuts for accessing various network monitor preferences.
  */
 
 exports.Prefs = new PrefsHelper("devtools.netmonitor", {
   networkDetailsWidth: ["Int", "panes-network-details-width"],
   networkDetailsHeight: ["Int", "panes-network-details-height"],
-  statistics: ["Bool", "statistics"],
   filters: ["Json", "filters"]
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/reducers/batching.js
@@ -0,0 +1,25 @@
+/* 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 { BATCH_ACTIONS } = require("../constants");
+
+/**
+ * A reducer to handle batched actions. For each action in the BATCH_ACTIONS array,
+ * the reducer is called successively on the array of batched actions, resulting in
+ * only one state update.
+ */
+function batchingReducer(nextReducer) {
+  return function reducer(state, action) {
+    switch (action.type) {
+      case BATCH_ACTIONS:
+        return action.actions.reduce(reducer, state);
+      default:
+        return nextReducer(state, action);
+    }
+  };
+}
+
+module.exports = batchingReducer;
--- a/devtools/client/netmonitor/reducers/filters.js
+++ b/devtools/client/netmonitor/reducers/filters.js
@@ -22,17 +22,17 @@ const FilterTypes = I.Record({
   media: false,
   flash: false,
   ws: false,
   other: false,
 });
 
 const Filters = I.Record({
   types: new FilterTypes({ all: true }),
-  url: "",
+  text: "",
 });
 
 function toggleFilterType(state, action) {
   let { filter } = action;
   let newState;
 
   // Ignore unknown filter type
   if (!state.has(filter)) {
@@ -67,15 +67,15 @@ function enableFilterTypeOnly(state, act
 
 function filters(state = new Filters(), action) {
   switch (action.type) {
     case TOGGLE_FILTER_TYPE:
       return state.set("types", toggleFilterType(state.types, action));
     case ENABLE_FILTER_TYPE_ONLY:
       return state.set("types", enableFilterTypeOnly(state.types, action));
     case SET_FILTER_TEXT:
-      return state.set("url", action.url);
+      return state.set("text", action.text);
     default:
       return state;
   }
 }
 
 module.exports = filters;
--- a/devtools/client/netmonitor/reducers/index.js
+++ b/devtools/client/netmonitor/reducers/index.js
@@ -1,16 +1,23 @@
 /* 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 { combineReducers } = require("devtools/client/shared/vendor/redux");
+const batchingReducer = require("./batching");
+const requests = require("./requests");
+const sort = require("./sort");
 const filters = require("./filters");
-const requests = require("./requests");
+const timingMarkers = require("./timing-markers");
 const ui = require("./ui");
 
-module.exports = combineReducers({
-  filters,
-  requests,
-  ui,
-});
+module.exports = batchingReducer(
+  combineReducers({
+    requests,
+    sort,
+    filters,
+    timingMarkers,
+    ui,
+  })
+);
--- a/devtools/client/netmonitor/reducers/moz.build
+++ b/devtools/client/netmonitor/reducers/moz.build
@@ -1,10 +1,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(
+    'batching.js',
     'filters.js',
     'index.js',
     'requests.js',
+    'sort.js',
+    'timing-markers.js',
     'ui.js',
 )
--- a/devtools/client/netmonitor/reducers/requests.js
+++ b/devtools/client/netmonitor/reducers/requests.js
@@ -1,29 +1,246 @@
 /* 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 I = require("devtools/client/shared/vendor/immutable");
+const { getUrlDetails } = require("../request-utils");
 const {
-  UPDATE_REQUESTS,
+  ADD_REQUEST,
+  UPDATE_REQUEST,
+  CLEAR_REQUESTS,
+  SELECT_REQUEST,
+  PRESELECT_REQUEST,
+  CLONE_SELECTED_REQUEST,
+  REMOVE_SELECTED_CUSTOM_REQUEST,
+  OPEN_SIDEBAR
 } = require("../constants");
 
+const Request = I.Record({
+  id: null,
+  // Set to true in case of a request that's being edited as part of "edit and resend"
+  isCustom: false,
+  // Request properties - at the beginning, they are unknown and are gradually filled in
+  startedMillis: undefined,
+  method: undefined,
+  url: undefined,
+  urlDetails: undefined,
+  remotePort: undefined,
+  remoteAddress: undefined,
+  isXHR: undefined,
+  cause: undefined,
+  fromCache: undefined,
+  fromServiceWorker: undefined,
+  status: undefined,
+  statusText: undefined,
+  httpVersion: undefined,
+  securityState: undefined,
+  securityInfo: undefined,
+  mimeType: undefined,
+  contentSize: undefined,
+  transferredSize: undefined,
+  totalTime: undefined,
+  eventTimings: undefined,
+  headersSize: undefined,
+  requestHeaders: undefined,
+  requestHeadersFromUploadStream: undefined,
+  requestCookies: undefined,
+  requestPostData: undefined,
+  responseHeaders: undefined,
+  responseCookies: undefined,
+  responseContent: undefined,
+  responseContentDataUri: undefined,
+});
+
 const Requests = I.Record({
-  items: [],
+  // The request list
+  requests: I.List(),
+  // Selection state
+  selectedId: null,
+  preselectedId: null,
+  // Auxiliary fields to hold requests stats
+  firstStartedMillis: +Infinity,
+  lastEndedMillis: -Infinity,
 });
 
-function updateRequests(state, action) {
-  return state.set("items", action.items || state.items);
-}
+const UPDATE_PROPS = [
+  "method",
+  "url",
+  "remotePort",
+  "remoteAddress",
+  "status",
+  "statusText",
+  "httpVersion",
+  "securityState",
+  "securityInfo",
+  "mimeType",
+  "contentSize",
+  "transferredSize",
+  "totalTime",
+  "eventTimings",
+  "headersSize",
+  "requestHeaders",
+  "requestHeadersFromUploadStream",
+  "requestCookies",
+  "requestPostData",
+  "responseHeaders",
+  "responseCookies",
+  "responseContent",
+  "responseContentDataUri"
+];
+
+function requestsReducer(state = new Requests(), action) {
+  switch (action.type) {
+    case ADD_REQUEST: {
+      return state.withMutations(st => {
+        let newRequest = new Request(Object.assign(
+          { id: action.id },
+          action.data,
+          { urlDetails: getUrlDetails(action.data.url) }
+        ));
+        st.requests = st.requests.push(newRequest);
+
+        // Update the started/ended timestamps
+        let { startedMillis } = action.data;
+        if (startedMillis < st.firstStartedMillis) {
+          st.firstStartedMillis = startedMillis;
+        }
+        if (startedMillis > st.lastEndedMillis) {
+          st.lastEndedMillis = startedMillis;
+        }
+
+        // Select the request if it was preselected and there is no other selection
+        if (st.preselectedId && st.preselectedId === action.id) {
+          st.selectedId = st.selectedId || st.preselectedId;
+          st.preselectedId = null;
+        }
+      });
+    }
+
+    case UPDATE_REQUEST: {
+      let { requests, lastEndedMillis } = state;
+
+      let updateIdx = requests.findIndex(r => r.id === action.id);
+      if (updateIdx === -1) {
+        return state;
+      }
+
+      requests = requests.update(updateIdx, r => r.withMutations(request => {
+        for (let [key, value] of Object.entries(action.data)) {
+          if (!UPDATE_PROPS.includes(key)) {
+            continue;
+          }
+
+          request[key] = value;
 
-function requests(state = new Requests(), action) {
-  switch (action.type) {
-    case UPDATE_REQUESTS:
-      return updateRequests(state, action);
+          switch (key) {
+            case "url":
+              // Compute the additional URL details
+              request.urlDetails = getUrlDetails(value);
+              break;
+            case "responseContent":
+              // If there's no mime type available when the response content
+              // is received, assume text/plain as a fallback.
+              if (!request.mimeType) {
+                request.mimeType = "text/plain";
+              }
+              break;
+            case "totalTime":
+              const endedMillis = request.startedMillis + value;
+              lastEndedMillis = Math.max(lastEndedMillis, endedMillis);
+              break;
+            case "requestPostData":
+              request.requestHeadersFromUploadStream = {
+                headers: [],
+                headersSize: 0,
+              };
+              break;
+          }
+        }
+      }));
+
+      return state.withMutations(st => {
+        st.requests = requests;
+        st.lastEndedMillis = lastEndedMillis;
+      });
+    }
+    case CLEAR_REQUESTS: {
+      return new Requests();
+    }
+    case SELECT_REQUEST: {
+      return state.set("selectedId", action.id);
+    }
+    case PRESELECT_REQUEST: {
+      return state.set("preselectedId", action.id);
+    }
+    case CLONE_SELECTED_REQUEST: {
+      let { requests, selectedId } = state;
+
+      if (!selectedId) {
+        return state;
+      }
+
+      let clonedIdx = requests.findIndex(r => r.id === selectedId);
+      if (clonedIdx === -1) {
+        return state;
+      }
+
+      let clonedRequest = requests.get(clonedIdx);
+      let newRequest = new Request({
+        id: clonedRequest.id + "-clone",
+        method: clonedRequest.method,
+        url: clonedRequest.url,
+        urlDetails: clonedRequest.urlDetails,
+        requestHeaders: clonedRequest.requestHeaders,
+        requestPostData: clonedRequest.requestPostData,
+        isCustom: true
+      });
+
+      // Insert the clone right after the original. This ensures that the requests
+      // are always sorted next to each other, even when multiple requests are
+      // equal according to the sorting criteria.
+      requests = requests.insert(clonedIdx + 1, newRequest);
+
+      return state.withMutations(st => {
+        st.requests = requests;
+        st.selectedId = newRequest.id;
+      });
+    }
+    case REMOVE_SELECTED_CUSTOM_REQUEST: {
+      let { requests, selectedId } = state;
+
+      if (!selectedId) {
+        return state;
+      }
+
+      let removedRequest = requests.find(r => r.id === selectedId);
+
+      // Only custom requests can be removed
+      if (!removedRequest || !removedRequest.isCustom) {
+        return state;
+      }
+
+      return state.withMutations(st => {
+        st.requests = requests.filter(r => r !== removedRequest);
+        st.selectedId = null;
+      });
+    }
+    case OPEN_SIDEBAR: {
+      if (!action.open) {
+        return state.set("selectedId", null);
+      }
+
+      if (!state.selectedId && !state.requests.isEmpty()) {
+        return state.set("selectedId", state.requests.get(0).id);
+      }
+
+      return state;
+    }
+
     default:
       return state;
   }
 }
 
-module.exports = requests;
+module.exports = requestsReducer;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/reducers/sort.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 I = require("devtools/client/shared/vendor/immutable");
+const { SORT_BY } = require("../constants");
+
+const Sort = I.Record({
+  // null means: sort by "waterfall", but don't highlight the table header
+  type: null,
+  ascending: true,
+});
+
+function sortReducer(state = new Sort(), action) {
+  switch (action.type) {
+    case SORT_BY: {
+      return state.withMutations(st => {
+        if (action.sortType == st.type) {
+          st.ascending = !st.ascending;
+        } else {
+          st.type = action.sortType;
+          st.ascending = true;
+        }
+      });
+    }
+    default:
+      return state;
+  }
+}
+
+module.exports = sortReducer;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/reducers/timing-markers.js
@@ -0,0 +1,54 @@
+/* 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 I = require("devtools/client/shared/vendor/immutable");
+const { ADD_TIMING_MARKER,
+        CLEAR_TIMING_MARKERS,
+        CLEAR_REQUESTS } = require("../constants");
+
+const TimingMarkers = I.Record({
+  firstDocumentDOMContentLoadedTimestamp: -1,
+  firstDocumentLoadTimestamp: -1,
+});
+
+function addTimingMarker(state, action) {
+  if (action.marker.name == "document::DOMContentLoaded" &&
+      state.firstDocumentDOMContentLoadedTimestamp == -1) {
+    return state.set("firstDocumentDOMContentLoadedTimestamp",
+                     action.marker.unixTime / 1000);
+  }
+
+  if (action.marker.name == "document::Load" &&
+      state.firstDocumentLoadTimestamp == -1) {
+    return state.set("firstDocumentLoadTimestamp",
+                     action.marker.unixTime / 1000);
+  }
+
+  return state;
+}
+
+function clearTimingMarkers(state) {
+  return state.withMutations(st => {
+    st.remove("firstDocumentDOMContentLoadedTimestamp");
+    st.remove("firstDocumentLoadTimestamp");
+  });
+}
+
+function timingMarkers(state = new TimingMarkers(), action) {
+  switch (action.type) {
+    case ADD_TIMING_MARKER:
+      return addTimingMarker(state, action);
+
+    case CLEAR_REQUESTS:
+    case CLEAR_TIMING_MARKERS:
+      return clearTimingMarkers(state);
+
+    default:
+      return state;
+  }
+}
+
+module.exports = timingMarkers;
--- a/devtools/client/netmonitor/reducers/ui.js
+++ b/devtools/client/netmonitor/reducers/ui.js
@@ -2,39 +2,39 @@
  * 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 I = require("devtools/client/shared/vendor/immutable");
 const {
   OPEN_SIDEBAR,
-  TOGGLE_SIDEBAR,
+  WATERFALL_RESIZE,
 } = require("../constants");
 
-const Sidebar = I.Record({
-  open: false,
-});
-
 const UI = I.Record({
-  sidebar: new Sidebar(),
+  sidebarOpen: false,
+  waterfallWidth: 300,
 });
 
 function openSidebar(state, action) {
-  return state.setIn(["sidebar", "open"], action.open);
+  return state.set("sidebarOpen", action.open);
 }
 
-function toggleSidebar(state, action) {
-  return state.setIn(["sidebar", "open"], !state.sidebar.open);
+// Safe bounds for waterfall width (px)
+const REQUESTS_WATERFALL_SAFE_BOUNDS = 90;
+
+function resizeWaterfall(state, action) {
+  return state.set("waterfallWidth", action.width - REQUESTS_WATERFALL_SAFE_BOUNDS);
 }
 
 function ui(state = new UI(), action) {
   switch (action.type) {
     case OPEN_SIDEBAR:
       return openSidebar(state, action);
-    case TOGGLE_SIDEBAR:
-      return toggleSidebar(state, action);
+    case WATERFALL_RESIZE:
+      return resizeWaterfall(state, action);
     default:
       return state;
   }
 }
 
 module.exports = ui;
--- a/devtools/client/netmonitor/request-list-context-menu.js
+++ b/devtools/client/netmonitor/request-list-context-menu.js
@@ -53,112 +53,110 @@ RequestListContextMenu.prototype = {
       visible: !!selectedItem,
       click: () => this.copyUrl(),
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-copy-url-params",
       label: L10N.getStr("netmonitor.context.copyUrlParams"),
       accesskey: L10N.getStr("netmonitor.context.copyUrlParams.accesskey"),
-      visible: !!(selectedItem && getUrlQuery(selectedItem.attachment.url)),
+      visible: !!(selectedItem && getUrlQuery(selectedItem.url)),
       click: () => this.copyUrlParams(),
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-copy-post-data",
       label: L10N.getStr("netmonitor.context.copyPostData"),
       accesskey: L10N.getStr("netmonitor.context.copyPostData.accesskey"),
-      visible: !!(selectedItem && selectedItem.attachment.requestPostData),
+      visible: !!(selectedItem && selectedItem.requestPostData),
       click: () => this.copyPostData(),
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-copy-as-curl",
       label: L10N.getStr("netmonitor.context.copyAsCurl"),
       accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"),
-      visible: !!(selectedItem && selectedItem.attachment),
+      visible: !!selectedItem,
       click: () => this.copyAsCurl(),
     }));
 
     menu.append(new MenuItem({
       type: "separator",
       visible: !!selectedItem,
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-copy-request-headers",
       label: L10N.getStr("netmonitor.context.copyRequestHeaders"),
       accesskey: L10N.getStr("netmonitor.context.copyRequestHeaders.accesskey"),
-      visible: !!(selectedItem && selectedItem.attachment.requestHeaders),
+      visible: !!(selectedItem && selectedItem.requestHeaders),
       click: () => this.copyRequestHeaders(),
     }));
 
     menu.append(new MenuItem({
       id: "response-menu-context-copy-response-headers",
       label: L10N.getStr("netmonitor.context.copyResponseHeaders"),
       accesskey: L10N.getStr("netmonitor.context.copyResponseHeaders.accesskey"),
-      visible: !!(selectedItem && selectedItem.attachment.responseHeaders),
+      visible: !!(selectedItem && selectedItem.responseHeaders),
       click: () => this.copyResponseHeaders(),
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-copy-response",
       label: L10N.getStr("netmonitor.context.copyResponse"),
       accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
       visible: !!(selectedItem &&
-               selectedItem.attachment.responseContent &&
-               selectedItem.attachment.responseContent.content.text &&
-               selectedItem.attachment.responseContent.content.text.length !== 0),
+               selectedItem.responseContent &&
+               selectedItem.responseContent.content.text &&
+               selectedItem.responseContent.content.text.length !== 0),
       click: () => this.copyResponse(),
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-copy-image-as-data-uri",
       label: L10N.getStr("netmonitor.context.copyImageAsDataUri"),
       accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"),
       visible: !!(selectedItem &&
-               selectedItem.attachment.responseContent &&
-               selectedItem.attachment.responseContent.content
-                 .mimeType.includes("image/")),
+               selectedItem.responseContent &&
+               selectedItem.responseContent.content.mimeType.includes("image/")),
       click: () => this.copyImageAsDataUri(),
     }));
 
     menu.append(new MenuItem({
       type: "separator",
       visible: !!selectedItem,
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-copy-all-as-har",
       label: L10N.getStr("netmonitor.context.copyAllAsHar"),
       accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"),
-      visible: !!this.items.length,
+      visible: this.items.size > 0,
       click: () => this.copyAllAsHar(),
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-save-all-as-har",
       label: L10N.getStr("netmonitor.context.saveAllAsHar"),
       accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"),
-      visible: !!this.items.length,
+      visible: this.items.size > 0,
       click: () => this.saveAllAsHar(),
     }));
 
     menu.append(new MenuItem({
       type: "separator",
       visible: !!selectedItem,
     }));
 
     menu.append(new MenuItem({
       id: "request-menu-context-resend",
       label: L10N.getStr("netmonitor.context.editAndResend"),
       accesskey: L10N.getStr("netmonitor.context.editAndResend.accesskey"),
       visible: !!(NetMonitorController.supportsCustomRequest &&
-               selectedItem &&
-               !selectedItem.attachment.isCustom),
+               selectedItem && !selectedItem.isCustom),
       click: () => NetMonitorView.RequestsMenu.cloneSelectedRequest(),
     }));
 
     menu.append(new MenuItem({
       type: "separator",
       visible: !!selectedItem,
     }));
 
@@ -182,44 +180,43 @@ RequestListContextMenu.prototype = {
     return menu;
   },
 
   /**
    * Opens selected item in a new tab.
    */
   openRequestInTab() {
     let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-    let { url } = this.selectedItem.attachment;
-    win.openUILinkIn(url, "tab", { relatedToCurrent: true });
+    win.openUILinkIn(this.selectedItem.url, "tab", { relatedToCurrent: true });
   },
 
   /**
    * Copy the request url from the currently selected item.
    */
   copyUrl() {
-    clipboardHelper.copyString(this.selectedItem.attachment.url);
+    clipboardHelper.copyString(this.selectedItem.url);
   },
 
   /**
    * Copy the request url query string parameters from the currently
    * selected item.
    */
   copyUrlParams() {
-    let { url } = this.selectedItem.attachment;
+    let { url } = this.selectedItem;
     let params = getUrlQuery(url).split("&");
     let string = params.join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n");
     clipboardHelper.copyString(string);
   },
 
   /**
    * Copy the request form data parameters (or raw payload) from
    * the currently selected item.
    */
   copyPostData: Task.async(function* () {
-    let selected = this.selectedItem.attachment;
+    let selected = this.selectedItem;
 
     // Try to extract any form data parameters.
     let formDataSections = yield getFormDataSections(
       selected.requestHeaders,
       selected.requestHeadersFromUploadStream,
       selected.requestPostData,
       gNetwork.getString.bind(gNetwork));
 
@@ -246,17 +243,17 @@ RequestListContextMenu.prototype = {
 
     clipboardHelper.copyString(string);
   }),
 
   /**
    * Copy a cURL command from the currently selected item.
    */
   copyAsCurl: Task.async(function* () {
-    let selected = this.selectedItem.attachment;
+    let selected = this.selectedItem;
 
     // Create a sanitized object for the Curl command generator.
     let data = {
       url: selected.url,
       method: selected.method,
       headers: [],
       httpVersion: selected.httpVersion,
       postDataText: null
@@ -276,55 +273,51 @@ RequestListContextMenu.prototype = {
 
     clipboardHelper.copyString(Curl.generateCommand(data));
   }),
 
   /**
    * Copy the raw request headers from the currently selected item.
    */
   copyRequestHeaders() {
-    let selected = this.selectedItem.attachment;
-    let rawHeaders = selected.requestHeaders.rawHeaders.trim();
+    let rawHeaders = this.selectedItem.requestHeaders.rawHeaders.trim();
     if (Services.appinfo.OS !== "WINNT") {
       rawHeaders = rawHeaders.replace(/\r/g, "");
     }
     clipboardHelper.copyString(rawHeaders);
   },
 
   /**
    * Copy the raw response headers from the currently selected item.
    */
   copyResponseHeaders() {
-    let selected = this.selectedItem.attachment;
-    let rawHeaders = selected.responseHeaders.rawHeaders.trim();
+    let rawHeaders = this.selectedItem.responseHeaders.rawHeaders.trim();
     if (Services.appinfo.OS !== "WINNT") {
       rawHeaders = rawHeaders.replace(/\r/g, "");
     }
     clipboardHelper.copyString(rawHeaders);
   },
 
   /**
    * Copy image as data uri.
    */
   copyImageAsDataUri() {
-    let selected = this.selectedItem.attachment;
-    let { mimeType, text, encoding } = selected.responseContent.content;
+    const { mimeType, text, encoding } = this.selectedItem.responseContent.content;
 
     gNetwork.getString(text).then(string => {
       let data = formDataURI(mimeType, encoding, string);
       clipboardHelper.copyString(data);
     });
   },
 
   /**
    * Copy response data as a string.
    */
   copyResponse() {
-    let selected = this.selectedItem.attachment;
-    let text = selected.responseContent.content.text;
+    const { text } = this.selectedItem.responseContent.content;
 
     gNetwork.getString(text).then(string => {
       clipboardHelper.copyString(string);
     });
   },
 
   /**
    * Copy HAR from the network panel content to the clipboard.
@@ -343,16 +336,15 @@ RequestListContextMenu.prototype = {
   },
 
   getDefaultHarOptions() {
     let form = NetMonitorController._target.form;
     let title = form.title || form.url;
 
     return {
       getString: gNetwork.getString.bind(gNetwork),
-      view: NetMonitorView.RequestsMenu,
-      items: NetMonitorView.RequestsMenu.items,
+      items: this.items,
       title: title
     };
   }
 };
 
 module.exports = RequestListContextMenu;
--- a/devtools/client/netmonitor/request-utils.js
+++ b/devtools/client/netmonitor/request-utils.js
@@ -45,18 +45,18 @@ function getKeyWithEvent(callback, onlyS
  * @param {object} postData - the "requestPostData".
  * @param {function} getString - callback to retrieve a string from a LongStringGrip.
  * @return {array} a promise list that is resolved with the extracted form data.
  */
 const getFormDataSections = Task.async(function* (headers, uploadHeaders, postData,
                                                     getString) {
   let formDataSections = [];
 
-  let { headers: requestHeaders } = headers;
-  let { headers: payloadHeaders } = uploadHeaders;
+  let requestHeaders = headers.headers;
+  let payloadHeaders = uploadHeaders ? uploadHeaders.headers : [];
   let allHeaders = [...payloadHeaders, ...requestHeaders];
 
   let contentTypeHeader = allHeaders.find(e => {
     return e.name.toLowerCase() == "content-type";
   });
 
   let contentTypeLongString = contentTypeHeader ? contentTypeHeader.value : "";
 
@@ -184,16 +184,47 @@ function getUrlHostName(url) {
  * @param {string} url - url string
  * @return {string} unicode host of a url
  */
 function getUrlHost(url) {
   return decodeUnicodeUrl((new URL(url)).host);
 }
 
 /**
+ * Extract several details fields from a URL at once.
+ */
+function getUrlDetails(url) {
+  let baseNameWithQuery = getUrlBaseNameWithQuery(url);
+  let host = getUrlHost(url);
+  let hostname = getUrlHostName(url);
+  let unicodeUrl = decodeUnicodeUrl(url);
+
+  // Mark local hosts specially, where "local" is  as defined in the W3C
+  // spec for secure contexts.
+  // http://www.w3.org/TR/powerful-features/
+  //
+  //  * If the name falls under 'localhost'
+  //  * If the name is an IPv4 address within 127.0.0.0/8
+  //  * If the name is an IPv6 address within ::1/128
+  //
+  // IPv6 parsing is a little sloppy; it assumes that the address has
+  // been validated before it gets here.
+  let isLocal = hostname.match(/(.+\.)?localhost$/) ||
+                hostname.match(/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}/) ||
+                hostname.match(/\[[0:]+1\]/);
+
+  return {
+    baseNameWithQuery,
+    host,
+    unicodeUrl,
+    isLocal
+  };
+}
+
+/**
  * Parse a url's query string into its components
  *
  * @param {string} query - query string of a url portion
  * @return {array} array of query params { name, value }
  */
 function parseQueryString(query) {
   if (!query) {
     return null;
@@ -248,11 +279,12 @@ module.exports = {
   writeHeaderText,
   decodeUnicodeUrl,
   getAbbreviatedMimeType,
   getUrlBaseName,
   getUrlQuery,
   getUrlBaseNameWithQuery,
   getUrlHostName,
   getUrlHost,
+  getUrlDetails,
   parseQueryString,
   loadCauseString,
 };
--- a/devtools/client/netmonitor/requests-menu-view.js
+++ b/devtools/client/netmonitor/requests-menu-view.js
@@ -1,90 +1,46 @@
 /* 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/. */
 
-/* eslint-disable mozilla/reject-some-requires */
-/* globals document, window, dumpn, $, gNetwork, EVENTS, Prefs,
-           NetMonitorController, NetMonitorView */
+/* globals window, dumpn, $, gNetwork, NetMonitorController, NetMonitorView */
 
 "use strict";
 
-const { Cu } = require("chrome");
-const {Task} = require("devtools/shared/task");
-const {DeferredTask} = Cu.import("resource://gre/modules/DeferredTask.jsm", {});
-const {SideMenuWidget} = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
-const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
-const {setImageTooltip, getImageDimensions} =
-  require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
-const {Heritage, WidgetMethods, setNamedTimeout} =
-  require("devtools/client/shared/widgets/view-helpers");
-const {CurlUtils} = require("devtools/client/shared/curl");
-const {Filters, isFreetextMatch} = require("./filter-predicates");
-const {Sorters} = require("./sort-predicates");
-const {L10N, WEBCONSOLE_L10N} = require("./l10n");
-const {formDataURI,
-       writeHeaderText,
-       decodeUnicodeUrl,
-       getKeyWithEvent,
-       getAbbreviatedMimeType,
-       getUrlBaseNameWithQuery,
-       getUrlHost,
-       getUrlHostName,
-       loadCauseString} = require("./request-utils");
+const { Task } = require("devtools/shared/task");
+const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const { CurlUtils } = require("devtools/client/shared/curl");
+const { L10N } = require("./l10n");
+const { EVENTS } = require("./events");
+const { createElement, createFactory } = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+const RequestList = createFactory(require("./components/request-list"));
+const RequestListContextMenu = require("./request-list-context-menu");
 const Actions = require("./actions/index");
-const RequestListContextMenu = require("./request-list-context-menu");
+const { Prefs } = require("./prefs");
+
+const {
+  formDataURI,
+  writeHeaderText,
+  loadCauseString
+} = require("./request-utils");
 
-const HTML_NS = "http://www.w3.org/1999/xhtml";
-const EPSILON = 0.001;
+const {
+  getActiveFilters,
+  getSortedRequests,
+  getDisplayedRequests,
+  getRequestById,
+  getSelectedRequest
+} = require("./selectors/index");
+
 // ms
 const RESIZE_REFRESH_RATE = 50;
-// ms
-const REQUESTS_REFRESH_RATE = 50;
-// tooltip show/hide delay in ms
-const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
-// px
-const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
-// px
-const REQUESTS_TOOLTIP_STACK_TRACE_WIDTH = 600;
-// px
-const REQUESTS_WATERFALL_SAFE_BOUNDS = 90;
-// ms
-const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5;
-// px
-const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60;
-// ms
-const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5;
-const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3;
-// px
-const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10;
-const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
-const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32;
-// byte
-const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32;
-const REQUESTS_WATERFALL_DOMCONTENTLOADED_TICKS_COLOR_RGBA = [255, 0, 0, 128];
-const REQUESTS_WATERFALL_LOAD_TICKS_COLOR_RGBA = [0, 0, 255, 128];
-
-// Constants for formatting bytes.
-const BYTES_IN_KB = 1024;
-const BYTES_IN_MB = Math.pow(BYTES_IN_KB, 2);
-const BYTES_IN_GB = Math.pow(BYTES_IN_KB, 3);
-const MAX_BYTES_SIZE = 1000;
-const MAX_KB_SIZE = 1000 * BYTES_IN_KB;
-const MAX_MB_SIZE = 1000 * BYTES_IN_MB;
-
-// TODO: duplicated from netmonitor-view.js. Move to a format-utils.js module.
-const REQUEST_TIME_DECIMALS = 2;
-const CONTENT_SIZE_DECIMALS = 2;
-
-const CONTENT_MIME_TYPE_ABBREVIATIONS = {
-  "ecmascript": "js",
-  "javascript": "js",
-  "x-javascript": "js"
-};
 
 // A smart store watcher to notify store changes as necessary
 function storeWatcher(initialValue, reduceValue, onChange) {
   let currentValue = initialValue;
 
   return () => {
     const oldValue = currentValue;
     const newValue = reduceValue(currentValue);
@@ -93,1462 +49,337 @@ function storeWatcher(initialValue, redu
       onChange(newValue, oldValue);
     }
   };
 }
 
 /**
  * Functions handling the requests menu (containing details about each request,
  * like status, method, file, domain, as well as a waterfall representing
- * timing imformation).
+ * timing information).
  */
 function RequestsMenuView() {
   dumpn("RequestsMenuView was instantiated");
-
-  this._flushRequests = this._flushRequests.bind(this);
-  this._onHover = this._onHover.bind(this);
-  this._onSelect = this._onSelect.bind(this);
-  this._onSwap = this._onSwap.bind(this);
-  this._onResize = this._onResize.bind(this);
-  this._onScroll = this._onScroll.bind(this);
-  this._onSecurityIconClick = this._onSecurityIconClick.bind(this);
 }
 
-RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
+RequestsMenuView.prototype = {
   /**
    * Initialization function, called when the network monitor is started.
    */
   initialize: function (store) {
     dumpn("Initializing the RequestsMenuView");
 
     this.store = store;
 
     this.contextMenu = new RequestListContextMenu();
 
-    let widgetParentEl = $("#requests-menu-contents");
-    this.widget = new SideMenuWidget(widgetParentEl);
-    this._splitter = $("#network-inspector-view-splitter");
-
-    // Create a tooltip for the newly appended network request item.
-    this.tooltip = new HTMLTooltip(NetMonitorController._toolbox.doc, { type: "arrow" });
-    this.tooltip.startTogglingOnHover(widgetParentEl, this._onHover, {
-      toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
-      interactive: true
-    });
-
-    this.sortContents((a, b) => Sorters.waterfall(a.attachment, b.attachment));
+    Prefs.filters.forEach(type => store.dispatch(Actions.toggleFilterType(type)));
 
-    this.allowFocusOnRightClick = true;
-    this.maintainSelectionVisible = true;
-
-    this.widget.addEventListener("select", this._onSelect, false);
-    this.widget.addEventListener("swap", this._onSwap, false);
-    this._splitter.addEventListener("mousemove", this._onResize, false);
-    window.addEventListener("resize", this._onResize, false);
+    // Watch selection changes
+    this.store.subscribe(storeWatcher(
+      null,
+      () => getSelectedRequest(this.store.getState()),
+      (newSelected, oldSelected) => this.onSelectionUpdate(newSelected, oldSelected)
+    ));
 
-    this.requestsMenuSortEvent = getKeyWithEvent(this.sortBy.bind(this));
-    this.requestsMenuSortKeyboardEvent = getKeyWithEvent(this.sortBy.bind(this), true);
-    this._onContextMenu = this._onContextMenu.bind(this);
     this._onContextPerfCommand = () => NetMonitorView.toggleFrontendMode();
-    this._onReloadCommand = () => NetMonitorView.reloadPage();
-    this._flushRequestsTask = new DeferredTask(this._flushRequests,
-      REQUESTS_REFRESH_RATE);
 
     this.sendCustomRequestEvent = this.sendCustomRequest.bind(this);
     this.closeCustomRequestEvent = this.closeCustomRequest.bind(this);
     this.cloneSelectedRequestEvent = this.cloneSelectedRequest.bind(this);
     this.toggleRawHeadersEvent = this.toggleRawHeaders.bind(this);
 
-    this.reFilterRequests = this.reFilterRequests.bind(this);
+    $("#toggle-raw-headers")
+      .addEventListener("click", this.toggleRawHeadersEvent, false);
 
-    $("#toolbar-labels").addEventListener("click",
-      this.requestsMenuSortEvent, false);
-    $("#toolbar-labels").addEventListener("keydown",
-      this.requestsMenuSortKeyboardEvent, false);
-    $("#toggle-raw-headers").addEventListener("click",
-      this.toggleRawHeadersEvent, false);
-    $("#requests-menu-contents").addEventListener("scroll", this._onScroll, true);
-    $("#requests-menu-contents").addEventListener("contextmenu", this._onContextMenu);
+    this._summary = $("#requests-menu-network-summary-button");
+    this._summary.setAttribute("label", L10N.getStr("networkMenu.empty"));
 
-    this.unsubscribeStore = store.subscribe(storeWatcher(
-      null,
-      () => store.getState().filters,
-      (newFilters) => {
-        this._activeFilters = newFilters.types
-          .toSeq()
-          .filter((checked, key) => checked)
-          .keySeq()
-          .toArray();
-        this._currentFreetextFilter = newFilters.url;
-        this.reFilterRequests();
-      }
-    ));
+    this.onResize = this.onResize.bind(this);
+    this._splitter = $("#network-inspector-view-splitter");
+    this._splitter.addEventListener("mousemove", this.onResize, false);
+    window.addEventListener("resize", this.onResize, false);
 
-    Prefs.filters.forEach(type =>
-      store.dispatch(Actions.toggleFilterType(type)));
+    this.tooltip = new HTMLTooltip(NetMonitorController._toolbox.doc, { type: "arrow" });
+
+    this.mountPoint = $("#network-table");
+    ReactDOM.render(createElement(Provider,
+      { store: this.store },
+      RequestList()
+    ), this.mountPoint);
 
     window.once("connected", this._onConnect.bind(this));
   },
 
-  _onConnect: function () {
-    $("#requests-menu-reload-notice-button").addEventListener("command",
-      this._onReloadCommand, false);
-
+  _onConnect() {
     if (NetMonitorController.supportsCustomRequest) {
-      $("#custom-request-send-button").addEventListener("click",
-        this.sendCustomRequestEvent, false);
-      $("#custom-request-close-button").addEventListener("click",
-        this.closeCustomRequestEvent, false);
-      $("#headers-summary-resend").addEventListener("click",
-        this.cloneSelectedRequestEvent, false);
+      $("#custom-request-send-button")
+        .addEventListener("click", this.sendCustomRequestEvent, false);
+      $("#custom-request-close-button")
+        .addEventListener("click", this.closeCustomRequestEvent, false);
+      $("#headers-summary-resend")
+        .addEventListener("click", this.cloneSelectedRequestEvent, false);
     } else {
       $("#headers-summary-resend").hidden = true;
     }
 
-    if (NetMonitorController.supportsPerfStats) {
-      $("#requests-menu-perf-notice-button").addEventListener("command",
-        this._onContextPerfCommand, false);
-      $("#network-statistics-back-button").addEventListener("command",
-        this._onContextPerfCommand, false);
-    } else {
-      $("#notice-perf-message").hidden = true;
-    }
-
-    if (!NetMonitorController.supportsTransferredResponseSize) {
-      $("#requests-menu-transferred-header-box").hidden = true;
-      $("#requests-menu-item-template .requests-menu-transferred")
-        .hidden = true;
-    }
+    $("#network-statistics-back-button")
+      .addEventListener("command", this._onContextPerfCommand, false);
   },
 
   /**
    * Destruction function, called when the network monitor is closed.
    */
-  destroy: function () {
+  destroy() {
     dumpn("Destroying the RequestsMenuView");
 
-    Prefs.filters = this._activeFilters;
-
-    /* Destroy the tooltip */
-    this.tooltip.stopTogglingOnHover();
-    this.tooltip.destroy();
-    $("#requests-menu-contents").removeEventListener("scroll", this._onScroll, true);
-    $("#requests-menu-contents").removeEventListener("contextmenu", this._onContextMenu);
+    Prefs.filters = getActiveFilters(this.store.getState());
 
-    this.widget.removeEventListener("select", this._onSelect, false);
-    this.widget.removeEventListener("swap", this._onSwap, false);
-    this._splitter.removeEventListener("mousemove", this._onResize, false);
-    window.removeEventListener("resize", this._onResize, false);
-
-    $("#toolbar-labels").removeEventListener("click",
-      this.requestsMenuSortEvent, false);
-    $("#toolbar-labels").removeEventListener("keydown",
-      this.requestsMenuSortKeyboardEvent, false);
+    // this.flushRequestsTask.disarm();
 
-    this._flushRequestsTask.disarm();
-
-    $("#requests-menu-reload-notice-button").removeEventListener("command",
-      this._onReloadCommand, false);
-    $("#requests-menu-perf-notice-button").removeEventListener("command",
-      this._onContextPerfCommand, false);
-    $("#network-statistics-back-button").removeEventListener("command",
-      this._onContextPerfCommand, false);
+    $("#network-statistics-back-button")
+      .removeEventListener("command", this._onContextPerfCommand, false);
+    $("#custom-request-send-button")
+      .removeEventListener("click", this.sendCustomRequestEvent, false);
+    $("#custom-request-close-button")
+      .removeEventListener("click", this.closeCustomRequestEvent, false);
+    $("#headers-summary-resend")
+      .removeEventListener("click", this.cloneSelectedRequestEvent, false);
+    $("#toggle-raw-headers")
+      .removeEventListener("click", this.toggleRawHeadersEvent, false);
 
-    $("#custom-request-send-button").removeEventListener("click",
-      this.sendCustomRequestEvent, false);
-    $("#custom-request-close-button").removeEventListener("click",
-      this.closeCustomRequestEvent, false);
-    $("#headers-summary-resend").removeEventListener("click",
-      this.cloneSelectedRequestEvent, false);
-    $("#toggle-raw-headers").removeEventListener("click",
-      this.toggleRawHeadersEvent, false);
+    this._splitter.removeEventListener("mousemove", this.onResize, false);
+    window.removeEventListener("resize", this.onResize, false);
 
-    this.unsubscribeStore();
+    this.tooltip.destroy();
+
+    ReactDOM.unmountComponentAtNode(this.mountPoint);
   },
 
   /**
    * Resets this container (removes all the networking information).
    */
-  reset: function () {
-    this.empty();
-    this._addQueue = [];
-    this._updateQueue = [];
-    this._firstRequestStartedMillis = -1;
-    this._lastRequestEndedMillis = -1;
+  reset() {
+    this.store.dispatch(Actions.batchReset());
+    this.store.dispatch(Actions.clearRequests());
   },
 
   /**
-   * Specifies if this view may be updated lazily.
+   * Removes all network requests and closes the sidebar if open.
    */
-  _lazyUpdate: true,
+  clear() {
+    this.store.dispatch(Actions.clearRequests());
+  },
+
+  addRequest(id, data) {
+    let { method, url, isXHR, cause, startedDateTime, fromCache,
+          fromServiceWorker } = data;
+
+    // Convert the received date/time string to a unix timestamp.
+    let startedMillis = Date.parse(startedDateTime);
 
-  get lazyUpdate() {
-    return this._lazyUpdate;
+    // Convert the cause from a Ci.nsIContentPolicy constant to a string
+    if (cause) {
+      let type = loadCauseString(cause.type);
+      cause = Object.assign({}, cause, { type });
+    }
+
+    const action = Actions.addRequest(
+      id,
+      {
+        startedMillis,
+        method,
+        url,
+        isXHR,
+        cause,
+        fromCache,
+        fromServiceWorker
+      },
+      true
+    );
+
+    this.store.dispatch(action).then(() => window.emit(EVENTS.REQUEST_ADDED, action.id));
   },
 
+  updateRequest: Task.async(function* (id, data) {
+    const action = Actions.updateRequest(id, data, true);
+    yield this.store.dispatch(action);
+
+    const { responseContent, requestPostData } = action.data;
+
+    // Fetch response data if the response is an image (to display thumbnail)
+    if (responseContent && responseContent.content) {
+      let request = getRequestById(this.store.getState(), action.id);
+      if (request) {
+        let { mimeType } = request;
+        if (mimeType.includes("image/")) {
+          let { text, encoding } = responseContent.content;
+          let responseBody = yield gNetwork.getString(text);
+          const dataUri = formDataURI(mimeType, encoding, responseBody);
+          yield this.store.dispatch(Actions.updateRequest(
+            action.id,
+            { responseContentDataUri: dataUri },
+            true
+          ));
+          window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
+        }
+      }
+    }
+
+    // Search the POST data upload stream for request headers and add
+    // them as a separate property, different from the classic headers.
+    if (requestPostData && requestPostData.postData) {
+      let { text } = requestPostData.postData;
+      let postData = yield gNetwork.getString(text);
+      const headers = CurlUtils.getHeadersFromMultipartText(postData);
+      const headersSize = headers.reduce((acc, { name, value }) => {
+        return acc + name.length + value.length + 2;
+      }, 0);
+      yield this.store.dispatch(Actions.updateRequest(action.id, {
+        requestHeadersFromUploadStream: { headers, headersSize }
+      }, true));
+    }
+  }),
+
+  /**
+   * Disable batched updates. Used by tests.
+   */
   set lazyUpdate(value) {
-    this._lazyUpdate = value;
-    if (!value) {
-      this._flushRequests();
+    this.store.dispatch(Actions.batchEnable(value));
+  },
+
+  get items() {
+    return getSortedRequests(this.store.getState());
+  },
+
+  get visibleItems() {
+    return getDisplayedRequests(this.store.getState());
+  },
+
+  get itemCount() {
+    return this.store.getState().requests.requests.size;
+  },
+
+  getItemAtIndex(index) {
+    return getSortedRequests(this.store.getState()).get(index);
+  },
+
+  get selectedIndex() {
+    const state = this.store.getState();
+    if (!state.requests.selectedId) {
+      return -1;
+    }
+    return getSortedRequests(state).findIndex(r => r.id === state.requests.selectedId);
+  },
+
+  set selectedIndex(index) {
+    const requests = getSortedRequests(this.store.getState());
+    let itemId = null;
+    if (index >= 0 && index < requests.size) {
+      itemId = requests.get(index).id;
+    }
+    this.store.dispatch(Actions.selectRequest(itemId));
+  },
+
+  get selectedItem() {
+    return getSelectedRequest(this.store.getState());
+  },
+
+  set selectedItem(item) {
+    this.store.dispatch(Actions.selectRequest(item ? item.id : null));
+  },
+
+  /**
+   * Updates the sidebar status when something about the selection changes
+   */
+  onSelectionUpdate(newSelected, oldSelected) {
+    if (newSelected && oldSelected && newSelected.id === oldSelected.id) {
+      // The same item is still selected, its data only got updated
+      NetMonitorView.NetworkDetails.populate(newSelected);
+    } else if (newSelected) {
+      // Another item just got selected
+      NetMonitorView.Sidebar.populate(newSelected);
+      NetMonitorView.Sidebar.toggle(true);
+    } else {
+      // Selection just got empty
+      NetMonitorView.Sidebar.toggle(false);
     }
   },
 
   /**
-   * Adds a network request to this container.
-   *
-   * @param string id
-   *        An identifier coming from the network monitor controller.
-   * @param string startedDateTime
-   *        A string representation of when the request was started, which
-   *        can be parsed by Date (for example "2012-09-17T19:50:03.699Z").
-   * @param string method
-   *        Specifies the request method (e.g. "GET", "POST", etc.)
-   * @param string url
-   *        Specifies the request's url.
-   * @param boolean isXHR
-   *        True if this request was initiated via XHR.
-   * @param object cause
-   *        Specifies the request's cause. Has the following properties:
-   *        - type: nsContentPolicyType constant
-   *        - loadingDocumentUri: URI of the request origin
-   *        - stacktrace: JS stacktrace of the request
-   * @param boolean fromCache
-   *        Indicates if the result came from the browser cache
-   * @param boolean fromServiceWorker
-   *        Indicates if the request has been intercepted by a Service Worker
+   * The resize listener for this container's window.
    */
-  addRequest: function (id, startedDateTime, method, url, isXHR, cause,
-    fromCache, fromServiceWorker) {
-    this._addQueue.push([id, startedDateTime, method, url, isXHR, cause,
-      fromCache, fromServiceWorker]);
-
-    // Lazy updating is disabled in some tests.
-    if (!this.lazyUpdate) {
-      return void this._flushRequests();
-    }
-
-    this._flushRequestsTask.arm();
-    return undefined;
+  onResize() {
+    // Allow requests to settle down first.
+    setNamedTimeout("resize-events", RESIZE_REFRESH_RATE, () => {
+      const waterfallHeaderEl = $("#requests-menu-waterfall-header-box");
+      if (waterfallHeaderEl) {
+        const { width } = waterfallHeaderEl.getBoundingClientRect();
+        this.store.dispatch(Actions.resizeWaterfall(width));
+      }
+    });
   },
 
   /**
    * Create a new custom request form populated with the data from
    * the currently selected request.
    */
-  cloneSelectedRequest: function () {
-    let selected = this.selectedItem.attachment;
+  cloneSelectedRequest() {
+    this.store.dispatch(Actions.cloneSelectedRequest());
+  },
 
-    // Create the element node for the network request item.
-    let menuView = this._createMenuView(selected.method, selected.url,
-      selected.cause);
+  /**
+   * Shows raw request/response headers in textboxes.
+   */
+  toggleRawHeaders: function () {
+    let requestTextarea = $("#raw-request-headers-textarea");
+    let responseTextarea = $("#raw-response-headers-textarea");
+    let rawHeadersHidden = $("#raw-headers").getAttribute("hidden");
 
-    // Append a network request item to this container.
-    let newItem = this.push([menuView], {
-      attachment: Object.create(selected, {
-        isCustom: { value: true }
-      })
-    });
-
-    // Immediately switch to new request pane.
-    this.selectedItem = newItem;
+    if (rawHeadersHidden) {
+      let selected = getSelectedRequest(this.store.getState());
+      let selectedRequestHeaders = selected.requestHeaders.headers;
+      let selectedResponseHeaders = selected.responseHeaders.headers;
+      requestTextarea.value = writeHeaderText(selectedRequestHeaders);
+      responseTextarea.value = writeHeaderText(selectedResponseHeaders);
+      $("#raw-headers").hidden = false;
+    } else {
+      requestTextarea.value = null;
+      responseTextarea.value = null;
+      $("#raw-headers").hidden = true;
+    }
   },
 
   /**
    * Send a new HTTP request using the data in the custom request form.
    */
   sendCustomRequest: function () {
-    let selected = this.selectedItem.attachment;
+    let selected = getSelectedRequest(this.store.getState());
 
     let data = {
       url: selected.url,
       method: selected.method,
       httpVersion: selected.httpVersion,
     };
     if (selected.requestHeaders) {
       data.headers = selected.requestHeaders.headers;
     }
     if (selected.requestPostData) {
       data.body = selected.requestPostData.postData.text;
     }
 
     NetMonitorController.webConsoleClient.sendHTTPRequest(data, response => {
       let id = response.eventActor.actor;
-      this._preferredItemId = id;
+      this.store.dispatch(Actions.preselectRequest(id));
     });
 
     this.closeCustomRequest();
   },
 
   /**
    * Remove the currently selected custom request.
    */
-  closeCustomRequest: function () {
-    this.remove(this.selectedItem);
-    NetMonitorView.Sidebar.toggle(false);
-  },
-
-  /**
-   * Shows raw request/response headers in textboxes.
-   */
-  toggleRawHeaders: function () {
-    let requestTextarea = $("#raw-request-headers-textarea");
-    let responseTextare = $("#raw-response-headers-textarea");
-    let rawHeadersHidden = $("#raw-headers").getAttribute("hidden");
-
-    if (rawHeadersHidden) {
-      let selected = this.selectedItem.attachment;
-      let selectedRequestHeaders = selected.requestHeaders.headers;
-      let selectedResponseHeaders = selected.responseHeaders.headers;
-      requestTextarea.value = writeHeaderText(selectedRequestHeaders);
-      responseTextare.value = writeHeaderText(selectedResponseHeaders);
-      $("#raw-headers").hidden = false;
-    } else {
-      requestTextarea.value = null;
-      responseTextare.value = null;
-      $("#raw-headers").hidden = true;
-    }
-  },
-
-  /**
-   * Refreshes the view contents with the newly selected filters
-   */
-  reFilterRequests: function () {
-    this.filterContents(this._filterPredicate);
-    this.updateRequests();
-    this.refreshZebra();
-  },
-
-  /**
-   * Returns a predicate that can be used to test if a request matches any of
-   * the active filters.
-   */
-  get _filterPredicate() {
-    let currentFreetextFilter = this._currentFreetextFilter;
-
-    return requestItem => {
-      const { attachment } = requestItem;
-      return this._activeFilters.some(filterName => Filters[filterName](attachment)) &&
-          isFreetextMatch(attachment, currentFreetextFilter);
-    };
-  },
-
-  /**
-   * Sorts all network requests in this container by a specified detail.
-   *
-   * @param string type
-   *        Either "status", "method", "file", "domain", "type", "transferred",
-   *        "size" or "waterfall".
-   */
-  sortBy: function (type = "waterfall") {
-    let target = $("#requests-menu-" + type + "-button");
-    let headers = document.querySelectorAll(".requests-menu-header-button");
-
-    for (let header of headers) {
-      if (header != target) {
-        header.removeAttribute("sorted");
-        header.removeAttribute("tooltiptext");
-        header.parentNode.removeAttribute("active");
-      }
-    }
-
-    let direction = "";
-    if (target) {
-      if (target.getAttribute("sorted") == "ascending") {
-        target.setAttribute("sorted", direction = "descending");
-        target.setAttribute("tooltiptext",
-          L10N.getStr("networkMenu.sortedDesc"));
-      } else {
-        target.setAttribute("sorted", direction = "ascending");
-        target.setAttribute("tooltiptext",
-          L10N.getStr("networkMenu.sortedAsc"));
-      }
-      // Used to style the next column.
-      target.parentNode.setAttribute("active", "true");
-    }
-
-    // Sort by whatever was requested.
-    switch (type) {
-      case "status":
-        if (direction == "ascending") {
-          this.sortContents((a, b) => Sorters.status(a.attachment, b.attachment));
-        } else {
-          this.sortContents((a, b) => -Sorters.status(a.attachment, b.attachment));
-        }
-        break;
-      case "method":
-        if (direction == "ascending") {
-          this.sortContents((a, b) => Sorters.method(a.attachment, b.attachment));
-        } else {
-          this.sortContents((a, b) => -Sorters.method(a.attachment, b.attachment));
-        }
-        break;
-      case "file":
-        if (direction == "ascending") {
-          this.sortContents((a, b) => Sorters.file(a.attachment, b.attachment));
-        } else {
-          this.sortContents((a, b) => -Sorters.file(a.attachment, b.attachment));
-        }
-        break;
-      case "domain":
-        if (direction == "ascending") {
-          this.sortContents((a, b) => Sorters.domain(a.attachment, b.attachment));
-        } else {
-          this.sortContents((a, b) => -Sorters.domain(a.attachment, b.attachment));
-        }
-        break;
-      case "cause":
-        if (direction == "ascending") {
-          this.sortContents((a, b) => Sorters.cause(a.attachment, b.attachment));
-        } else {
-          this.sortContents((a, b) => -Sorters.cause(a.attachment, b.attachment));
-        }
-        break;
-      case "type":
-        if (direction == "ascending") {
-          this.sortContents((a, b) => Sorters.type(a.attachment, b.attachment));
-        } else {
-          this.sortContents((a, b) => -Sorters.type(a.attachment, b.attachment));
-        }
-        break;
-      case "transferred":
-        if (direction == "ascending") {
-          this.sortContents((a, b) => Sorters.transferred(a.attachment, b.attachment));
-        } else {
-          this.sortContents((a, b) => -Sorters.transferred(a.attachment, b.attachment));
-        }
-        break;
-      case "size":
-        if (direction == "ascending") {
-          this.sortContents((a, b) => Sorters.size(a.attachment, b.attachment));
-        } else {
-          this.sortContents((a, b) => -Sorters.size(a.attachment, b.attachment));
-        }
-        break;
-      case "waterfall":
-        if (direction == "ascending") {
-          this.sortContents((a, b) => Sorters.waterfall(a.attachment, b.attachment));
-        } else {
-          this.sortContents((a, b) => -Sorters.waterfall(a.attachment, b.attachment));
-        }
-        break;
-    }
-
-    this.updateRequests();
-    this.refreshZebra();
-  },
-
-  /**
-   * Removes all network requests and closes the sidebar if open.
-   */
-  clear: function () {
-    NetMonitorController.NetworkEventsHandler.clearMarkers();
-    NetMonitorView.Sidebar.toggle(false);
-
-    $("#requests-menu-empty-notice").hidden = false;
-
-    this.empty();
-    this.updateRequests();
-  },
-
-  /**
-   * Update store request itmes and trigger related UI update
-   */
-  updateRequests: function () {
-    this.store.dispatch(Actions.updateRequests(this.visibleItems));
-  },
-
-  /**
-   * Adds odd/even attributes to all the visible items in this container.
-   */
-  refreshZebra: function () {
-    let visibleItems = this.visibleItems;
-
-    for (let i = 0, len = visibleItems.length; i < len; i++) {
-      let requestItem = visibleItems[i];
-      let requestTarget = requestItem.target;
-
-      if (i % 2 == 0) {
-        requestTarget.setAttribute("even", "");
-        requestTarget.removeAttribute("odd");
-      } else {
-        requestTarget.setAttribute("odd", "");
-        requestTarget.removeAttribute("even");
-      }
-    }
-  },
-
-  /**
-   * Attaches security icon click listener for the given request menu item.
-   *
-   * @param object item
-   *        The network request item to attach the listener to.
-   */
-  attachSecurityIconClickListener: function ({ target }) {
-    let icon = $(".requests-security-state-icon", target);
-    icon.addEventListener("click", this._onSecurityIconClick);
-  },
-
-  /**
-   * Schedules adding additional information to a network request.
-   *
-   * @param string id
-   *        An identifier coming from the network monitor controller.
-   * @param object data
-   *        An object containing several { key: value } tuples of network info.
-   *        Supported keys are "httpVersion", "status", "statusText" etc.
-   * @param function callback
-   *        A function to call once the request has been updated in the view.
-   */
-  updateRequest: function (id, data, callback) {
-    this._updateQueue.push([id, data, callback]);
-
-    // Lazy updating is disabled in some tests.
-    if (!this.lazyUpdate) {
-      return void this._flushRequests();
-    }
-
-    this._flushRequestsTask.arm();
-    return undefined;
-  },
-
-  /**
-   * Starts adding all queued additional information about network requests.
-   */
-  _flushRequests: function () {
-    // Prevent displaying any updates received after the target closed.
-    if (NetMonitorView._isDestroyed) {
-      return;
-    }
-
-    let widget = NetMonitorView.RequestsMenu.widget;
-    let isScrolledToBottom = widget.isScrolledToBottom();
-
-    for (let [id, startedDateTime, method, url, isXHR, cause, fromCache,
-      fromServiceWorker] of this._addQueue) {
-      // Convert the received date/time string to a unix timestamp.
-      let unixTime = Date.parse(startedDateTime);
-
-      // Create the element node for the network request item.
-      let menuView = this._createMenuView(method, url, cause);
-
-      // Remember the first and last event boundaries.
-      this._registerFirstRequestStart(unixTime);
-      this._registerLastRequestEnd(unixTime);
-
-      // Append a network request item to this container.
-      let requestItem = this.push([menuView, id], {
-        attachment: {
-          startedDeltaMillis: unixTime - this._firstRequestStartedMillis,
-          startedMillis: unixTime,
-          method: method,
-          url: url,
-          isXHR: isXHR,
-          cause: cause,
-          fromCache: fromCache,
-          fromServiceWorker: fromServiceWorker
-        }
-      });
-
-      if (id == this._preferredItemId) {
-        this.selectedItem = requestItem;
-      }
-
-      window.emit(EVENTS.REQUEST_ADDED, id);
-    }
-
-    if (isScrolledToBottom && this._addQueue.length) {
-      widget.scrollToBottom();
-    }
-
-    // For each queued additional information packet, get the corresponding
-    // request item in the view and update it based on the specified data.
-    for (let [id, data, callback] of this._updateQueue) {
-      let requestItem = this.getItemByValue(id);
-      if (!requestItem) {
-        // Packet corresponds to a dead request item, target navigated.
-        continue;
-      }
-
-      // Each information packet may contain several { key: value } tuples of
-      // network info, so update the view based on each one.
-      for (let key in data) {
-        let val = data[key];
-        if (val === undefined) {
-          // The information in the packet is empty, it can be safely ignored.
-          continue;
-        }
-
-        switch (key) {
-          case "requestHeaders":
-            requestItem.attachment.requestHeaders = val;
-            break;
-          case "requestCookies":
-            requestItem.attachment.requestCookies = val;
-            break;
-          case "requestPostData":
-            // Search the POST data upload stream for request headers and add
-            // them to a separate store, different from the classic headers.
-            // XXX: Be really careful here! We're creating a function inside
-            // a loop, so remember the actual request item we want to modify.
-            let currentItem = requestItem;
-            let currentStore = { headers: [], headersSize: 0 };
-
-            Task.spawn(function* () {
-              let postData = yield gNetwork.getString(val.postData.text);
-              let payloadHeaders = CurlUtils.getHeadersFromMultipartText(
-                postData);
-
-              currentStore.headers = payloadHeaders;
-              currentStore.headersSize = payloadHeaders.reduce(
-                (acc, { name, value }) =>
-                  acc + name.length + value.length + 2, 0);
-
-              // The `getString` promise is async, so we need to refresh the
-              // information displayed in the network details pane again here.
-              refreshNetworkDetailsPaneIfNecessary(currentItem);
-            });
-
-            requestItem.attachment.requestPostData = val;
-            requestItem.attachment.requestHeadersFromUploadStream =
-              currentStore;
-            break;
-          case "securityState":
-            requestItem.attachment.securityState = val;
-            this.updateMenuView(requestItem, key, val);
-            break;
-          case "securityInfo":
-            requestItem.attachment.securityInfo = val;
-            break;
-          case "responseHeaders":
-            requestItem.attachment.responseHeaders = val;
-            break;
-          case "responseCookies":
-            requestItem.attachment.responseCookies = val;
-            break;
-          case "httpVersion":
-            requestItem.attachment.httpVersion = val;
-            break;
-          case "remoteAddress":
-            requestItem.attachment.remoteAddress = val;
-            this.updateMenuView(requestItem, key, val);
-            break;
-          case "remotePort":
-            requestItem.attachment.remotePort = val;
-            break;
-          case "status":
-            requestItem.attachment.status = val;
-            this.updateMenuView(requestItem, key, {
-              status: val,
-              cached: requestItem.attachment.fromCache,
-              serviceWorker: requestItem.attachment.fromServiceWorker
-            });
-            break;
-          case "statusText":
-            requestItem.attachment.statusText = val;
-            let text = (requestItem.attachment.status + " " +
-                        requestItem.attachment.statusText);
-            if (requestItem.attachment.fromCache) {
-              text += " (cached)";
-            } else if (requestItem.attachment.fromServiceWorker) {
-              text += " (service worker)";
-            }
-
-            this.updateMenuView(requestItem, key, text);
-            break;
-          case "headersSize":
-            requestItem.attachment.headersSize = val;
-            break;
-          case "contentSize":
-            requestItem.attachment.contentSize = val;
-            this.updateMenuView(requestItem, key, val);
-            break;
-          case "transferredSize":
-            if (requestItem.attachment.fromCache) {
-              requestItem.attachment.transferredSize = 0;
-              this.updateMenuView(requestItem, key, "cached");
-            } else if (requestItem.attachment.fromServiceWorker) {
-              requestItem.attachment.transferredSize = 0;
-              this.updateMenuView(requestItem, key, "service worker");
-            } else {
-              requestItem.attachment.transferredSize = val;
-              this.updateMenuView(requestItem, key, val);
-            }
-            break;
-          case "mimeType":
-            requestItem.attachment.mimeType = val;
-            this.updateMenuView(requestItem, key, val);
-            break;
-          case "responseContent":
-            // If there's no mime type available when the response content
-            // is received, assume text/plain as a fallback.
-            if (!requestItem.attachment.mimeType) {
-              requestItem.attachment.mimeType = "text/plain";
-              this.updateMenuView(requestItem, "mimeType", "text/plain");
-            }
-            requestItem.attachment.responseContent = val;
-            this.updateMenuView(requestItem, key, val);
-            break;
-          case "totalTime":
-            requestItem.attachment.totalTime = val;
-            requestItem.attachment.endedMillis =
-              requestItem.attachment.startedMillis + val;
-
-            this.updateMenuView(requestItem, key, val);
-            this._registerLastRequestEnd(requestItem.attachment.endedMillis);
-            break;
-          case "eventTimings":
-            requestItem.attachment.eventTimings = val;
-            this._createWaterfallView(
-              requestItem, val.timings,
-              requestItem.attachment.fromCache ||
-              requestItem.attachment.fromServiceWorker
-            );
-            break;
-        }
-      }
-      refreshNetworkDetailsPaneIfNecessary(requestItem);
-
-      if (callback) {
-        callback();
-      }
-    }
-
-    /**
-     * Refreshes the information displayed in the sidebar, in case this update
-     * may have additional information about a request which isn't shown yet
-     * in the network details pane.
-     *
-     * @param object requestItem
-     *        The item to repopulate the sidebar with in case it's selected in
-     *        this requests menu.
-     */
-    function refreshNetworkDetailsPaneIfNecessary(requestItem) {
-      let selectedItem = NetMonitorView.RequestsMenu.selectedItem;
-      if (selectedItem == requestItem) {
-        NetMonitorView.NetworkDetails.populate(selectedItem.attachment);
-      }
-    }
-
-    // We're done flushing all the requests, clear the update queue.
-    this._updateQueue = [];
-    this._addQueue = [];
-
-    $("#requests-menu-empty-notice").hidden = !!this.itemCount;
-
-    // Make sure all the requests are sorted and filtered.
-    // Freshly added requests may not yet contain all the information required
-    // for sorting and filtering predicates, so this is done each time the
-    // network requests table is flushed (don't worry, events are drained first
-    // so this doesn't happen once per network event update).
-    this.sortContents();
-    this.filterContents();
-    this.updateRequests();
-    this.refreshZebra();
-
-    // Rescale all the waterfalls so that everything is visible at once.
-    this._flushWaterfallViews();
-  },
-
-  /**
-   * Customization function for creating an item's UI.
-   *
-   * @param string method
-   *        Specifies the request method (e.g. "GET", "POST", etc.)
-   * @param string url
-   *        Specifies the request's url.
-   * @param object cause
-   *        Specifies the request's cause. Has two properties:
-   *        - type: nsContentPolicyType constant
-   *        - uri: URI of the request origin
-   * @return nsIDOMNode
-   *         The network request view.
-   */
-  _createMenuView: function (method, url, cause) {
-    let template = $("#requests-menu-item-template");
-    let fragment = document.createDocumentFragment();
-
-    // Flatten the DOM by removing one redundant box (the template container).
-    for (let node of template.childNodes) {
-      fragment.appendChild(node.cloneNode(true));
-    }
-
-    this.updateMenuView(fragment, "method", method);
-    this.updateMenuView(fragment, "url", url);
-    this.updateMenuView(fragment, "cause", cause);
-
-    return fragment;
-  },
-
-  /**
-   * Get a human-readable string from a number of bytes, with the B, KB, MB, or
-   * GB value. Note that the transition between abbreviations is by 1000 rather
-   * than 1024 in order to keep the displayed digits smaller as "1016 KB" is
-   * more awkward than 0.99 MB"
-   */
-  getFormattedSize(bytes) {
-    if (bytes < MAX_BYTES_SIZE) {
-      return L10N.getFormatStr("networkMenu.sizeB", bytes);
-    } else if (bytes < MAX_KB_SIZE) {
-      let kb = bytes / BYTES_IN_KB;
-      let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS);
-      return L10N.getFormatStr("networkMenu.sizeKB", size);
-    } else if (bytes < MAX_MB_SIZE) {
-      let mb = bytes / BYTES_IN_MB;
-      let size = L10N.numberWithDecimals(mb, CONTENT_SIZE_DECIMALS);
-      return L10N.getFormatStr("networkMenu.sizeMB", size);
-    }
-    let gb = bytes / BYTES_IN_GB;
-    let size = L10N.numberWithDecimals(gb, CONTENT_SIZE_DECIMALS);
-    return L10N.getFormatStr("networkMenu.sizeGB", size);
+  closeCustomRequest() {
+    this.store.dispatch(Actions.removeSelectedCustomRequest());
   },
-
-  /**
-   * Updates the information displayed in a network request item view.
-   *
-   * @param object item
-   *        The network request item in this container.
-   * @param string key
-   *        The type of information that is to be updated.
-   * @param any value
-   *        The new value to be shown.
-   * @return object
-   *         A promise that is resolved once the information is displayed.
-   */
-  updateMenuView: Task.async(function* (item, key, value) {
-    let target = item.target || item;
-
-    switch (key) {
-      case "method": {
-        let node = $(".requests-menu-method", target);
-        node.setAttribute("value", value);
-        break;
-      }
-      case "url": {
-        let nameWithQuery = getUrlBaseNameWithQuery(value);
-        let hostPort = getUrlHost(value);
-        let host = getUrlHostName(value);
-        let unicodeUrl = decodeUnicodeUrl(value);
-
-        let file = $(".requests-menu-file", target);
-        file.setAttribute("value", nameWithQuery);
-        file.setAttribute("tooltiptext", unicodeUrl);
-
-        let domain = $(".requests-menu-domain", target);
-        domain.setAttribute("value", hostPort);
-        domain.setAttribute("tooltiptext", hostPort);
-
-        // Mark local hosts specially, where "local" is  as defined in the W3C
-        // spec for secure contexts.
-        // http://www.w3.org/TR/powerful-features/
-        //
-        //  * If the name falls under 'localhost'
-        //  * If the name is an IPv4 address within 127.0.0.0/8
-        //  * If the name is an IPv6 address within ::1/128
-        //
-        // IPv6 parsing is a little sloppy; it assumes that the address has
-        // been validated before it gets here.
-        let icon = $(".requests-security-state-icon", target);
-        icon.classList.remove("security-state-local");
-        if (host.match(/(.+\.)?localhost$/) ||
-            host.match(/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}/) ||
-            host.match(/\[[0:]+1\]/)) {
-          let tooltip = L10N.getStr("netmonitor.security.state.secure");
-          icon.classList.add("security-state-local");
-          icon.setAttribute("tooltiptext", tooltip);
-        }
-
-        break;
-      }
-      case "remoteAddress":
-        let domain = $(".requests-menu-domain", target);
-        let tooltip = (domain.getAttribute("value") +
-                       (value ? " (" + value + ")" : ""));
-        domain.setAttribute("tooltiptext", tooltip);
-        break;
-      case "securityState": {
-        let icon = $(".requests-security-state-icon", target);
-        this.attachSecurityIconClickListener(item);
-
-        // Security icon for local hosts is set in the "url" branch
-        if (icon.classList.contains("security-state-local")) {
-          break;
-        }
-
-        let tooltip2 = L10N.getStr("netmonitor.security.state." + value);
-        icon.classList.add("security-state-" + value);
-        icon.setAttribute("tooltiptext", tooltip2);
-        break;
-      }
-      case "status": {
-        let node = $(".requests-menu-status-icon", target);
-        // "code" attribute is only used by css to determine the icon color
-        let code;
-        if (value.cached) {
-          code = "cached";
-        } else if (value.serviceWorker) {
-          code = "service worker";
-        } else {
-          code = value.status;
-        }
-        node.setAttribute("code", code);
-        let codeNode = $(".requests-menu-status-code", target);
-        codeNode.setAttribute("value", value.status);
-        break;
-      }
-      case "statusText": {
-        let node = $(".requests-menu-status", target);
-        node.setAttribute("tooltiptext", value);
-        break;
-      }
-      case "cause": {
-        let labelNode = $(".requests-menu-cause-label", target);
-        labelNode.setAttribute("value", loadCauseString(value.type));
-        if (value.loadingDocumentUri) {
-          labelNode.setAttribute("tooltiptext", value.loadingDocumentUri);
-        }
-
-        let stackNode = $(".requests-menu-cause-stack", target);
-        if (value.stacktrace && value.stacktrace.length > 0) {
-          stackNode.removeAttribute("hidden");
-        }
-        break;
-      }
-      case "contentSize": {
-        let node = $(".requests-menu-size", target);
-
-        let text = this.getFormattedSize(value);
-
-        node.setAttribute("value", text);
-        node.setAttribute("tooltiptext", text);
-        break;
-      }
-      case "transferredSize": {
-        let node = $(".requests-menu-transferred", target);
-
-        let text;
-        if (value === null) {
-          text = L10N.getStr("networkMenu.sizeUnavailable");
-        } else if (value === "cached") {
-          text = L10N.getStr("networkMenu.sizeCached");
-          node.classList.add("theme-comment");
-        } else if (value === "service worker") {
-          text = L10N.getStr("networkMenu.sizeServiceWorker");
-          node.classList.add("theme-comment");
-        } else {
-          text = this.getFormattedSize(value);
-        }
-
-        node.setAttribute("value", text);
-        node.setAttribute("tooltiptext", text);
-        break;
-      }
-      case "mimeType": {
-        let type = getAbbreviatedMimeType(value);
-        let node = $(".requests-menu-type", target);
-        let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type;
-        node.setAttribute("value", text);
-        node.setAttribute("tooltiptext", value);
-        break;
-      }
-      case "responseContent": {
-        let { mimeType } = item.attachment;
-
-        if (mimeType.includes("image/")) {
-          let { text, encoding } = value.content;
-          let responseBody = yield gNetwork.getString(text);
-          let node = $(".requests-menu-icon", item.target);
-          node.src = formDataURI(mimeType, encoding, responseBody);
-          node.setAttribute("type", "thumbnail");
-          node.removeAttribute("hidden");
-
-          window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
-        }
-        break;
-      }
-      case "totalTime": {
-        let node = $(".requests-menu-timings-total", target);
-
-        // integer
-        let text = L10N.getFormatStr("networkMenu.totalMS", value);
-        node.setAttribute("value", text);
-        node.setAttribute("tooltiptext", text);
-        break;
-      }
-    }
-  }),
-
-  /**
-   * Creates a waterfall representing timing information in a network
-   * request item view.
-   *
-   * @param object item
-   *        The network request item in this container.
-   * @param object timings
-   *        An object containing timing information.
-   * @param boolean fromCache
-   *        Indicates if the result came from the browser cache or
-   *        a service worker
-   */
-  _createWaterfallView: function (item, timings, fromCache) {
-    let { target } = item;
-    let sections = ["blocked", "dns", "connect", "send", "wait", "receive"];
-    // Skipping "blocked" because it doesn't work yet.
-
-    let timingsNode = $(".requests-menu-timings", target);
-    let timingsTotal = $(".requests-menu-timings-total", timingsNode);
-
-    if (fromCache) {
-      timingsTotal.style.display = "none";
-      return;
-    }
-
-    // Add a set of boxes representing timing information.
-    for (let key of sections) {
-      let width = timings[key];
-
-      // Don't render anything if it surely won't be visible.
-      // One millisecond == one unscaled pixel.
-      if (width > 0) {
-        let timingBox = document.createElement("hbox");
-        timingBox.className = "requests-menu-timings-box " + key;
-        timingBox.setAttribute("width", width);
-        timingsNode.insertBefore(timingBox, timingsTotal);
-      }
-    }
-  },
-
-  /**
-   * Rescales and redraws all the waterfall views in this container.
-   *
-   * @param boolean reset
-   *        True if this container's width was changed.
-   */
-  _flushWaterfallViews: function (reset) {
-    // Don't paint things while the waterfall view isn't even visible,
-    // or there are no items added to this container.
-    if (NetMonitorView.currentFrontendMode !=
-      "network-inspector-view" || !this.itemCount) {
-      return;
-    }
-
-    // To avoid expensive operations like getBoundingClientRect() and
-    // rebuilding the waterfall background each time a new request comes in,
-    // stuff is cached. However, in certain scenarios like when the window
-    // is resized, this needs to be invalidated.
-    if (reset) {
-      this._cachedWaterfallWidth = 0;
-    }
-
-    // Determine the scaling to be applied to all the waterfalls so that
-    // everything is visible at once. One millisecond == one unscaled pixel.
-    let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
-    let longestWidth = this._lastRequestEndedMillis -
-      this._firstRequestStartedMillis;
-    let scale = Math.min(Math.max(availableWidth / longestWidth, EPSILON), 1);
-
-    // Redraw and set the canvas background for each waterfall view.
-    this._showWaterfallDivisionLabels(scale);
-    this._drawWaterfallBackground(scale);
-
-    // Apply CSS transforms to each waterfall in this container totalTime
-    // accurately translate and resize as needed.
-    for (let { target, attachment } of this) {
-      let timingsNode = $(".requests-menu-timings", target);
-      let totalNode = $(".requests-menu-timings-total", target);
-      let direction = window.isRTL ? -1 : 1;
-
-      // Render the timing information at a specific horizontal translation
-      // based on the delta to the first monitored event network.
-      let translateX = "translateX(" + (direction *
-        attachment.startedDeltaMillis) + "px)";
-
-      // Based on the total time passed until the last request, rescale
-      // all the waterfalls to a reasonable size.
-      let scaleX = "scaleX(" + scale + ")";
-
-      // Certain nodes should not be scaled, even if they're children of
-      // another scaled node. In this case, apply a reversed transformation.
-      let revScaleX = "scaleX(" + (1 / scale) + ")";
-
-      timingsNode.style.transform = scaleX + " " + translateX;
-      totalNode.style.transform = revScaleX;
-    }
-  },
-
-  /**
-   * Creates the labels displayed on the waterfall header in this container.
-   *
-   * @param number scale
-   *        The current waterfall scale.
-   */
-  _showWaterfallDivisionLabels: function (scale) {
-    let container = $("#requests-menu-waterfall-label-wrapper");
-    let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
-
-    // Nuke all existing labels.
-    while (container.hasChildNodes()) {
-      container.firstChild.remove();
-    }
-
-    // Build new millisecond tick labels...
-    let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE;
-    let optimalTickIntervalFound = false;
-
-    while (!optimalTickIntervalFound) {
-      // Ignore any divisions that would end up being too close to each other.
-      let scaledStep = scale * timingStep;
-      if (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) {
-        timingStep <<= 1;
-        continue;
-      }
-      optimalTickIntervalFound = true;
-
-      // Insert one label for each division on the current scale.
-      let fragment = document.createDocumentFragment();
-      let direction = window.isRTL ? -1 : 1;
-
-      for (let x = 0; x < availableWidth; x += scaledStep) {
-        let translateX = "translateX(" + ((direction * x) | 0) + "px)";
-        let millisecondTime = x / scale;
-
-        let normalizedTime = millisecondTime;
-        let divisionScale = "millisecond";
-
-        // If the division is greater than 1 minute.
-        if (normalizedTime > 60000) {
-          normalizedTime /= 60000;
-          divisionScale = "minute";
-        } else if (normalizedTime > 1000) {
-          // If the division is greater than 1 second.
-          normalizedTime /= 1000;
-          divisionScale = "second";
-        }
-
-        // Showing too many decimals is bad UX.
-        if (divisionScale == "millisecond") {
-          normalizedTime |= 0;
-        } else {
-          normalizedTime = L10N.numberWithDecimals(normalizedTime,
-            REQUEST_TIME_DECIMALS);
-        }
-
-        let node = document.createElement("label");
-        let text = L10N.getFormatStr("networkMenu." +
-          divisionScale, normalizedTime);
-        node.className = "plain requests-menu-timings-division";
-        node.setAttribute("division-scale", divisionScale);
-        node.style.transform = translateX;
-
-        node.setAttribute("value", text);
-        fragment.appendChild(node);
-      }
-      container.appendChild(fragment);
-
-      container.className = "requests-menu-waterfall-visible";
-    }
-  },
-
-  /**
-   * Creates the background displayed on each waterfall view in this container.
-   *
-   * @param number scale
-   *        The current waterfall scale.
-   */
-  _drawWaterfallBackground: function (scale) {
-    if (!this._canvas || !this._ctx) {
-      this._canvas = document.createElementNS(HTML_NS, "canvas");
-      this._ctx = this._canvas.getContext("2d");
-    }
-    let canvas = this._canvas;
-    let ctx = this._ctx;
-
-    // Nuke the context.
-    let canvasWidth = canvas.width = this._waterfallWidth;
-    // Awww yeah, 1px, repeats on Y axis.
-    let canvasHeight = canvas.height = 1;
-
-    // Start over.
-    let imageData = ctx.createImageData(canvasWidth, canvasHeight);
-    let pixelArray = imageData.data;
-
-    let buf = new ArrayBuffer(pixelArray.length);
-    let view8bit = new Uint8ClampedArray(buf);
-    let view32bit = new Uint32Array(buf);
-
-    // Build new millisecond tick lines...
-    let timingStep = REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE;
-    let [r, g, b] = REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
-    let alphaComponent = REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
-    let optimalTickIntervalFound = false;
-
-    while (!optimalTickIntervalFound) {
-      // Ignore any divisions that would end up being too close to each other.
-      let scaledStep = scale * timingStep;
-      if (scaledStep < REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN) {
-        timingStep <<= 1;
-        continue;
-      }
-      optimalTickIntervalFound = true;
-
-      // Insert one pixel for each division on each scale.
-      for (let i = 1; i <= REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
-        let increment = scaledStep * Math.pow(2, i);
-        for (let x = 0; x < canvasWidth; x += increment) {
-          let position = (window.isRTL ? canvasWidth - x : x) | 0;
-          view32bit[position] =
-            (alphaComponent << 24) | (b << 16) | (g << 8) | r;
-        }
-        alphaComponent += REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
-      }
-    }
-
-    {
-      let t = NetMonitorController.NetworkEventsHandler
-        .firstDocumentDOMContentLoadedTimestamp;
-
-      let delta = Math.floor((t - this._firstRequestStartedMillis) * scale);
-      let [r1, g1, b1, a1] =
-        REQUESTS_WATERFALL_DOMCONTENTLOADED_TICKS_COLOR_RGBA;
-      view32bit[delta] = (a1 << 24) | (r1 << 16) | (g1 << 8) | b1;
-    }
-    {
-      let t = NetMonitorController.NetworkEventsHandler
-        .firstDocumentLoadTimestamp;
-
-      let delta = Math.floor((t - this._firstRequestStartedMillis) * scale);
-      let [r2, g2, b2, a2] = REQUESTS_WATERFALL_LOAD_TICKS_COLOR_RGBA;
-      view32bit[delta] = (a2 << 24) | (r2 << 16) | (g2 << 8) | b2;
-    }
-
-    // Flush the image data and cache the waterfall background.
-    pixelArray.set(view8bit);
-    ctx.putImageData(imageData, 0, 0);
-    document.mozSetImageElement("waterfall-background", canvas);
-  },
-
-  /**
-   * The selection listener for this container.
-   */
-  _onSelect: function ({ detail: item }) {
-    if (item) {
-      NetMonitorView.Sidebar.populate(item.attachment);
-      NetMonitorView.Sidebar.toggle(true);
-    } else {
-      NetMonitorView.Sidebar.toggle(false);
-    }
-  },
-
-  /**
-   * The swap listener for this container.
-   * Called when two items switch places, when the contents are sorted.
-   */
-  _onSwap: function ({ detail: [firstItem, secondItem] }) {
-    // Reattach click listener to the security icons
-    this.attachSecurityIconClickListener(firstItem);
-    this.attachSecurityIconClickListener(secondItem);
-  },
-
-  /**
-   * The predicate used when deciding whether a popup should be shown
-   * over a request item or not.
-   *
-   * @param nsIDOMNode target
-   *        The element node currently being hovered.
-   * @param object tooltip
-   *        The current tooltip instance.
-   * @return {Promise}
-   */
-  _onHover: Task.async(function* (target, tooltip) {
-    let requestItem = this.getItemForElement(target);
-    if (!requestItem) {
-      return false;
-    }
-
-    let hovered = requestItem.attachment;
-    if (hovered.responseContent && target.closest(".requests-menu-icon-and-file")) {
-      return this._setTooltipImageContent(tooltip, requestItem);
-    } else if (hovered.cause && target.closest(".requests-menu-cause-stack")) {
-      return this._setTooltipStackTraceContent(tooltip, requestItem);
-    }
-
-    return false;
-  }),
-
-  _setTooltipImageContent: Task.async(function* (tooltip, requestItem) {
-    let { mimeType, text, encoding } = requestItem.attachment.responseContent.content;
-
-    if (!mimeType || !mimeType.includes("image/")) {
-      return false;
-    }
-
-    let string = yield gNetwork.getString(text);
-    let src = formDataURI(mimeType, encoding, string);
-    let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
-    let { naturalWidth, naturalHeight } = yield getImageDimensions(tooltip.doc, src);
-    let options = { maxDim, naturalWidth, naturalHeight };
-    setImageTooltip(tooltip, tooltip.doc, src, options);
-
-    return $(".requests-menu-icon", requestItem.target);
-  }),
-
-  _setTooltipStackTraceContent: Task.async(function* (tooltip, requestItem) {
-    let {stacktrace} = requestItem.attachment.cause;
-
-    if (!stacktrace || stacktrace.length == 0) {
-      return false;
-    }
-
-    let doc = tooltip.doc;
-    let el = doc.createElementNS(HTML_NS, "div");
-    el.className = "stack-trace-tooltip devtools-monospace";
-
-    for (let f of stacktrace) {
-      let { functionName, filename, lineNumber, columnNumber, asyncCause } = f;
-
-      if (asyncCause) {
-        // if there is asyncCause, append a "divider" row into the trace
-        let asyncFrameEl = doc.createElementNS(HTML_NS, "div");
-        asyncFrameEl.className = "stack-frame stack-frame-async";
-        asyncFrameEl.textContent =
-          WEBCONSOLE_L10N.getFormatStr("stacktrace.asyncStack", asyncCause);
-        el.appendChild(asyncFrameEl);
-      }
-
-      // Parse a source name in format "url -> url"
-      let sourceUrl = filename.split(" -> ").pop();
-
-      let frameEl = doc.createElementNS(HTML_NS, "div");
-      frameEl.className = "stack-frame stack-frame-call";
-
-      let funcEl = doc.createElementNS(HTML_NS, "span");
-      funcEl.className = "stack-frame-function-name";
-      funcEl.textContent =
-        functionName || WEBCONSOLE_L10N.getStr("stacktrace.anonymousFunction");
-      frameEl.appendChild(funcEl);
-
-      let sourceEl = doc.createElementNS(HTML_NS, "span");
-      sourceEl.className = "stack-frame-source-name";
-      frameEl.appendChild(sourceEl);
-
-      let sourceInnerEl = doc.createElementNS(HTML_NS, "span");
-      sourceInnerEl.className = "stack-frame-source-name-inner";
-      sourceEl.appendChild(sourceInnerEl);
-
-      sourceInnerEl.textContent = sourceUrl;
-      sourceInnerEl.title = sourceUrl;
-
-      let lineEl = doc.createElementNS(HTML_NS, "span");
-      lineEl.className = "stack-frame-line";
-      lineEl.textContent = `:${lineNumber}:${columnNumber}`;
-      sourceInnerEl.appendChild(lineEl);
-
-      frameEl.addEventListener("click", () => {
-        // hide the tooltip immediately, not after delay
-        tooltip.hide();
-        NetMonitorController.viewSourceInDebugger(filename, lineNumber);
-      }, false);
-
-      el.appendChild(frameEl);
-    }
-
-    tooltip.setContent(el, {width: REQUESTS_TOOLTIP_STACK_TRACE_WIDTH});
-
-    return true;
-  }),
-
-  /**
-   * A handler that opens the security tab in the details view if secure or
-   * broken security indicator is clicked.
-   */
-  _onSecurityIconClick: function (e) {
-    let state = this.selectedItem.attachment.securityState;
-    if (state !== "insecure") {
-      // Choose the security tab.
-      NetMonitorView.NetworkDetails.widget.selectedIndex = 5;
-    }
-  },
-
-  /**
-   * The resize listener for this container's window.
-   */
-  _onResize: function (e) {
-    // Allow requests to settle down first.
-    setNamedTimeout("resize-events",
-      RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
-  },
-
-  /**
-   * Scroll listener for the requests menu view.
-   */
-  _onScroll: function () {
-    this.tooltip.hide();
-  },
-
-  /**
-   * Open context menu
-   */
-  _onContextMenu: function (e) {
-    e.preventDefault();
-    this.contextMenu.open(e);
-  },
-
-  /**
-   * Checks if the specified unix time is the first one to be known of,
-   * and saves it if so.
-   *
-   * @param number unixTime
-   *        The milliseconds to check and save.
-   */
-  _registerFirstRequestStart: function (unixTime) {
-    if (this._firstRequestStartedMillis == -1) {
-      this._firstRequestStartedMillis = unixTime;
-    }
-  },
-
-  /**
-   * Checks if the specified unix time is the last one to be known of,
-   * and saves it if so.
-   *
-   * @param number unixTime
-   *        The milliseconds to check and save.
-   */
-  _registerLastRequestEnd: function (unixTime) {
-    if (this._lastRequestEndedMillis < unixTime) {
-      this._lastRequestEndedMillis = unixTime;
-    }
-  },
-
-  /**
-   * Gets the available waterfall width in this container.
-   * @return number
-   */
-  get _waterfallWidth() {
-    if (this._cachedWaterfallWidth == 0) {
-      let container = $("#requests-menu-toolbar");
-      let waterfall = $("#requests-menu-waterfall-header-box");
-      let containerBounds = container.getBoundingClientRect();
-      let waterfallBounds = waterfall.getBoundingClientRect();
-      if (!window.isRTL) {
-        this._cachedWaterfallWidth = containerBounds.width -
-          waterfallBounds.left;
-      } else {
-        this._cachedWaterfallWidth = waterfallBounds.right;
-      }
-    }
-    return this._cachedWaterfallWidth;
-  },
-
-  _splitter: null,
-  _summary: null,
-  _canvas: null,
-  _ctx: null,
-  _cachedWaterfallWidth: 0,
-  _firstRequestStartedMillis: -1,
-  _lastRequestEndedMillis: -1,
-  _updateQueue: [],
-  _addQueue: [],
-  _updateTimeout: null,
-  _resizeTimeout: null,
-  _activeFilters: ["all"],
-  _currentFreetextFilter: ""
-});
+};
 
 exports.RequestsMenuView = RequestsMenuView;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/selectors/filters.js
@@ -0,0 +1,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/. */
+
+"use strict";
+
+function getActiveFilters(state) {
+  return state.filters.types.toSeq().filter(checked => checked).keySeq().toArray();
+}
+
+module.exports = {
+  getActiveFilters
+};
--- a/devtools/client/netmonitor/selectors/index.js
+++ b/devtools/client/netmonitor/selectors/index.js
@@ -1,63 +1,15 @@
 /* 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 { createSelector } = require("devtools/client/shared/vendor/reselect");
-
-/**
- * Gets the total number of bytes representing the cumulated content size of
- * a set of requests. Returns 0 for an empty set.
- *
- * @param {array} items - an array of request items
- * @return {number} total bytes of requests
- */
-function getTotalBytesOfRequests(items) {
-  if (!items.length) {
-    return 0;
-  }
-
-  let result = 0;
-  items.forEach((item) => {
-    let size = item.attachment.contentSize;
-    result += (typeof size == "number") ? size : 0;
-  });
-
-  return result;
-}
+const filters = require("./filters");
+const requests = require("./requests");
+const ui = require("./ui");
 
-/**
- * Gets the total milliseconds for all requests. Returns null for an
- * empty set.
- *
- * @param {array} items - an array of request items
- * @return {object} total milliseconds for all requests
- */
-function getTotalMillisOfRequests(items) {
-  if (!items.length) {
-    return null;
-  }
-
-  const oldest = items.reduce((prev, curr) =>
-    prev.attachment.startedMillis < curr.attachment.startedMillis ?
-      prev : curr);
-  const newest = items.reduce((prev, curr) =>
-    prev.attachment.startedMillis > curr.attachment.startedMillis ?
-      prev : curr);
-
-  return newest.attachment.endedMillis - oldest.attachment.startedMillis;
-}
-
-const getSummary = createSelector(
-  (state) => state.requests.items,
-  (requests) => ({
-    count: requests.length,
-    totalBytes: getTotalBytesOfRequests(requests),
-    totalMillis: getTotalMillisOfRequests(requests),
-  })
+Object.assign(exports,
+  filters,
+  requests,
+  ui
 );
-
-module.exports = {
-  getSummary,
-};
--- a/devtools/client/netmonitor/selectors/moz.build
+++ b/devtools/client/netmonitor/selectors/moz.build
@@ -1,7 +1,10 @@
 # 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(
-    'index.js'
+    'filters.js',
+    'index.js',
+    'requests.js',
+    'ui.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/selectors/requests.js
@@ -0,0 +1,119 @@
+/* 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 { createSelector } = require("devtools/client/shared/vendor/reselect");
+const { Filters, isFreetextMatch } = require("../filter-predicates");
+const { Sorters } = require("../sort-predicates");
+
+/**
+ * Check if the given requests is a clone, find and return the original request if it is.
+ * Cloned requests are sorted by comparing the original ones.
+ */
+function getOrigRequest(requests, req) {
+  if (!req.id.endsWith("-clone")) {
+    return req;
+  }
+
+  const origId = req.id.replace(/-clone$/, "");
+  return requests.find(r => r.id === origId);
+}
+
+const getFilterFn = createSelector(
+  state => state.filters,
+  filters => r => {
+    const matchesType = filters.types.some((enabled, filter) => {
+      return enabled && Filters[filter] && Filters[filter](r);
+    });
+    return matchesType && isFreetextMatch(r, filters.text);
+  }
+);
+
+const getSortFn = createSelector(
+  state => state.requests.requests,
+  state => state.sort,
+  (requests, sort) => {
+    let dataSorter = Sorters[sort.type || "waterfall"];
+
+    function sortWithClones(a, b) {
+      // If one request is a clone of the other, sort them next to each other
+      if (a.id == b.id + "-clone") {
+        return +1;
+      } else if (a.id + "-clone" == b.id) {
+        return -1;
+      }
+
+      // Otherwise, get the original requests and compare them
+      return dataSorter(
+        getOrigRequest(requests, a),
+        getOrigRequest(requests, b)
+      );
+    }
+
+    const ascending = sort.ascending ? +1 : -1;
+    return (a, b) => ascending * sortWithClones(a, b, dataSorter);
+  }
+);
+
+const getSortedRequests = createSelector(
+  state => state.requests.requests,
+  getSortFn,
+  (requests, sortFn) => requests.sort(sortFn)
+);
+
+const getDisplayedRequests = createSelector(
+  state => state.requests.requests,
+  getFilterFn,
+  getSortFn,
+  (requests, filterFn, sortFn) => requests.filter(filterFn).sort(sortFn)
+);
+
+const getDisplayedRequestsSummary = createSelector(
+  getDisplayedRequests,
+  state => state.requests.lastEndedMillis - state.requests.firstStartedMillis,
+  (requests, totalMillis) => {
+    if (requests.size == 0) {
+      return { count: 0, bytes: 0, millis: 0 };
+    }
+
+    const totalBytes = requests.reduce((total, item) => {
+      if (typeof item.contentSize == "number") {
+        total += item.contentSize;
+      }
+      return total;
+    }, 0);
+
+    return {
+      count: requests.size,
+      bytes: totalBytes,
+      millis: totalMillis,
+    };
+  }
+);
+
+function getRequestById(state, id) {
+  return state.requests.requests.find(r => r.id === id);
+}
+
+function getDisplayedRequestById(state, id) {
+  return getDisplayedRequests(state).find(r => r.id === id);
+}
+
+function getSelectedRequest(state) {
+  if (!state.requests.selectedId) {
+    return null;
+  }
+
+  return getRequestById(state, state.requests.selectedId);
+}
+
+module.exports = {
+  getSortedRequests,
+  getDisplayedRequests,
+  getDisplayedRequestsSummary,
+  getRequestById,
+  getDisplayedRequestById,
+  getSelectedRequest,
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/selectors/ui.js
@@ -0,0 +1,32 @@
+/* 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 { getDisplayedRequests } = require("./requests");
+
+function isSidebarToggleButtonDisabled(state) {
+  return getDisplayedRequests(state).isEmpty();
+}
+
+const EPSILON = 0.001;
+
+function getWaterfallScale(state) {
+  const { requests, timingMarkers, ui } = state;
+
+  if (requests.firstStartedMillis == +Infinity) {
+    return null;
+  }
+
+  const lastEventMillis = Math.max(requests.lastEndedMillis,
+                                   timingMarkers.firstDocumentDOMContentLoadedTimestamp,
+                                   timingMarkers.firstDocumentLoadTimestamp);
+  const longestWidth = lastEventMillis - requests.firstStartedMillis;
+  return Math.min(Math.max(ui.waterfallWidth / longestWidth, EPSILON), 1);
+}
+
+module.exports = {
+  isSidebarToggleButtonDisabled,
+  getWaterfallScale,
+};
--- a/devtools/client/netmonitor/sidebar-view.js
+++ b/devtools/client/netmonitor/sidebar-view.js
@@ -1,14 +1,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/. */
 
-/* import-globals-from ./netmonitor-controller.js */
-/* globals dumpn, $, NetMonitorView */
+/* globals window, dumpn, $, NetMonitorView */
 
 "use strict";
 
 const { Task } = require("devtools/shared/task");
 const { EVENTS } = require("./events");
 
 /**
  * Functions handling the sidebar details view.
@@ -21,17 +20,16 @@ SidebarView.prototype = {
   /**
    * Sets this view hidden or visible. It's visible by default.
    *
    * @param boolean visibleFlag
    *        Specifies the intended visibility.
    */
   toggle: function (visibleFlag) {
     NetMonitorView.toggleDetailsPane({ visible: visibleFlag });
-    NetMonitorView.RequestsMenu._flushWaterfallViews(true);
   },
 
   /**
    * Populates this view with the specified data.
    *
    * @param object data
    *        The data source (this should be the attachment of a request item).
    * @return object
--- a/devtools/client/netmonitor/sort-predicates.js
+++ b/devtools/client/netmonitor/sort-predicates.js
@@ -3,17 +3,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
   getAbbreviatedMimeType,
   getUrlBaseNameWithQuery,
   getUrlHost,
-  loadCauseString,
 } = require("./request-utils");
 
 /**
  * Predicates used when sorting items.
  *
  * @param object first
  *        The first item used in the comparison.
  * @param object second
@@ -55,18 +54,18 @@ function domain(first, second) {
   let secondDomain = getUrlHost(second.url).toLowerCase();
   if (firstDomain == secondDomain) {
     return first.startedMillis - second.startedMillis;
   }
   return firstDomain > secondDomain ? 1 : -1;
 }
 
 function cause(first, second) {
-  let firstCause = loadCauseString(first.cause.type);
-  let secondCause = loadCauseString(second.cause.type);
+  let firstCause = first.cause.type;
+  let secondCause = second.cause.type;
   if (firstCause == secondCause) {
     return first.startedMillis - second.startedMillis;
   }
   return firstCause > secondCause ? 1 : -1;
 }
 
 function type(first, second) {
   let firstType = getAbbreviatedMimeType(first.mimeType).toLowerCase();
--- a/devtools/client/netmonitor/store.js
+++ b/devtools/client/netmonitor/store.js
@@ -1,14 +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 createStore = require("devtools/client/shared/redux/create-store");
-const reducers = require("./reducers/index");
+const { createStore, applyMiddleware } = require("devtools/client/shared/vendor/redux");
+const { thunk } = require("devtools/client/shared/redux/middleware/thunk");
+const batching = require("./middleware/batching");
+const rootReducer = require("./reducers/index");
 
 function configureStore() {
-  return createStore()(reducers);
+  return createStore(
+    rootReducer,
+    applyMiddleware(
+      thunk,
+      batching
+    )
+  );
 }
 
 exports.configureStore = configureStore;
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -49,17 +49,16 @@ support-files =
   !/devtools/client/framework/test/shared-head.js
 
 [browser_net_aaa_leaktest.js]
 [browser_net_accessibility-01.js]
 [browser_net_accessibility-02.js]
 skip-if = (toolkit == "cocoa" && e10s) # bug 1252254
 [browser_net_api-calls.js]
 [browser_net_autoscroll.js]
-skip-if = true # Bug 1309191 - replace with rewritten version in React
 [browser_net_cached-status.js]
 [browser_net_cause.js]
 [browser_net_cause_redirect.js]
 [browser_net_service-worker-status.js]
 [browser_net_charts-01.js]
 [browser_net_charts-02.js]
 [browser_net_charts-03.js]
 [browser_net_charts-04.js]
@@ -84,17 +83,17 @@ subsuite = clipboard
 [browser_net_copy_headers.js]
 subsuite = clipboard
 [browser_net_copy_as_curl.js]
 subsuite = clipboard
 [browser_net_cors_requests.js]
 [browser_net_cyrillic-01.js]
 [browser_net_cyrillic-02.js]
 [browser_net_details-no-duplicated-content.js]
-skip-if = (os == 'linux' && e10s && debug) # Bug 1242204
+skip-if = true # Test broken in React version, is too low-level
 [browser_net_frame.js]
 [browser_net_filter-01.js]
 [browser_net_filter-02.js]
 [browser_net_filter-03.js]
 [browser_net_filter-04.js]
 [browser_net_footer-summary.js]
 [browser_net_html-preview.js]
 [browser_net_icon-preview.js]
@@ -135,19 +134,21 @@ skip-if = (os == 'linux' && e10s && debu
 [browser_net_send-beacon-other-tab.js]
 [browser_net_simple-init.js]
 [browser_net_simple-request-data.js]
 skip-if = true # Bug 1258809
 [browser_net_simple-request-details.js]
 skip-if = true # Bug 1258809
 [browser_net_simple-request.js]
 [browser_net_sort-01.js]
+skip-if = true # Redundant for React/Redux version
 [browser_net_sort-02.js]
 [browser_net_sort-03.js]
 [browser_net_statistics-01.js]
 [browser_net_statistics-02.js]
 [browser_net_statistics-03.js]
 [browser_net_status-codes.js]
 [browser_net_streaming-response.js]
 [browser_net_throttle.js]
 [browser_net_timeline_ticks.js]
+skip-if = true # TODO: fix the test
 [browser_net_timing-division.js]
 [browser_net_persistent_logs.js]
--- a/devtools/client/netmonitor/test/browser_net_accessibility-01.js
+++ b/devtools/client/netmonitor/test/browser_net_accessibility-01.js
@@ -3,23 +3,25 @@
 
 "use strict";
 
 /**
  * Tests if focus modifiers work for the SideMenuWidget.
  */
 
 add_task(function* () {
+  let Actions = require("devtools/client/netmonitor/actions/index");
+
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
   info("Starting test... ");
 
   // It seems that this test may be slow on Ubuntu builds running on ec2.
   requestLongerTimeout(2);
 
-  let { NetMonitorView } = monitor.panelWin;
+  let { NetMonitorView, gStore } = monitor.panelWin;
   let { RequestsMenu } = NetMonitorView;
 
   RequestsMenu.lazyUpdate = false;
 
   let count = 0;
   function check(selectedIndex, paneVisibility) {
     info("Performing check " + (count++) + ".");
 
@@ -32,56 +34,51 @@ add_task(function* () {
   let wait = waitForNetworkEvents(monitor, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(2);
   });
   yield wait;
 
   check(-1, false);
 
-  RequestsMenu.focusLastVisibleItem();
+  gStore.dispatch(Actions.selectDelta(+Infinity));
   check(1, true);
-  RequestsMenu.focusFirstVisibleItem();
+  gStore.dispatch(Actions.selectDelta(-Infinity));
   check(0, true);
 
-  RequestsMenu.focusNextItem();
+  gStore.dispatch(Actions.selectDelta(+1));
   check(1, true);
-  RequestsMenu.focusPrevItem();
+  gStore.dispatch(Actions.selectDelta(-1));
   check(0, true);
 
-  RequestsMenu.focusItemAtDelta(+1);
+  gStore.dispatch(Actions.selectDelta(+10));
   check(1, true);
-  RequestsMenu.focusItemAtDelta(-1);
-  check(0, true);
-
-  RequestsMenu.focusItemAtDelta(+10);
-  check(1, true);
-  RequestsMenu.focusItemAtDelta(-10);
+  gStore.dispatch(Actions.selectDelta(-10));
   check(0, true);
 
   wait = waitForNetworkEvents(monitor, 18);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(18);
   });
   yield wait;
 
-  RequestsMenu.focusLastVisibleItem();
+  gStore.dispatch(Actions.selectDelta(+Infinity));
   check(19, true);
-  RequestsMenu.focusFirstVisibleItem();
+  gStore.dispatch(Actions.selectDelta(-Infinity));
   check(0, true);
 
-  RequestsMenu.focusNextItem();
+  gStore.dispatch(Actions.selectDelta(+1));
   check(1, true);
-  RequestsMenu.focusPrevItem();
+  gStore.dispatch(Actions.selectDelta(-1));
   check(0, true);
 
-  RequestsMenu.focusItemAtDelta(+10);
+  gStore.dispatch(Actions.selectDelta(+10));
   check(10, true);
-  RequestsMenu.focusItemAtDelta(-10);
+  gStore.dispatch(Actions.selectDelta(-10));
   check(0, true);
 
-  RequestsMenu.focusItemAtDelta(+100);
+  gStore.dispatch(Actions.selectDelta(+100));
   check(19, true);
-  RequestsMenu.focusItemAtDelta(-100);
+  gStore.dispatch(Actions.selectDelta(-100));
   check(0, true);
 
-  yield teardown(monitor);
+  return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_accessibility-02.js
+++ b/devtools/client/netmonitor/test/browser_net_accessibility-02.js
@@ -30,16 +30,18 @@ add_task(function* () {
   }
 
   let wait = waitForNetworkEvents(monitor, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(2);
   });
   yield wait;
 
+  $(".requests-menu-contents").focus();
+
   check(-1, false);
 
   EventUtils.sendKey("DOWN", window);
   check(0, true);
   EventUtils.sendKey("UP", window);
   check(0, true);
 
   EventUtils.sendKey("PAGE_DOWN", window);
@@ -118,13 +120,13 @@ add_task(function* () {
   EventUtils.sendKey("END", window);
   check(19, true);
   EventUtils.sendKey("DOWN", window);
   check(19, true);
 
   EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
   check(-1, false);
 
-  EventUtils.sendMouseEvent({ type: "mousedown" }, $(".side-menu-widget-item"));
+  EventUtils.sendMouseEvent({ type: "mousedown" }, $(".request-list-item"));
   check(0, true);
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_api-calls.js
+++ b/devtools/client/netmonitor/test/browser_net_api-calls.js
@@ -27,13 +27,13 @@ add_task(function* () {
 
   let wait = waitForNetworkEvents(monitor, 5);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   REQUEST_URIS.forEach(function (uri, index) {
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(index), "GET", uri);
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(index), "GET", uri);
   });
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_autoscroll.js
+++ b/devtools/client/netmonitor/test/browser_net_autoscroll.js
@@ -5,71 +5,83 @@
 
 /**
  * Bug 863102 - Automatically scroll down upon new network requests.
  */
 add_task(function* () {
   requestLongerTimeout(2);
 
   let { monitor } = yield initNetMonitor(INFINITE_GET_URL);
-  let win = monitor.panelWin;
-  let topNode = win.document.getElementById("requests-menu-contents");
-  let requestsContainer = topNode.getElementsByTagName("scrollbox")[0];
-  ok(!!requestsContainer, "Container element exists as expected.");
+  let { $ } = monitor.panelWin;
+
+  // Wait until the first request makes the empty notice disappear
+  yield waitForRequestListToAppear();
+
+  let requestsContainer = $(".requests-menu-contents");
+  ok(requestsContainer, "Container element exists as expected.");
 
   // (1) Check that the scroll position is maintained at the bottom
   // when the requests overflow the vertical size of the container.
   yield waitForRequestsToOverflowContainer();
   yield waitForScroll();
-  ok(scrolledToBottom(requestsContainer), "Scrolled to bottom on overflow.");
+  ok(true, "Scrolled to bottom on overflow.");
 
-  // (2) Now set the scroll position somewhere in the middle and check
+  // (2) Now set the scroll position to the first item and check
   // that additional requests do not change the scroll position.
-  let children = requestsContainer.childNodes;
-  let middleNode = children.item(children.length / 2);
-  middleNode.scrollIntoView();
+  let firstNode = requestsContainer.firstChild;
+  firstNode.scrollIntoView();
+  yield waitSomeTime();
   ok(!scrolledToBottom(requestsContainer), "Not scrolled to bottom.");
   // save for comparison later
   let scrollTop = requestsContainer.scrollTop;
   yield waitForNetworkEvents(monitor, 8);
   yield waitSomeTime();
   is(requestsContainer.scrollTop, scrollTop, "Did not scroll.");
 
   // (3) Now set the scroll position back at the bottom and check that
   // additional requests *do* cause the container to scroll down.
   requestsContainer.scrollTop = requestsContainer.scrollHeight;
   ok(scrolledToBottom(requestsContainer), "Set scroll position to bottom.");
   yield waitForNetworkEvents(monitor, 8);
   yield waitForScroll();
-  ok(scrolledToBottom(requestsContainer), "Still scrolled to bottom.");
+  ok(true, "Still scrolled to bottom.");
 
   // (4) Now select an item in the list and check that additional requests
   // do not change the scroll position.
   monitor.panelWin.NetMonitorView.RequestsMenu.selectedIndex = 0;
   yield waitForNetworkEvents(monitor, 8);
   yield waitSomeTime();
   is(requestsContainer.scrollTop, 0, "Did not scroll.");
 
   // Done: clean up.
-  yield teardown(monitor);
+  return teardown(monitor);
+
+  function waitForRequestListToAppear() {
+    info("Waiting until the empty notice disappears and is replaced with the list");
+    return waitUntil(() => !!$(".requests-menu-contents"));
+  }
 
   function* waitForRequestsToOverflowContainer() {
+    info("Waiting for enough requests to overflow the container");
     while (true) {
+      info("Waiting for one network request");
       yield waitForNetworkEvents(monitor, 1);
       if (requestsContainer.scrollHeight > requestsContainer.clientHeight) {
+        info("The list is long enough, returning");
         return;
       }
     }
   }
 
   function scrolledToBottom(element) {
     return element.scrollTop + element.clientHeight >= element.scrollHeight;
   }
 
   function waitSomeTime() {
     // Wait to make sure no scrolls happen
     return wait(50);
   }
 
   function waitForScroll() {
-    return monitor._view.RequestsMenu.widget.once("scroll-to-bottom");
+    info("Waiting for the list to scroll to bottom");
+    return waitUntil(() => scrolledToBottom(requestsContainer));
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_brotli.js
+++ b/devtools/client/netmonitor/test/browser_net_brotli.js
@@ -22,17 +22,17 @@ add_task(function* () {
   RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, BROTLI_REQUESTS);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
     "GET", HTTPS_CONTENT_TYPE_SJS + "?fmt=br", {
       status: 200,
       statusText: "Connected",
       type: "plain",
       fullMimeType: "text/plain",
       transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 10),
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 64),
       time: true
--- a/devtools/client/netmonitor/test/browser_net_cached-status.js
+++ b/devtools/client/netmonitor/test/browser_net_cached-status.js
@@ -88,17 +88,18 @@ add_task(function* () {
   info("Performing requests #2...");
   yield performRequestsAndWait();
 
   let index = 0;
   for (let request of REQUEST_DATA) {
     let item = RequestsMenu.getItemAtIndex(index);
 
     info("Verifying request #" + index);
-    yield verifyRequestItemTarget(item, request.method, request.uri, request.details);
+    yield verifyRequestItemTarget(RequestsMenu, item,
+      request.method, request.uri, request.details);
 
     index++;
   }
 
   yield teardown(monitor);
 
   function* performRequestsAndWait() {
     let wait = waitForNetworkEvents(monitor, 3);
--- a/devtools/client/netmonitor/test/browser_net_cause.js
+++ b/devtools/client/netmonitor/test/browser_net_cause.js
@@ -10,17 +10,17 @@
 const CAUSE_FILE_NAME = "html_cause-test-page.html";
 const CAUSE_URL = EXAMPLE_URL + CAUSE_FILE_NAME;
 
 const EXPECTED_REQUESTS = [
   {
     method: "GET",
     url: CAUSE_URL,
     causeType: "document",
-    causeUri: "",
+    causeUri: null,
     // The document load has internal privileged JS code on the stack
     stack: true
   },
   {
     method: "GET",
     url: EXAMPLE_URL + "stylesheet_request",
     causeType: "stylesheet",
     causeUri: CAUSE_URL,
@@ -98,21 +98,21 @@ add_task(function* () {
 
   is(RequestsMenu.itemCount, EXPECTED_REQUESTS.length,
     "All the page events should be recorded.");
 
   EXPECTED_REQUESTS.forEach((spec, i) => {
     let { method, url, causeType, causeUri, stack } = spec;
 
     let requestItem = RequestsMenu.getItemAtIndex(i);
-    verifyRequestItemTarget(requestItem,
+    verifyRequestItemTarget(RequestsMenu, requestItem,
       method, url, { cause: { type: causeType, loadingDocumentUri: causeUri } }
     );
 
-    let { stacktrace } = requestItem.attachment.cause;
+    let { stacktrace } = requestItem.cause;
     let stackLen = stacktrace ? stacktrace.length : 0;
 
     if (stack) {
       ok(stacktrace, `Request #${i} has a stacktrace`);
       ok(stackLen > 0,
         `Request #${i} (${causeType}) has a stacktrace with ${stackLen} items`);
 
       // if "stack" is array, check the details about the top stack frames
@@ -132,16 +132,14 @@ add_task(function* () {
       is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`);
     }
   });
 
   // Sort the requests by cause and check the order
   EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-cause-button"));
   let expectedOrder = EXPECTED_REQUESTS.map(r => r.causeType).sort();
   expectedOrder.forEach((expectedCause, i) => {
-    let { target } = RequestsMenu.getItemAtIndex(i);
-    let causeLabel = target.querySelector(".requests-menu-cause-label");
-    let cause = causeLabel.getAttribute("value");
+    const cause = RequestsMenu.getItemAtIndex(i).cause.type;
     is(cause, expectedCause, `The request #${i} has the expected cause after sorting`);
   });
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_cause_redirect.js
+++ b/devtools/client/netmonitor/test/browser_net_cause_redirect.js
@@ -22,21 +22,21 @@ add_task(function* () {
   let { RequestsMenu } = monitor.panelWin.NetMonitorView;
   RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, EXPECTED_REQUESTS.length);
   yield performRequests(2, HSTS_SJS);
   yield wait;
 
   EXPECTED_REQUESTS.forEach(({status, hasStack}, i) => {
-    let { attachment } = RequestsMenu.getItemAtIndex(i);
+    let item = RequestsMenu.getItemAtIndex(i);
 
-    is(attachment.status, status, `Request #${i} has the expected status`);
+    is(item.status, status, `Request #${i} has the expected status`);
 
-    let { stacktrace } = attachment.cause;
+    let { stacktrace } = item.cause;
     let stackLen = stacktrace ? stacktrace.length : 0;
 
     if (hasStack) {
       ok(stacktrace, `Request #${i} has a stacktrace`);
       ok(stackLen > 0, `Request #${i} has a stacktrace with ${stackLen} items`);
     } else {
       is(stackLen, 0, `Request #${i} has an empty stacktrace`);
     }
--- a/devtools/client/netmonitor/test/browser_net_content-type.js
+++ b/devtools/client/netmonitor/test/browser_net_content-type.js
@@ -19,72 +19,72 @@ add_task(function* () {
   RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
     "GET", CONTENT_TYPE_SJS + "?fmt=xml", {
       status: 200,
       statusText: "OK",
       type: "xml",
       fullMimeType: "text/xml; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 42),
       time: true
     });
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(1),
     "GET", CONTENT_TYPE_SJS + "?fmt=css", {
       status: 200,
       statusText: "OK",
       type: "css",
       fullMimeType: "text/css; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 34),
       time: true
     });
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(2),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(2),
     "GET", CONTENT_TYPE_SJS + "?fmt=js", {
       status: 200,
       statusText: "OK",
       type: "js",
       fullMimeType: "application/javascript; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 34),
       time: true
     });
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(3),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(3),
     "GET", CONTENT_TYPE_SJS + "?fmt=json", {
       status: 200,
       statusText: "OK",
       type: "json",
       fullMimeType: "application/json; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
       time: true
     });
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(4),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(4),
     "GET", CONTENT_TYPE_SJS + "?fmt=bogus", {
       status: 404,
       statusText: "Not Found",
       type: "html",
       fullMimeType: "text/html; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 24),
       time: true
     });
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(5),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(5),
     "GET", TEST_IMAGE, {
       fuzzyUrl: true,
       status: 200,
       statusText: "OK",
       type: "png",
       fullMimeType: "image/png",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 580),
       time: true
     });
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(6),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(6),
     "GET", CONTENT_TYPE_SJS + "?fmt=gzip", {
       status: 200,
       statusText: "OK",
       type: "plain",
       fullMimeType: "text/plain",
       transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 73),
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 10.73),
       time: true
--- a/devtools/client/netmonitor/test/browser_net_copy_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_headers.js
@@ -18,17 +18,17 @@ add_task(function* () {
 
   let wait = waitForNetworkEvents(monitor, 1);
   tab.linkedBrowser.reload();
   yield wait;
 
   let requestItem = RequestsMenu.getItemAtIndex(0);
   RequestsMenu.selectedItem = requestItem;
 
-  let { method, httpVersion, status, statusText } = requestItem.attachment;
+  let { method, httpVersion, status, statusText } = requestItem;
 
   const EXPECTED_REQUEST_HEADERS = [
     `${method} ${SIMPLE_URL} ${httpVersion}`,
     "Host: example.com",
     "User-Agent: " + navigator.userAgent + "",
     "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
     "Accept-Language: " + navigator.languages.join(",") + ";q=0.5",
     "Accept-Encoding: gzip, deflate",
--- a/devtools/client/netmonitor/test/browser_net_copy_url.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_url.js
@@ -20,12 +20,12 @@ add_task(function* () {
   });
   yield wait;
 
   let requestItem = RequestsMenu.getItemAtIndex(0);
   RequestsMenu.selectedItem = requestItem;
 
   yield waitForClipboardPromise(function setup() {
     RequestsMenu.contextMenu.copyUrl();
-  }, requestItem.attachment.url);
+  }, requestItem.url);
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_cors_requests.js
+++ b/devtools/client/netmonitor/test/browser_net_cors_requests.js
@@ -20,13 +20,14 @@ add_task(function* () {
     content.wrappedJSObject.performRequests(url, "triggering/preflight", "post-data");
   });
 
   info("Waiting until the requests appear in netmonitor");
   yield wait;
 
   info("Checking the preflight and flight methods");
   ["OPTIONS", "POST"].forEach((method, i) => {
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i), method, requestUrl);
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(i),
+      method, requestUrl);
   });
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_curl-utils.js
+++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js
@@ -26,29 +26,29 @@ add_task(function* () {
 
   let requests = {
     get: RequestsMenu.getItemAtIndex(0),
     post: RequestsMenu.getItemAtIndex(1),
     multipart: RequestsMenu.getItemAtIndex(2),
     multipartForm: RequestsMenu.getItemAtIndex(3)
   };
 
-  let data = yield createCurlData(requests.get.attachment, gNetwork);
+  let data = yield createCurlData(requests.get, gNetwork);
   testFindHeader(data);
 
-  data = yield createCurlData(requests.post.attachment, gNetwork);
+  data = yield createCurlData(requests.post, gNetwork);
   testIsUrlEncodedRequest(data);
   testWritePostDataTextParams(data);
 
-  data = yield createCurlData(requests.multipart.attachment, gNetwork);
+  data = yield createCurlData(requests.multipart, gNetwork);
   testIsMultipartRequest(data);
   testGetMultipartBoundary(data);
   testRemoveBinaryDataFromMultipartText(data);
 
-  data = yield createCurlData(requests.multipartForm.attachment, gNetwork);
+  data = yield createCurlData(requests.multipartForm, gNetwork);
   testGetHeadersFromMultipartText(data);
 
   if (Services.appinfo.OS != "WINNT") {
     testEscapeStringPosix();
   } else {
     testEscapeStringWin();
   }
 
--- a/devtools/client/netmonitor/test/browser_net_cyrillic-01.js
+++ b/devtools/client/netmonitor/test/browser_net_cyrillic-01.js
@@ -17,17 +17,17 @@ add_task(function* () {
   RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
     "GET", CONTENT_TYPE_SJS + "?fmt=txt", {
       status: 200,
       statusText: "DA DA DA"
     });
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.getElementById("details-pane-toggle"));
   EventUtils.sendMouseEvent({ type: "mousedown" },
--- a/devtools/client/netmonitor/test/browser_net_cyrillic-02.js
+++ b/devtools/client/netmonitor/test/browser_net_cyrillic-02.js
@@ -16,17 +16,17 @@ add_task(function* () {
   let { RequestsMenu } = NetMonitorView;
 
   RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   tab.linkedBrowser.reload();
   yield wait;
 
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
     "GET", CYRILLIC_URL, {
       status: 200,
       statusText: "OK"
     });
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.getElementById("details-pane-toggle"));
   EventUtils.sendMouseEvent({ type: "mousedown" },
--- a/devtools/client/netmonitor/test/browser_net_filter-01.js
+++ b/devtools/client/netmonitor/test/browser_net_filter-01.js
@@ -23,18 +23,119 @@ const REQUESTS_WITH_MEDIA_AND_FLASH = RE
   { url: "sjs_content-type-test-server.sjs?fmt=flash" },
 ]);
 
 const REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS = REQUESTS_WITH_MEDIA_AND_FLASH.concat([
   /* "Upgrade" is a reserved header and can not be set on XMLHttpRequest */
   { url: "sjs_content-type-test-server.sjs?fmt=ws" },
 ]);
 
+const EXPECTED_REQUESTS = [
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=html",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "html",
+      fullMimeType: "text/html; charset=utf-8"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=css",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "css",
+      fullMimeType: "text/css; charset=utf-8"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=js",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "js",
+      fullMimeType: "application/javascript; charset=utf-8"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=font",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "woff",
+      fullMimeType: "font/woff"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=image",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "png",
+      fullMimeType: "image/png"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=audio",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "ogg",
+      fullMimeType: "audio/ogg"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=video",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "webm",
+      fullMimeType: "video/webm"
+    },
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=flash",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "x-shockwave-flash",
+      fullMimeType: "application/x-shockwave-flash"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=ws",
+    data: {
+      fuzzyUrl: true,
+      status: 101,
+      statusText: "Switching Protocols",
+    }
+  }
+];
+
 add_task(function* () {
   let Actions = require("devtools/client/netmonitor/actions/index");
+
   let { monitor } = yield initNetMonitor(FILTERING_URL);
   let { gStore } = monitor.panelWin;
 
   function setFreetextFilter(value) {
     gStore.dispatch(Actions.setFilterText(value));
   }
 
   info("Starting test... ");
@@ -175,90 +276,30 @@ add_task(function* () {
   function testContents(visibility) {
     isnot(RequestsMenu.selectedItem, null,
       "There should still be a selected item after filtering.");
     is(RequestsMenu.selectedIndex, 0,
       "The first item should be still selected after filtering.");
     is(NetMonitorView.detailsPaneHidden, false,
       "The details pane should still be visible after filtering.");
 
-    is(RequestsMenu.items.length, visibility.length,
+    const items = RequestsMenu.items;
+    const visibleItems = RequestsMenu.visibleItems;
+
+    is(items.size, visibility.length,
       "There should be a specific amount of items in the requests menu.");
-    is(RequestsMenu.visibleItems.length, visibility.filter(e => e).length,
-      "There should be a specific amount of visbile items in the requests menu.");
+    is(visibleItems.size, visibility.filter(e => e).length,
+      "There should be a specific amount of visible items in the requests menu.");
 
     for (let i = 0; i < visibility.length; i++) {
-      is(RequestsMenu.getItemAtIndex(i).target.hidden, !visibility[i],
-        "The item at index " + i + " doesn't have the correct hidden state.");
-    }
+      let itemId = items.get(i).id;
+      let shouldBeVisible = !!visibility[i];
+      let isThere = visibleItems.some(r => r.id == itemId);
+      is(isThere, shouldBeVisible,
+        `The item at index ${i} has visibility=${shouldBeVisible}`);
 
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
-      "GET", CONTENT_TYPE_SJS + "?fmt=html", {
-        fuzzyUrl: true,
-        status: 200,
-        statusText: "OK",
-        type: "html",
-        fullMimeType: "text/html; charset=utf-8"
-      });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1),
-      "GET", CONTENT_TYPE_SJS + "?fmt=css", {
-        fuzzyUrl: true,
-        status: 200,
-        statusText: "OK",
-        type: "css",
-        fullMimeType: "text/css; charset=utf-8"
-      });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(2),
-      "GET", CONTENT_TYPE_SJS + "?fmt=js", {
-        fuzzyUrl: true,
-        status: 200,
-        statusText: "OK",
-        type: "js",
-        fullMimeType: "application/javascript; charset=utf-8"
-      });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(3),
-      "GET", CONTENT_TYPE_SJS + "?fmt=font", {
-        fuzzyUrl: true,
-        status: 200,
-        statusText: "OK",
-        type: "woff",
-        fullMimeType: "font/woff"
-      });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(4),
-      "GET", CONTENT_TYPE_SJS + "?fmt=image", {
-        fuzzyUrl: true,
-        status: 200,
-        statusText: "OK",
-        type: "png",
-        fullMimeType: "image/png"
-      });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(5),
-      "GET", CONTENT_TYPE_SJS + "?fmt=audio", {
-        fuzzyUrl: true,
-        status: 200,
-        statusText: "OK",
-        type: "ogg",
-        fullMimeType: "audio/ogg"
-      });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(6),
-      "GET", CONTENT_TYPE_SJS + "?fmt=video", {
-        fuzzyUrl: true,
-        status: 200,
-        statusText: "OK",
-        type: "webm",
-        fullMimeType: "video/webm"
-      });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(7),
-      "GET", CONTENT_TYPE_SJS + "?fmt=flash", {
-        fuzzyUrl: true,
-        status: 200,
-        statusText: "OK",
-        type: "x-shockwave-flash",
-        fullMimeType: "application/x-shockwave-flash"
-      });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(8),
-      "GET", CONTENT_TYPE_SJS + "?fmt=ws", {
-        fuzzyUrl: true,
-        status: 101,
-        statusText: "Switching Protocols",
-      });
+      if (shouldBeVisible) {
+        let { method, url, data } = EXPECTED_REQUESTS[i];
+        verifyRequestItemTarget(RequestsMenu, items.get(i), method, url, data);
+      }
+    }
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_filter-02.js
+++ b/devtools/client/netmonitor/test/browser_net_filter-02.js
@@ -24,16 +24,116 @@ const REQUESTS_WITH_MEDIA_AND_FLASH = RE
   { url: "sjs_content-type-test-server.sjs?fmt=flash" },
 ]);
 
 const REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS = REQUESTS_WITH_MEDIA_AND_FLASH.concat([
   /* "Upgrade" is a reserved header and can not be set on XMLHttpRequest */
   { url: "sjs_content-type-test-server.sjs?fmt=ws" },
 ]);
 
+const EXPECTED_REQUESTS = [
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=html",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "html",
+      fullMimeType: "text/html; charset=utf-8"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=css",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "css",
+      fullMimeType: "text/css; charset=utf-8"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=js",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "js",
+      fullMimeType: "application/javascript; charset=utf-8"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=font",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "woff",
+      fullMimeType: "font/woff"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=image",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "png",
+      fullMimeType: "image/png"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=audio",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "ogg",
+      fullMimeType: "audio/ogg"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=video",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "webm",
+      fullMimeType: "video/webm"
+    },
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=flash",
+    data: {
+      fuzzyUrl: true,
+      status: 200,
+      statusText: "OK",
+      type: "x-shockwave-flash",
+      fullMimeType: "application/x-shockwave-flash"
+    }
+  },
+  {
+    method: "GET",
+    url: CONTENT_TYPE_SJS + "?fmt=ws",
+    data: {
+      fuzzyUrl: true,
+      status: 101,
+      statusText: "Switching Protocols",
+    }
+  }
+];
+
 add_task(function* () {
   let { monitor } = yield initNetMonitor(FILTERING_URL);
   info("Starting test... ");
 
   // It seems that this test may be slow on Ubuntu builds running on ec2.
   requestLongerTimeout(2);
 
   let { $, NetMonitorView } = monitor.panelWin;
@@ -93,108 +193,34 @@ add_task(function* () {
   function testContents(visibility) {
     isnot(RequestsMenu.selectedItem, null,
       "There should still be a selected item after filtering.");
     is(RequestsMenu.selectedIndex, 0,
       "The first item should be still selected after filtering.");
     is(NetMonitorView.detailsPaneHidden, false,
       "The details pane should still be visible after filtering.");
 
-    is(RequestsMenu.items.length, visibility.length,
+    const items = RequestsMenu.items;
+    const visibleItems = RequestsMenu.visibleItems;
+
+    is(items.size, visibility.length,
       "There should be a specific amount of items in the requests menu.");
-    is(RequestsMenu.visibleItems.length, visibility.filter(e => e).length,
-      "There should be a specific amount of visbile items in the requests menu.");
+    is(visibleItems.size, visibility.filter(e => e).length,
+      "There should be a specific amount of visible items in the requests menu.");
 
     for (let i = 0; i < visibility.length; i++) {
-      is(RequestsMenu.getItemAtIndex(i).target.hidden, !visibility[i],
-        "The item at index " + i + " doesn't have the correct hidden state.");
+      let itemId = items.get(i).id;
+      let shouldBeVisible = !!visibility[i];
+      let isThere = visibleItems.some(r => r.id == itemId);
+      is(isThere, shouldBeVisible,
+        `The item at index ${i} has visibility=${shouldBeVisible}`);
     }
 
-    for (let i = 0; i < visibility.length; i += 9) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
-        "GET", CONTENT_TYPE_SJS + "?fmt=html", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "html",
-          fullMimeType: "text/html; charset=utf-8"
-        });
-    }
-    for (let i = 1; i < visibility.length; i += 9) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
-        "GET", CONTENT_TYPE_SJS + "?fmt=css", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "css",
-          fullMimeType: "text/css; charset=utf-8"
-        });
-    }
-    for (let i = 2; i < visibility.length; i += 9) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
-        "GET", CONTENT_TYPE_SJS + "?fmt=js", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "js",
-          fullMimeType: "application/javascript; charset=utf-8"
-        });
-    }
-    for (let i = 3; i < visibility.length; i += 9) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
-        "GET", CONTENT_TYPE_SJS + "?fmt=font", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "woff",
-          fullMimeType: "font/woff"
-        });
-    }
-    for (let i = 4; i < visibility.length; i += 9) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
-        "GET", CONTENT_TYPE_SJS + "?fmt=image", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "png",
-          fullMimeType: "image/png"
-        });
-    }
-    for (let i = 5; i < visibility.length; i += 9) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
-        "GET", CONTENT_TYPE_SJS + "?fmt=audio", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "ogg",
-          fullMimeType: "audio/ogg"
-        });
-    }
-    for (let i = 6; i < visibility.length; i += 9) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
-        "GET", CONTENT_TYPE_SJS + "?fmt=video", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "webm",
-          fullMimeType: "video/webm"
-        });
-    }
-    for (let i = 7; i < visibility.length; i += 9) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
-        "GET", CONTENT_TYPE_SJS + "?fmt=flash", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "x-shockwave-flash",
-          fullMimeType: "application/x-shockwave-flash"
-        });
-    }
-    for (let i = 8; i < visibility.length; i += 9) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
-        "GET", CONTENT_TYPE_SJS + "?fmt=ws", {
-          fuzzyUrl: true,
-          status: 101,
-          statusText: "Switching Protocols"
-        });
+    for (let i = 0; i < EXPECTED_REQUESTS.length; i++) {
+      let { method, url, data } = EXPECTED_REQUESTS[i];
+      for (let j = i; j < visibility.length; j += EXPECTED_REQUESTS.length) {
+        if (visibility[j]) {
+          verifyRequestItemTarget(RequestsMenu, items.get(j), method, url, data);
+        }
+      }
     }
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_filter-03.js
+++ b/devtools/client/netmonitor/test/browser_net_filter-03.js
@@ -99,87 +99,11 @@ add_task(function* () {
     is(RequestsMenu.selectedIndex, selection,
       "The first item should be still selected after filtering.");
     is(NetMonitorView.detailsPaneHidden, false,
       "The details pane should still be visible after filtering.");
 
     is(RequestsMenu.items.length, order.length,
       "There should be a specific amount of items in the requests menu.");
     is(RequestsMenu.visibleItems.length, visible,
-      "There should be a specific amount of visbile items in the requests menu.");
-
-    for (let i = 0; i < order.length; i++) {
-      is(RequestsMenu.getItemAtIndex(i), RequestsMenu.items[i],
-        "The requests menu items aren't ordered correctly. Misplaced item " + i + ".");
-    }
-
-    for (let i = 0, len = order.length / 7; i < len; i++) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i]),
-        "GET", CONTENT_TYPE_SJS + "?fmt=html", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "html",
-          fullMimeType: "text/html; charset=utf-8"
-        });
-    }
-    for (let i = 0, len = order.length / 7; i < len; i++) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len]),
-        "GET", CONTENT_TYPE_SJS + "?fmt=css", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "css",
-          fullMimeType: "text/css; charset=utf-8"
-        });
-    }
-    for (let i = 0, len = order.length / 7; i < len; i++) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 2]),
-        "GET", CONTENT_TYPE_SJS + "?fmt=js", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "js",
-          fullMimeType: "application/javascript; charset=utf-8"
-        });
-    }
-    for (let i = 0, len = order.length / 7; i < len; i++) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 3]),
-        "GET", CONTENT_TYPE_SJS + "?fmt=font", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "woff",
-          fullMimeType: "font/woff"
-        });
-    }
-    for (let i = 0, len = order.length / 7; i < len; i++) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 4]),
-        "GET", CONTENT_TYPE_SJS + "?fmt=image", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "png",
-          fullMimeType: "image/png"
-        });
-    }
-    for (let i = 0, len = order.length / 7; i < len; i++) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 5]),
-        "GET", CONTENT_TYPE_SJS + "?fmt=audio", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "ogg",
-          fullMimeType: "audio/ogg"
-        });
-    }
-    for (let i = 0, len = order.length / 7; i < len; i++) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 6]),
-        "GET", CONTENT_TYPE_SJS + "?fmt=video", {
-          fuzzyUrl: true,
-          status: 200,
-          statusText: "OK",
-          type: "webm",
-          fullMimeType: "video/webm"
-        });
-    }
+      "There should be a specific amount of visible items in the requests menu.");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_footer-summary.js
+++ b/devtools/client/netmonitor/test/browser_net_footer-summary.js
@@ -1,31 +1,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 /**
- * Test if the summary text displayed in the network requests menu footer
- * is correct.
+ * Test if the summary text displayed in the network requests menu footer is correct.
  */
 
 add_task(function* () {
   requestLongerTimeout(2);
 
   let { tab, monitor } = yield initNetMonitor(FILTERING_URL);
   info("Starting test... ");
 
-  let { $, NetMonitorView, gStore } = monitor.panelWin;
+  let { $, NetMonitorView, gStore, windowRequire } = monitor.panelWin;
   let { RequestsMenu } = NetMonitorView;
 
-  let winRequire = monitor.panelWin.require;
-  let { getSummary } = winRequire("devtools/client/netmonitor/selectors/index");
-  let { L10N } = winRequire("devtools/client/netmonitor/l10n");
-  let { PluralForm } = winRequire("devtools/shared/plural-form");
+  let { getDisplayedRequestsSummary } =
+    windowRequire("devtools/client/netmonitor/selectors/index");
+  let { L10N } = windowRequire("devtools/client/netmonitor/l10n");
+  let { PluralForm } = windowRequire("devtools/shared/plural-form");
 
   RequestsMenu.lazyUpdate = false;
   testStatus();
 
   for (let i = 0; i < 2; i++) {
     info(`Performing requests in batch #${i}`);
     let wait = waitForNetworkEvents(monitor, 8);
     yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
@@ -41,31 +40,32 @@ add_task(function* () {
       EventUtils.sendMouseEvent({ type: "click" }, buttonEl);
       testStatus();
     }
   }
 
   yield teardown(monitor);
 
   function testStatus() {
-    const { count, totalBytes, totalMillis } = getSummary(gStore.getState());
     let value = $("#requests-menu-network-summary-button").textContent;
     info("Current summary: " + value);
 
-    let totalRequestsCount = RequestsMenu.itemCount;
-    info("Current requests: " + count + " of " + totalRequestsCount + ".");
+    let state = gStore.getState();
+    let totalRequestsCount = state.requests.requests.size;
+    let requestsSummary = getDisplayedRequestsSummary(state);
+    info(`Current requests: ${requestsSummary.count} of ${totalRequestsCount}.`);
 
-    if (!totalRequestsCount || !count) {
+    if (!totalRequestsCount || !requestsSummary.count) {
       is(value, L10N.getStr("networkMenu.empty"),
         "The current summary text is incorrect, expected an 'empty' label.");
       return;
     }
 
-    info("Computed total bytes: " + totalBytes);
-    info("Computed total millis: " + totalMillis);
+    info(`Computed total bytes: ${requestsSummary.bytes}`);
+    info(`Computed total millis: ${requestsSummary.millis}`);
 
-    is(value, PluralForm.get(count, L10N.getStr("networkMenu.summary"))
-      .replace("#1", count)
-      .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, 2))
-      .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, 2))
-    , "The current summary text is incorrect.");
+    is(value, PluralForm.get(requestsSummary.count, L10N.getStr("networkMenu.summary"))
+      .replace("#1", requestsSummary.count)
+      .replace("#2", L10N.numberWithDecimals(requestsSummary.bytes / 1024, 2))
+      .replace("#3", L10N.numberWithDecimals(requestsSummary.millis / 1000, 2))
+    , "The current summary text is correct.");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_frame.js
+++ b/devtools/client/netmonitor/test/browser_net_frame.js
@@ -12,17 +12,17 @@ const SUB_FILE_NAME = "html_frame-subdoc
 const TOP_URL = EXAMPLE_URL + TOP_FILE_NAME;
 const SUB_URL = EXAMPLE_URL + SUB_FILE_NAME;
 
 const EXPECTED_REQUESTS_TOP = [
   {
     method: "GET",
     url: TOP_URL,
     causeType: "document",
-    causeUri: "",
+    causeUri: null,
     stack: true
   },
   {
     method: "GET",
     url: EXAMPLE_URL + "stylesheet_request",
     causeType: "stylesheet",
     causeUri: TOP_URL,
     stack: false
@@ -171,32 +171,31 @@ add_task(function* () {
   // While there is a defined order for requests in each document separately, the requests
   // from different documents may interleave in various ways that change per test run, so
   // there is not a single order when considering all the requests together.
   let currentTop = 0;
   let currentSub = 0;
   for (let i = 0; i < REQUEST_COUNT; i++) {
     let requestItem = RequestsMenu.getItemAtIndex(i);
 
-    let itemUrl = requestItem.attachment.url;
-    let itemCauseUri = requestItem.target.querySelector(".requests-menu-cause-label")
-                                         .getAttribute("tooltiptext");
+    let itemUrl = requestItem.url;
+    let itemCauseUri = requestItem.cause.loadingDocumentUri;
     let spec;
     if (itemUrl == SUB_URL || itemCauseUri == SUB_URL) {
       spec = EXPECTED_REQUESTS_SUB[currentSub++];
     } else {
       spec = EXPECTED_REQUESTS_TOP[currentTop++];
     }
     let { method, url, causeType, causeUri, stack } = spec;
 
-    verifyRequestItemTarget(requestItem,
+    verifyRequestItemTarget(RequestsMenu, requestItem,
       method, url, { cause: { type: causeType, loadingDocumentUri: causeUri } }
     );
 
-    let { stacktrace } = requestItem.attachment.cause;
+    let { stacktrace } = requestItem.cause;
     let stackLen = stacktrace ? stacktrace.length : 0;
 
     if (stack) {
       ok(stacktrace, `Request #${i} has a stacktrace`);
       ok(stackLen > 0,
         `Request #${i} (${causeType}) has a stacktrace with ${stackLen} items`);
 
       // if "stack" is array, check the details about the top stack frames
--- a/devtools/client/netmonitor/test/browser_net_icon-preview.js
+++ b/devtools/client/netmonitor/test/browser_net_icon-preview.js
@@ -19,17 +19,17 @@ add_task(function* () {
 
   let wait = waitForEvents();
   yield performRequests();
   yield wait;
 
   info("Checking the image thumbnail when all items are shown.");
   checkImageThumbnail();
 
-  RequestsMenu.sortBy("size");
+  gStore.dispatch(Actions.sortBy("size"));
   info("Checking the image thumbnail when all items are sorted.");
   checkImageThumbnail();
 
   gStore.dispatch(Actions.toggleFilterType("images"));
   info("Checking the image thumbnail when only images are shown.");
   checkImageThumbnail();
 
   info("Reloading the debuggee and performing all requests again...");
@@ -56,16 +56,16 @@ add_task(function* () {
   }
 
   function* reloadAndPerformRequests() {
     yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
     yield performRequests();
   }
 
   function checkImageThumbnail() {
-    is($all(".requests-menu-icon[type=thumbnail]").length, 1,
+    is($all(".requests-menu-icon[data-type=thumbnail]").length, 1,
       "There should be only one image request with a thumbnail displayed.");
-    is($(".requests-menu-icon[type=thumbnail]").src, TEST_IMAGE_DATA_URI,
+    is($(".requests-menu-icon[data-type=thumbnail]").src, TEST_IMAGE_DATA_URI,
       "The image requests-menu-icon thumbnail is displayed correctly.");
-    is($(".requests-menu-icon[type=thumbnail]").hidden, false,
+    is($(".requests-menu-icon[data-type=thumbnail]").hidden, false,
       "The image requests-menu-icon thumbnail should not be hidden.");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_image-tooltip.js
+++ b/devtools/client/netmonitor/test/browser_net_image-tooltip.js
@@ -21,38 +21,38 @@ add_task(function* test() {
   let onEvents = waitForNetworkEvents(monitor, IMAGE_TOOLTIP_REQUESTS);
   let onThumbnail = monitor.panelWin.once(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
 
   yield performRequests();
   yield onEvents;
   yield onThumbnail;
 
   info("Checking the image thumbnail after a few requests were made...");
-  yield showTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.items[0]);
+  yield showTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.getItemAtIndex(0));
 
   // Hide tooltip before next test, to avoid the situation that tooltip covers
   // the icon for the request of the next test.
   info("Checking the image thumbnail gets hidden...");
-  yield hideTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.items[0]);
+  yield hideTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.getItemAtIndex(0));
 
   // +1 extra document reload
   onEvents = waitForNetworkEvents(monitor, IMAGE_TOOLTIP_REQUESTS + 1);
   onThumbnail = monitor.panelWin.once(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
 
   info("Reloading the debuggee and performing all requests again...");
   yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
   yield performRequests();
   yield onEvents;
   yield onThumbnail;
 
   info("Checking the image thumbnail after a reload.");
-  yield showTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.items[1]);
+  yield showTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.getItemAtIndex(1));
 
   info("Checking if the image thumbnail is hidden when mouse leaves the menu widget");
-  let requestsMenuEl = $("#requests-menu-contents");
+  let requestsMenuEl = $(".requests-menu-contents");
   let onHidden = RequestsMenu.tooltip.once("hidden");
   EventUtils.synthesizeMouse(requestsMenuEl, 0, 0, {type: "mouseout"}, monitor.panelWin);
   yield onHidden;
 
   yield teardown(monitor);
 
   function performRequests() {
     return ContentTask.spawn(tab.linkedBrowser, {}, function* () {
@@ -60,17 +60,17 @@ add_task(function* test() {
     });
   }
 
   /**
    * Show a tooltip on the {requestItem} and verify that it was displayed
    * with the expected content.
    */
   function* showTooltipAndVerify(tooltip, requestItem) {
-    let anchor = $(".requests-menu-file", requestItem.target);
+    let anchor = $(".requests-menu-file", getItemTarget(RequestsMenu, requestItem));
     yield showTooltipOn(tooltip, anchor);
 
     info("Tooltip was successfully opened for the image request.");
     is(tooltip.panel.querySelector("img").src, TEST_IMAGE_DATA_URI,
       "The tooltip's image content is displayed correctly.");
   }
 
   /**
@@ -83,19 +83,19 @@ add_task(function* test() {
     EventUtils.synthesizeMouseAtCenter(element, {type: "mousemove"}, win);
     return onShown;
   }
 
   /**
    * Hide a tooltip on the {requestItem} and verify that it was closed.
    */
   function* hideTooltipAndVerify(tooltip, requestItem) {
-    // Hovering method hides tooltip.
-    let anchor = $(".requests-menu-method", requestItem.target);
+    // Hovering over the "method" column hides the tooltip.
+    let anchor = $(".requests-menu-method", getItemTarget(RequestsMenu, requestItem));
 
-    let onHidden = tooltip.once("hidden");
+    let onTooltipHidden = tooltip.once("hidden");
     let win = anchor.ownerDocument.defaultView;
     EventUtils.synthesizeMouseAtCenter(anchor, {type: "mousemove"}, win);
-    yield onHidden;
+    yield onTooltipHidden;
 
     info("Tooltip was successfully closed.");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_json-long.js
+++ b/devtools/client/netmonitor/test/browser_net_json-long.js
@@ -23,17 +23,17 @@ add_task(function* () {
   RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
     "GET", CONTENT_TYPE_SJS + "?fmt=json-long", {
       status: 200,
       statusText: "OK",
       type: "json",
       fullMimeType: "text/json; charset=utf-8",
       size: L10N.getFormatStr("networkMenu.sizeKB",
         L10N.numberWithDecimals(85975 / 1024, 2)),
       time: true
--- a/devtools/client/netmonitor/test/browser_net_json-malformed.js
+++ b/devtools/client/netmonitor/test/browser_net_json-malformed.js
@@ -17,17 +17,17 @@ add_task(function* () {
   RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
     "GET", CONTENT_TYPE_SJS + "?fmt=json-malformed", {
       status: 200,
       statusText: "OK",
       type: "json",
       fullMimeType: "text/json; charset=utf-8"
     });
 
   let onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
--- a/devtools/client/netmonitor/test/browser_net_json_custom_mime.js
+++ b/devtools/client/netmonitor/test/browser_net_json_custom_mime.js
@@ -19,17 +19,17 @@ add_task(function* () {
   RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
     "GET", CONTENT_TYPE_SJS + "?fmt=json-custom-mime", {
       status: 200,
       statusText: "OK",
       type: "x-bigcorp-json",
       fullMimeType: "text/x-bigcorp-json; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41),
       time: true
     });
--- a/devtools/client/netmonitor/test/browser_net_json_text_mime.js
+++ b/devtools/client/netmonitor/test/browser_net_json_text_mime.js
@@ -19,17 +19,17 @@ add_task(function* () {
   RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
     "GET", CONTENT_TYPE_SJS + "?fmt=json-text-mime", {
       status: 200,
       statusText: "OK",
       type: "plain",
       fullMimeType: "text/plain; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41),
       time: true
     });
--- a/devtools/client/netmonitor/test/browser_net_jsonp.js
+++ b/devtools/client/netmonitor/test/browser_net_jsonp.js
@@ -20,26 +20,26 @@ add_task(function* () {
   NetworkDetails._json.lazyEmpty = false;
 
   let wait = waitForNetworkEvents(monitor, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
     "GET", CONTENT_TYPE_SJS + "?fmt=jsonp&jsonp=$_0123Fun", {
       status: 200,
       statusText: "OK",
       type: "json",
       fullMimeType: "text/json; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41),
       time: true
     });
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(1),
     "GET", CONTENT_TYPE_SJS + "?fmt=jsonp2&jsonp=$_4567Sad", {
       status: 200,
       statusText: "OK",
       type: "json",
       fullMimeType: "text/json; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 54),
       time: true
     });
--- a/devtools/client/netmonitor/test/browser_net_large-response.js
+++ b/devtools/client/netmonitor/test/browser_net_large-response.js
@@ -23,17 +23,17 @@ add_task(function* () {
   RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, HTML_LONG_URL, function* (url) {
     content.wrappedJSObject.performRequests(1, url);
   });
   yield wait;
 
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
     "GET", CONTENT_TYPE_SJS + "?fmt=html-long", {
       status: 200,
       statusText: "OK"
     });
 
   let onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.getElementById("details-pane-toggle"));
--- a/devtools/client/netmonitor/test/browser_net_page-nav.js
+++ b/devtools/client/netmonitor/test/browser_net_page-nav.js
@@ -54,16 +54,16 @@ add_task(function* () {
 
   function* testClose() {
     info("Closing...");
 
     let onDestroyed = monitor.once("destroyed");
     removeTab(tab);
     yield onDestroyed;
 
-    ok(!monitor._controller.client,
+    ok(!monitor.panelWin.NetMonitorController.client,
       "There shouldn't be a client available after destruction.");
-    ok(!monitor._controller.tabClient,
+    ok(!monitor.panelWin.NetMonitorController.tabClient,
       "There shouldn't be a tabClient available after destruction.");
-    ok(!monitor._controller.webConsoleClient,
+    ok(!monitor.panelWin.NetMonitorController.webConsoleClient,
       "There shouldn't be a webConsoleClient available after destruction.");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_post-data-01.js
+++ b/devtools/client/netmonitor/test/browser_net_post-data-01.js
@@ -20,26 +20,26 @@ add_task(function* () {
   NetworkDetails._params.lazyEmpty = false;
 
   let wait = waitForNetworkEvents(monitor, 0, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
     "POST", SIMPLE_SJS + "?foo=bar&baz=42&type=urlencoded", {
       status: 200,
       statusText: "Och Aye",
       type: "plain",
       fullMimeType: "text/plain; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
       time: true
     });
-  verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1),
+  verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(1),
     "POST", SIMPLE_SJS + "?foo=bar&baz=42&type=multipart", {
       status: 200,
       statusText: "Och Aye",
       type: "plain",
       fullMimeType: "text/plain; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
       time: true
     });
--- a/devtools/client/netmonitor/test/browser_net_prefs-reload.js
+++ b/devtools/client/netmonitor/test/browser_net_prefs-reload.js
@@ -4,36 +4,38 @@
 "use strict";
 
 /**
  * Tests if the prefs that should survive across tool reloads work.
  */
 
 add_task(function* () {
   let Actions = require("devtools/client/netmonitor/actions/index");
+  let { getActiveFilters } = require("devtools/client/netmonitor/selectors/index");
+
   let { monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
   // This test reopens the network monitor a bunch of times, for different
   // hosts (bottom, side, window). This seems to be slow on debug builds.
   requestLongerTimeout(3);
 
   // Use these getters instead of caching instances inside the panel win,
   // since the tool is reopened a bunch of times during this test
   // and the instances will differ.
-  let getView = () => monitor.panelWin.NetMonitorView;
   let getStore = () => monitor.panelWin.gStore;
+  let getState = () => getStore().getState();
 
   let prefsToCheck = {
     filters: {
       // A custom new value to be used for the verified preference.
       newValue: ["html", "css"],
       // Getter used to retrieve the current value from the frontend, in order
       // to verify that the pref was applied properly.
-      validateValue: ($) => getView().RequestsMenu._activeFilters,
+      validateValue: ($) => getActiveFilters(getState()),
       // Predicate used to modify the frontend when setting the new pref value,
       // before trying to validate the changes.
       modifyFrontend: ($, value) => value.forEach(e =>
         getStore().dispatch(Actions.toggleFilterType(e)))
     },
     networkDetailsWidth: {
       newValue: ~~(Math.random() * 200 + 100),
       validateValue: ($) => ~~$("#details-pane").getAttribute("width"),
--- a/devtools/client/netmonitor/test/browser_net_raw_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_raw_headers.js
@@ -26,37 +26,37 @@ add_task(function* () {
 
   let onTabEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
   RequestsMenu.selectedItem = origItem;
   yield onTabEvent;
 
   EventUtils.sendMouseEvent({ type: "click" },
     document.getElementById("toggle-raw-headers"));
 
-  testShowRawHeaders(origItem.attachment);
+  testShowRawHeaders(origItem);
 
   EventUtils.sendMouseEvent({ type: "click" },
     document.getElementById("toggle-raw-headers"));
 
   testHideRawHeaders(document);
 
   return teardown(monitor);
 
   /*
    * Tests that raw headers were displayed correctly
    */
   function testShowRawHeaders(data) {
     let requestHeaders = document.getElementById("raw-request-headers-textarea").value;
     for (let header of data.requestHeaders.headers) {
-      ok(requestHeaders.indexOf(header.name + ": " + header.value) >= 0,
+      ok(requestHeaders.includes(header.name + ": " + header.value),
         "textarea contains request headers");
     }
     let responseHeaders = document.getElementById("raw-response-headers-textarea").value;
     for (let header of data.responseHeaders.headers) {
-      ok(responseHeaders.indexOf(header.name + ": " + header.value) >= 0,
+      ok(responseHeaders.includes(header.name + ": " + header.value),
         "textarea contains response headers");
     }
   }
 
   /*
    * Tests that raw headers textareas are hidden and empty
    */
   function testHideRawHeaders() {
--- a/devtools/client/netmonitor/test/browser_net_reload-button.js
+++ b/devtools/client/netmonitor/test/browser_net_reload-button.js
@@ -3,23 +3,23 @@
 
 "use strict";
 
 /**
  * Tests if the empty-requests reload button works.
  */
 
 add_task(function* () {
-  let { monitor } = yield initNetMonitor(SINGLE_GET_URL);
+  let { monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
   let { document, NetMonitorView } = monitor.panelWin;
   let { RequestsMenu } = NetMonitorView;
 
-  let wait = waitForNetworkEvents(monitor, 2);
+  let wait = waitForNetworkEvents(monitor, 1);
   let button = document.querySelector("#requests-menu-reload-notice-button");
   button.click();
   yield wait;
 
-  is(RequestsMenu.itemCount, 2, "The request menu should have two items after reloading");
+  is(RequestsMenu.itemCount, 1, "The request menu should have one item after reloading");
 
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_reload-markers.js
+++ b/devtools/client/netmonitor/test/browser_net_reload-markers.js
@@ -3,30 +3,30 @@
 
 "use strict";
 
 /**
  * Tests if the empty-requests reload button works.
  */
 
 add_task(function* () {
-  let { monitor } = yield initNetMonitor(SINGLE_GET_URL);
+  let { monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
   let { document, EVENTS } = monitor.panelWin;
   let button = document.querySelector("#requests-menu-reload-notice-button");
   button.click();
 
   let markers = [];
 
   monitor.panelWin.on(EVENTS.TIMELINE_EVENT, (_, marker) => {
     markers.push(marker);
   });
 
-  yield waitForNetworkEvents(monitor, 2);
+  yield waitForNetworkEvents(monitor, 1);
   yield waitUntil(() => markers.length == 2);
 
   ok(true, "Reloading finished");
 
   is(markers[0].name, "document::DOMContentLoaded",
     "The first received marker is correct.");
   is(markers[1].name, "document::Load",
     "The second received marker is correct.");
--- a/devtools/client/netmonitor/test/browser_net_req-resp-bodies.js
+++ b/devtools/client/netmonitor/test/browser_net_req-resp-bodies.js
@@ -49,17 +49,17 @@ add_task(function* () {
   });
   yield wait;
 
   verifyRequest(1);
 
   return teardown(monitor);
 
   function verifyRequest(offset) {
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(offset),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(offset),
       "GET", CONTENT_TYPE_SJS + "?fmt=json-long", {
         status: 200,
         statusText: "OK",
         type: "json",
         fullMimeType: "text/json; charset=utf-8",
         size: L10N.getFormatStr("networkMenu.sizeKB",
           L10N.numberWithDecimals(85975 / 1024, 2)),
         time: true
--- a/devtools/client/netmonitor/test/browser_net_resend.js
+++ b/devtools/client/netmonitor/test/browser_net_resend.js
@@ -34,55 +34,47 @@ add_task(function* () {
   RequestsMenu.selectedItem = origItem;
   yield onTabUpdated;
 
   // add a new custom request cloned from selected request
   let onPopulated = panelWin.once(EVENTS.CUSTOMREQUESTVIEW_POPULATED);
   RequestsMenu.cloneSelectedRequest();
   yield onPopulated;
 
-  testCustomForm(origItem.attachment);
+  testCustomForm(origItem);
 
   let customItem = RequestsMenu.selectedItem;
   testCustomItem(customItem, origItem);
 
   // edit the custom request
   yield editCustomForm();
+  // FIXME: reread the customItem, it's been replaced by a new object (immutable!)
+  customItem = RequestsMenu.selectedItem;
   testCustomItemChanged(customItem, origItem);
 
   // send the new request
   wait = waitForNetworkEvents(monitor, 0, 1);
   RequestsMenu.sendCustomRequest();
   yield wait;
 
   let sentItem = RequestsMenu.selectedItem;
-  testSentRequest(sentItem.attachment, origItem.attachment);
+  testSentRequest(sentItem, origItem);
 
   return teardown(monitor);
 
   function testCustomItem(item, orig) {
-    let method = item.target.querySelector(".requests-menu-method").value;
-    let origMethod = orig.target.querySelector(".requests-menu-method").value;
-    is(method, origMethod, "menu item is showing the same method as original request");
-
-    let file = item.target.querySelector(".requests-menu-file").value;
-    let origFile = orig.target.querySelector(".requests-menu-file").value;
-    is(file, origFile, "menu item is showing the same file name as original request");
-
-    let domain = item.target.querySelector(".requests-menu-domain").value;
-    let origDomain = orig.target.querySelector(".requests-menu-domain").value;
-    is(domain, origDomain, "menu item is showing the same domain as original request");
+    is(item.method, orig.method, "item is showing the same method as original request");
+    is(item.url, orig.url, "item is showing the same URL as original request");
   }
 
   function testCustomItemChanged(item, orig) {
-    let file = item.target.querySelector(".requests-menu-file").value;
-    let expectedFile = orig.target.querySelector(".requests-menu-file").value +
-      "&" + ADD_QUERY;
+    let url = item.url;
+    let expectedUrl = orig.url + "&" + ADD_QUERY;
 
-    is(file, expectedFile, "menu item is updated to reflect url entered in form");
+    is(url, expectedUrl, "menu item is updated to reflect url entered in form");
   }
 
   /*
    * Test that the New Request form was populated correctly
    */
   function testCustomForm(data) {
     is(document.getElementById("custom-method-value").value, data.method,
        "new request form showing correct method");
--- a/devtools/client/netmonitor/test/browser_net_resend_cors.js
+++ b/devtools/client/netmonitor/test/browser_net_resend_cors.js
@@ -25,19 +25,19 @@ add_task(function* () {
     content.wrappedJSObject.performRequests(url, "triggering/preflight", "post-data");
   });
   yield wait;
 
   const METHODS = ["OPTIONS", "POST"];
 
   // Check the requests that were sent
   for (let [i, method] of METHODS.entries()) {
-    let { attachment } = RequestsMenu.getItemAtIndex(i);
-    is(attachment.method, method, `The ${method} request has the right method`);
-    is(attachment.url, requestUrl, `The ${method} request has the right URL`);
+    let item = RequestsMenu.getItemAtIndex(i);
+    is(item.method, method, `The ${method} request has the right method`);
+    is(item.url, requestUrl, `The ${method} request has the right URL`);
   }
 
   // Resend both requests without modification. Wait for resent OPTIONS, then POST.
   // POST is supposed to have no preflight OPTIONS request this time (CORS is disabled)
   let onRequests = waitForNetworkEvents(monitor, 1, 1);
   for (let [i, method] of METHODS.entries()) {
     let item = RequestsMenu.getItemAtIndex(i);
 
@@ -56,17 +56,17 @@ add_task(function* () {
   }
 
   info("Waiting for both resent requests");
   yield onRequests;
 
   // Check the resent requests
   for (let [i, method] of METHODS.entries()) {
     let index = i + 2;
-    let item = RequestsMenu.getItemAtIndex(index).attachment;
+    let item = RequestsMenu.getItemAtIndex(index);
     is(item.method, method, `The ${method} request has the right method`);
     is(item.url, requestUrl, `The ${method} request has the right URL`);
     is(item.status, 200, `The ${method} response has the right status`);
 
     if (method === "POST") {
       is(item.requestPostData.postData.text, "post-data",
         "The POST request has the right POST data");
       // eslint-disable-next-line mozilla/no-cpows-in-tests
--- a/devtools/client/netmonitor/test/browser_net_resend_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_resend_headers.js
@@ -30,31 +30,31 @@ add_task(function* () {
   NetMonitorController.webConsoleClient.sendHTTPRequest({
     url: requestUrl,
     method: "POST",
     headers: requestHeaders,
     body: "Hello"
   });
   yield wait;
 
-  let { attachment } = RequestsMenu.getItemAtIndex(0);
-  is(attachment.method, "POST", "The request has the right method");
-  is(attachment.url, requestUrl, "The request has the right URL");
+  let item = RequestsMenu.getItemAtIndex(0);
+  is(item.method, "POST", "The request has the right method");
+  is(item.url, requestUrl, "The request has the right URL");
 
-  for (let { name, value } of attachment.requestHeaders.headers) {
+  for (let { name, value } of item.requestHeaders.headers) {
     info(`Request header: ${name}: ${value}`);
   }
 
   function hasRequestHeader(name, value) {
-    let { headers } = attachment.requestHeaders;
+    let { headers } = item.requestHeaders;
     return headers.some(h => h.name === name && h.value === value);
   }
 
   function hasNotRequestHeader(name) {
-    let { headers } = attachment.requestHeaders;
+    let { headers } = item.requestHeaders;
     return headers.every(h => h.name !== name);
   }
 
   for (let { name, value } of requestHeaders) {
     ok(hasRequestHeader(name, value), `The ${name} header has the right value`);
   }
 
   // Check that the Cookie header was not added silently (i.e., that the request is
--- a/devtools/client/netmonitor/test/browser_net_security-icon-click.js
+++ b/devtools/client/netmonitor/test/browser_net_security-icon-click.js
@@ -37,21 +37,21 @@ add_task(function* () {
     let wait = waitForNetworkEvents(monitor, 1);
     yield ContentTask.spawn(tab.linkedBrowser, { url }, function* (args) {
       content.wrappedJSObject.performRequests(1, args.url);
     });
     return wait;
   }
 
   function* clickAndTestSecurityIcon() {
-    let item = RequestsMenu.items[0];
-    let icon = $(".requests-security-state-icon", item.target);
+    let item = RequestsMenu.getItemAtIndex(0);
+    let target = getItemTarget(RequestsMenu, item);
+    let icon = $(".requests-security-state-icon", target);
 
-    info("Clicking security icon of the first request and waiting for the " +
-         "panel to update.");
+    info("Clicking security icon of the first request and waiting for panel update.");
+    EventUtils.synthesizeMouseAtCenter(icon, {}, monitor.panelWin);
 
-    icon.click();
     yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
 
     is(NetworkDetails.widget.selectedPanel, $("#security-tabpanel"),
       "Security tab is selected.");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_security-redirect.js
+++ b/devtools/client/netmonitor/test/browser_net_security-redirect.js
@@ -17,21 +17,23 @@ add_task(function* () {
   let wait = waitForNetworkEvents(monitor, 2);
   yield ContentTask.spawn(tab.linkedBrowser, HTTPS_REDIRECT_SJS, function* (url) {
     content.wrappedJSObject.performRequests(1, url);
   });
   yield wait;
 
   is(RequestsMenu.itemCount, 2, "There were two requests due to redirect.");
 
-  let initial = RequestsMenu.items[0];
-  let redirect = RequestsMenu.items[1];
+  let initial = RequestsMenu.getItemAtIndex(0);
+  let redirect = RequestsMenu.getItemAtIndex(1);
 
-  let initialSecurityIcon = $(".requests-security-state-icon", initial.target);
-  let redirectSecurityIcon = $(".requests-security-state-icon", redirect.target);
+  let initialSecurityIcon =
+    $(".requests-security-state-icon", getItemTarget(RequestsMenu, initial));
+  let redirectSecurityIcon =
+    $(".requests-security-state-icon", getItemTarget(RequestsMenu, redirect));
 
   ok(initialSecurityIcon.classList.contains("security-state-insecure"),
      "Initial request was marked insecure.");
 
   ok(redirectSecurityIcon.classList.contains("security-state-secure"),
      "Redirected request was marked secure.");
 
   yield teardown(monitor);
--- a/devtools/client/netmonitor/test/browser_net_security-state.js
+++ b/devtools/client/netmonitor/test/browser_net_security-state.js
@@ -19,22 +19,23 @@ add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
   let { $, EVENTS, NetMonitorView } = monitor.panelWin;
   let { RequestsMenu } = NetMonitorView;
   RequestsMenu.lazyUpdate = false;
 
   yield performRequests();
 
   for (let item of RequestsMenu.items) {
-    let domain = $(".requests-menu-domain", item.target).value;
+    let target = getItemTarget(RequestsMenu, item);
+    let domain = $(".requests-menu-domain", target).textContent;
 
     info("Found a request to " + domain);
     ok(domain in EXPECTED_SECURITY_STATES, "Domain " + domain + " was expected.");
 
-    let classes = $(".requests-security-state-icon", item.target).classList;
+    let classes = $(".requests-security-state-icon", target).classList;
     let expectedClass = EXPECTED_SECURITY_STATES[domain];
 
     info("Classes of security state icon are: " + classes);
     info("Security state icon is expected to contain class: " + expectedClass);
     ok(classes.contains(expectedClass), "Icon contained the correct class name.");
   }
 
   return teardown(monitor);
--- a/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
+++ b/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
@@ -53,29 +53,29 @@ add_task(function* () {
     });
 
     info("Waiting for new network event.");
     yield onNewItem;
 
     info("Selecting the request.");
     RequestsMenu.selectedIndex = 0;
 
-    is(RequestsMenu.selectedItem.attachment.securityState, undefined,
+    is(RequestsMenu.selectedItem.securityState, undefined,
        "Security state has not yet arrived.");
     is(tabEl.hidden, !testcase.visibleOnNewEvent,
        "Security tab is " +
         (testcase.visibleOnNewEvent ? "visible" : "hidden") +
        " after new request was added to the menu.");
     is(tabpanel.hidden, false,
       "#security-tabpanel is visible after new request was added to the menu.");
 
     info("Waiting for security information to arrive.");
     yield onSecurityInfo;
 
-    ok(RequestsMenu.selectedItem.attachment.securityState,
+    ok(RequestsMenu.selectedItem.securityState,
        "Security state arrived.");
     is(tabEl.hidden, !testcase.visibleOnSecurityInfo,
        "Security tab is " +
         (testcase.visibleOnSecurityInfo ? "visible" : "hidden") +
        " after security information arrived.");
     is(tabpanel.hidden, false,
       "#security-tabpanel is visible after security information arrived.");
 
--- a/devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js
+++ b/devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js
@@ -21,14 +21,14 @@ add_task(function* () {
   yield ContentTask.spawn(beaconTab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequest();
   });
   tab.linkedBrowser.reload();
   yield wait;
 
   is(RequestsMenu.itemCount, 1, "Only the reload should be recorded.");
   let request = RequestsMenu.getItemAtIndex(0);
-  is(request.attachment.method, "GET", "The method is correct.");
-  is(request.attachment.status, "200", "The status is correct.");
+  is(request.method, "GET", "The method is correct.");
+  is(request.status, "200", "The status is correct.");
 
   yield removeTab(beaconTab);
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_send-beacon.js
+++ b/devtools/client/netmonitor/test/browser_net_send-beacon.js
@@ -18,14 +18,14 @@ add_task(function* () {
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequest();
   });
   yield wait;
 
   is(RequestsMenu.itemCount, 1, "The beacon should be recorded.");
   let request = RequestsMenu.getItemAtIndex(0);
-  is(request.attachment.method, "POST", "The method is correct.");
-  ok(request.attachment.url.endsWith("beacon_request"), "The URL is correct.");
-  is(request.attachment.status, "404", "The status is correct.");
+  is(request.method, "POST", "The method is correct.");
+  ok(request.url.endsWith("beacon_request"), "The URL is correct.");
+  is(request.status, "404", "The status is correct.");
 
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_service-worker-status.js
+++ b/devtools/client/netmonitor/test/browser_net_service-worker-status.js
@@ -46,19 +46,20 @@ add_task(function* () {
   });
   yield wait;
 
   let index = 0;
   for (let request of REQUEST_DATA) {
     let item = RequestsMenu.getItemAtIndex(index);
 
     info(`Verifying request #${index}`);
-    yield verifyRequestItemTarget(item, request.method, request.uri, request.details);
+    yield verifyRequestItemTarget(RequestsMenu, item,
+      request.method, request.uri, request.details);
 
-    let { stacktrace } = item.attachment.cause;
+    let { stacktrace } = item.cause;
     let stackLen = stacktrace ? stacktrace.length : 0;
 
     ok(stacktrace, `Request #${index} has a stacktrace`);
     ok(stackLen >= request.stackFunctions.length,
       `Request #${index} has a stacktrace with enough (${stackLen}) items`);
 
     request.stackFunctions.forEach((functionName, j) => {
       is(stacktrace[j].functionName, functionName,
--- a/devtools/client/netmonitor/test/browser_net_simple-init.js
+++ b/devtools/client/netmonitor/test/browser_net_simple-init.js
@@ -17,77 +17,77 @@ function test() {
     info("Starting test... ");
 
     is(tab.linkedBrowser.currentURI.spec, SIMPLE_URL,
       "The current tab's location is the correct one.");
 
     function checkIfInitialized(tag) {
       info(`Checking if initialization is ok (${tag}).`);
 
-      ok(monitor._view,
+      ok(monitor.panelWin.NetMonitorView,
         `The network monitor view object exists (${tag}).`);
-      ok(monitor._controller,
+      ok(monitor.panelWin.NetMonitorController,
         `The network monitor controller object exists (${tag}).`);
-      ok(monitor._controller._startup,
+      ok(monitor.panelWin.NetMonitorController._startup,
         `The network monitor controller object exists and is initialized (${tag}).`);
 
       ok(monitor.isReady,
         `The network monitor panel appears to be ready (${tag}).`);
 
-      ok(monitor._controller.tabClient,
+      ok(monitor.panelWin.NetMonitorController.tabClient,
         `There should be a tabClient available at this point (${tag}).`);
-      ok(monitor._controller.webConsoleClient,
+      ok(monitor.panelWin.NetMonitorController.webConsoleClient,
         `There should be a webConsoleClient available at this point (${tag}).`);
-      ok(monitor._controller.timelineFront,
+      ok(monitor.panelWin.NetMonitorController.timelineFront,
         `There should be a timelineFront available at this point (${tag}).`);
     }
 
     function checkIfDestroyed(tag) {
       gInfo("Checking if destruction is ok.");
 
-      gOk(monitor._view,
+      gOk(monitor.panelWin.NetMonitorView,
         `The network monitor view object still exists (${tag}).`);
-      gOk(monitor._controller,
+      gOk(monitor.panelWin.NetMonitorController,
         `The network monitor controller object still exists (${tag}).`);
-      gOk(monitor._controller._shutdown,
+      gOk(monitor.panelWin.NetMonitorController._shutdown,
         `The network monitor controller object still exists and is destroyed (${tag}).`);
 
-      gOk(!monitor._controller.tabClient,
+      gOk(!monitor.panelWin.NetMonitorController.tabClient,
         `There shouldn't be a tabClient available after destruction (${tag}).`);
-      gOk(!monitor._controller.webConsoleClient,
+      gOk(!monitor.panelWin.NetMonitorController.webConsoleClient,
         `There shouldn't be a webConsoleClient available after destruction (${tag}).`);
-      gOk(!monitor._controller.timelineFront,
+      gOk(!monitor.panelWin.NetMonitorController.timelineFront,
         `There shouldn't be a timelineFront available after destruction (${tag}).`);
     }
 
     executeSoon(() => {
       checkIfInitialized(1);
 
-      monitor._controller.startupNetMonitor()
+      monitor.panelWin.NetMonitorController.startupNetMonitor()
         .then(() => {
           info("Starting up again shouldn't do anything special.");
           checkIfInitialized(2);
-          return monitor._controller.connect();
+          return monitor.panelWin.NetMonitorController.connect();
         })
         .then(() => {
           info("Connecting again shouldn't do anything special.");
           checkIfInitialized(3);
           return teardown(monitor);
         })
         .then(finish);
     });
 
     registerCleanupFunction(() => {
       checkIfDestroyed(1);
 
-      monitor._controller.shutdownNetMonitor()
+      monitor.panelWin.NetMonitorController.shutdownNetMonitor()
         .then(() => {
           gInfo("Shutting down again shouldn't do anything special.");
           checkIfDestroyed(2);
-          return monitor._controller.disconnect();
+          return monitor.panelWin.NetMonitorController.disconnect();
         })
         .then(() => {
           gInfo("Disconnecting again shouldn't do anything special.");
           checkIfDestroyed(3);
         });
     });
   });
 }
--- a/devtools/client/netmonitor/test/browser_net_simple-request-data.js
+++ b/devtools/client/netmonitor/test/browser_net_simple-request-data.js
@@ -27,221 +27,210 @@ function test() {
         "There shouldn't be any selected item in the requests menu.");
       is(RequestsMenu.itemCount, 1,
         "The requests menu should not be empty after the first request.");
       is(NetMonitorView.detailsPaneHidden, true,
         "The details pane should still be hidden after the first request.");
 
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
-      is(typeof requestItem.value, "string",
+      is(typeof requestItem.id, "string",
         "The attached request id is incorrect.");
-      isnot(requestItem.value, "",
+      isnot(requestItem.id, "",
         "The attached request id should not be empty.");
 
-      is(typeof requestItem.attachment.startedDeltaMillis, "number",
-        "The attached startedDeltaMillis is incorrect.");
-      is(requestItem.attachment.startedDeltaMillis, 0,
-        "The attached startedDeltaMillis should be zero.");
-
-      is(typeof requestItem.attachment.startedMillis, "number",
+      is(typeof requestItem.startedMillis, "number",
         "The attached startedMillis is incorrect.");
-      isnot(requestItem.attachment.startedMillis, 0,
+      isnot(requestItem.startedMillis, 0,
         "The attached startedMillis should not be zero.");
 
-      is(requestItem.attachment.requestHeaders, undefined,
+      is(requestItem.requestHeaders, undefined,
         "The requestHeaders should not yet be set.");
-      is(requestItem.attachment.requestCookies, undefined,
+      is(requestItem.requestCookies, undefined,
         "The requestCookies should not yet be set.");
-      is(requestItem.attachment.requestPostData, undefined,
+      is(requestItem.requestPostData, undefined,
         "The requestPostData should not yet be set.");
 
-      is(requestItem.attachment.responseHeaders, undefined,
+      is(requestItem.responseHeaders, undefined,
         "The responseHeaders should not yet be set.");
-      is(requestItem.attachment.responseCookies, undefined,
+      is(requestItem.responseCookies, undefined,
         "The responseCookies should not yet be set.");
 
-      is(requestItem.attachment.httpVersion, undefined,
+      is(requestItem.httpVersion, undefined,
         "The httpVersion should not yet be set.");
-      is(requestItem.attachment.status, undefined,
+      is(requestItem.status, undefined,
         "The status should not yet be set.");
-      is(requestItem.attachment.statusText, undefined,
+      is(requestItem.statusText, undefined,
         "The statusText should not yet be set.");
 
-      is(requestItem.attachment.headersSize, undefined,
+      is(requestItem.headersSize, undefined,
         "The headersSize should not yet be set.");
-      is(requestItem.attachment.transferredSize, undefined,
+      is(requestItem.transferredSize, undefined,
         "The transferredSize should not yet be set.");
-      is(requestItem.attachment.contentSize, undefined,
+      is(requestItem.contentSize, undefined,
         "The contentSize should not yet be set.");
 
-      is(requestItem.attachment.mimeType, undefined,
+      is(requestItem.mimeType, undefined,
         "The mimeType should not yet be set.");
-      is(requestItem.attachment.responseContent, undefined,
+      is(requestItem.responseContent, undefined,
         "The responseContent should not yet be set.");
 
-      is(requestItem.attachment.totalTime, undefined,
+      is(requestItem.totalTime, undefined,
         "The totalTime should not yet be set.");
-      is(requestItem.attachment.eventTimings, undefined,
+      is(requestItem.eventTimings, undefined,
         "The eventTimings should not yet be set.");
 
-      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS);
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_REQUEST_HEADERS, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
-
-      ok(requestItem.attachment.requestHeaders,
-        "There should be a requestHeaders attachment available.");
-      is(requestItem.attachment.requestHeaders.headers.length, 9,
-        "The requestHeaders attachment has an incorrect |headers| property.");
-      isnot(requestItem.attachment.requestHeaders.headersSize, 0,
-        "The requestHeaders attachment has an incorrect |headersSize| property.");
+      ok(requestItem.requestHeaders,
+        "There should be a requestHeaders data available.");
+      is(requestItem.requestHeaders.headers.length, 10,
+        "The requestHeaders data has an incorrect |headers| property.");
+      isnot(requestItem.requestHeaders.headersSize, 0,
+        "The requestHeaders data has an incorrect |headersSize| property.");
       // Can't test for the exact request headers size because the value may
       // vary across platforms ("User-Agent" header differs).
 
       verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_REQUEST_COOKIES, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
-      ok(requestItem.attachment.requestCookies,
-        "There should be a requestCookies attachment available.");
-      is(requestItem.attachment.requestCookies.cookies.length, 2,
-        "The requestCookies attachment has an incorrect |cookies| property.");
+      ok(requestItem.requestCookies,
+        "There should be a requestCookies data available.");
+      is(requestItem.requestCookies.cookies.length, 2,
+        "The requestCookies data has an incorrect |cookies| property.");
 
-      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS);
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_REQUEST_POST_DATA, () => {
       ok(false, "Trap listener: this request doesn't have any post data.");
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_HEADERS, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
-      ok(requestItem.attachment.responseHeaders,
-        "There should be a responseHeaders attachment available.");
-      is(requestItem.attachment.responseHeaders.headers.length, 10,
-        "The responseHeaders attachment has an incorrect |headers| property.");
-      is(requestItem.attachment.responseHeaders.headersSize, 330,
-        "The responseHeaders attachment has an incorrect |headersSize| property.");
+      ok(requestItem.responseHeaders,
+        "There should be a responseHeaders data available.");
+      is(requestItem.responseHeaders.headers.length, 10,
+        "The responseHeaders data has an incorrect |headers| property.");
+      is(requestItem.responseHeaders.headersSize, 330,
+        "The responseHeaders data has an incorrect |headersSize| property.");
 
-      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS);
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_COOKIES, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
-      ok(requestItem.attachment.responseCookies,
-        "There should be a responseCookies attachment available.");
-      is(requestItem.attachment.responseCookies.cookies.length, 2,
-        "The responseCookies attachment has an incorrect |cookies| property.");
+      ok(requestItem.responseCookies,
+        "There should be a responseCookies data available.");
+      is(requestItem.responseCookies.cookies.length, 2,
+        "The responseCookies data has an incorrect |cookies| property.");
 
-      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS);
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.STARTED_RECEIVING_RESPONSE, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
-      is(requestItem.attachment.httpVersion, "HTTP/1.1",
-        "The httpVersion attachment has an incorrect value.");
-      is(requestItem.attachment.status, "200",
-        "The status attachment has an incorrect value.");
-      is(requestItem.attachment.statusText, "Och Aye",
-        "The statusText attachment has an incorrect value.");
-      is(requestItem.attachment.headersSize, 330,
-        "The headersSize attachment has an incorrect value.");
+      is(requestItem.httpVersion, "HTTP/1.1",
+        "The httpVersion data has an incorrect value.");
+      is(requestItem.status, "200",
+        "The status data has an incorrect value.");
+      is(requestItem.statusText, "Och Aye",
+        "The statusText data has an incorrect value.");
+      is(requestItem.headersSize, 330,
+        "The headersSize data has an incorrect value.");
 
-      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
         status: "200",
         statusText: "Och Aye"
       });
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.UPDATING_RESPONSE_CONTENT, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
-      is(requestItem.attachment.transferredSize, "12",
-        "The transferredSize attachment has an incorrect value.");
-      is(requestItem.attachment.contentSize, "12",
-        "The contentSize attachment has an incorrect value.");
-      is(requestItem.attachment.mimeType, "text/plain; charset=utf-8",
-        "The mimeType attachment has an incorrect value.");
+      is(requestItem.transferredSize, "12",
+        "The transferredSize data has an incorrect value.");
+      is(requestItem.contentSize, "12",
+        "The contentSize data has an incorrect value.");
+      is(requestItem.mimeType, "text/plain; charset=utf-8",
+        "The mimeType data has an incorrect value.");
 
-      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
         type: "plain",
         fullMimeType: "text/plain; charset=utf-8",
-        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
-        size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
       });
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_CONTENT, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
-      ok(requestItem.attachment.responseContent,
-        "There should be a responseContent attachment available.");
-      is(requestItem.attachment.responseContent.content.mimeType,
+      ok(requestItem.responseContent,
+        "There should be a responseContent data available.");
+      is(requestItem.responseContent.content.mimeType,
         "text/plain; charset=utf-8",
-        "The responseContent attachment has an incorrect |content.mimeType| property.");
-      is(requestItem.attachment.responseContent.content.text,
+        "The responseContent data has an incorrect |content.mimeType| property.");
+      is(requestItem.responseContent.content.text,
         "Hello world!",
-        "The responseContent attachment has an incorrect |content.text| property.");
-      is(requestItem.attachment.responseContent.content.size,
+        "The responseContent data has an incorrect |content.text| property.");
+      is(requestItem.responseContent.content.size,
         12,
-        "The responseContent attachment has an incorrect |content.size| property.");
+        "The responseContent data has an incorrect |content.size| property.");
 
-      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
         type: "plain",
         fullMimeType: "text/plain; charset=utf-8",
-        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
-        size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
       });
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.UPDATING_EVENT_TIMINGS, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
-      is(typeof requestItem.attachment.totalTime, "number",
+      is(typeof requestItem.totalTime, "number",
         "The attached totalTime is incorrect.");
-      ok(requestItem.attachment.totalTime >= 0,
+      ok(requestItem.totalTime >= 0,
         "The attached totalTime should be positive.");
 
-      is(typeof requestItem.attachment.endedMillis, "number",
-        "The attached endedMillis is incorrect.");
-      ok(requestItem.attachment.endedMillis >= 0,
-        "The attached endedMillis should be positive.");
-
-      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
         time: true
       });
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_EVENT_TIMINGS, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
-      ok(requestItem.attachment.eventTimings,
-        "There should be a eventTimings attachment available.");
-      is(typeof requestItem.attachment.eventTimings.timings.blocked, "number",
-        "The eventTimings attachment has an incorrect |timings.blocked| property.");
-      is(typeof requestItem.attachment.eventTimings.timings.dns, "number",
-        "The eventTimings attachment has an incorrect |timings.dns| property.");
-      is(typeof requestItem.attachment.eventTimings.timings.connect, "number",
-        "The eventTimings attachment has an incorrect |timings.connect| property.");
-      is(typeof requestItem.attachment.eventTimings.timings.send, "number",
-        "The eventTimings attachment has an incorrect |timings.send| property.");
-      is(typeof requestItem.attachment.eventTimings.timings.wait, "number",
-        "The eventTimings attachment has an incorrect |timings.wait| property.");
-      is(typeof requestItem.attachment.eventTimings.timings.receive, "number",
-        "The eventTimings attachment has an incorrect |timings.receive| property.");
-      is(typeof requestItem.attachment.eventTimings.totalTime, "number",
-        "The eventTimings attachment has an incorrect |totalTime| property.");
+      ok(requestItem.eventTimings,
+        "There should be a eventTimings data available.");
+      is(typeof requestItem.eventTimings.timings.blocked, "number",
+        "The eventTimings data has an incorrect |timings.blocked| property.");
+      is(typeof requestItem.eventTimings.timings.dns, "number",
+        "The eventTimings data has an incorrect |timings.dns| property.");
+      is(typeof requestItem.eventTimings.timings.connect, "number",
+        "The eventTimings data has an incorrect |timings.connect| property.");
+      is(typeof requestItem.eventTimings.timings.send, "number",
+        "The eventTimings data has an incorrect |timings.send| property.");
+      is(typeof requestItem.eventTimings.timings.wait, "number",
+        "The eventTimings data has an incorrect |timings.wait| property.");
+      is(typeof requestItem.eventTimings.timings.receive, "number",
+        "The eventTimings data has an incorrect |timings.receive| property.");
+      is(typeof requestItem.eventTimings.totalTime, "number",
+        "The eventTimings data has an incorrect |totalTime| property.");
 
-      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
         time: true
       });
     });
 
     tab.linkedBrowser.reload();
   });
 }
--- a/devtools/client/netmonitor/test/browser_net_simple-request-details.js
+++ b/devtools/client/netmonitor/test/browser_net_simple-request-details.js
@@ -57,17 +57,17 @@ add_task(function* () {
     is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
       SIMPLE_SJS, "The url summary value is incorrect.");
     is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("tooltiptext"),
       SIMPLE_SJS, "The url summary tooltiptext is incorrect.");
     is(tabpanel.querySelector("#headers-summary-method-value").getAttribute("value"),
       "GET", "The method summary value is incorrect.");
     is(tabpanel.querySelector("#headers-summary-address-value").getAttribute("value"),
       "127.0.0.1:8888", "The remote address summary value is incorrect.");
-    is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("code"),
+    is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("data-code"),
       "200", "The status summary code is incorrect.");
     is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
       "200 Och Aye", "The status summary value is incorrect.");
 
     is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
       "There should be 2 header scopes displayed in this tabpanel.");
     is(tabpanel.querySelectorAll(".variable-or-property").length, 19,
       "There should be 19 header values displayed in this tabpanel.");
--- a/devtools/client/netmonitor/test/browser_net_simple-request.js
+++ b/devtools/client/netmonitor/test/browser_net_simple-request.js
@@ -18,50 +18,50 @@ add_task(function* () {
 
   let { document, NetMonitorView } = monitor.panelWin;
   let { RequestsMenu } = NetMonitorView;
 
   RequestsMenu.lazyUpdate = false;
 
   is(document.querySelector("#details-pane-toggle").hasAttribute("disabled"), true,
     "The pane toggle button should be disabled when the frontend is opened.");
-  is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), false,
+  ok(document.querySelector("#requests-menu-empty-notice"),
     "An empty notice should be displayed when the frontend is opened.");
   is(RequestsMenu.itemCount, 0,
     "The requests menu should be empty when the frontend is opened.");
   is(NetMonitorView.detailsPaneHidden, true,
     "The details pane should be hidden when the frontend is opened.");
 
   yield reloadAndWait();
 
   is(document.querySelector("#details-pane-toggle").hasAttribute("disabled"), false,
     "The pane toggle button should be enabled after the first request.");
-  is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), true,
+  ok(!document.querySelector("#requests-menu-empty-notice"),
     "The empty notice should be hidden after the first request.");
   is(RequestsMenu.itemCount, 1,
     "The requests menu should not be empty after the first request.");
   is(NetMonitorView.detailsPaneHidden, true,
     "The details pane should still be hidden after the first request.");
 
   yield reloadAndWait();
 
   is(document.querySelector("#details-pane-toggle").hasAttribute("disabled"), false,
     "The pane toggle button should be still be enabled after a reload.");
-  is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), true,
+  ok(!document.querySelector("#requests-menu-empty-notice"),
     "The empty notice should be still hidden after a reload.");
   is(RequestsMenu.itemCount, 1,
     "The requests menu should not be empty after a reload.");
   is(NetMonitorView.detailsPaneHidden, true,
     "The details pane should still be hidden after a reload.");
 
   RequestsMenu.clear();
 
   is(document.querySelector("#details-pane-toggle").hasAttribute("disabled"), true,
     "The pane toggle button should be disabled when after clear.");
-  is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), false,
+  ok(document.querySelector("#requests-menu-empty-notice"),
     "An empty notice should be displayed again after clear.");
   is(RequestsMenu.itemCount, 0,
     "The requests menu should be empty after clear.");
   is(NetMonitorView.detailsPaneHidden, true,
     "The details pane should be hidden after clear.");
 
   return teardown(monitor);
 
--- a/devtools/client/netmonitor/test/browser_net_sort-01.js
+++ b/devtools/client/netmonitor/test/browser_net_sort-01.js
@@ -157,71 +157,60 @@ add_task(function* () {
 
   return teardown(monitor);
 
   function testContents([a, b, c, d, e]) {
     is(RequestsMenu.items.length, 5,
       "There should be a total of 5 items in the requests menu.");
     is(RequestsMenu.visibleItems.length, 5,
       "There should be a total of 5 visbile items in the requests menu.");
-    is($all(".side-menu-widget-item").length, 5,
+    is($all(".request-list-item").length, 5,
       "The visible items in the requests menu are, in fact, visible!");
 
-    is(RequestsMenu.getItemAtIndex(0), RequestsMenu.items[0],
-      "The requests menu items aren't ordered correctly. First item is misplaced.");
-    is(RequestsMenu.getItemAtIndex(1), RequestsMenu.items[1],
-      "The requests menu items aren't ordered correctly. Second item is misplaced.");
-    is(RequestsMenu.getItemAtIndex(2), RequestsMenu.items[2],
-      "The requests menu items aren't ordered correctly. Third item is misplaced.");
-    is(RequestsMenu.getItemAtIndex(3), RequestsMenu.items[3],
-      "The requests menu items aren't ordered correctly. Fourth item is misplaced.");
-    is(RequestsMenu.getItemAtIndex(4), RequestsMenu.items[4],
-      "The requests menu items aren't ordered correctly. Fifth item is misplaced.");
-
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(a),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(a),
       "GET", STATUS_CODES_SJS + "?sts=100", {
         status: 101,
         statusText: "Switching Protocols",
         type: "plain",
         fullMimeType: "text/plain; charset=utf-8",
         transferred: L10N.getStr("networkMenu.sizeUnavailable"),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
         time: true
       });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(b),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(b),
       "GET", STATUS_CODES_SJS + "?sts=200", {
         status: 202,
         statusText: "Created",
         type: "plain",
         fullMimeType: "text/plain; charset=utf-8",
         transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
         time: true
       });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(c),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(c),
       "GET", STATUS_CODES_SJS + "?sts=300", {
         status: 303,
         statusText: "See Other",
         type: "plain",
         fullMimeType: "text/plain; charset=utf-8",
         transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
         time: true
       });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(d),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(d),
       "GET", STATUS_CODES_SJS + "?sts=400", {
         status: 404,
         statusText: "Not Found",
         type: "plain",
         fullMimeType: "text/plain; charset=utf-8",
         transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
         time: true
       });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(e),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(e),
       "GET", STATUS_CODES_SJS + "?sts=500", {
         status: 501,
         statusText: "Not Implemented",
         type: "plain",
         fullMimeType: "text/plain; charset=utf-8",
         transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
         time: true
--- a/devtools/client/netmonitor/test/browser_net_sort-02.js
+++ b/devtools/client/netmonitor/test/browser_net_sort-02.js
@@ -167,102 +167,91 @@ add_task(function* () {
 
   function testHeaders(sortType, direction) {
     let doc = monitor.panelWin.document;
     let target = doc.querySelector("#requests-menu-" + sortType + "-button");
     let headers = doc.querySelectorAll(".requests-menu-header-button");
 
     for (let header of headers) {
       if (header != target) {
-        is(header.hasAttribute("sorted"), false,
-          "The " + header.id + " header should not have a 'sorted' attribute.");
-        is(header.hasAttribute("tooltiptext"), false,
-          "The " + header.id + " header should not have a 'tooltiptext' attribute.");
+        ok(!header.hasAttribute("data-sorted"),
+          "The " + header.id + " header does not have a 'data-sorted' attribute.");
+        ok(!header.getAttribute("title"),
+          "The " + header.id + " header does not have a 'title' attribute.");
       } else {
-        is(header.getAttribute("sorted"), direction,
-          "The " + header.id + " header has an incorrect 'sorted' attribute.");
-        is(header.getAttribute("tooltiptext"), direction == "ascending"
+        is(header.getAttribute("data-sorted"), direction,
+          "The " + header.id + " header has a correct 'data-sorted' attribute.");
+        is(header.getAttribute("title"), direction == "ascending"
           ? L10N.getStr("networkMenu.sortedAsc")
           : L10N.getStr("networkMenu.sortedDesc"),
-          "The " + header.id + " has an incorrect 'tooltiptext' attribute.");
+          "The " + header.id + " header has a correct 'title' attribute.");
       }
     }
   }
 
   function testContents([a, b, c, d, e]) {
     isnot(RequestsMenu.selectedItem, null,
       "There should still be a selected item after sorting.");
     is(RequestsMenu.selectedIndex, a,
       "The first item should be still selected after sorting.");
     is(NetMonitorView.detailsPaneHidden, false,
       "The details pane should still be visible after sorting.");
 
     is(RequestsMenu.items.length, 5,
       "There should be a total of 5 items in the requests menu.");
     is(RequestsMenu.visibleItems.length, 5,
-      "There should be a total of 5 visbile items in the requests menu.");
-    is($all(".side-menu-widget-item").length, 5,
+      "There should be a total of 5 visible items in the requests menu.");
+    is($all(".request-list-item").length, 5,
       "The visible items in the requests menu are, in fact, visible!");
 
-    is(RequestsMenu.getItemAtIndex(0), RequestsMenu.items[0],
-      "The requests menu items aren't ordered correctly. First item is misplaced.");
-    is(RequestsMenu.getItemAtIndex(1), RequestsMenu.items[1],
-      "The requests menu items aren't ordered correctly. Second item is misplaced.");
-    is(RequestsMenu.getItemAtIndex(2), RequestsMenu.items[2],
-      "The requests menu items aren't ordered correctly. Third item is misplaced.");
-    is(RequestsMenu.getItemAtIndex(3), RequestsMenu.items[3],
-      "The requests menu items aren't ordered correctly. Fourth item is misplaced.");
-    is(RequestsMenu.getItemAtIndex(4), RequestsMenu.items[4],
-      "The requests menu items aren't ordered correctly. Fifth item is misplaced.");
-
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(a),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(a),
       "GET1", SORTING_SJS + "?index=1", {
         fuzzyUrl: true,
         status: 101,
         statusText: "Meh",
         type: "1",
         fullMimeType: "text/1",
         transferred: L10N.getStr("networkMenu.sizeUnavailable"),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
         time: true
       });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(b),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(b),
       "GET2", SORTING_SJS + "?index=2", {
         fuzzyUrl: true,
         status: 200,
         statusText: "Meh",
         type: "2",
         fullMimeType: "text/2",
         transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
         time: true
       });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(c),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(c),
       "GET3", SORTING_SJS + "?index=3", {
         fuzzyUrl: true,
         status: 300,
         statusText: "Meh",
         type: "3",
         fullMimeType: "text/3",
         transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
         time: true
       });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(d),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(d),
       "GET4", SORTING_SJS + "?index=4", {
         fuzzyUrl: true,
         status: 400,
         statusText: "Meh",
         type: "4",
         fullMimeType: "text/4",
         transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
         time: true
       });
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(e),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(e),
       "GET5", SORTING_SJS + "?index=5", {
         fuzzyUrl: true,
         status: 500,
         statusText: "Meh",
         type: "5",
         fullMimeType: "text/5",
         transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
         size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
--- a/devtools/client/netmonitor/test/browser_net_sort-03.js
+++ b/devtools/client/netmonitor/test/browser_net_sort-03.js
@@ -100,105 +100,105 @@ add_task(function* () {
 
   function testHeaders(sortType, direction) {
     let doc = monitor.panelWin.document;
     let target = doc.querySelector("#requests-menu-" + sortType + "-button");
     let headers = doc.querySelectorAll(".requests-menu-header-button");
 
     for (let header of headers) {
       if (header != target) {
-        is(header.hasAttribute("sorted"), false,
-          "The " + header.id + " header should not have a 'sorted' attribute.");
-        is(header.hasAttribute("tooltiptext"), false,
-          "The " + header.id + " header should not have a 'tooltiptext' attribute.");
+        ok(!header.hasAttribute("data-sorted"),
+          "The " + header.id + " header does not have a 'data-sorted' attribute.");
+        ok(!header.getAttribute("title"),
+          "The " + header.id + " header does not have a 'title' attribute.");
       } else {
-        is(header.getAttribute("sorted"), direction,
-          "The " + header.id + " header has an incorrect 'sorted' attribute.");
-        is(header.getAttribute("tooltiptext"), direction == "ascending"
+        is(header.getAttribute("data-sorted"), direction,
+          "The " + header.id + " header has a correct 'data-sorted' attribute.");
+        is(header.getAttribute("title"), direction == "ascending"
           ? L10N.getStr("networkMenu.sortedAsc")
           : L10N.getStr("networkMenu.sortedDesc"),
-          "The " + header.id + " has an incorrect 'tooltiptext' attribute.");
+          "The " + header.id + " header has a correct 'title' attribute.");
       }
     }
   }
 
   function testContents(order, selection) {
     isnot(RequestsMenu.selectedItem, null,
       "There should still be a selected item after sorting.");
     is(RequestsMenu.selectedIndex, selection,
       "The first item should be still selected after sorting.");
     is(NetMonitorView.detailsPaneHidden, false,
       "The details pane should still be visible after sorting.");
 
     is(RequestsMenu.items.length, order.length,
       "There should be a specific number of items in the requests menu.");
     is(RequestsMenu.visibleItems.length, order.length,
       "There should be a specific number of visbile items in the requests menu.");
-    is($all(".side-menu-widget-item").length, order.length,
+    is($all(".request-list-item").length, order.length,
       "The visible items in the requests menu are, in fact, visible!");
 
-    for (let i = 0; i < order.length; i++) {
-      is(RequestsMenu.getItemAtIndex(i), RequestsMenu.items[i],
-        "The requests menu items aren't ordered correctly. Misplaced item " + i + ".");
-    }
-
     for (let i = 0, len = order.length / 5; i < len; i++) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i]),
+      verifyRequestItemTarget(RequestsMenu,
+        RequestsMenu.getItemAtIndex(order[i]),
         "GET1", SORTING_SJS + "?index=1", {
           fuzzyUrl: true,
           status: 101,
           statusText: "Meh",
           type: "1",
           fullMimeType: "text/1",
           transferred: L10N.getStr("networkMenu.sizeUnavailable"),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
           time: true
         });
     }
     for (let i = 0, len = order.length / 5; i < len; i++) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len]),
+      verifyRequestItemTarget(RequestsMenu,
+        RequestsMenu.getItemAtIndex(order[i + len]),
         "GET2", SORTING_SJS + "?index=2", {
           fuzzyUrl: true,
           status: 200,
           statusText: "Meh",
           type: "2",
           fullMimeType: "text/2",
           transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
           time: true
         });
     }
     for (let i = 0, len = order.length / 5; i < len; i++) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 2]),
+      verifyRequestItemTarget(RequestsMenu,
+        RequestsMenu.getItemAtIndex(order[i + len * 2]),
         "GET3", SORTING_SJS + "?index=3", {
           fuzzyUrl: true,
           status: 300,
           statusText: "Meh",
           type: "3",
           fullMimeType: "text/3",
           transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
           time: true
         });
     }
     for (let i = 0, len = order.length / 5; i < len; i++) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 3]),
+      verifyRequestItemTarget(RequestsMenu,
+        RequestsMenu.getItemAtIndex(order[i + len * 3]),
         "GET4", SORTING_SJS + "?index=4", {
           fuzzyUrl: true,
           status: 400,
           statusText: "Meh",
           type: "4",
           fullMimeType: "text/4",
           transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
           time: true
         });
     }
     for (let i = 0, len = order.length / 5; i < len; i++) {
-      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 4]),
+      verifyRequestItemTarget(RequestsMenu,
+        RequestsMenu.getItemAtIndex(order[i + len * 4]),
         "GET5", SORTING_SJS + "?index=5", {
           fuzzyUrl: true,
           status: 500,
           statusText: "Meh",
           type: "5",
           fullMimeType: "text/5",
           transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
--- a/devtools/client/netmonitor/test/browser_net_statistics-03.js
+++ b/devtools/client/netmonitor/test/browser_net_statistics-03.js
@@ -15,17 +15,17 @@ add_task(function* () {
   let panel = monitor.panelWin;
   let { $, EVENTS, NetMonitorView } = panel;
 
   EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
   EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
   EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-js-button"));
   EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-ws-button"));
   EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-other-button"));
-  testFilterButtonsCustom(monitor, [0, 1, 1, 1, 0, 0, 0, 0, 0, 1]);
+  testFilterButtonsCustom(monitor, [0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1]);
   info("The correct filtering predicates are used before entering perf. analysis mode.");
 
   let onEvents = promise.all([
     panel.once(EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
     panel.once(EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
   ]);
   NetMonitorView.toggleFrontendMode();
   yield onEvents;
--- a/devtools/client/netmonitor/test/browser_net_status-codes.js
+++ b/devtools/client/netmonitor/test/browser_net_status-codes.js
@@ -109,17 +109,18 @@ add_task(function* () {
   function* verifyRequests() {
     info("Verifying requests contain correct information.");
     let index = 0;
     for (let request of REQUEST_DATA) {
       let item = RequestsMenu.getItemAtIndex(index);
       requestItems[index] = item;
 
       info("Verifying request #" + index);
-      yield verifyRequestItemTarget(item, request.method, request.uri, request.details);
+      yield verifyRequestItemTarget(RequestsMenu, item,
+        request.method, request.uri, request.details);
 
       index++;
     }
   }
 
   /**
    * A helper that opens a given tab of request details pane, selects and passes
    * all requests to the given test function.
@@ -154,17 +155,17 @@ add_task(function* () {
   function* testSummary(data) {
     let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
 
     let { method, uri, details: { status, statusText } } = data;
     is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
       uri, "The url summary value is incorrect.");
     is(tabpanel.querySelector("#headers-summary-method-value").getAttribute("value"),
       method, "The method summary value is incorrect.");
-    is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("code"),
+    is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("data-code"),
       status, "The status summary code is incorrect.");
     is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
       status + " " + statusText, "The status summary value is incorrect.");
   }
 
   /**
    * A function that tests "Params" tab contains correct information.
    */
@@ -202,12 +203,13 @@ add_task(function* () {
   }
 
   /**
    * A helper that clicks on a specified request and returns a promise resolved
    * when NetworkDetails has been populated with the data of the given request.
    */
   function chooseRequest(index) {
     let onTabUpdated = monitor.panelWin.once(EVENTS.TAB_UPDATED);
-    EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[index].target);
+    let target = getItemTarget(RequestsMenu, requestItems[index]);
+    EventUtils.sendMouseEvent({ type: "mousedown" }, target);
     return onTabUpdated;
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_streaming-response.js
+++ b/devtools/client/netmonitor/test/browser_net_streaming-response.js
@@ -28,17 +28,17 @@ add_task(function* () {
     let url = CONTENT_TYPE_SJS + "?fmt=" + fmt;
     yield ContentTask.spawn(tab.linkedBrowser, { url }, function* (args) {
       content.wrappedJSObject.performRequests(1, args.url);
     });
   }
   yield wait;
 
   REQUESTS.forEach(([ fmt ], i) => {
-    verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(i),
       "GET", CONTENT_TYPE_SJS + "?fmt=" + fmt, {
         status: 200,
         statusText: "OK"
       });
   });
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.getElementById("details-pane-toggle"));
--- a/devtools/client/netmonitor/test/browser_net_throttle.js
+++ b/devtools/client/netmonitor/test/browser_net_throttle.js
@@ -27,31 +27,31 @@ function* throttleTest(actuallyThrottle)
       latencyMean: 0,
       latencyMax: 0,
       downloadBPSMean: size,
       downloadBPSMax: size,
       uploadBPSMean: 10000,
       uploadBPSMax: 10000,
     },
   };
-  let client = monitor._controller.webConsoleClient;
+  let client = NetMonitorController.webConsoleClient;
 
   info("sending throttle request");
   let deferred = promise.defer();
   client.setPreferences(request, response => {
     deferred.resolve(response);
   });
   yield deferred.promise;
 
   let eventPromise = monitor.panelWin.once(EVENTS.RECEIVED_EVENT_TIMINGS);
   yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED);
   yield eventPromise;
 
   let requestItem = NetMonitorView.RequestsMenu.getItemAtIndex(0);
-  const reportedOneSecond = requestItem.attachment.eventTimings.timings.receive > 1000;
+  const reportedOneSecond = requestItem.eventTimings.timings.receive > 1000;
   if (actuallyThrottle) {
     ok(reportedOneSecond, "download reported as taking more than one second");
   } else {
     ok(!reportedOneSecond, "download reported as taking less than one second");
   }
 
   yield teardown(monitor);
 }
--- a/devtools/client/netmonitor/test/browser_net_timeline_ticks.js
+++ b/devtools/client/netmonitor/test/browser_net_timeline_ticks.js
@@ -14,18 +14,18 @@ add_task(function* () {
   info("Starting test... ");
 
   let { $, $all, NetMonitorView, NetMonitorController } = monitor.panelWin;
   let { RequestsMenu } = NetMonitorView;
 
   // Disable transferred size column support for this test.
   // Without this, the waterfall only has enough room for one division, which
   // would remove most of the value of this test.
-  $("#requests-menu-transferred-header-box").hidden = true;
-  $("#requests-menu-item-template .requests-menu-transferred").hidden = true;
+  // $("#requests-menu-transferred-header-box").hidden = true;
+  // $("#requests-menu-item-template .requests-menu-transferred").hidden = true;
 
   RequestsMenu.lazyUpdate = false;
 
   ok($("#requests-menu-waterfall-label"),
     "An timeline label should be displayed when the frontend is opened.");
   ok($all(".requests-menu-timings-division").length == 0,
     "No tick labels should be displayed when the frontend is opened.");
 
@@ -41,32 +41,27 @@ add_task(function* () {
   NetMonitorController.NetworkEventsHandler.clearMarkers();
   RequestsMenu._flushWaterfallViews(true);
 
   ok(!$("#requests-menu-waterfall-label"),
     "The timeline label should be hidden after the first request.");
   ok($all(".requests-menu-timings-division").length >= 3,
     "There should be at least 3 tick labels in the network requests header.");
 
-  is($all(".requests-menu-timings-division")[0].getAttribute("value"),
-    L10N.getFormatStr("networkMenu.millisecond", 0),
-    "The first tick label has an incorrect value");
-  is($all(".requests-menu-timings-division")[1].getAttribute("value"),
-    L10N.getFormatStr("networkMenu.millisecond", 80),
-    "The second tick label has an incorrect value");
-  is($all(".requests-menu-timings-division")[2].getAttribute("value"),
-    L10N.getFormatStr("networkMenu.millisecond", 160),
-    "The third tick label has an incorrect value");
+  let timingDivisionEls = $all(".requests-menu-timings-division");
+  is(timingDivisionEls[0].textContent, L10N.getFormatStr("networkMenu.millisecond", 0),
+    "The first tick label has correct value");
+  is(timingDivisionEls[1].textContent, L10N.getFormatStr("networkMenu.millisecond", 80),
+    "The second tick label has correct value");
+  is(timingDivisionEls[2].textContent, L10N.getFormatStr("networkMenu.millisecond", 160),
+    "The third tick label has correct value");
 
-  is($all(".requests-menu-timings-division")[0].style.transform, "translateX(0px)",
-    "The first tick label has an incorrect translation");
-  is($all(".requests-menu-timings-division")[1].style.transform, "translateX(80px)",
-    "The second tick label has an incorrect translation");
-  is($all(".requests-menu-timings-division")[2].style.transform, "translateX(160px)",
-    "The third tick label has an incorrect translation");
+  is(timingDivisionEls[0].style.width, "78px", "The first tick label has correct width");
+  is(timingDivisionEls[1].style.width, "80px", "The second tick label has correct width");
+  is(timingDivisionEls[2].style.width, "80px", "The third tick label has correct width");
 
   ok(RequestsMenu._canvas, "A canvas should be created after the first request.");
   ok(RequestsMenu._ctx, "A 2d context should be created after the first request.");
 
   let imageData = RequestsMenu._ctx.getImageData(0, 0, 161, 1);
   ok(imageData, "The image data should have been created.");
 
   let data = imageData.data;
--- a/devtools/client/netmonitor/test/browser_net_timing-division.js
+++ b/devtools/client/netmonitor/test/browser_net_timing-division.js
@@ -18,44 +18,37 @@ add_task(function* () {
 
   let wait = waitForNetworkEvents(monitor, 2);
   // Timeout needed for having enough divisions on the time scale.
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(2, null, 3000);
   });
   yield wait;
 
-  let milDivs = $all(".requests-menu-timings-division[division-scale=millisecond]");
-  let secDivs = $all(".requests-menu-timings-division[division-scale=second]");
-  let minDivs = $all(".requests-menu-timings-division[division-scale=minute]");
+  let milDivs = $all(".requests-menu-timings-division[data-division-scale=millisecond]");
+  let secDivs = $all(".requests-menu-timings-division[data-division-scale=second]");
+  let minDivs = $all(".requests-menu-timings-division[data-division-scale=minute]");
 
   info("Number of millisecond divisions: " + milDivs.length);
   info("Number of second divisions: " + secDivs.length);
   info("Number of minute divisions: " + minDivs.length);
 
-  for (let div of milDivs) {
-    info("Millisecond division: " + div.getAttribute("value"));
-  }
-  for (let div of secDivs) {
-    info("Second division: " + div.getAttribute("value"));
-  }
-  for (let div of minDivs) {
-    info("Minute division: " + div.getAttribute("value"));
-  }
+  milDivs.forEach(div => info(`Millisecond division: ${div.textContent}`));
+  secDivs.forEach(div => info(`Second division: ${div.textContent}`));
+  minDivs.forEach(div => info(`Minute division: ${div.textContent}`));
 
-  is(RequestsMenu.itemCount, 2,
-    "There should be only two requests made.");
+  is(RequestsMenu.itemCount, 2, "There should be only two requests made.");
 
   let firstRequest = RequestsMenu.getItemAtIndex(0);
   let lastRequest = RequestsMenu.getItemAtIndex(1);
 
   info("First request happened at: " +
-    firstRequest.attachment.responseHeaders.headers.find(e => e.name == "Date").value);
+    firstRequest.responseHeaders.headers.find(e => e.name == "Date").value);
   info("Last request happened at: " +
-    lastRequest.attachment.responseHeaders.headers.find(e => e.name == "Date").value);
+    lastRequest.responseHeaders.headers.find(e => e.name == "Date").value);
 
   ok(secDivs.length,
     "There should be at least one division on the seconds time scale.");
-  ok(secDivs[0].getAttribute("value").match(/\d+\.\d{2}\s\w+/),
+  ok(secDivs[0].textContent.match(/\d+\.\d{2}\s\w+/),
     "The division on the seconds time scale looks legit.");
 
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -245,132 +245,135 @@ function waitForNetworkEvents(aMonitor, 
       executeSoon(deferred.resolve);
     }
   }
 
   awaitedEventsToListeners.forEach(([e, l]) => panel.on(events[e], l));
   return deferred.promise;
 }
 
-function verifyRequestItemTarget(aRequestItem, aMethod, aUrl, aData = {}) {
+/**
+ * Convert a store record (model) to the rendered element. Tests that need to use
+ * this should be rewritten - test the rendered markup at unit level, integration
+ * mochitest should check only the store state.
+ */
+function getItemTarget(requestList, requestItem) {
+  const items = requestList.mountPoint.querySelectorAll(".request-list-item");
+  return [...items].find(el => el.dataset.id == requestItem.id);
+}
+
+function verifyRequestItemTarget(requestList, requestItem, aMethod, aUrl, aData = {}) {
   info("> Verifying: " + aMethod + " " + aUrl + " " + aData.toSource());
   // This bloats log sizes significantly in automation (bug 992485)
-  // info("> Request: " + aRequestItem.attachment.toSource());
+  // info("> Request: " + requestItem.toSource());
 
-  let requestsMenu = aRequestItem.ownerView;
-  let widgetIndex = requestsMenu.indexOfItem(aRequestItem);
-  let visibleIndex = requestsMenu.visibleItems.indexOf(aRequestItem);
+  let visibleIndex = requestList.visibleItems.indexOf(requestItem);
 
-  info("Widget index of item: " + widgetIndex);
   info("Visible index of item: " + visibleIndex);
 
   let { fuzzyUrl, status, statusText, cause, type, fullMimeType,
         transferred, size, time, displayedStatus } = aData;
-  let { attachment, target } = aRequestItem;
+
+  let target = getItemTarget(requestList, requestItem);
 
   let unicodeUrl = decodeUnicodeUrl(aUrl);
   let name = getUrlBaseName(aUrl);
   let query = getUrlQuery(aUrl);
   let hostPort = getUrlHost(aUrl);
-  let remoteAddress = attachment.remoteAddress;
+  let remoteAddress = requestItem.remoteAddress;
 
   if (fuzzyUrl) {
-    ok(attachment.method.startsWith(aMethod), "The attached method is correct.");
-    ok(attachment.url.startsWith(aUrl), "The attached url is correct.");
+    ok(requestItem.method.startsWith(aMethod), "The attached method is correct.");
+    ok(requestItem.url.startsWith(aUrl), "The attached url is correct.");
   } else {
-    is(attachment.method, aMethod, "The attached method is correct.");
-    is(attachment.url, aUrl, "The attached url is correct.");
+    is(requestItem.method, aMethod, "The attached method is correct.");
+    is(requestItem.url, aUrl, "The attached url is correct.");
   }
 
-  is(target.querySelector(".requests-menu-method").getAttribute("value"),
+  is(target.querySelector(".requests-menu-method").textContent,
     aMethod, "The displayed method is correct.");
 
   if (fuzzyUrl) {
-    ok(target.querySelector(".requests-menu-file").getAttribute("value").startsWith(
+    ok(target.querySelector(".requests-menu-file").textContent.startsWith(
       name + (query ? "?" + query : "")), "The displayed file is correct.");
-    ok(target.querySelector(".requests-menu-file").getAttribute("tooltiptext").startsWith(unicodeUrl),
+    ok(target.querySelector(".requests-menu-file").getAttribute("title").startsWith(unicodeUrl),
       "The tooltip file is correct.");
   } else {
-    is(target.querySelector(".requests-menu-file").getAttribute("value"),
+    is(target.querySelector(".requests-menu-file").textContent,
       name + (query ? "?" + query : ""), "The displayed file is correct.");
-    is(target.querySelector(".requests-menu-file").getAttribute("tooltiptext"),
+    is(target.querySelector(".requests-menu-file").getAttribute("title"),
       unicodeUrl, "The tooltip file is correct.");
   }
 
-  is(target.querySelector(".requests-menu-domain").getAttribute("value"),
+  is(target.querySelector(".requests-menu-domain").textContent,
     hostPort, "The displayed domain is correct.");
 
   let domainTooltip = hostPort + (remoteAddress ? " (" + remoteAddress + ")" : "");
-  is(target.querySelector(".requests-menu-domain").getAttribute("tooltiptext"),
+  is(target.querySelector(".requests-menu-domain").getAttribute("title"),
     domainTooltip, "The tooltip domain is correct.");
 
   if (status !== undefined) {
-    let value = target.querySelector(".requests-menu-status-icon").getAttribute("code");
-    let codeValue = target.querySelector(".requests-menu-status-code").getAttribute("value");
-    let tooltip = target.querySelector(".requests-menu-status").getAttribute("tooltiptext");
+    let value = target.querySelector(".requests-menu-status-icon").getAttribute("data-code");
+    let codeValue = target.querySelector(".requests-menu-status-code").textContent;
+    let tooltip = target.querySelector(".requests-menu-status").getAttribute("title");
     info("Displayed status: " + value);
     info("Displayed code: " + codeValue);
     info("Tooltip status: " + tooltip);
     is(value, displayedStatus ? displayedStatus : status, "The displayed status is correct.");
     is(codeValue, status, "The displayed status code is correct.");
     is(tooltip, status + " " + statusText, "The tooltip status is correct.");
   }
   if (cause !== undefined) {
-    let causeLabel = target.querySelector(".requests-menu-cause-label");
-    let value = causeLabel.getAttribute("value");
-    let tooltip = causeLabel.getAttribute("tooltiptext");
+    let value = target.querySelector(".requests-menu-cause > .subitem-label").textContent;
+    let tooltip = target.querySelector(".requests-menu-cause").getAttribute("title");
     info("Displayed cause: " + value);
     info("Tooltip cause: " + tooltip);
     is(value, cause.type, "The displayed cause is correct.");
     is(tooltip, cause.loadingDocumentUri, "The tooltip cause is correct.")
   }
   if (type !== undefined) {
-    let value = target.querySelector(".requests-menu-type").getAttribute("value");
-    let tooltip = target.querySelector(".requests-menu-type").getAttribute("tooltiptext");
+    let value = target.querySelector(".requests-menu-type").textContent;
+    let tooltip = target.querySelector(".requests-menu-type").getAttribute("title");
     info("Displayed type: " + value);
     info("Tooltip type: " + tooltip);
     is(value, type, "The displayed type is correct.");
     is(tooltip, fullMimeType, "The tooltip type is correct.");
   }
   if (transferred !== undefined) {
-    let value = target.querySelector(".requests-menu-transferred").getAttribute("value");
-    let tooltip = target.querySelector(".requests-menu-transferred").getAttribute("tooltiptext");
+    let value = target.querySelector(".requests-menu-transferred").textContent;
+    let tooltip = target.querySelector(".requests-menu-transferred").getAttribute("title");
     info("Displayed transferred size: " + value);
     info("Tooltip transferred size: " + tooltip);
     is(value, transferred, "The displayed transferred size is correct.");
     is(tooltip, transferred, "The tooltip transferred size is correct.");
   }
   if (size !== undefined) {
-    let value = target.querySelector(".requests-menu-size").getAttribute("value");
-    let tooltip = target.querySelector(".requests-menu-size").getAttribute("tooltiptext");
+    let value = target.querySelector(".requests-menu-size").textContent;
+    let tooltip = target.querySelector(".requests-menu-size").getAttribute("title");
     info("Displayed size: " + value);
     info("Tooltip size: " + tooltip);
     is(value, size, "The displayed size is correct.");
     is(tooltip, size, "The tooltip size is correct.");
   }
   if (time !== undefined) {
-    let value = target.querySelector(".requests-menu-timings-total").getAttribute("value");
-    let tooltip = target.querySelector(".requests-menu-timings-total").getAttribute("tooltiptext");
+    let value = target.querySelector(".requests-menu-timings-total").textContent;
+    let tooltip = target.querySelector(".requests-menu-timings-total").getAttribute("title");
     info("Displayed time: " + value);
     info("Tooltip time: " + tooltip);
     ok(~~(value.match(/[0-9]+/)) >= 0, "The displayed time is correct.");
     ok(~~(tooltip.match(/[0-9]+/)) >= 0, "The tooltip time is correct.");
   }
 
   if (visibleIndex != -1) {
     if (visibleIndex % 2 == 0) {
-      ok(aRequestItem.target.hasAttribute("even"),
-        aRequestItem.value + " should have 'even' attribute.");
-      ok(!aRequestItem.target.hasAttribute("odd"),
-        aRequestItem.value + " shouldn't have 'odd' attribute.");
+      ok(target.classList.contains("even"), "Item should have 'even' class.");
+      ok(!target.classList.contains("odd"), "Item shouldn't have 'odd' class.");
     } else {
-      ok(!aRequestItem.target.hasAttribute("even"),
-        aRequestItem.value + " shouldn't have 'even' attribute.");
-      ok(aRequestItem.target.hasAttribute("odd"),
-        aRequestItem.value + " should have 'odd' attribute.");
+      ok(!target.classList.contains("even"), "Item shouldn't have 'even' class.");
+      ok(target.classList.contains("odd"), "Item should have 'odd' class.");
     }
   }
 }
 
 /**
  * Helper function for waiting for an event to fire before resolving a promise.
  * Example: waitFor(aMonitor.panelWin, aMonitor.panelWin.EVENTS.TAB_UPDATED);
  *
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/utils/format-utils.js
@@ -0,0 +1,44 @@
+/* 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 { L10N } = require("../l10n");
+
+// Constants for formatting bytes.
+const BYTES_IN_KB = 1024;
+const BYTES_IN_MB = Math.pow(BYTES_IN_KB, 2);
+const BYTES_IN_GB = Math.pow(BYTES_IN_KB, 3);
+const MAX_BYTES_SIZE = 1000;
+const MAX_KB_SIZE = 1000 * BYTES_IN_KB;
+const MAX_MB_SIZE = 1000 * BYTES_IN_MB;
+
+const CONTENT_SIZE_DECIMALS = 2;
+
+/**
+ * Get a human-readable string from a number of bytes, with the B, KB, MB, or
+ * GB value. Note that the transition between abbreviations is by 1000 rather
+ * than 1024 in order to keep the displayed digits smaller as "1016 KB" is
+ * more awkward than 0.99 MB"
+ */
+function getFormattedSize(bytes) {
+  if (bytes < MAX_BYTES_SIZE) {
+    return L10N.getFormatStr("networkMenu.sizeB", bytes);
+  } else if (bytes < MAX_KB_SIZE) {
+    let kb = bytes / BYTES_IN_KB;
+    let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS);
+    return L10N.getFormatStr("networkMenu.sizeKB", size);
+  } else if (bytes < MAX_MB_SIZE) {
+    let mb = bytes / BYTES_IN_MB;
+    let size = L10N.numberWithDecimals(mb, CONTENT_SIZE_DECIMALS);
+    return L10N.getFormatStr("networkMenu.sizeMB", size);
+  }
+  let gb = bytes / BYTES_IN_GB;
+  let size = L10N.numberWithDecimals(gb, CONTENT_SIZE_DECIMALS);
+  return L10N.getFormatStr("networkMenu.sizeGB", size);
+}
+
+module.exports = {
+  getFormattedSize
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/utils/moz.build
@@ -0,0 +1,8 @@
+# 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(
+    'format-utils.js'
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/waterfall-background.js
@@ -0,0 +1,135 @@
+/* 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 HTML_NS = "http://www.w3.org/1999/xhtml";
+// ms
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5;
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3;
+// px
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10;
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
+// 8-bit value of the alpha component of the tick color
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32;
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32;
+// RGBA colors for the timing markers
+const REQUESTS_WATERFALL_DOMCONTENTLOADED_TICKS_COLOR_RGBA = [0, 0, 255, 128];
+const REQUESTS_WATERFALL_LOAD_TICKS_COLOR_RGBA = [255, 0, 0, 128];
+
+const STATE_KEYS = [
+  "scale",
+  "waterfallWidth",
+  "firstRequestStartedMillis",
+  "timingMarkers",
+];
+
+/**
+ * Creates the background displayed on each waterfall view in this container.
+ */
+function WaterfallBackground(document) {
+  this.document = document;
+  this.canvas = document.createElementNS(HTML_NS, "canvas");
+  this.ctx = this.canvas.getContext("2d");
+  this.prevState = {};
+}
+
+WaterfallBackground.prototype = {
+  draw(state) {
+    // Do a shallow compare of the previous and the new state
+    const shouldUpdate = STATE_KEYS.some(key => this.prevState[key] !== state[key]);
+    if (!shouldUpdate) {
+      return;
+    }
+
+    this.prevState = state;
+
+    if (state.scale == null) {
+      this.document.mozSetImageElement("waterfall-background", null);
+      return;
+    }
+
+    // Nuke the context.
+    let canvasWidth = this.canvas.width = state.waterfallWidth;
+    // Awww yeah, 1px, repeats on Y axis.
+    let canvasHeight = this.canvas.height = 1;
+
+    // Start over.
+    let imageData = this.ctx.createImageData(canvasWidth, canvasHeight);
+    let pixelArray = imageData.data;
+
+    let buf = new ArrayBuffer(pixelArray.length);
+    let view8bit = new Uint8ClampedArray(buf);
+    let view32bit = new Uint32Array(buf);
+
+    // Build new millisecond tick lines...
+    let timingStep = REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE;
+    let optimalTickIntervalFound = false;
+    let scaledStep;
+
+    while (!optimalTickIntervalFound) {
+      // Ignore any divisions that would end up being too close to each other.
+      scaledStep = state.scale * timingStep;
+      if (scaledStep < REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN) {
+        timingStep <<= 1;
+        continue;
+      }
+      optimalTickIntervalFound = true;
+    }
+
+    const isRTL = isDocumentRTL(this.document);
+    const [r, g, b] = REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
+    let alphaComponent = REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
+
+    function drawPixelAt(offset, color) {
+      let position = (isRTL ? canvasWidth - offset : offset - 1) | 0;
+      let [rc, gc, bc, ac] = color;
+      view32bit[position] = (ac << 24) | (bc << 16) | (gc << 8) | rc;
+    }
+
+    // Insert one pixel for each division on each scale.
+    for (let i = 1; i <= REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
+      let increment = scaledStep * Math.pow(2, i);
<