Merge m-i to m-c, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Fri, 02 Dec 2016 22:17:31 -0800
changeset 325159 557548714db55136b51e1129d649e2599797985f
parent 325158 a8e20bf36959426f0a6eb4b5df29ef3dd85ad4bd (current diff)
parent 325138 d2c5b6581d6ab82d04b6ee8d454bb7debe9f4d0e (diff)
child 325160 1b2237e0b5e010ae3b182e4dbe79037635ee7997
child 325179 4f87b0fb164521efb5f21c9318ecdd26c4940093
child 325181 f1bb9b5c75c039a7ac3b4d95411fd4fbca947e05
push id84610
push userphilringnalda@gmail.com
push dateSat, 03 Dec 2016 06:28:13 +0000
treeherdermozilla-inbound@1b2237e0b5e0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone53.0a1
first release with
nightly linux32
557548714db5 / 53.0a1 / 20161203030204 / files
nightly linux64
557548714db5 / 53.0a1 / 20161203030204 / files
nightly mac
557548714db5 / 53.0a1 / 20161203030204 / files
nightly win32
557548714db5 / 53.0a1 / 20161203030204 / files
nightly win64
557548714db5 / 53.0a1 / 20161203030204 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-i to m-c, a=merge MozReview-Commit-ID: ByPMe4s1FrG
devtools/client/netmonitor/actions/batching.js
devtools/client/netmonitor/actions/selection.js
devtools/client/netmonitor/actions/sort.js
devtools/client/netmonitor/actions/timing-markers.js
devtools/client/netmonitor/components/request-list-content.js
devtools/client/netmonitor/components/request-list-empty.js
devtools/client/netmonitor/components/request-list-header.js
devtools/client/netmonitor/components/request-list-item.js
devtools/client/netmonitor/components/request-list-tooltip.js
devtools/client/netmonitor/components/request-list.js
devtools/client/netmonitor/middleware/batching.js
devtools/client/netmonitor/middleware/moz.build
devtools/client/netmonitor/netmonitor.js
devtools/client/netmonitor/reducers/batching.js
devtools/client/netmonitor/reducers/sort.js
devtools/client/netmonitor/reducers/timing-markers.js
devtools/client/netmonitor/selectors/filters.js
devtools/client/netmonitor/selectors/requests.js
devtools/client/netmonitor/selectors/ui.js
devtools/client/netmonitor/utils/format-utils.js
devtools/client/netmonitor/utils/moz.build
devtools/client/netmonitor/waterfall-background.js
layout/base/nsCSSFrameConstructor.cpp
testing/web-platform/meta/html/browsers/origin/cross-origin-objects/cross-origin-objects-on-new-window.html.ini
--- a/browser/base/content/test/general/browser_windowopen_reflows.js
+++ b/browser/base/content/test/general/browser_windowopen_reflows.js
@@ -5,52 +5,52 @@
 
 const EXPECTED_REFLOWS = [
   // handleEvent flushes layout to get the tabstrip width after a resize.
   "handleEvent@chrome://browser/content/tabbrowser.xml|",
 
   // Loading a tab causes a reflow.
   "loadTabs@chrome://browser/content/tabbrowser.xml|" +
     "loadOneOrMoreURIs@chrome://browser/content/browser.js|" +
-    "gBrowserInit._delayedStartup@chrome://browser/content/browser.js|",
+    "_delayedStartup@chrome://browser/content/browser.js|",
 
   // Selecting the address bar causes a reflow.
   "select@chrome://global/content/bindings/textbox.xml|" +
     "focusAndSelectUrlBar@chrome://browser/content/browser.js|" +
-    "gBrowserInit._delayedStartup@chrome://browser/content/browser.js|",
+    "_delayedStartup@chrome://browser/content/browser.js|",
 
   // Focusing the content area causes a reflow.
-  "gBrowserInit._delayedStartup@chrome://browser/content/browser.js|",
+  "_delayedStartup@chrome://browser/content/browser.js|",
 
   // Sometimes sessionstore collects data during this test, which causes a sync reflow
   // (https://bugzilla.mozilla.org/show_bug.cgi?id=892154 will fix this)
   "ssi_getWindowDimension@resource:///modules/sessionstore/SessionStore.jsm",
 ];
 
 if (Services.appinfo.OS == "WINNT" || Services.appinfo.OS == "Darwin") {
   // TabsInTitlebar._update causes a reflow on OS X and Windows trying to do calculations
   // since layout info is already dirty. This doesn't seem to happen before
   // MozAfterPaint on Linux.
-  EXPECTED_REFLOWS.push("TabsInTitlebar._update/rect@chrome://browser/content/browser-tabsintitlebar.js|" +
-                          "TabsInTitlebar._update@chrome://browser/content/browser-tabsintitlebar.js|" +
+  EXPECTED_REFLOWS.push("rect@chrome://browser/content/browser-tabsintitlebar.js|" +
+                          "_update@chrome://browser/content/browser-tabsintitlebar.js|" +
                           "updateAppearance@chrome://browser/content/browser-tabsintitlebar.js|" +
                           "handleEvent@chrome://browser/content/tabbrowser.xml|");
 }
 
 if (Services.appinfo.OS == "Darwin") {
   // _onOverflow causes a reflow getting widths.
-  EXPECTED_REFLOWS.push("OverflowableToolbar.prototype._onOverflow@resource:///modules/CustomizableUI.jsm|" +
-                        "OverflowableToolbar.prototype.init@resource:///modules/CustomizableUI.jsm|" +
-                        "OverflowableToolbar.prototype.observe@resource:///modules/CustomizableUI.jsm|" +
-                        "gBrowserInit._delayedStartup@chrome://browser/content/browser.js|");
+  EXPECTED_REFLOWS.push("_onOverflow@resource:///modules/CustomizableUI.jsm|" +
+                        "init@resource:///modules/CustomizableUI.jsm|" +
+                        "observe@resource:///modules/CustomizableUI.jsm|" +
+                        "_delayedStartup@chrome://browser/content/browser.js|");
   // Same as above since in packaged builds there are no function names and the resource URI includes "app"
   EXPECTED_REFLOWS.push("@resource://app/modules/CustomizableUI.jsm|" +
                           "@resource://app/modules/CustomizableUI.jsm|" +
                           "@resource://app/modules/CustomizableUI.jsm|" +
-                          "gBrowserInit._delayedStartup@chrome://browser/content/browser.js|");
+                          "_delayedStartup@chrome://browser/content/browser.js|");
 }
 
 /*
  * This test ensures that there are no unexpected
  * uninterruptible reflows when opening new windows.
  */
 function test() {
   waitForExplicitFinish();
new file mode 100644
--- /dev/null
+++ b/browser/config/mozconfigs/win32/clang
@@ -0,0 +1,19 @@
+MOZ_AUTOMATION_BUILD_SYMBOLS=0
+MOZ_AUTOMATION_PACKAGE_TESTS=0
+MOZ_AUTOMATION_L10N_CHECK=0
+
+. "$topsrcdir/build/mozconfig.win-common"
+. "$topsrcdir/browser/config/mozconfigs/common"
+
+ac_add_options --enable-optimize
+
+#Work to make the clang-plugin work on Windows is ongoing in bug 1316545.
+#ac_add_options --enable-clang-plugin
+
+. $topsrcdir/build/win32/mozconfig.vs-latest
+
+. "$topsrcdir/build/mozconfig.rust"
+
+. "$topsrcdir/build/mozconfig.common.override"
+. "$topsrcdir/build/mozconfig.cache"
+. "$topsrcdir/build/mozconfig.clang-cl"
new file mode 100644
--- /dev/null
+++ b/browser/config/mozconfigs/win32/clang-debug
@@ -0,0 +1,20 @@
+MOZ_AUTOMATION_BUILD_SYMBOLS=0
+MOZ_AUTOMATION_PACKAGE_TESTS=0
+MOZ_AUTOMATION_L10N_CHECK=0
+
+. "$topsrcdir/build/mozconfig.win-common"
+. "$topsrcdir/browser/config/mozconfigs/common"
+
+ac_add_options --enable-optimize
+ac_add_options --enable-debug
+
+#Work to make the clang-plugin work on Windows is ongoing in bug 1316545.
+#ac_add_options --enable-clang-plugin
+
+. $topsrcdir/build/win32/mozconfig.vs-latest
+
+. "$topsrcdir/build/mozconfig.rust"
+
+. "$topsrcdir/build/mozconfig.common.override"
+. "$topsrcdir/build/mozconfig.cache"
+. "$topsrcdir/build/mozconfig.clang-cl"
new file mode 100644
--- /dev/null
+++ b/browser/config/mozconfigs/win64/clang
@@ -0,0 +1,21 @@
+MOZ_AUTOMATION_L10N_CHECK=0
+MOZ_AUTOMATION_PACKAGE_TEST=0
+
+. "$topsrcdir/build/mozconfig.win-common"
+. "$topsrcdir/browser/config/mozconfigs/common"
+
+ac_add_options --target=x86_64-pc-mingw32
+ac_add_options --host=x86_64-pc-mingw32
+
+ac_add_options --enable-optimize
+
+#Work to make the clang-plugin work on Windows is ongoing in bug 1316545.
+#ac_add_options --enable-clang-plugin
+
+. $topsrcdir/build/win64/mozconfig.vs-latest
+
+. "$topsrcdir/build/mozconfig.rust"
+
+. "$topsrcdir/build/mozconfig.common.override"
+. "$topsrcdir/build/mozconfig.cache"
+. "$topsrcdir/build/mozconfig.clang-cl"
new file mode 100644
--- /dev/null
+++ b/browser/config/mozconfigs/win64/clang-debug
@@ -0,0 +1,22 @@
+MOZ_AUTOMATION_L10N_CHECK=0
+MOZ_AUTOMATION_PACKAGE_TEST=0
+
+. "$topsrcdir/build/mozconfig.win-common"
+. "$topsrcdir/browser/config/mozconfigs/common"
+
+ac_add_options --target=x86_64-pc-mingw32
+ac_add_options --host=x86_64-pc-mingw32
+
+ac_add_options --enable-optimize
+ac_add_options --enable-debug
+
+#Work to make the clang-plugin work on Windows is ongoing in bug 1316545.
+#ac_add_options --enable-clang-plugin
+
+. $topsrcdir/build/win64/mozconfig.vs-latest
+
+. "$topsrcdir/build/mozconfig.rust"
+
+. "$topsrcdir/build/mozconfig.common.override"
+. "$topsrcdir/build/mozconfig.cache"
+. "$topsrcdir/build/mozconfig.clang-cl"
--- a/browser/config/tooltool-manifests/win32/clang.manifest
+++ b/browser/config/tooltool-manifests/win32/clang.manifest
@@ -18,16 +18,24 @@
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "sccache2.tar.bz2",
 "unpack": true,
 "digest": "7dee5c5602b3830cb8ac45ebaa8542714bbac0e50eabbff58a06972a02ceeab75ed7c56ff22a23f760b8317ae8e9a01cdecfaf75a7acbd2a4cdd817967170d2e",
 "size": 1179901
 },
 {
-"version": "clang 3.9.0/r262971",
-"size": 169861843,
-"digest": "8caa5a89aea981b618233d39f01bb934b79b85ee8167104bfa4f07936145df5e8ca5e8e007123d75ccc12d2baa926ffc827b40bf793fa9d4bc2670fa519b0ec0",
+"version": "Visual Studio 2015 Update 3 14.0.25425.01 / SDK 10.0.14393.0",
+"size": 326656969,
+"digest": "babc414ffc0457d27f5a1ed24a8e4873afbe2f1c1a4075469a27c005e1babc3b2a788f643f825efedff95b79686664c67ec4340ed535487168a3482e68559bc7",
 "algorithm": "sha512",
-"filename": "clang32.tar.bz2",
+"filename": "vs2015u3.zip",
+"unpack": true
+},
+{
+"version": "clang 4.0pre/r286542",
+"size": 206088429,
+"digest": "a6de709a5097d98084b6111e340f53cd8397736f783aa6f76c9d45bcb694d3983ab3b7aea7f96756fcc4ee44f7b0e57df5ebabbd59242cd51942742fe1769d9a",
+"algorithm": "sha512",
+"filename": "clang.tar.bz2",
 "unpack": true
 }
 ]
--- a/browser/config/tooltool-manifests/win64/clang.manifest
+++ b/browser/config/tooltool-manifests/win64/clang.manifest
@@ -19,16 +19,24 @@
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "sccache2.tar.bz2",
 "unpack": true,
 "digest": "7dee5c5602b3830cb8ac45ebaa8542714bbac0e50eabbff58a06972a02ceeab75ed7c56ff22a23f760b8317ae8e9a01cdecfaf75a7acbd2a4cdd817967170d2e",
 "size": 1179901
 },
 {
-"version": "clang 3.9.0/r262971",
-"size": 173306045,
-"digest": "806413640a964dad44c0c6055d2a89a1075730fb6f659ff37341378a14a7dc032e672941765225608b71f6b385ac721ed36bfa04eb38c211b07e2776cb72a0ee",
+"version": "Visual Studio 2015 Update 3 14.0.25425.01 / SDK 10.0.14393.0",
+"size": 326656969,
+"digest": "babc414ffc0457d27f5a1ed24a8e4873afbe2f1c1a4075469a27c005e1babc3b2a788f643f825efedff95b79686664c67ec4340ed535487168a3482e68559bc7",
 "algorithm": "sha512",
-"filename": "clang64.tar.bz2",
+"filename": "vs2015u3.zip",
+"unpack": true
+},
+{
+"version": "clang 4.0pre/r286542",
+"size": 210049255,
+"digest": "8da8c653ea1948c13aa152cf0b1b0fa94e9619c49006ec80cc625d33cbf2dafa3533764f38fc0213e469cc786a738e5099f0e39d52e30f9a711718e8a3124311",
+"algorithm": "sha512",
+"filename": "clang.tar.bz2",
 "unpack": true
 }
 ]
--- a/build/templates.mozbuild
+++ b/build/templates.mozbuild
@@ -56,22 +56,25 @@ def CppUnitTests(names, ext='.cpp'):
 
 @template
 def Library(name):
     '''Template for libraries.'''
     LIBRARY_NAME = name
 
 
 @template
-def RustLibrary(name):
+def RustLibrary(name, features=None):
     '''Template for Rust libraries.'''
     Library(name)
 
     IS_RUST_LIBRARY = True
 
+    if features:
+        RUST_LIBRARY_FEATURES = features
+
 
 @template
 def SharedLibrary(name):
     '''Template for shared libraries.'''
     Library(name)
 
     FORCE_SHARED_LIB = True
 
--- a/build/valgrind/x86_64-redhat-linux-gnu.sup
+++ b/build/valgrind/x86_64-redhat-linux-gnu.sup
@@ -100,17 +100,16 @@
    fun:g_cclosure_marshal_VOID__OBJECTv
    fun:_g_closure_invoke_va
    fun:g_signal_emit_valist
    fun:g_signal_emit
    fun:gtk_combo_box_constructor
    fun:g_object_newv
    fun:g_object_new_valist
    fun:g_object_new
-   fun:_ZN13nsLookAndFeel4InitEv
    ...
 }
 # set_color() in gtkstyle.c of GTK version 3.4.4 only can leak GdkRGBA
 # allocations when the theme has transparent colors:
 # https://git.gnome.org/browse/gtk+/tree/gtk/deprecated/gtkstyle.c?h=3.4.4#n676
 {
    Bug 1250704
    Memcheck:Leak
--- a/config/rules.mk
+++ b/config/rules.mk
@@ -937,41 +937,47 @@ endif
 # choices, and Cargo only supports two, we choose to enable various
 # optimization levels in our Cargo.toml files all the time, and override the
 # optimization level here, if necessary.  (The Cargo.toml files already
 # specify debug-assertions appropriately for --{disable,enable}-debug.)
 ifndef MOZ_OPTIMIZE
 rustflags_override = RUSTFLAGS='-C opt-level=0'
 endif
 
+CARGO_BUILD = env $(rustflags_override) CARGO_TARGET_DIR=. RUSTC=$(RUSTC) MOZ_DIST=$(DIST) $(CARGO) build $(cargo_build_flags)
+
 ifdef RUST_LIBRARY_FILE
 
+ifdef RUST_LIBRARY_FEATURES
+rust_features_flag := --features "$(RUST_LIBRARY_FEATURES)"
+endif
+
 # Assume any system libraries rustc links against are already in the target's LIBS.
 #
 # We need to run cargo unconditionally, because cargo is the only thing that
 # has full visibility into how changes in Rust sources might affect the final
 # build.
 force-cargo-library-build:
 	$(REPORT_BUILD)
-	env $(rustflags_override) CARGO_TARGET_DIR=. RUSTC=$(RUSTC) $(CARGO) build --lib $(cargo_build_flags) $(cargo_target_flag) --
+	$(CARGO_BUILD) --lib $(cargo_target_flag) $(rust_features_flag)
 
 $(RUST_LIBRARY_FILE): force-cargo-library-build
 endif # RUST_LIBRARY_FILE
 
 ifdef RUST_PROGRAMS
 force-cargo-program-build:
 	$(REPORT_BUILD)
-	env $(rustflags_override) CARGO_TARGET_DIR=. RUSTC=$(RUSTC) $(CARGO) build $(addprefix --bin ,$(RUST_CARGO_PROGRAMS)) $(cargo_build_flags) $(cargo_target_flag) --
+	$(CARGO_BUILD) $(addprefix --bin ,$(RUST_CARGO_PROGRAMS)) $(cargo_target_flag)
 
 $(RUST_PROGRAMS): force-cargo-program-build
 endif # RUST_PROGRAMS
 ifdef HOST_RUST_PROGRAMS
 force-cargo-host-program-build:
 	$(REPORT_BUILD)
-	env $(rustflags_override) CARGO_TARGET_DIR=. RUSTC=$(RUSTC) $(CARGO) build $(addprefix --bin ,$(HOST_RUST_CARGO_PROGRAMS)) $(cargo_build_flags) $(cargo_host_flag) --
+	$(CARGO_BUILD) $(addprefix --bin ,$(HOST_RUST_CARGO_PROGRAMS)) $(cargo_host_flag)
 
 $(HOST_RUST_PROGRAMS): force-cargo-host-program-build
 endif # HOST_RUST_PROGRAMS
 endif # MOZ_RUST
 
 $(SOBJS):
 	$(REPORT_BUILD)
 	$(AS) -o $@ $(DEFINES) $(ASFLAGS) $($(notdir $<)_FLAGS) $(LOCAL_INCLUDES) -c $<
--- a/devtools/client/debugger/test/mochitest/browser_dbg_closure-inspection.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_closure-inspection.js
@@ -48,23 +48,23 @@ function test() {
       return personFetched.then(() => {
         is(personNode.expanded, true,
           "|person| should be expanded at this point.");
 
         is(personNode.get("getName").target.querySelector(".name")
            .getAttribute("value"), "getName",
           "Should have the right property name for 'getName' in person.");
         is(personNode.get("getName").target.querySelector(".value")
-           .getAttribute("value"), "_pfactory/<.getName()",
+           .getAttribute("value"), "getName()",
           "'getName' in person should have the right value.");
         is(personNode.get("getFoo").target.querySelector(".name")
            .getAttribute("value"), "getFoo",
           "Should have the right property name for 'getFoo' in person.");
         is(personNode.get("getFoo").target.querySelector(".value")
-           .getAttribute("value"), "_pfactory/<.getFoo()",
+           .getAttribute("value"), "getFoo()",
           "'getFoo' in person should have the right value.");
 
         // Expand the function nodes. This causes their properties to be
         // retrieved and displayed.
         let getFooNode = personNode.get("getFoo");
         let getNameNode = personNode.get("getName");
         let funcsFetched = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 2);
         let funcClosuresFetched = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES, 2);
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -9,17 +9,18 @@ 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.js (netmonitor/netmonitor.js)
+    content/netmonitor/netmonitor-controller.js (netmonitor/netmonitor-controller.js)
+    content/netmonitor/netmonitor-view.js (netmonitor/netmonitor-view.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)
deleted file mode 100644
--- a/devtools/client/netmonitor/actions/batching.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/* 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} text - A filter text is going to be set
+ * @param {string} url - A filter text is going to be set
  */
-function setFilterText(text) {
+function setFilterText(url) {
   return {
     type: SET_FILTER_TEXT,
-    text,
+    url,
   };
 }
 
 module.exports = {
   toggleFilterType,
   enableFilterTypeOnly,
   setFilterText,
 };
--- a/devtools/client/netmonitor/actions/index.js
+++ b/devtools/client/netmonitor/actions/index.js
@@ -1,23 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "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");
 
-Object.assign(exports,
-  batching,
-  filters,
-  requests,
-  selection,
-  sort,
-  timingMarkers,
-  ui
-);
+module.exports = Object.assign({}, filters, requests, ui);
--- a/devtools/client/netmonitor/actions/moz.build
+++ b/devtools/client/netmonitor/actions/moz.build
@@ -1,14 +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(
-    '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,65 +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 {
-  ADD_REQUEST,
-  UPDATE_REQUEST,
-  CLONE_SELECTED_REQUEST,
-  REMOVE_SELECTED_CUSTOM_REQUEST,
-  CLEAR_REQUESTS,
+  UPDATE_REQUESTS,
 } = require("../constants");
 
-function addRequest(id, data, batch) {
-  return {
-    type: ADD_REQUEST,
-    id,
-    data,
-    meta: { batch },
-  };
-}
-
-function updateRequest(id, data, batch) {
+/**
+ * Update request items
+ *
+ * @param {array} requests - visible request items
+ */
+function updateRequests(items) {
   return {
-    type: UPDATE_REQUEST,
-    id,
-    data,
-    meta: { batch },
-  };
-}
-
-/**
- * Clone the currently selected request, set the "isCustom" attribute.
- * Used by the "Edit and Resend" feature.
- */
-function cloneSelectedRequest() {
-  return {
-    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
+    type: UPDATE_REQUESTS,
+    items,
   };
 }
 
 module.exports = {
-  addRequest,
-  updateRequest,
-  cloneSelectedRequest,
-  removeSelectedCustomRequest,
-  clearRequests,
+  updateRequests,
 };
deleted file mode 100644
--- a/devtools/client/netmonitor/actions/selection.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/* 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,
-};
deleted file mode 100644
--- a/devtools/client/netmonitor/actions/sort.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/* 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
-};
deleted file mode 100644
--- a/devtools/client/netmonitor/actions/timing-markers.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/* 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,
-  WATERFALL_RESIZE,
+  TOGGLE_SIDEBAR,
 } = require("../constants");
 
 /**
  * Change sidebar open state.
  *
  * @param {boolean} open - open state
  */
 function openSidebar(open) {
@@ -20,26 +20,17 @@ 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: WATERFALL_RESIZE,
-    width
+    type: TOGGLE_SIDEBAR,
   };
 }
 
 module.exports = {
   openSidebar,
   toggleSidebar,
-  resizeWaterfall,
 };
--- a/devtools/client/netmonitor/components/clear-button.js
+++ b/devtools/client/netmonitor/components/clear-button.js
@@ -1,32 +1,29 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+/* 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({ onClick }) {
+function ClearButton() {
   return button({
     id: "requests-menu-clear-button",
     className: "devtools-button devtools-clear-icon",
     title: L10N.getStr("netmonitor.toolbar.clear"),
-    onClick,
+    onClick: () => {
+      NetMonitorView.RequestsMenu.clear();
+    },
   });
 }
 
-module.exports = connect(
-  undefined,
-  dispatch => ({
-    onClick: () => dispatch(Actions.clearRequests())
-  })
-)(ClearButton);
+module.exports = ClearButton;
--- a/devtools/client/netmonitor/components/moz.build
+++ b/devtools/client/netmonitor/components/moz.build
@@ -1,18 +1,12 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 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',
 )
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-content.js
+++ /dev/null
@@ -1,255 +0,0 @@
-/* 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);
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-empty.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/* 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);
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-header.js
+++ /dev/null
@@ -1,197 +0,0 @@
-/* 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);
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-item.js
+++ /dev/null
@@ -1,346 +0,0 @@
-/* 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;
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-tooltip.js
+++ /dev/null
@@ -1,107 +0,0 @@
-/* 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,
-};
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/* 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,30 +9,32 @@
 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 { getDisplayedRequestsSummary } = require("../selectors/index");
+const { getSummary } = require("../selectors/index");
 
 const { button, span } = DOM;
 
 function SummaryButton({
   summary,
   triggerSummary,
 }) {
-  let { count, bytes, millis } = summary;
+  let { count, totalBytes, totalMillis } = summary;
   const text = (count === 0) ? L10N.getStr("networkMenu.empty") :
     PluralForm.get(count, L10N.getStr("networkMenu.summary"))
     .replace("#1", count)
-    .replace("#2", L10N.numberWithDecimals(bytes / 1024, CONTENT_SIZE_DECIMALS))
-    .replace("#3", L10N.numberWithDecimals(millis / 1000, REQUEST_TIME_DECIMALS));
+    .replace("#2", L10N.numberWithDecimals(totalBytes / 1024,
+      CONTENT_SIZE_DECIMALS))
+    .replace("#3", L10N.numberWithDecimals(totalMillis / 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" }),
@@ -40,16 +42,16 @@ function SummaryButton({
 }
 
 SummaryButton.propTypes = {
   summary: PropTypes.object.isRequired,
 };
 
 module.exports = connect(
   (state) => ({
-    summary: getDisplayedRequestsSummary(state),
+    summary: getSummary(state),
   }),
   (dispatch) => ({
     triggerSummary: () => {
       NetMonitorView.toggleFrontendMode();
     },
   })
 )(SummaryButton);
--- a/devtools/client/netmonitor/components/toggle-button.js
+++ b/devtools/client/netmonitor/components/toggle-button.js
@@ -1,51 +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 { 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,
-  onToggle,
+  triggerSidebar,
 }) {
   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: onToggle,
+    onMouseDown: triggerSidebar,
   });
 }
 
 ToggleButton.propTypes = {
   disabled: PropTypes.bool.isRequired,
-  onToggle: PropTypes.func.isRequired,
+  triggerSidebar: PropTypes.func.isRequired,
 };
 
 module.exports = connect(
   (state) => ({
-    disabled: isSidebarToggleButtonDisabled(state),
-    open: state.ui.sidebarOpen,
+    disabled: state.requests.items.length === 0,
+    open: state.ui.sidebar.open,
   }),
   (dispatch) => ({
-    onToggle: () => dispatch(Actions.toggleSidebar())
+    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;
+      }
+    },
   })
 )(ToggleButton);
--- a/devtools/client/netmonitor/constants.js
+++ b/devtools/client/netmonitor/constants.js
@@ -6,45 +6,17 @@
 
 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",
-  WATERFALL_RESIZE: "WATERFALL_RESIZE",
+  TOGGLE_SIDEBAR: "TOGGLE_SIDEBAR",
+  UPDATE_REQUESTS: "UPDATE_REQUESTS",
 };
 
-// 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);
+module.exports = Object.assign({}, general, actionTypes);
--- a/devtools/client/netmonitor/custom-request-view.js
+++ b/devtools/client/netmonitor/custom-request-view.js
@@ -2,21 +2,22 @@
  * 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 Actions = require("./actions/index");
+const {
+  writeHeaderText,
+  getKeyWithEvent,
+  getUrlQuery,
+  parseQueryString,
+} = require("./request-utils");
 
 /**
  * Functions handling the custom request view.
  */
 function CustomRequestView() {
   dumpn("CustomRequestView was instantiated");
 }
 
@@ -70,51 +71,47 @@ 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();
-        store.dispatch(Actions.updateRequest(selectedItem.id, { method: value }));
+        selectedItem.attachment.method = value;
         break;
       case "url":
         value = $("#custom-url-value").value;
         this.updateCustomQuery(value);
-        store.dispatch(Actions.updateRequest(selectedItem.id, { url: value }));
+        selectedItem.attachment.url = value;
         break;
       case "query":
         let query = $("#custom-query-value").value;
         this.updateCustomUrl(query);
+        field = "url";
         value = $("#custom-url-value").value;
-        store.dispatch(Actions.updateRequest(selectedItem.id, { url: value }));
+        selectedItem.attachment.url = value;
         break;
       case "body":
         value = $("#custom-postdata-value").value;
-        store.dispatch(Actions.updateRequest(selectedItem.id, {
-          requestPostData: {
-            postData: { text: value }
-          }
-        }));
+        selectedItem.attachment.requestPostData = { postData: { text: value } };
         break;
       case "headers":
         let headersText = $("#custom-headers-value").value;
         value = parseHeadersText(headersText);
-        store.dispatch(Actions.updateRequest(selectedItem.id, {
-          requestHeaders: { headers: value }
-        }));
+        selectedItem.attachment.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.
    */
@@ -159,17 +156,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 representation
+ *        Text of query string represetation
  * @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 window, dumpn, $, NetMonitorView, gNetwork */
+/* globals 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,16 +262,20 @@ 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));
   },
@@ -319,17 +323,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("data-code", code);
+      $("#headers-summary-status-circle").setAttribute("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,17 +58,19 @@ HarBuilder.prototype = {
    */
   build: function () {
     this.promises = [];
 
     // Build basic structure for data.
     let log = this.buildLog();
 
     // Build entries.
-    for (let file of this._options.items) {
+    let items = this._options.items;
+    for (let i = 0; i < items.length; i++) {
+      let file = items[i].attachment;
       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,17 +196,19 @@ HarCollector.prototype = {
       method: method,
       url: url,
       isXHR: isXHR
     };
 
     this.files.set(actor, file);
 
     // Mimic the Net panel data structure
-    this.items.push(file);
+    this.items.push({
+      attachment: 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 { NetMonitorController, NetMonitorView } = monitor.panelWin;
+  let { 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 = NetMonitorController.webConsoleClient;
+  let client = monitor._controller.webConsoleClient;
 
   info("sending throttle request");
   let deferred = promise.defer();
   client.setPreferences(request, response => {
     deferred.resolve(response);
   });
   yield deferred.promise;
 
deleted file mode 100644
--- a/devtools/client/netmonitor/middleware/batching.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/* 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;
deleted file mode 100644
--- a/devtools/client/netmonitor/middleware/moz.build
+++ /dev/null
@@ -1,7 +0,0 @@
-# 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,37 +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/.
 
 DIRS += [
     'actions',
     'components',
     'har',
-    'middleware',
     'reducers',
-    'selectors',
-    'utils',
+    'selectors'
 ]
 
 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,40 +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/. */
 
 /* eslint-disable mozilla/reject-some-requires */
-/* globals window, NetMonitorView, gStore, dumpn */
+/* globals window, document, NetMonitorView, gStore, Actions */
+/* exported loader */
 
 "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 { ACTIVITY_TYPE } = require("./constants");
-const { EVENTS } = require("./events");
-const { configureStore } = require("./store");
+const {Prefs} = require("./prefs");
+const {EVENTS} = require("./events");
 const Actions = require("./actions/index");
-const { getDisplayedRequestById } = require("./selectors/index");
-const { Prefs } = require("./prefs");
 
-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",
+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",
   "resource://devtools/client/shared/widgets/Chart.jsm");
 
-// Initialize the global Redux store
-window.gStore = configureStore();
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+  "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
 
 /**
  * Object defining the network monitor controller components.
  */
 var NetMonitorController = {
   /**
    * Initializes the view and connects the monitor client.
    *
@@ -61,17 +86,16 @@ 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;
   }),
@@ -258,28 +282,29 @@ 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 () {
-      request = getDisplayedRequestById(gStore.getState(), requestId);
+      let predicate = i => i.value === requestId;
+      request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate);
       if (!request) {
         // Reset filters so that the request is visible.
         gStore.dispatch(Actions.toggleFilterType("all"));
-        request = getDisplayedRequestById(gStore.getState(), requestId);
+        request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate);
       }
 
       // 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);
-        gStore.dispatch(Actions.selectRequest(request.id));
+        NetMonitorView.RequestsMenu.selectedItem = request;
         deferred.resolve();
       }
     };
 
     inspector();
     if (!request) {
       window.on(EVENTS.REQUEST_ADDED, inspector);
     }
@@ -368,24 +393,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();
-        } else {
-          // If the log is persistent, just clear all accumulated timing markers.
-          gStore.dispatch(Actions.clearTimingMarkers());
+          NetMonitorView.Sidebar.toggle(false);
         }
         // 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;
       }
@@ -399,16 +424,18 @@ 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);
@@ -424,16 +451,29 @@ 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);
 
@@ -480,17 +520,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);
-    gStore.dispatch(Actions.addTimingMarker(marker));
+    this._markers.push(marker);
   },
 
   /**
    * The "networkEvent" message type handler.
    *
    * @param string type
    *        Message type.
    * @param object networkInfo
@@ -502,17 +542,18 @@ 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
@@ -591,139 +632,155 @@ 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(window);
+EventEmitter.decorate(this);
 
 /**
  * Preliminary setup for the NetMonitorController object.
  */
 NetMonitorController.TargetEventsHandler = new TargetEventsHandler();
 NetMonitorController.NetworkEventsHandler = new NetworkEventsHandler();
 
 /**
@@ -733,9 +790,19 @@ Object.defineProperties(window, {
   "gNetwork": {
     get: function () {
       return NetMonitorController.NetworkEventsHandler;
     },
     configurable: true
   }
 });
 
-exports.NetMonitorController = NetMonitorController;
+/**
+ * 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-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 $, gStore, NetMonitorController, dumpn */
+/* globals Prefs, setInterval, setTimeout, clearInterval, clearTimeout, btoa */
+/* exported $, $all */
 
 "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");
-const { ACTIVITY_TYPE } = require("./constants");
-const Actions = require("./actions/index");
-const { Prefs } = require("./prefs");
+
+// Initialize the global redux variables
+var gStore = configureStore();
 
 // 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,16 +75,22 @@ 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");
 
@@ -158,16 +164,17 @@ 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");
 
@@ -180,27 +187,26 @@ 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.store, [
+        yield whenDataAvailable(requestsView, [
           "responseHeaders", "status", "contentSize", "mimeType", "totalTime"
         ]);
       } catch (ex) {
         // Timed out while waiting for data. Continue with what we have.
         console.error(ex);
       }
 
-      const requests = requestsView.store.getState().requests.requests;
-      statisticsView.createPrimedCacheChart(requests);
-      statisticsView.createEmptyCacheChart(requests);
+      statisticsView.createPrimedCacheChart(requestsView.items);
+      statisticsView.createEmptyCacheChart(requestsView.items);
     });
   },
 
   reloadPage: function () {
     NetMonitorController.triggerActivity(
       ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT);
   },
 
@@ -231,53 +237,56 @@ 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 Store dataStore
- *        A Redux store for which to check the availability of properties.
+ * @param array dataStore
+ *        The request view object from which to fetch the item list.
  * @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(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
-        )
-      );
+function whenDataAvailable(requestsView, mandatoryFields) {
+  let deferred = promise.defer();
 
-      if (allFieldsPresent) {
-        clearInterval(interval);
-        clearTimeout(timer);
-        resolve();
-      }
-    }, WDA_DEFAULT_VERIFY_INTERVAL);
+  let interval = setInterval(() => {
+    const { attachments } = requestsView;
+    if (attachments.length > 0 && attachments.every(item => {
+      return mandatoryFields.every(field => field in item);
+    })) {
+      clearInterval(interval);
+      clearTimeout(timer);
+      deferred.resolve();
+    }
+  }, WDA_DEFAULT_VERIFY_INTERVAL);
 
-    let timer = setTimeout(() => {
-      clearInterval(interval);
-      reject(new Error("Timed out while waiting for data"));
-    }, WDA_DEFAULT_GIVE_UP_TIMEOUT);
-  });
+  let timer = setTimeout(() => {
+    clearInterval(interval);
+    deferred.reject(new Error("Timed out while waiting for data"));
+  }, WDA_DEFAULT_GIVE_UP_TIMEOUT);
+
+  return deferred.promise;
 }
 
 /**
  * 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;
deleted file mode 100644
--- a/devtools/client/netmonitor/netmonitor.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/* 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,33 +7,212 @@
 <?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.js"/>
+  <script type="text/javascript" src="netmonitor-controller.js"/>
+  <script type="text/javascript" src="netmonitor-view.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">
-        <html:div xmlns="http://www.w3.org/1999/xhtml"
-                  id="network-table"
-                  class="devtools-main-content">
-        </html:div>
+        <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>
 
         <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,17 +9,20 @@ 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._netmonitor = new iframeWindow.Netmonitor(toolbox);
+  this._view = this.panelWin.NetMonitorView;
+  this._controller = this.panelWin.NetMonitorController;
+  this._controller._target = this.target;
+  this._controller._toolbox = this._toolbox;
 
   EventEmitter.decorate(this);
 }
 
 exports.NetMonitorPanel = NetMonitorPanel;
 
 NetMonitorPanel.prototype = {
   /**
@@ -38,18 +41,17 @@ 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._netmonitor.init();
-
+    yield this._controller.startupNetMonitor();
     this.isReady = true;
     this.emit("ready");
 
     deferred.resolve(this);
     return this._opening;
   }),
 
   // DevToolPanel API
@@ -60,15 +62,15 @@ NetMonitorPanel.prototype = {
 
   destroy: Task.async(function* () {
     if (this._destroying) {
       return this._destroying;
     }
     let deferred = promise.defer();
     this._destroying = deferred.promise;
 
-    yield this._netmonitor.destroy();
+    yield this._controller.shutdownNetMonitor();
     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,27 +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/. */
 
-/* eslint-disable mozilla/reject-some-requires */
-/* globals $, window, document, NetMonitorView */
+/* import-globals-from ./netmonitor-controller.js */
+/* globals $ */
 
 "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.
@@ -172,17 +167,17 @@ PerformanceStatisticsView.prototype = {
       cached: 0,
       count: 0,
       label: e,
       size: 0,
       time: 0
     }));
 
     for (let requestItem of items) {
-      let details = requestItem;
+      let details = requestItem.attachment;
       let type;
 
       if (Filters.html(details)) {
         // "html"
         type = 0;
       } else if (Filters.css(details)) {
         // "css"
         type = 1;
@@ -237,18 +232,21 @@ 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.find(e => e.name.toLowerCase() == "cache-control");
-  let expires = list.find(e => e.name.toLowerCase() == "expires");
+  let cacheControl = list.filter(e => {
+    return e.name.toLowerCase() == "cache-control";
+  })[0];
+
+  let expires = list.filter(e => e.name.toLowerCase() == "expires")[0];
 
   // 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,10 +8,11 @@ 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"]
 });
deleted file mode 100644
--- a/devtools/client/netmonitor/reducers/batching.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/* 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 }),
-  text: "",
+  url: "",
 });
 
 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("text", action.text);
+      return state.set("url", action.url);
     default:
       return state;
   }
 }
 
 module.exports = filters;
--- a/devtools/client/netmonitor/reducers/index.js
+++ b/devtools/client/netmonitor/reducers/index.js
@@ -1,23 +1,16 @@
 /* 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 filters = require("./filters");
 const requests = require("./requests");
-const sort = require("./sort");
-const filters = require("./filters");
-const timingMarkers = require("./timing-markers");
 const ui = require("./ui");
 
-module.exports = batchingReducer(
-  combineReducers({
-    requests,
-    sort,
-    filters,
-    timingMarkers,
-    ui,
-  })
-);
+module.exports = combineReducers({
+  filters,
+  requests,
+  ui,
+});
--- a/devtools/client/netmonitor/reducers/moz.build
+++ b/devtools/client/netmonitor/reducers/moz.build
@@ -1,13 +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(
-    '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,246 +1,29 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const I = require("devtools/client/shared/vendor/immutable");
-const { getUrlDetails } = require("../request-utils");
 const {
-  ADD_REQUEST,
-  UPDATE_REQUEST,
-  CLEAR_REQUESTS,
-  SELECT_REQUEST,
-  PRESELECT_REQUEST,
-  CLONE_SELECTED_REQUEST,
-  REMOVE_SELECTED_CUSTOM_REQUEST,
-  OPEN_SIDEBAR
+  UPDATE_REQUESTS,
 } = 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({
-  // The request list
-  requests: I.List(),
-  // Selection state
-  selectedId: null,
-  preselectedId: null,
-  // Auxiliary fields to hold requests stats
-  firstStartedMillis: +Infinity,
-  lastEndedMillis: -Infinity,
+  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 updateRequests(state, action) {
+  return state.set("items", action.items || state.items);
+}
 
-          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;
-    }
-
+function requests(state = new Requests(), action) {
+  switch (action.type) {
+    case UPDATE_REQUESTS:
+      return updateRequests(state, action);
     default:
       return state;
   }
 }
 
-module.exports = requestsReducer;
+module.exports = requests;
deleted file mode 100644
--- a/devtools/client/netmonitor/reducers/sort.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/* 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;
deleted file mode 100644
--- a/devtools/client/netmonitor/reducers/timing-markers.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/* 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,
-  WATERFALL_RESIZE,
+  TOGGLE_SIDEBAR,
 } = require("../constants");
 
+const Sidebar = I.Record({
+  open: false,
+});
+
 const UI = I.Record({
-  sidebarOpen: false,
-  waterfallWidth: 300,
+  sidebar: new Sidebar(),
 });
 
 function openSidebar(state, action) {
-  return state.set("sidebarOpen", action.open);
+  return state.setIn(["sidebar", "open"], action.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 toggleSidebar(state, action) {
+  return state.setIn(["sidebar", "open"], !state.sidebar.open);
 }
 
 function ui(state = new UI(), action) {
   switch (action.type) {
     case OPEN_SIDEBAR:
       return openSidebar(state, action);
-    case WATERFALL_RESIZE:
-      return resizeWaterfall(state, action);
+    case TOGGLE_SIDEBAR:
+      return toggleSidebar(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,110 +53,112 @@ 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.url)),
+      visible: !!(selectedItem && getUrlQuery(selectedItem.attachment.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.requestPostData),
+      visible: !!(selectedItem && selectedItem.attachment.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,
+      visible: !!(selectedItem && selectedItem.attachment),
       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.requestHeaders),
+      visible: !!(selectedItem && selectedItem.attachment.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.responseHeaders),
+      visible: !!(selectedItem && selectedItem.attachment.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.responseContent &&
-               selectedItem.responseContent.content.text &&
-               selectedItem.responseContent.content.text.length !== 0),
+               selectedItem.attachment.responseContent &&
+               selectedItem.attachment.responseContent.content.text &&
+               selectedItem.attachment.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.responseContent &&
-               selectedItem.responseContent.content.mimeType.includes("image/")),
+               selectedItem.attachment.responseContent &&
+               selectedItem.attachment.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.size > 0,
+      visible: !!this.items.length,
       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.size > 0,
+      visible: !!this.items.length,
       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.isCustom),
+               selectedItem &&
+               !selectedItem.attachment.isCustom),
       click: () => NetMonitorView.RequestsMenu.cloneSelectedRequest(),
     }));
 
     menu.append(new MenuItem({
       type: "separator",
       visible: !!selectedItem,
     }));
 
@@ -180,43 +182,44 @@ RequestListContextMenu.prototype = {
     return menu;
   },
 
   /**
    * Opens selected item in a new tab.
    */
   openRequestInTab() {
     let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-    win.openUILinkIn(this.selectedItem.url, "tab", { relatedToCurrent: true });
+    let { url } = this.selectedItem.attachment;
+    win.openUILinkIn(url, "tab", { relatedToCurrent: true });
   },
 
   /**
    * Copy the request url from the currently selected item.
    */
   copyUrl() {
-    clipboardHelper.copyString(this.selectedItem.url);
+    clipboardHelper.copyString(this.selectedItem.attachment.url);
   },
 
   /**
    * Copy the request url query string parameters from the currently
    * selected item.
    */
   copyUrlParams() {
-    let { url } = this.selectedItem;
+    let { url } = this.selectedItem.attachment;
     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;
+    let selected = this.selectedItem.attachment;
 
     // Try to extract any form data parameters.
     let formDataSections = yield getFormDataSections(
       selected.requestHeaders,
       selected.requestHeadersFromUploadStream,
       selected.requestPostData,
       gNetwork.getString.bind(gNetwork));
 
@@ -243,17 +246,17 @@ RequestListContextMenu.prototype = {
 
     clipboardHelper.copyString(string);
   }),
 
   /**
    * Copy a cURL command from the currently selected item.
    */
   copyAsCurl: Task.async(function* () {
-    let selected = this.selectedItem;
+    let selected = this.selectedItem.attachment;
 
     // Create a sanitized object for the Curl command generator.
     let data = {
       url: selected.url,
       method: selected.method,
       headers: [],
       httpVersion: selected.httpVersion,
       postDataText: null
@@ -273,51 +276,55 @@ RequestListContextMenu.prototype = {
 
     clipboardHelper.copyString(Curl.generateCommand(data));
   }),
 
   /**
    * Copy the raw request headers from the currently selected item.
    */
   copyRequestHeaders() {
-    let rawHeaders = this.selectedItem.requestHeaders.rawHeaders.trim();
+    let selected = this.selectedItem.attachment;
+    let rawHeaders = selected.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 rawHeaders = this.selectedItem.responseHeaders.rawHeaders.trim();
+    let selected = this.selectedItem.attachment;
+    let rawHeaders = selected.responseHeaders.rawHeaders.trim();
     if (Services.appinfo.OS !== "WINNT") {
       rawHeaders = rawHeaders.replace(/\r/g, "");
     }
     clipboardHelper.copyString(rawHeaders);
   },
 
   /**
    * Copy image as data uri.
    */
   copyImageAsDataUri() {
-    const { mimeType, text, encoding } = this.selectedItem.responseContent.content;
+    let selected = this.selectedItem.attachment;
+    let { mimeType, text, encoding } = selected.responseContent.content;
 
     gNetwork.getString(text).then(string => {
       let data = formDataURI(mimeType, encoding, string);
       clipboardHelper.copyString(data);
     });
   },
 
   /**
    * Copy response data as a string.
    */
   copyResponse() {
-    const { text } = this.selectedItem.responseContent.content;
+    let selected = this.selectedItem.attachment;
+    let text = selected.responseContent.content.text;
 
     gNetwork.getString(text).then(string => {
       clipboardHelper.copyString(string);
     });
   },
 
   /**
    * Copy HAR from the network panel content to the clipboard.
@@ -336,15 +343,16 @@ RequestListContextMenu.prototype = {
   },
 
   getDefaultHarOptions() {
     let form = NetMonitorController._target.form;
     let title = form.title || form.url;
 
     return {
       getString: gNetwork.getString.bind(gNetwork),
-      items: this.items,
+      view: NetMonitorView.RequestsMenu,
+      items: NetMonitorView.RequestsMenu.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 requestHeaders = headers.headers;
-  let payloadHeaders = uploadHeaders ? uploadHeaders.headers : [];
+  let { headers: requestHeaders } = headers;
+  let { headers: payloadHeaders } = uploadHeaders;
   let allHeaders = [...payloadHeaders, ...requestHeaders];
 
   let contentTypeHeader = allHeaders.find(e => {
     return e.name.toLowerCase() == "content-type";
   });
 
   let contentTypeLongString = contentTypeHeader ? contentTypeHeader.value : "";
 
@@ -184,47 +184,16 @@ 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;
@@ -279,12 +248,11 @@ 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,46 +1,90 @@
 /* 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, dumpn, $, gNetwork, NetMonitorController, NetMonitorView */
+/* eslint-disable mozilla/reject-some-requires */
+/* globals document, window, dumpn, $, gNetwork, EVENTS, Prefs,
+           NetMonitorController, NetMonitorView */
 
 "use strict";
 
-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 { 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 Actions = require("./actions/index");
 const RequestListContextMenu = require("./request-list-context-menu");
-const Actions = require("./actions/index");
-const { Prefs } = require("./prefs");
 
-const {
-  formDataURI,
-  writeHeaderText,
-  loadCauseString
-} = require("./request-utils");
-
-const {
-  getActiveFilters,
-  getSortedRequests,
-  getDisplayedRequests,
-  getRequestById,
-  getSelectedRequest
-} = require("./selectors/index");
-
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const EPSILON = 0.001;
 // 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);
@@ -49,337 +93,1462 @@ 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 information).
+ * timing imformation).
  */
 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 = {
+RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
   /**
    * Initialization function, called when the network monitor is started.
    */
   initialize: function (store) {
     dumpn("Initializing the RequestsMenuView");
 
     this.store = store;
 
     this.contextMenu = new RequestListContextMenu();
 
-    Prefs.filters.forEach(type => store.dispatch(Actions.toggleFilterType(type)));
+    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));
 
-    // Watch selection changes
-    this.store.subscribe(storeWatcher(
-      null,
-      () => getSelectedRequest(this.store.getState()),
-      (newSelected, oldSelected) => this.onSelectionUpdate(newSelected, oldSelected)
-    ));
+    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);
 
+    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);
 
-    $("#toggle-raw-headers")
-      .addEventListener("click", this.toggleRawHeadersEvent, false);
+    this.reFilterRequests = this.reFilterRequests.bind(this);
 
-    this._summary = $("#requests-menu-network-summary-button");
-    this._summary.setAttribute("label", L10N.getStr("networkMenu.empty"));
+    $("#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.onResize = this.onResize.bind(this);
-    this._splitter = $("#network-inspector-view-splitter");
-    this._splitter.addEventListener("mousemove", this.onResize, false);
-    window.addEventListener("resize", this.onResize, false);
+    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.tooltip = new HTMLTooltip(NetMonitorController._toolbox.doc, { type: "arrow" });
-
-    this.mountPoint = $("#network-table");
-    ReactDOM.render(createElement(Provider,
-      { store: this.store },
-      RequestList()
-    ), this.mountPoint);
+    Prefs.filters.forEach(type =>
+      store.dispatch(Actions.toggleFilterType(type)));
 
     window.once("connected", this._onConnect.bind(this));
   },
 
-  _onConnect() {
+  _onConnect: function () {
+    $("#requests-menu-reload-notice-button").addEventListener("command",
+      this._onReloadCommand, false);
+
     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;
     }
 
-    $("#network-statistics-back-button")
-      .addEventListener("command", this._onContextPerfCommand, false);
+    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;
+    }
   },
 
   /**
    * Destruction function, called when the network monitor is closed.
    */
-  destroy() {
+  destroy: function () {
     dumpn("Destroying the RequestsMenuView");
 
-    Prefs.filters = getActiveFilters(this.store.getState());
+    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);
 
-    // this.flushRequestsTask.disarm();
+    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);
 
-    $("#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);
+    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);
 
-    this._splitter.removeEventListener("mousemove", this.onResize, false);
-    window.removeEventListener("resize", this.onResize, 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.tooltip.destroy();
-
-    ReactDOM.unmountComponentAtNode(this.mountPoint);
+    this.unsubscribeStore();
   },
 
   /**
    * Resets this container (removes all the networking information).
    */
-  reset() {
-    this.store.dispatch(Actions.batchReset());
-    this.store.dispatch(Actions.clearRequests());
+  reset: function () {
+    this.empty();
+    this._addQueue = [];
+    this._updateQueue = [];
+    this._firstRequestStartedMillis = -1;
+    this._lastRequestEndedMillis = -1;
   },
 
   /**
-   * Removes all network requests and closes the sidebar if open.
+   * Specifies if this view may be updated lazily.
    */
-  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);
+  _lazyUpdate: true,
 
-    // 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));
+  get lazyUpdate() {
+    return this._lazyUpdate;
   },
 
-  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.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);
+    this._lazyUpdate = value;
+    if (!value) {
+      this._flushRequests();
     }
   },
 
   /**
-   * The resize listener for this container's window.
+   * 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
    */
-  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));
-      }
-    });
+  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;
   },
 
   /**
    * Create a new custom request form populated with the data from
    * the currently selected request.
    */
-  cloneSelectedRequest() {
-    this.store.dispatch(Actions.cloneSelectedRequest());
-  },
+  cloneSelectedRequest: function () {
+    let selected = this.selectedItem.attachment;
 
-  /**
-   * 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");
+    // Create the element node for the network request item.
+    let menuView = this._createMenuView(selected.method, selected.url,
+      selected.cause);
 
-    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;
-    }
+    // 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;
   },
 
   /**
    * Send a new HTTP request using the data in the custom request form.
    */
   sendCustomRequest: function () {
-    let selected = getSelectedRequest(this.store.getState());
+    let selected = this.selectedItem.attachment;
 
     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.store.dispatch(Actions.preselectRequest(id));
+      this._preferredItemId = id;
     });
 
     this.closeCustomRequest();
   },
 
   /**
    * Remove the currently selected custom request.
    */
-  closeCustomRequest() {
-    this.store.dispatch(Actions.removeSelectedCustomRequest());
+  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);
   },
-};
+
+  /**
+   * 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;
deleted file mode 100644
--- a/devtools/client/netmonitor/selectors/filters.js
+++ /dev/null
@@ -1,13 +0,0 @@
-/* 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,15 +1,63 @@
 /* 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 filters = require("./filters");
-const requests = require("./requests");
-const ui = require("./ui");
+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;
+}
 
-Object.assign(exports,
-  filters,
-  requests,
-  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),
+  })
 );
+
+module.exports = {
+  getSummary,
+};
--- a/devtools/client/netmonitor/selectors/moz.build
+++ b/devtools/client/netmonitor/selectors/moz.build
@@ -1,10 +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(
-    'filters.js',
-    'index.js',
-    'requests.js',
-    'ui.js',
+    'index.js'
 )
deleted file mode 100644
--- a/devtools/client/netmonitor/selectors/requests.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/* 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,
-};
deleted file mode 100644
--- a/devtools/client/netmonitor/selectors/ui.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/* 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,13 +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/. */
 
-/* globals window, dumpn, $, NetMonitorView */
+/* import-globals-from ./netmonitor-controller.js */
+/* globals dumpn, $, NetMonitorView */
 
 "use strict";
 
 const { Task } = require("devtools/shared/task");
 const { EVENTS } = require("./events");
 
 /**
  * Functions handling the sidebar details view.
@@ -20,16 +21,17 @@ 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,16 +3,17 @@
  * 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
@@ -54,18 +55,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 = first.cause.type;
-  let secondCause = second.cause.type;
+  let firstCause = loadCauseString(first.cause.type);
+  let secondCause = loadCauseString(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,22 +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/. */
 
 "use strict";
 
-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");
+const createStore = require("devtools/client/shared/redux/create-store");
+const reducers = require("./reducers/index");
 
 function configureStore() {
-  return createStore(
-    rootReducer,
-    applyMiddleware(
-      thunk,
-      batching
-    )
-  );
+  return createStore()(reducers);
 }
 
 exports.configureStore = configureStore;
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -49,16 +49,17 @@ 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]
@@ -83,17 +84,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 = true # Test broken in React version, is too low-level
+skip-if = (os == 'linux' && e10s && debug) # Bug 1242204
 [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]
@@ -134,21 +135,19 @@ skip-if = true # Test broken in React ve
 [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,25 +3,23 @@
 
 "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, gStore } = monitor.panelWin;
+  let { NetMonitorView } = monitor.panelWin;
   let { RequestsMenu } = NetMonitorView;
 
   RequestsMenu.lazyUpdate = false;
 
   let count = 0;
   function check(selectedIndex, paneVisibility) {
     info("Performing check " + (count++) + ".");
 
@@ -34,51 +32,56 @@ add_task(function* () {
   let wait = waitForNetworkEvents(monitor, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(2);
   });
   yield wait;
 
   check(-1, false);
 
-  gStore.dispatch(Actions.selectDelta(+Infinity));
+  RequestsMenu.focusLastVisibleItem();
   check(1, true);
-  gStore.dispatch(Actions.selectDelta(-Infinity));
+  RequestsMenu.focusFirstVisibleItem();
   check(0, true);
 
-  gStore.dispatch(Actions.selectDelta(+1));
+  RequestsMenu.focusNextItem();
   check(1, true);
-  gStore.dispatch(Actions.selectDelta(-1));
+  RequestsMenu.focusPrevItem();
   check(0, true);
 
-  gStore.dispatch(Actions.selectDelta(+10));
+  RequestsMenu.focusItemAtDelta(+1);
   check(1, true);
-  gStore.dispatch(Actions.selectDelta(-10));
+  RequestsMenu.focusItemAtDelta(-1);
+  check(0, true);
+
+  RequestsMenu.focusItemAtDelta(+10);
+  check(1, true);
+  RequestsMenu.focusItemAtDelta(-10);
   check(0, true);
 
   wait = waitForNetworkEvents(monitor, 18);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(18);
   });
   yield wait;
 
-  gStore.dispatch(Actions.selectDelta(+Infinity));
+  RequestsMenu.focusLastVisibleItem();
   check(19, true);
-  gStore.dispatch(Actions.selectDelta(-Infinity));
+  RequestsMenu.focusFirstVisibleItem();
   check(0, true);
 
-  gStore.dispatch(Actions.selectDelta(+1));
+  RequestsMenu.focusNextItem();
   check(1, true);
-  gStore.dispatch(Actions.selectDelta(-1));
+  RequestsMenu.focusPrevItem();
   check(0, true);
 
-  gStore.dispatch(Actions.selectDelta(+10));
+  RequestsMenu.focusItemAtDelta(+10);
   check(10, true);
-  gStore.dispatch(Actions.selectDelta(-10));
+  RequestsMenu.focusItemAtDelta(-10);
   check(0, true);
 
-  gStore.dispatch(Actions.selectDelta(+100));
+  RequestsMenu.focusItemAtDelta(+100);
   check(19, true);
-  gStore.dispatch(Actions.selectDelta(-100));
+  RequestsMenu.focusItemAtDelta(-100);
   check(0, true);
 
-  return teardown(monitor);
+  yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_accessibility-02.js
+++ b/devtools/client/netmonitor/test/browser_net_accessibility-02.js
@@ -30,18 +30,16 @@ 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);
@@ -120,13 +118,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" }, $(".request-list-item"));
+  EventUtils.sendMouseEvent({ type: "mousedown" }, $(".side-menu-widget-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, RequestsMenu.getItemAtIndex(index), "GET", uri);
+    verifyRequestItemTarget(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,83 +5,71 @@
 
 /**
  * Bug 863102 - Automatically scroll down upon new network requests.
  */
 add_task(function* () {
   requestLongerTimeout(2);
 
   let { monitor } = yield initNetMonitor(INFINITE_GET_URL);
-  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.");
+  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.");
 
   // (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(true, "Scrolled to bottom on overflow.");
+  ok(scrolledToBottom(requestsContainer), "Scrolled to bottom on overflow.");
 
-  // (2) Now set the scroll position to the first item and check
+  // (2) Now set the scroll position somewhere in the middle and check
   // that additional requests do not change the scroll position.
-  let firstNode = requestsContainer.firstChild;
-  firstNode.scrollIntoView();
-  yield waitSomeTime();
+  let children = requestsContainer.childNodes;
+  let middleNode = children.item(children.length / 2);
+  middleNode.scrollIntoView();
   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(true, "Still scrolled to bottom.");
+  ok(scrolledToBottom(requestsContainer), "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.
-  return teardown(monitor);
-
-  function waitForRequestListToAppear() {
-    info("Waiting until the empty notice disappears and is replaced with the list");
-    return waitUntil(() => !!$(".requests-menu-contents"));
-  }
+  yield teardown(monitor);
 
   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() {
-    info("Waiting for the list to scroll to bottom");
-    return waitUntil(() => scrolledToBottom(requestsContainer));
+    return monitor._view.RequestsMenu.widget.once("scroll-to-bottom");
   }
 });
--- 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, RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(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,18 +88,17 @@ 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(RequestsMenu, item,
-      request.method, request.uri, request.details);
+    yield verifyRequestItemTarget(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: null,
+    causeUri: "",
     // 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(RequestsMenu, requestItem,
+    verifyRequestItemTarget(requestItem,
       method, url, { cause: { type: causeType, loadingDocumentUri: causeUri } }
     );
 
-    let { stacktrace } = requestItem.cause;
+    let { stacktrace } = requestItem.attachment.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,14 +132,16 @@ 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) => {
-    const cause = RequestsMenu.getItemAtIndex(i).cause.type;
+    let { target } = RequestsMenu.getItemAtIndex(i);
+    let causeLabel = target.querySelector(".requests-menu-cause-label");
+    let cause = causeLabel.getAttribute("value");
     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 item = RequestsMenu.getItemAtIndex(i);
+    let { attachment } = RequestsMenu.getItemAtIndex(i);
 
-    is(item.status, status, `Request #${i} has the expected status`);
+    is(attachment.status, status, `Request #${i} has the expected status`);
 
-    let { stacktrace } = item.cause;
+    let { stacktrace } = attachment.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, RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(1),
+  verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(2),
+  verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(3),
+  verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(4),
+  verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(5),
+  verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(6),
+  verifyRequestItemTarget(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;
+  let { method, httpVersion, status, statusText } = requestItem.attachment;
 
   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.url);
+  }, requestItem.attachment.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,14 +20,13 @@ 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, RequestsMenu.getItemAtIndex(i),
-      method, requestUrl);
+    verifyRequestItemTarget(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, gNetwork);
+  let data = yield createCurlData(requests.get.attachment, gNetwork);
   testFindHeader(data);
 
-  data = yield createCurlData(requests.post, gNetwork);
+  data = yield createCurlData(requests.post.attachment, gNetwork);
   testIsUrlEncodedRequest(data);
   testWritePostDataTextParams(data);
 
-  data = yield createCurlData(requests.multipart, gNetwork);
+  data = yield createCurlData(requests.multipart.attachment, gNetwork);
   testIsMultipartRequest(data);
   testGetMultipartBoundary(data);
   testRemoveBinaryDataFromMultipartText(data);
 
-  data = yield createCurlData(requests.multipartForm, gNetwork);
+  data = yield createCurlData(requests.multipartForm.attachment, 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, RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(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,119 +23,18 @@ 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... ");
@@ -276,30 +175,90 @@ 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.");
 
-    const items = RequestsMenu.items;
-    const visibleItems = RequestsMenu.visibleItems;
-
-    is(items.size, visibility.length,
+    is(RequestsMenu.items.length, visibility.length,
       "There should be a specific amount of 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.");
+    is(RequestsMenu.visibleItems.length, visibility.filter(e => e).length,
+      "There should be a specific amount of visbile items in the requests menu.");
 
     for (let i = 0; i < visibility.length; i++) {
-      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}`);
+      is(RequestsMenu.getItemAtIndex(i).target.hidden, !visibility[i],
+        "The item at index " + i + " doesn't have the correct hidden state.");
+    }
 
-      if (shouldBeVisible) {
-        let { method, url, data } = EXPECTED_REQUESTS[i];
-        verifyRequestItemTarget(RequestsMenu, items.get(i), method, url, data);
-      }
-    }
+    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",
+      });
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_filter-02.js
+++ b/devtools/client/netmonitor/test/browser_net_filter-02.js
@@ -24,116 +24,16 @@ 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;
@@ -193,34 +93,108 @@ 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.");
 
-    const items = RequestsMenu.items;
-    const visibleItems = RequestsMenu.visibleItems;
-
-    is(items.size, visibility.length,
+    is(RequestsMenu.items.length, visibility.length,
       "There should be a specific amount of 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.");
+    is(RequestsMenu.visibleItems.length, visibility.filter(e => e).length,
+      "There should be a specific amount of visbile items in the requests menu.");
 
     for (let i = 0; i < visibility.length; i++) {
-      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}`);
+      is(RequestsMenu.getItemAtIndex(i).target.hidden, !visibility[i],
+        "The item at index " + i + " doesn't have the correct hidden state.");
     }
 
-    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);
-        }
-      }
+    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"
+        });
     }
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_filter-03.js
+++ b/devtools/client/netmonitor/test/browser_net_filter-03.js
@@ -99,11 +99,87 @@ 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 visible items in the requests menu.");
+      "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"
+        });
+    }
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_footer-summary.js
+++ b/devtools/client/netmonitor/test/browser_net_footer-summary.js
@@ -1,30 +1,31 @@
 /* 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, windowRequire } = monitor.panelWin;
+  let { $, NetMonitorView, gStore } = monitor.panelWin;
   let { RequestsMenu } = NetMonitorView;
 
-  let { getDisplayedRequestsSummary } =
-    windowRequire("devtools/client/netmonitor/selectors/index");
-  let { L10N } = windowRequire("devtools/client/netmonitor/l10n");
-  let { PluralForm } = windowRequire("devtools/shared/plural-form");
+  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");
 
   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* () {
@@ -40,32 +41,31 @@ 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 state = gStore.getState();
-    let totalRequestsCount = state.requests.requests.size;
-    let requestsSummary = getDisplayedRequestsSummary(state);
-    info(`Current requests: ${requestsSummary.count} of ${totalRequestsCount}.`);
+    let totalRequestsCount = RequestsMenu.itemCount;
+    info("Current requests: " + count + " of " + totalRequestsCount + ".");
 
-    if (!totalRequestsCount || !requestsSummary.count) {
+    if (!totalRequestsCount || !count) {
       is(value, L10N.getStr("networkMenu.empty"),
         "The current summary text is incorrect, expected an 'empty' label.");
       return;
     }
 
-    info(`Computed total bytes: ${requestsSummary.bytes}`);
-    info(`Computed total millis: ${requestsSummary.millis}`);
+    info("Computed total bytes: " + totalBytes);
+    info("Computed total millis: " + totalMillis);
 
-    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.");
+    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.");
   }
 });
--- 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: null,
+    causeUri: "",
     stack: true
   },
   {
     method: "GET",
     url: EXAMPLE_URL + "stylesheet_request",
     causeType: "stylesheet",
     causeUri: TOP_URL,
     stack: false
@@ -171,31 +171,32 @@ 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.url;
-    let itemCauseUri = requestItem.cause.loadingDocumentUri;
+    let itemUrl = requestItem.attachment.url;
+    let itemCauseUri = requestItem.target.querySelector(".requests-menu-cause-label")
+                                         .getAttribute("tooltiptext");
     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(RequestsMenu, requestItem,
+    verifyRequestItemTarget(requestItem,
       method, url, { cause: { type: causeType, loadingDocumentUri: causeUri } }
     );
 
-    let { stacktrace } = requestItem.cause;
+    let { stacktrace } = requestItem.attachment.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();
 
-  gStore.dispatch(Actions.sortBy("size"));
+  RequestsMenu.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[data-type=thumbnail]").length, 1,
+    is($all(".requests-menu-icon[type=thumbnail]").length, 1,
       "There should be only one image request with a thumbnail displayed.");
-    is($(".requests-menu-icon[data-type=thumbnail]").src, TEST_IMAGE_DATA_URI,
+    is($(".requests-menu-icon[type=thumbnail]").src, TEST_IMAGE_DATA_URI,
       "The image requests-menu-icon thumbnail is displayed correctly.");
-    is($(".requests-menu-icon[data-type=thumbnail]").hidden, false,
+    is($(".requests-menu-icon[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.getItemAtIndex(0));
+  yield showTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.items[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.getItemAtIndex(0));
+  yield hideTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.items[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.getItemAtIndex(1));
+  yield showTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.items[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", getItemTarget(RequestsMenu, requestItem));
+    let anchor = $(".requests-menu-file", requestItem.target);
     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 over the "method" column hides the tooltip.
-    let anchor = $(".requests-menu-method", getItemTarget(RequestsMenu, requestItem));
+    // Hovering method hides tooltip.
+    let anchor = $(".requests-menu-method", requestItem.target);
 
-    let onTooltipHidden = tooltip.once("hidden");
+    let onHidden = tooltip.once("hidden");
     let win = anchor.ownerDocument.defaultView;
     EventUtils.synthesizeMouseAtCenter(anchor, {type: "mousemove"}, win);
-    yield onTooltipHidden;
+    yield onHidden;
 
     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, RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(1),
+  verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(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.panelWin.NetMonitorController.client,
+    ok(!monitor._controller.client,
       "There shouldn't be a client available after destruction.");
-    ok(!monitor.panelWin.NetMonitorController.tabClient,
+    ok(!monitor._controller.tabClient,
       "There shouldn't be a tabClient available after destruction.");
-    ok(!monitor.panelWin.NetMonitorController.webConsoleClient,
+    ok(!monitor._controller.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, RequestsMenu.getItemAtIndex(0),
+  verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(1),
+  verifyRequestItemTarget(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,38 +4,36 @@
 "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: ($) => getActiveFilters(getState()),
+      validateValue: ($) => getView().RequestsMenu._activeFilters,
       // 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);
+  testShowRawHeaders(origItem.attachment);
 
   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.includes(header.name + ": " + header.value),
+      ok(requestHeaders.indexOf(header.name + ": " + header.value) >= 0,
         "textarea contains request headers");
     }
     let responseHeaders = document.getElementById("raw-response-headers-textarea").value;
     for (let header of data.responseHeaders.headers) {
-      ok(responseHeaders.includes(header.name + ": " + header.value),
+      ok(responseHeaders.indexOf(header.name + ": " + header.value) >= 0,
         "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(SIMPLE_URL);
+  let { monitor } = yield initNetMonitor(SINGLE_GET_URL);
   info("Starting test... ");
 
   let { document, NetMonitorView } = monitor.panelWin;
   let { RequestsMenu } = NetMonitorView;
 
-  let wait = waitForNetworkEvents(monitor, 1);
+  let wait = waitForNetworkEvents(monitor, 2);
   let button = document.querySelector("#requests-menu-reload-notice-button");
   button.click();
   yield wait;
 
-  is(RequestsMenu.itemCount, 1, "The request menu should have one item after reloading");
+  is(RequestsMenu.itemCount, 2, "The request menu should have two items 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(SIMPLE_URL);
+  let { monitor } = yield initNetMonitor(SINGLE_GET_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, 1);
+  yield waitForNetworkEvents(monitor, 2);
   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, RequestsMenu.getItemAtIndex(offset),
+    verifyRequestItemTarget(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,47 +34,55 @@ 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);
+  testCustomForm(origItem.attachment);
 
   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, origItem);
+  testSentRequest(sentItem.attachment, origItem.attachment);
 
   return teardown(monitor);
 
   function testCustomItem(item, orig) {
-    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");
+    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");
   }
 
   function testCustomItemChanged(item, orig) {
-    let url = item.url;
-    let expectedUrl = orig.url + "&" + ADD_QUERY;
+    let file = item.target.querySelector(".requests-menu-file").value;
+    let expectedFile = orig.target.querySelector(".requests-menu-file").value +
+      "&" + ADD_QUERY;
 
-    is(url, expectedUrl, "menu item is updated to reflect url entered in form");
+    is(file, expectedFile, "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 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`);
+    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`);
   }
 
   // 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);
+    let item = RequestsMenu.getItemAtIndex(index).attachment;
     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 item = RequestsMenu.getItemAtIndex(0);
-  is(item.method, "POST", "The request has the right method");
-  is(item.url, requestUrl, "The request has the right URL");
+  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");
 
-  for (let { name, value } of item.requestHeaders.headers) {
+  for (let { name, value } of attachment.requestHeaders.headers) {
     info(`Request header: ${name}: ${value}`);
   }
 
   function hasRequestHeader(name, value) {
-    let { headers } = item.requestHeaders;
+    let { headers } = attachment.requestHeaders;
     return headers.some(h => h.name === name && h.value === value);
   }
 
   function hasNotRequestHeader(name) {
-    let { headers } = item.requestHeaders;
+    let { headers } = attachment.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.getItemAtIndex(0);
-    let target = getItemTarget(RequestsMenu, item);
-    let icon = $(".requests-security-state-icon", target);
+    let item = RequestsMenu.items[0];
+    let icon = $(".requests-security-state-icon", item.target);
 
-    info("Clicking security icon of the first request and waiting for panel update.");
-    EventUtils.synthesizeMouseAtCenter(icon, {}, monitor.panelWin);
+    info("Clicking security icon of the first request and waiting for the " +
+         "panel to update.");
 
+    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,23 +17,21 @@ 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.getItemAtIndex(0);
-  let redirect = RequestsMenu.getItemAtIndex(1);
+  let initial = RequestsMenu.items[0];
+  let redirect = RequestsMenu.items[1];
 
-  let initialSecurityIcon =
-    $(".requests-security-state-icon", getItemTarget(RequestsMenu, initial));
-  let redirectSecurityIcon =
-    $(".requests-security-state-icon", getItemTarget(RequestsMenu, redirect));
+  let initialSecurityIcon = $(".requests-security-state-icon", initial.target);
+  let redirectSecurityIcon = $(".requests-security-state-icon", redirect.target);
 
   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,23 +19,22 @@ 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 target = getItemTarget(RequestsMenu, item);
-    let domain = $(".requests-menu-domain", target).textContent;
+    let domain = $(".requests-menu-domain", item.target).value;
 
     info("Found a request to " + domain);
     ok(domain in EXPECTED_SECURITY_STATES, "Domain " + domain + " was expected.");
 
-    let classes = $(".requests-security-state-icon", target).classList;
+    let classes = $(".requests-security-state-icon", item.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.securityState, undefined,
+    is(RequestsMenu.selectedItem.attachment.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.securityState,
+    ok(RequestsMenu.selectedItem.attachment.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.method, "GET", "The method is correct.");
-  is(request.status, "200", "The status is correct.");
+  is(request.attachment.method, "GET", "The method is correct.");
+  is(request.attachment.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.method, "POST", "The method is correct.");
-  ok(request.url.endsWith("beacon_request"), "The URL is correct.");
-  is(request.status, "404", "The status is correct.");
+  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.");
 
   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,20 +46,19 @@ 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(RequestsMenu, item,
-      request.method, request.uri, request.details);
+    yield verifyRequestItemTarget(item, request.method, request.uri, request.details);
 
-    let { stacktrace } = item.cause;
+    let { stacktrace } = item.attachment.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.panelWin.NetMonitorView,
+      ok(monitor._view,
         `The network monitor view object exists (${tag}).`);
-      ok(monitor.panelWin.NetMonitorController,
+      ok(monitor._controller,
         `The network monitor controller object exists (${tag}).`);
-      ok(monitor.panelWin.NetMonitorController._startup,
+      ok(monitor._controller._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.panelWin.NetMonitorController.tabClient,
+      ok(monitor._controller.tabClient,
         `There should be a tabClient available at this point (${tag}).`);
-      ok(monitor.panelWin.NetMonitorController.webConsoleClient,
+      ok(monitor._controller.webConsoleClient,
         `There should be a webConsoleClient available at this point (${tag}).`);
-      ok(monitor.panelWin.NetMonitorController.timelineFront,
+      ok(monitor._controller.timelineFront,
         `There should be a timelineFront available at this point (${tag}).`);
     }
 
     function checkIfDestroyed(tag) {
       gInfo("Checking if destruction is ok.");
 
-      gOk(monitor.panelWin.NetMonitorView,
+      gOk(monitor._view,
         `The network monitor view object still exists (${tag}).`);
-      gOk(monitor.panelWin.NetMonitorController,
+      gOk(monitor._controller,
         `The network monitor controller object still exists (${tag}).`);
-      gOk(monitor.panelWin.NetMonitorController._shutdown,
+      gOk(monitor._controller._shutdown,
         `The network monitor controller object still exists and is destroyed (${tag}).`);
 
-      gOk(!monitor.panelWin.NetMonitorController.tabClient,
+      gOk(!monitor._controller.tabClient,
         `There shouldn't be a tabClient available after destruction (${tag}).`);
-      gOk(!monitor.panelWin.NetMonitorController.webConsoleClient,
+      gOk(!monitor._controller.webConsoleClient,
         `There shouldn't be a webConsoleClient available after destruction (${tag}).`);
-      gOk(!monitor.panelWin.NetMonitorController.timelineFront,
+      gOk(!monitor._controller.timelineFront,
         `There shouldn't be a timelineFront available after destruction (${tag}).`);
     }
 
     executeSoon(() => {
       checkIfInitialized(1);
 
-      monitor.panelWin.NetMonitorController.startupNetMonitor()
+      monitor._controller.startupNetMonitor()
         .then(() => {
           info("Starting up again shouldn't do anything special.");
           checkIfInitialized(2);
-          return monitor.panelWin.NetMonitorController.connect();
+          return monitor._controller.connect();
         })
         .then(() => {
           info("Connecting again shouldn't do anything special.");
           checkIfInitialized(3);
           return teardown(monitor);
         })
         .then(finish);
     });
 
     registerCleanupFunction(() => {
       checkIfDestroyed(1);
 
-      monitor.panelWin.NetMonitorController.shutdownNetMonitor()
+      monitor._controller.shutdownNetMonitor()
         .then(() => {
           gInfo("Shutting down again shouldn't do anything special.");
           checkIfDestroyed(2);
-          return monitor.panelWin.NetMonitorController.disconnect();
+          return monitor._controller.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,210 +27,221 @@ 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.id, "string",
+      is(typeof requestItem.value, "string",
         "The attached request id is incorrect.");
-      isnot(requestItem.id, "",
+      isnot(requestItem.value, "",
         "The attached request id should not be empty.");
 
-      is(typeof requestItem.startedMillis, "number",
+      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",
         "The attached startedMillis is incorrect.");
-      isnot(requestItem.startedMillis, 0,
+      isnot(requestItem.attachment.startedMillis, 0,
         "The attached startedMillis should not be zero.");
 
-      is(requestItem.requestHeaders, undefined,
+      is(requestItem.attachment.requestHeaders, undefined,
         "The requestHeaders should not yet be set.");
-      is(requestItem.requestCookies, undefined,
+      is(requestItem.attachment.requestCookies, undefined,
         "The requestCookies should not yet be set.");
-      is(requestItem.requestPostData, undefined,
+      is(requestItem.attachment.requestPostData, undefined,
         "The requestPostData should not yet be set.");
 
-      is(requestItem.responseHeaders, undefined,
+      is(requestItem.attachment.responseHeaders, undefined,
         "The responseHeaders should not yet be set.");
-      is(requestItem.responseCookies, undefined,
+      is(requestItem.attachment.responseCookies, undefined,
         "The responseCookies should not yet be set.");
 
-      is(requestItem.httpVersion, undefined,
+      is(requestItem.attachment.httpVersion, undefined,
         "The httpVersion should not yet be set.");
-      is(requestItem.status, undefined,
+      is(requestItem.attachment.status, undefined,
         "The status should not yet be set.");
-      is(requestItem.statusText, undefined,
+      is(requestItem.attachment.statusText, undefined,
         "The statusText should not yet be set.");
 
-      is(requestItem.headersSize, undefined,
+      is(requestItem.attachment.headersSize, undefined,
         "The headersSize should not yet be set.");
-      is(requestItem.transferredSize, undefined,
+      is(requestItem.attachment.transferredSize, undefined,
         "The transferredSize should not yet be set.");
-      is(requestItem.contentSize, undefined,
+      is(requestItem.attachment.contentSize, undefined,
         "The contentSize should not yet be set.");
 
-      is(requestItem.mimeType, undefined,
+      is(requestItem.attachment.mimeType, undefined,
         "The mimeType should not yet be set.");
-      is(requestItem.responseContent, undefined,
+      is(requestItem.attachment.responseContent, undefined,
         "The responseContent should not yet be set.");
 
-      is(requestItem.totalTime, undefined,
+      is(requestItem.attachment.totalTime, undefined,
         "The totalTime should not yet be set.");
-      is(requestItem.eventTimings, undefined,
+      is(requestItem.attachment.eventTimings, undefined,
         "The eventTimings should not yet be set.");
 
-      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS);
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_REQUEST_HEADERS, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
-      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.");
+
+      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.");
       // 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.requestCookies,
-        "There should be a requestCookies data available.");
-      is(requestItem.requestCookies.cookies.length, 2,
-        "The requestCookies data has an incorrect |cookies| property.");
+      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.");
 
-      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS);
+      verifyRequestItemTarget(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.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.");
+      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.");
 
-      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS);
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_COOKIES, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
-      ok(requestItem.responseCookies,
-        "There should be a responseCookies data available.");
-      is(requestItem.responseCookies.cookies.length, 2,
-        "The responseCookies data has an incorrect |cookies| property.");
+      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.");
 
-      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS);
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.STARTED_RECEIVING_RESPONSE, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
-      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.");
+      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.");
 
-      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
+      verifyRequestItemTarget(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.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.");
+      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.");
 
-      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
         type: "plain",
         fullMimeType: "text/plain; charset=utf-8",
-        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
-        size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
+        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
       });
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_CONTENT, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
-      ok(requestItem.responseContent,
-        "There should be a responseContent data available.");
-      is(requestItem.responseContent.content.mimeType,
+      ok(requestItem.attachment.responseContent,
+        "There should be a responseContent attachment available.");
+      is(requestItem.attachment.responseContent.content.mimeType,
         "text/plain; charset=utf-8",
-        "The responseContent data has an incorrect |content.mimeType| property.");
-      is(requestItem.responseContent.content.text,
+        "The responseContent attachment has an incorrect |content.mimeType| property.");
+      is(requestItem.attachment.responseContent.content.text,
         "Hello world!",
-        "The responseContent data has an incorrect |content.text| property.");
-      is(requestItem.responseContent.content.size,
+        "The responseContent attachment has an incorrect |content.text| property.");
+      is(requestItem.attachment.responseContent.content.size,
         12,
-        "The responseContent data has an incorrect |content.size| property.");
+        "The responseContent attachment has an incorrect |content.size| property.");
 
-      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
         type: "plain",
         fullMimeType: "text/plain; charset=utf-8",
-        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
-        size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
+        transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
       });
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.UPDATING_EVENT_TIMINGS, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
-      is(typeof requestItem.totalTime, "number",
+      is(typeof requestItem.attachment.totalTime, "number",
         "The attached totalTime is incorrect.");
-      ok(requestItem.totalTime >= 0,
+      ok(requestItem.attachment.totalTime >= 0,
         "The attached totalTime should be positive.");
 
-      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
+      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, {
         time: true
       });
     });
 
     monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_EVENT_TIMINGS, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
-      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.");
+      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.");
 
-      verifyRequestItemTarget(RequestsMenu, requestItem, "GET", SIMPLE_SJS, {
+      verifyRequestItemTarget(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("data-code"),
+    is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("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.");
-  ok(document.querySelector("#requests-menu-empty-notice"),
+  is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), false,
     "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.");
-  ok(!document.querySelector("#requests-menu-empty-notice"),
+  is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), true,
     "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.");
-  ok(!document.querySelector("#requests-menu-empty-notice"),
+  is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), true,
     "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.");
-  ok(document.querySelector("#requests-menu-empty-notice"),
+  is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), false,
     "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,60 +157,71 @@ 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(".request-list-item").length, 5,
+    is($all(".side-menu-widget-item").length, 5,
       "The visible items in the requests menu are, in fact, visible!");
 
-    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(a),
+    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),
       "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, RequestsMenu.getItemAtIndex(b),
+    verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(c),
+    verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(d),
+    verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(e),
+    verifyRequestItemTarget(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,91 +167,102 @@ 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) {
-        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.");
+        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.");
       } else {
-        is(header.getAttribute("data-sorted"), direction,
-          "The " + header.id + " header has a correct 'data-sorted' attribute.");
-        is(header.getAttribute("title"), direction == "ascending"
+        is(header.getAttribute("sorted"), direction,
+          "The " + header.id + " header has an incorrect 'sorted' attribute.");
+        is(header.getAttribute("tooltiptext"), direction == "ascending"
           ? L10N.getStr("networkMenu.sortedAsc")
           : L10N.getStr("networkMenu.sortedDesc"),
-          "The " + header.id + " header has a correct 'title' attribute.");
+          "The " + header.id + " has an incorrect 'tooltiptext' 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 visible items in the requests menu.");
-    is($all(".request-list-item").length, 5,
+      "There should be a total of 5 visbile items in the requests menu.");
+    is($all(".side-menu-widget-item").length, 5,
       "The visible items in the requests menu are, in fact, visible!");
 
-    verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(a),
+    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),
       "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, RequestsMenu.getItemAtIndex(b),
+    verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(c),
+    verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(d),
+    verifyRequestItemTarget(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, RequestsMenu.getItemAtIndex(e),
+    verifyRequestItemTarget(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) {
-        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.");
+        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.");
       } else {
-        is(header.getAttribute("data-sorted"), direction,
-          "The " + header.id + " header has a correct 'data-sorted' attribute.");
-        is(header.getAttribute("title"), direction == "ascending"
+        is(header.getAttribute("sorted"), direction,
+          "The " + header.id + " header has an incorrect 'sorted' attribute.");
+        is(header.getAttribute("tooltiptext"), direction == "ascending"
           ? L10N.getStr("networkMenu.sortedAsc")
           : L10N.getStr("networkMenu.sortedDesc"),
-          "The " + header.id + " header has a correct 'title' attribute.");
+          "The " + header.id + " has an incorrect 'tooltiptext' 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(".request-list-item").length, order.length,
+    is($all(".side-menu-widget-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,
-        RequestsMenu.getItemAtIndex(order[i]),
+      verifyRequestItemTarget(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,
-        RequestsMenu.getItemAtIndex(order[i + len]),
+      verifyRequestItemTarget(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,
-        RequestsMenu.getItemAtIndex(order[i + len * 2]),
+      verifyRequestItemTarget(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,
-        RequestsMenu.getItemAtIndex(order[i + len * 3]),
+      verifyRequestItemTarget(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,
-        RequestsMenu.getItemAtIndex(order[i + len * 4]),
+      verifyRequestItemTarget(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, 1]);
+  testFilterButtonsCustom(monitor, [0, 1, 1, 1, 0, 0, 0, 0, 0, 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,18 +109,17 @@ 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(RequestsMenu, item,
-        request.method, request.uri, request.details);
+      yield verifyRequestItemTarget(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.
@@ -155,17 +154,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("data-code"),
+    is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("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.
    */
@@ -203,13 +202,12 @@ 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);
-    let target = getItemTarget(RequestsMenu, requestItems[index]);
-    EventUtils.sendMouseEvent({ type: "mousedown" }, target);
+    EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[index].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, RequestsMenu.getItemAtIndex(i),
+    verifyRequestItemTarget(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 = NetMonitorController.webConsoleClient;
+  let client = monitor._controller.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.eventTimings.timings.receive > 1000;
+  const reportedOneSecond = requestItem.attachment.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,27 +41,32 @@ 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.");
 
-  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].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");
 
-  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");
+  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");
 
   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,37 +18,44 @@ 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[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]");
+  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]");
 
   info("Number of millisecond divisions: " + milDivs.length);
   info("Number of second divisions: " + secDivs.length);
   info("Number of minute divisions: " + minDivs.length);
 
-  milDivs.forEach(div => info(`Millisecond division: ${div.textContent}`));
-  secDivs.forEach(div => info(`Second division: ${div.textContent}`));
-  minDivs.forEach(div => info(`Minute division: ${div.textContent}`));
+  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"));
+  }
 
-  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.responseHeaders.headers.find(e => e.name == "Date").value);
+    firstRequest.attachment.responseHeaders.headers.find(e => e.name == "Date").value);
   info("Last request happened at: " +
-    lastRequest.responseHeaders.headers.find(e => e.name == "Date").value);
+    lastRequest.attachment.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].textContent.match(/\d+\.\d{2}\s\w+/),
+  ok(secDivs[0].getAttribute("value").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,135 +245,132 @@ function waitForNetworkEvents(aMonitor, 
       executeSoon(deferred.resolve);
     }
   }
 
   awaitedEventsToListeners.forEach(([e, l]) => panel.on(events[e], l));
   return deferred.promise;
 }
 
-/**
- * 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 = {}) {
+function verifyRequestItemTarget(aRequestItem, aMethod, aUrl, aData = {}) {
   info("> Verifying: " + aMethod + " " + aUrl + " " + aData.toSource());
   // This bloats log sizes significantly in automation (bug 992485)
-  // info("> Request: " + requestItem.toSource());
+  // info("> Request: " + aRequestItem.attachment.toSource());
 
-  let visibleIndex = requestList.visibleItems.indexOf(requestItem);
+  let requestsMenu = aRequestItem.ownerView;
+  let widgetIndex = requestsMenu.indexOfItem(aRequestItem);
+  let visibleIndex = requestsMenu.visibleItems.indexOf(aRequestItem);
 
+  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 target = getItemTarget(requestList, requestItem);
+  let { attachment, target } = aRequestItem;
 
   let unicodeUrl = decodeUnicodeUrl(aUrl);
   let name = getUrlBaseName(aUrl);
   let query = getUrlQuery(aUrl);
   let hostPort = getUrlHost(aUrl);
-  let remoteAddress = requestItem.remoteAddress;
+  let remoteAddress = attachment.remoteAddress;
 
   if (fuzzyUrl) {
-    ok(requestItem.method.startsWith(aMethod), "The attached method is correct.");
-    ok(requestItem.url.startsWith(aUrl), "The attached url is correct.");
+    ok(attachment.method.startsWith(aMethod), "The attached method is correct.");
+    ok(attachment.url.startsWith(aUrl), "The attached url is correct.");
   } else {
-    is(requestItem.method, aMethod, "The attached method is correct.");
-    is(requestItem.url, aUrl, "The attached url is correct.");
+    is(attachment.method, aMethod, "The attached method is correct.");
+    is(attachment.url, aUrl, "The attached url is correct.");
   }
 
-  is(target.querySelector(".requests-menu-method").textContent,
+  is(target.querySelector(".requests-menu-method").getAttribute("value"),
     aMethod, "The displayed method is correct.");
 
   if (fuzzyUrl) {
-    ok(target.querySelector(".requests-menu-file").textContent.startsWith(
+    ok(target.querySelector(".requests-menu-file").getAttribute("value").startsWith(
       name + (query ? "?" + query : "")), "The displayed file is correct.");
-    ok(target.querySelector(".requests-menu-file").getAttribute("title").startsWith(unicodeUrl),
+    ok(target.querySelector(".requests-menu-file").getAttribute("tooltiptext").startsWith(unicodeUrl),
       "The tooltip file is correct.");
   } else {
-    is(target.querySelector(".requests-menu-file").textContent,
+    is(target.querySelector(".requests-menu-file").getAttribute("value"),
       name + (query ? "?" + query : ""), "The displayed file is correct.");
-    is(target.querySelector(".requests-menu-file").getAttribute("title"),
+    is(target.querySelector(".requests-menu-file").getAttribute("tooltiptext"),
       unicodeUrl, "The tooltip file is correct.");
   }
 
-  is(target.querySelector(".requests-menu-domain").textContent,
+  is(target.querySelector(".requests-menu-domain").getAttribute("value"),
     hostPort, "The displayed domain is correct.");
 
   let domainTooltip = hostPort + (remoteAddress ? " (" + remoteAddress + ")" : "");
-  is(target.querySelector(".requests-menu-domain").getAttribute("title"),
+  is(target.querySelector(".requests-menu-domain").getAttribute("tooltiptext"),
     domainTooltip, "The tooltip domain is correct.");
 
   if (status !== undefined) {
-    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");
+    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");
     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 value = target.querySelector(".requests-menu-cause > .subitem-label").textContent;
-    let tooltip = target.querySelector(".requests-menu-cause").getAttribute("title");
+    let causeLabel = target.querySelector(".requests-menu-cause-label");
+    let value = causeLabel.getAttribute("value");
+    let tooltip = causeLabel.getAttribute("tooltiptext");
     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").textContent;
-    let tooltip = target.querySelector(".requests-menu-type").getAttribute("title");
+    let value = target.querySelector(".requests-menu-type").getAttribute("value");
+    let tooltip = target.querySelector(".requests-menu-type").getAttribute("tooltiptext");
     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").textContent;
-    let tooltip = target.querySelector(".requests-menu-transferred").getAttribute("title");
+    let value = target.querySelector(".requests-menu-transferred").getAttribute("value");
+    let tooltip = target.querySelector(".requests-menu-transferred").getAttribute("tooltiptext");
     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").textContent;
-    let tooltip = target.querySelector(".requests-menu-size").getAttribute("title");
+    let value = target.querySelector(".requests-menu-size").getAttribute("value");
+    let tooltip = target.querySelector(".requests-menu-size").getAttribute("tooltiptext");
     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").textContent;
-    let tooltip = target.querySelector(".requests-menu-timings-total").getAttribute("title");
+    let value = target.querySelector(".requests-menu-timings-total").getAttribute("value");
+    let tooltip = target.querySelector(".requests-menu-timings-total").getAttribute("tooltiptext");
     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(target.classList.contains("even"), "Item should have 'even' class.");
-      ok(!target.classList.contains("odd"), "Item shouldn't have 'odd' class.");
+      ok(aRequestItem.target.hasAttribute("even"),
+        aRequestItem.value + " should have 'even' attribute.");
+      ok(!aRequestItem.target.hasAttribute("odd"),
+        aRequestItem.value + " shouldn't have 'odd' attribute.");
     } else {
-      ok(!target.classList.contains("even"), "Item shouldn't have 'even' class.");
-      ok(target.classList.contains("odd"), "Item should have 'odd' class.");
+      ok(!aRequestItem.target.hasAttribute("even"),
+        aRequestItem.value + " shouldn't have 'even' attribute.");
+      ok(aRequestItem.target.hasAttribute("odd"),
+        aRequestItem.value + " should have 'odd' attribute.");
     }
   }
 }
 
 /**
  * Helper function for waiting for an event to fire before resolving a promise.
  * Example: waitFor(aMonitor.panelWin, aMonitor.panelWin.EVENTS.TAB_UPDATED);
  *
deleted file mode 100644
--- a/devtools/client/netmonitor/utils/format-utils.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/* 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);