Backed out changeset abcb8d9139a2 (bug 1309866) to have complete backout
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 09 Dec 2016 15:46:10 +0100
changeset 372732 8404d26166a35406f46ff237ed132735c98882b2
parent 372731 6a75cf810d963de5b83c3aacb4c776604474302f
child 372733 959cb556ad70b411221fe67eaa513e4fb38c1589
child 372774 5159fd693f92397c8c9622e51a9e7a3475e9153a
child 372838 6e7b8826047c4f8b8b167f49a498047b5c9d3ebe
child 390914 73bdd29461aef3297b099ba25e8b317a18a0a4b9
push id1419
push userjlund@mozilla.com
push dateMon, 10 Apr 2017 20:44:07 +0000
treeherdermozilla-release@5e6801b73ef6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1309866
milestone53.0a1
backs outabcb8d9139a2d3b0cee9084313ef58326409b349
first release with
nightly linux32
8404d26166a3 / 53.0a1 / 20161210030206 / files
nightly linux64
8404d26166a3 / 53.0a1 / 20161210030206 / files
nightly mac
8404d26166a3 / 53.0a1 / 20161210030206 / files
nightly win32
8404d26166a3 / 53.0a1 / 20161210030206 / files
nightly win64
8404d26166a3 / 53.0a1 / 20161210030206 / 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
Backed out changeset abcb8d9139a2 (bug 1309866) to have complete backout
devtools/client/netmonitor/actions/batching.js
devtools/client/netmonitor/actions/filters.js
devtools/client/netmonitor/actions/index.js
devtools/client/netmonitor/actions/moz.build
devtools/client/netmonitor/actions/requests.js
devtools/client/netmonitor/actions/selection.js
devtools/client/netmonitor/actions/sort.js
devtools/client/netmonitor/actions/timing-markers.js
devtools/client/netmonitor/actions/ui.js
devtools/client/netmonitor/components/clear-button.js
devtools/client/netmonitor/components/moz.build
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/components/summary-button.js
devtools/client/netmonitor/components/toggle-button.js
devtools/client/netmonitor/constants.js
devtools/client/netmonitor/custom-request-view.js
devtools/client/netmonitor/details-view.js
devtools/client/netmonitor/har/har-builder.js
devtools/client/netmonitor/har/har-collector.js
devtools/client/netmonitor/middleware/batching.js
devtools/client/netmonitor/middleware/moz.build
devtools/client/netmonitor/moz.build
devtools/client/netmonitor/netmonitor-controller.js
devtools/client/netmonitor/netmonitor-view.js
devtools/client/netmonitor/netmonitor.xul
devtools/client/netmonitor/performance-statistics-view.js
devtools/client/netmonitor/prefs.js
devtools/client/netmonitor/reducers/batching.js
devtools/client/netmonitor/reducers/filters.js
devtools/client/netmonitor/reducers/index.js
devtools/client/netmonitor/reducers/moz.build
devtools/client/netmonitor/reducers/requests.js
devtools/client/netmonitor/reducers/sort.js
devtools/client/netmonitor/reducers/timing-markers.js
devtools/client/netmonitor/reducers/ui.js
devtools/client/netmonitor/request-list-context-menu.js
devtools/client/netmonitor/request-utils.js
devtools/client/netmonitor/requests-menu-view.js
devtools/client/netmonitor/selectors/filters.js
devtools/client/netmonitor/selectors/index.js
devtools/client/netmonitor/selectors/moz.build
devtools/client/netmonitor/selectors/requests.js
devtools/client/netmonitor/selectors/ui.js
devtools/client/netmonitor/sidebar-view.js
devtools/client/netmonitor/sort-predicates.js
devtools/client/netmonitor/store.js
devtools/client/netmonitor/test/browser.ini
devtools/client/netmonitor/test/browser_net_accessibility-01.js
devtools/client/netmonitor/test/browser_net_accessibility-02.js
devtools/client/netmonitor/test/browser_net_api-calls.js
devtools/client/netmonitor/test/browser_net_autoscroll.js
devtools/client/netmonitor/test/browser_net_brotli.js
devtools/client/netmonitor/test/browser_net_cached-status.js
devtools/client/netmonitor/test/browser_net_cause.js
devtools/client/netmonitor/test/browser_net_cause_redirect.js
devtools/client/netmonitor/test/browser_net_content-type.js
devtools/client/netmonitor/test/browser_net_copy_headers.js
devtools/client/netmonitor/test/browser_net_copy_url.js
devtools/client/netmonitor/test/browser_net_cors_requests.js
devtools/client/netmonitor/test/browser_net_curl-utils.js
devtools/client/netmonitor/test/browser_net_cyrillic-01.js
devtools/client/netmonitor/test/browser_net_cyrillic-02.js
devtools/client/netmonitor/test/browser_net_filter-01.js
devtools/client/netmonitor/test/browser_net_filter-02.js
devtools/client/netmonitor/test/browser_net_filter-03.js
devtools/client/netmonitor/test/browser_net_footer-summary.js
devtools/client/netmonitor/test/browser_net_frame.js
devtools/client/netmonitor/test/browser_net_icon-preview.js
devtools/client/netmonitor/test/browser_net_image-tooltip.js
devtools/client/netmonitor/test/browser_net_json-long.js
devtools/client/netmonitor/test/browser_net_json-malformed.js
devtools/client/netmonitor/test/browser_net_json_custom_mime.js
devtools/client/netmonitor/test/browser_net_json_text_mime.js
devtools/client/netmonitor/test/browser_net_jsonp.js
devtools/client/netmonitor/test/browser_net_large-response.js
devtools/client/netmonitor/test/browser_net_post-data-01.js
devtools/client/netmonitor/test/browser_net_prefs-reload.js
devtools/client/netmonitor/test/browser_net_raw_headers.js
devtools/client/netmonitor/test/browser_net_reload-button.js
devtools/client/netmonitor/test/browser_net_reload-markers.js
devtools/client/netmonitor/test/browser_net_req-resp-bodies.js
devtools/client/netmonitor/test/browser_net_resend.js
devtools/client/netmonitor/test/browser_net_resend_cors.js
devtools/client/netmonitor/test/browser_net_resend_headers.js
devtools/client/netmonitor/test/browser_net_security-icon-click.js
devtools/client/netmonitor/test/browser_net_security-redirect.js
devtools/client/netmonitor/test/browser_net_security-state.js
devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js
devtools/client/netmonitor/test/browser_net_send-beacon.js
devtools/client/netmonitor/test/browser_net_service-worker-status.js
devtools/client/netmonitor/test/browser_net_simple-request-data.js
devtools/client/netmonitor/test/browser_net_simple-request-details.js
devtools/client/netmonitor/test/browser_net_simple-request.js
devtools/client/netmonitor/test/browser_net_sort-01.js
devtools/client/netmonitor/test/browser_net_sort-02.js
devtools/client/netmonitor/test/browser_net_sort-03.js
devtools/client/netmonitor/test/browser_net_statistics-03.js
devtools/client/netmonitor/test/browser_net_status-codes.js
devtools/client/netmonitor/test/browser_net_streaming-response.js
devtools/client/netmonitor/test/browser_net_throttle.js
devtools/client/netmonitor/test/browser_net_timeline_ticks.js
devtools/client/netmonitor/test/browser_net_timing-division.js
devtools/client/netmonitor/test/head.js
devtools/client/netmonitor/utils/format-utils.js
devtools/client/netmonitor/utils/moz.build
devtools/client/netmonitor/waterfall-background.js
devtools/client/preferences/devtools.js
devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js
devtools/client/themes/netmonitor.css
devtools/client/themes/toolbars.css
devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js
devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js
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,28 +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",
 };
 
 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
@@ -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
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,20 +1,18 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
     'actions',
     'components',
     'har',
-    'middleware',
     'reducers',
-    'selectors',
-    'utils',
+    'selectors'
 ]
 
 DevToolsModules(
     'constants.js',
     'custom-request-view.js',
     'details-view.js',
     'events.js',
     'filter-predicates.js',
@@ -24,12 +22,11 @@ DevToolsModules(
     '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,14 +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/. */
 
 /* eslint-disable mozilla/reject-some-requires */
-/* globals window, NetMonitorView, gStore, Actions */
+/* 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 = {
@@ -36,27 +36,31 @@ var { loader, require } = BrowserLoaderM
 
 const promise = require("promise");
 const Services = require("Services");
 const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
 const EventEmitter = require("devtools/shared/event-emitter");
 const Editor = require("devtools/client/sourceeditor/editor");
 const {TimelineFront} = require("devtools/shared/fronts/timeline");
 const {Task} = require("devtools/shared/task");
+const {Prefs} = require("./prefs");
 const {EVENTS} = require("./events");
 const Actions = require("./actions/index");
-const { getDisplayedRequestById } = require("./selectors/index");
 
 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");
 
+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.
    *
    * @return object
@@ -82,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;
   }),
@@ -279,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);
     }
@@ -389,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;
       }
@@ -420,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);
@@ -445,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);
 
@@ -501,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
@@ -523,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
@@ -612,136 +632,152 @@ NetworkEventsHandler.prototype = {
    * Handles additional information received for a "requestHeaders" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onRequestHeaders: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       requestHeaders: response
-    }).then(() => {
+    }, () => {
       window.emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "requestCookies" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onRequestCookies: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       requestCookies: response
-    }).then(() => {
+    }, () => {
       window.emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "requestPostData" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onRequestPostData: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       requestPostData: response
-    }).then(() => {
+    }, () => {
       window.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "securityInfo" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onSecurityInfo: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       securityInfo: response.securityInfo
-    }).then(() => {
+    }, () => {
       window.emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "responseHeaders" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onResponseHeaders: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       responseHeaders: response
-    }).then(() => {
+    }, () => {
       window.emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "responseCookies" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onResponseCookies: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       responseCookies: response
-    }).then(() => {
+    }, () => {
       window.emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "responseContent" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onResponseContent: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       responseContent: response
-    }).then(() => {
+    }, () => {
       window.emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
     });
   },
 
   /**
    * Handles additional information received for a "eventTimings" packet.
    *
    * @param object response
    *        The message received from the server.
    */
   _onEventTimings: function (response) {
     NetMonitorView.RequestsMenu.updateRequest(response.from, {
       eventTimings: response
-    }).then(() => {
+    }, () => {
       window.emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
     });
   },
 
   /**
+   * Clears all accumulated markers.
+   */
+  clearMarkers: function () {
+    this._markers.length = 0;
+  },
+
+  /**
    * Fetches the full text of a LongString.
    *
    * @param object | string stringGrip
    *        The long string grip containing the corresponding actor.
    *        If you pass in a plain string (by accident or because you're lazy),
    *        then a promise of the same string is simply returned.
    * @return object Promise
    *         A promise that is resolved when the full string contents
    *         are available, or rejected if something goes wrong.
    */
   getString: function (stringGrip) {
     return this.webConsoleClient.getString(stringGrip);
   }
 };
 
 /**
+ * Returns true if this is document is in RTL mode.
+ * @return boolean
+ */
+XPCOMUtils.defineLazyGetter(window, "isRTL", function () {
+  return window.getComputedStyle(document.documentElement, null)
+    .direction == "rtl";
+});
+
+/**
  * Convenient way of emitting events from the panel window.
  */
 EventEmitter.decorate(this);
 
 /**
  * Preliminary setup for the NetMonitorController object.
  */
 NetMonitorController.TargetEventsHandler = new TargetEventsHandler();
--- a/devtools/client/netmonitor/netmonitor-view.js
+++ b/devtools/client/netmonitor/netmonitor-view.js
@@ -13,17 +13,16 @@ const { testing: isTesting } = require("
 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");
-var {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
@@ -76,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");
 
@@ -159,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");
 
@@ -181,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);
   },
 
@@ -241,46 +246,44 @@ var NetMonitorView = {
  * 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();
--- a/devtools/client/netmonitor/netmonitor.xul
+++ b/devtools/client/netmonitor/netmonitor.xul
@@ -21,20 +21,198 @@
         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/performance-statistics-view.js
+++ b/devtools/client/netmonitor/performance-statistics-view.js
@@ -167,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;
@@ -232,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,47 +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/. */
 
 /* eslint-disable mozilla/reject-some-requires */
-/* globals window, dumpn, $, gNetwork, EVENTS, Prefs,
+/* 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 {
-  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);
@@ -50,344 +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");
 
-    // Watch selection changes
-    this.store.subscribe(storeWatcher(
-      null,
-      () => getSelectedRequest(this.store.getState()),
-      (newSelected, oldSelected) => this.onSelectionUpdate(newSelected, oldSelected)
-    ));
+    // 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 the sidebar status and resize the waterfall column on change
-    this.store.subscribe(storeWatcher(
-      false,
-      () => this.store.getState().ui.sidebarOpen,
-      () => this.onResize()
-    ));
+    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("mouseup", 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("mouseup", 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
@@ -21,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,21 +84,19 @@ 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]
-skip-if = (os == 'linux' && debug && bits == 32) # Bug 1321434
 [browser_net_filter-01.js]
-skip-if = (os == 'linux' && debug && bits == 32) # Bug 1303439
 [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]
 [browser_net_image-tooltip.js]
 [browser_net_json-long.js]
@@ -136,21 +135,19 @@ skip-if = (os == 'linux' && debug && bit
 [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
@@ -9,23 +9,21 @@
 
 add_task(function* () {
   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, windowRequire } = monitor.panelWin;
+  let { NetMonitorView } = monitor.panelWin;
   let { RequestsMenu } = NetMonitorView;
 
   RequestsMenu.lazyUpdate = false;
 
-  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
-
   let count = 0;
   function check(selectedIndex, paneVisibility) {
     info("Performing check " + (count++) + ".");
 
     is(RequestsMenu.selectedIndex, selectedIndex,
       "The selected item in the requests menu was incorrect.");
     is(NetMonitorView.detailsPaneHidden, !paneVisibility,
       "The network requests details pane visibility state was incorrect.");
@@ -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,29 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 /**
- * Test if the summary text displayed in the network requests menu footer is correct.
+ * Test if the summary text displayed in the network requests menu footer
+ * is correct.
  */
 
 add_task(function* () {
   requestLongerTimeout(2);
 
   let { tab, monitor } = yield initNetMonitor(FILTERING_URL);
   info("Starting test... ");
 
   let { $, NetMonitorView, gStore } = monitor.panelWin;
   let { RequestsMenu } = NetMonitorView;
 
   let winRequire = monitor.panelWin.require;
-  let { getDisplayedRequestsSummary } =
-    winRequire("devtools/client/netmonitor/selectors/index");
+  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}`);
@@ -41,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_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-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
@@ -41,17 +41,17 @@ function* throttleTest(actuallyThrottle)
   });
   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");