Bug 1336379 - Implement StatisticsPanel r?honza draft
authorRicky Chien <rchien@mozilla.com>
Tue, 31 Jan 2017 19:29:00 +0800
changeset 478952 459fc3b0b0552a0dd80646177baad95d99ed9159
parent 478951 f40c3c80e3b924c37b6eabcdc40ffebc0af873c5
child 544547 fecf0cda41e606704e0322c95c02ee2a5f7099fb
push id44106
push userbmo:rchien@mozilla.com
push dateSat, 04 Feb 2017 07:36:54 +0000
reviewershonza
bugs1336379
milestone54.0a1
Bug 1336379 - Implement StatisticsPanel r?honza MozReview-Commit-ID: FNCSetNPzz6
devtools/client/netmonitor/components/moz.build
devtools/client/netmonitor/components/statistics-panel.js
devtools/client/netmonitor/moz.build
devtools/client/netmonitor/netmonitor-view.js
devtools/client/netmonitor/netmonitor.xul
devtools/client/netmonitor/statistics-view.js
devtools/client/netmonitor/test/browser_net_charts-01.js
devtools/client/netmonitor/test/browser_net_charts-02.js
devtools/client/netmonitor/test/browser_net_charts-03.js
devtools/client/netmonitor/test/browser_net_charts-04.js
devtools/client/netmonitor/test/browser_net_charts-05.js
devtools/client/netmonitor/test/browser_net_charts-06.js
devtools/client/netmonitor/test/browser_net_charts-07.js
devtools/client/netmonitor/test/browser_net_statistics-01.js
devtools/client/netmonitor/test/browser_net_statistics-02.js
devtools/client/themes/netmonitor.css
--- a/devtools/client/netmonitor/components/moz.build
+++ b/devtools/client/netmonitor/components/moz.build
@@ -4,10 +4,11 @@
 
 DevToolsModules(
     'request-list-content.js',
     'request-list-empty.js',
     'request-list-header.js',
     'request-list-item.js',
     'request-list-tooltip.js',
     'request-list.js',
+    'statistics-panel.js',
     'toolbar.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/statistics-panel.js
@@ -0,0 +1,229 @@
+/* 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,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { Chart } = require("devtools/client/shared/widgets/Chart");
+const { PluralForm } = require("devtools/shared/plural-form");
+const Actions = require("../actions/index");
+const { Filters } = require("../filter-predicates");
+const { L10N } = require("../l10n");
+const {
+  getSizeWithDecimals,
+  getTimeWithDecimals
+} = require("../utils/format-utils");
+
+const { button, div } = DOM;
+
+const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200;
+const BACK_BUTTON = L10N.getStr("netmonitor.backButton");
+const CHARTS_CACHE_ENABLED = L10N.getStr("charts.cacheEnabled");
+const CHARTS_CACHE_DISABLED = L10N.getStr("charts.cacheDisabled");
+
+/*
+ * Statistics panel component
+ * Performance analysis tool which shows you how long the browser takes to
+ * download the different parts of your site.
+ */
+const StatisticsPanel = createClass({
+  displayName: "StatisticsPanel",
+
+  propTypes: {
+    closeStatistics: PropTypes.func.isRequired,
+    enableRequestFilterTypeOnly: PropTypes.func.isRequired,
+    requests: PropTypes.object,
+  },
+
+  componentDidUpdate(prevProps) {
+    const { requests } = this.props;
+    let ready = requests && requests.every((req) =>
+      req.contentSize !== undefined && req.mimeType && req.responseHeaders &&
+      req.status !== undefined && req.totalTime !== undefined
+    );
+
+    this.createChart({
+      id: "primedCacheChart",
+      title: CHARTS_CACHE_ENABLED,
+      data: ready ? this.sanitizeChartDataSource(requests, false) : null,
+    });
+
+    this.createChart({
+      id: "emptyCacheChart",
+      title: CHARTS_CACHE_DISABLED,
+      data: ready ? this.sanitizeChartDataSource(requests, true) : null,
+    });
+  },
+
+  createChart({ id, title, data }) {
+    // Create a new chart.
+    let chart = Chart.PieTable(document, {
+      diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
+      title,
+      data,
+      strings: {
+        size: (value) =>
+          L10N.getFormatStr("charts.sizeKB", getSizeWithDecimals(value / 1024)),
+        time: (value) =>
+          L10N.getFormatStr("charts.totalS", getTimeWithDecimals(value / 1000)),
+      },
+      totals: {
+        cached: (total) => L10N.getFormatStr("charts.totalCached", total),
+        count: (total) => L10N.getFormatStr("charts.totalCount", total),
+        size: (total) =>
+          L10N.getFormatStr("charts.totalSize", getSizeWithDecimals(total / 1024)),
+        time: (total) => {
+          let seconds = total / 1000;
+          let string = getTimeWithDecimals(seconds);
+          return PluralForm.get(seconds,
+            L10N.getStr("charts.totalSeconds")).replace("#1", string);
+        },
+      },
+      sorted: true,
+    });
+
+    chart.on("click", (_, { label }) => {
+      // Reset FilterButtons and enable one filter exclusively
+      this.props.closeStatistics();
+      this.props.enableRequestFilterTypeOnly(label);
+    });
+
+    let container = this.refs[id];
+
+    // Nuke all existing charts of the specified type.
+    while (container.hasChildNodes()) {
+      container.firstChild.remove();
+    }
+
+    container.appendChild(chart.node);
+  },
+
+  sanitizeChartDataSource(requests, emptyCache) {
+    let data = [
+      "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "ws", "other"
+    ].map((type) => ({ cached: 0, count: 0, label: type, size: 0, time: 0 }));
+
+    for (let request of requests) {
+      let type;
+
+      if (Filters.html(request)) {
+        // "html"
+        type = 0;
+      } else if (Filters.css(request)) {
+        // "css"
+        type = 1;
+      } else if (Filters.js(request)) {
+        // "js"
+        type = 2;
+      } else if (Filters.fonts(request)) {
+        // "fonts"
+        type = 4;
+      } else if (Filters.images(request)) {
+        // "images"
+        type = 5;
+      } else if (Filters.media(request)) {
+        // "media"
+        type = 6;
+      } else if (Filters.flash(request)) {
+        // "flash"
+        type = 7;
+      } else if (Filters.ws(request)) {
+        // "ws"
+        type = 8;
+      } else if (Filters.xhr(request)) {
+        // Verify XHR last, to categorize other mime types in their own blobs.
+        // "xhr"
+        type = 3;
+      } else {
+        // "other"
+        type = 9;
+      }
+
+      if (emptyCache || !this.responseIsFresh(request)) {
+        data[type].time += request.totalTime || 0;
+        data[type].size += request.contentSize || 0;
+      } else {
+        data[type].cached++;
+      }
+      data[type].count++;
+    }
+
+    return data.filter(e => e.count > 0);
+  },
+
+  /**
+   * Checks if the "Expiration Calculations" defined in section 13.2.4 of the
+   * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers.
+   *
+   * @param object
+   *        An object containing the { responseHeaders, status } properties.
+   * @return boolean
+   *         True if the response is fresh and loaded from cache.
+   */
+  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");
+
+    // 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) {
+        return true;
+      }
+    }
+
+    // Check the "Expires" header for a valid date.
+    if (expires && Date.parse(expires.value)) {
+      return true;
+    }
+
+    return false;
+  },
+
+  render() {
+    const { closeStatistics } = this.props;
+    return (
+      div({ className: "statistics-panel" },
+        button({
+          className: "back-button devtools-toolbarbutton",
+          "data-text-only": "true",
+          title: BACK_BUTTON,
+          onClick: closeStatistics,
+        }, BACK_BUTTON),
+        div({ className: "charts-container devtools-responsive-container" },
+          div({ ref: "primedCacheChart", className: "charts primed-cache-chart" }),
+          div({ className: "splitter devtools-side-splitter" }),
+          div({ ref: "emptyCacheChart", className: "charts empty-cache-chart" }),
+        ),
+      )
+    );
+  }
+});
+
+module.exports = connect(
+  (state) => ({
+    requests: state.requests.requests.valueSeq(),
+  }),
+  (dispatch) => ({
+    closeStatistics: () => dispatch(Actions.openStatistics(false)),
+    enableRequestFilterTypeOnly: (label) =>
+      dispatch(Actions.enableRequestFilterTypeOnly(label)),
+  })
+)(StatisticsPanel);
--- a/devtools/client/netmonitor/moz.build
+++ b/devtools/client/netmonitor/moz.build
@@ -23,17 +23,16 @@ DevToolsModules(
     'netmonitor-view.js',
     'panel.js',
     'prefs.js',
     'request-list-context-menu.js',
     'request-utils.js',
     'requests-menu-view.js',
     'sidebar-view.js',
     'sort-predicates.js',
-    'statistics-view.js',
     'store.js',
     'waterfall-background.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Developer Tools: Netmonitor')
--- a/devtools/client/netmonitor/netmonitor-view.js
+++ b/devtools/client/netmonitor/netmonitor-view.js
@@ -1,45 +1,33 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* eslint-disable mozilla/reject-some-requires */
 /* globals $, gStore, NetMonitorController, dumpn */
 
 "use strict";
 
-const { testing: isTesting } = require("devtools/shared/flags");
 const { Task } = require("devtools/shared/task");
 const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers");
 const { RequestsMenuView } = require("./requests-menu-view");
 const { CustomRequestView } = require("./custom-request-view");
 const { SidebarView } = require("./sidebar-view");
-const { StatisticsView } = require("./statistics-view");
 const { ACTIVITY_TYPE } = require("./constants");
 const { Prefs } = require("./prefs");
 const { createFactory } = require("devtools/client/shared/vendor/react");
 const Actions = require("./actions/index");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
 
 // Components
 const DetailsPanel = createFactory(require("./shared/components/details-panel"));
+const StatisticsPanel = createFactory(require("./components/statistics-panel"));
 const Toolbar = createFactory(require("./components/toolbar"));
 
-// ms
-const WDA_DEFAULT_VERIFY_INTERVAL = 50;
-
-// Use longer timeout during testing as the tests need this process to succeed
-// and two seconds is quite short on slow debug builds. The timeout here should
-// be at least equal to the general mochitest timeout of 45 seconds so that this
-// never gets hit during testing.
-// ms
-const WDA_DEFAULT_GIVE_UP_TIMEOUT = isTesting ? 45000 : 2000;
-
 /**
  * Object defining the network monitor view components.
  */
 var NetMonitorView = {
   /**
    * Initializes the network monitor view.
    */
   initialize: function () {
@@ -47,26 +35,32 @@ var NetMonitorView = {
 
     this.detailsPanel = $("#react-details-panel-hook");
 
     ReactDOM.render(Provider(
       { store: gStore },
       DetailsPanel({ toolbox: NetMonitorController._toolbox }),
     ), this.detailsPanel);
 
+    this.statisticsPanel = $("#statistics-panel");
+
+    ReactDOM.render(Provider(
+      { store: gStore },
+      StatisticsPanel(),
+    ), this.statisticsPanel);
+
     this.toolbar = $("#react-toolbar-hook");
 
     ReactDOM.render(Provider(
       { store: gStore },
       Toolbar(),
     ), this.toolbar);
 
     this.RequestsMenu.initialize(gStore);
     this.CustomRequest.initialize();
-    this.Statistics.initialize(gStore);
 
     // Store watcher here is for observing the statisticsOpen state change.
     // It should be removed once we migrate to react and apply react/redex binding.
     this.unsubscribeStore = gStore.subscribe(storeWatcher(
       false,
       () => gStore.getState().ui.statisticsOpen,
       this.toggleFrontendMode.bind(this)
     ));
@@ -74,18 +68,18 @@ var NetMonitorView = {
 
   /**
    * Destroys the network monitor view.
    */
   destroy: function () {
     this._isDestroyed = true;
     this.RequestsMenu.destroy();
     this.CustomRequest.destroy();
-    this.Statistics.destroy();
     ReactDOM.unmountComponentAtNode(this.detailsPanel);
+    ReactDOM.unmountComponentAtNode(this.statisticsPanel);
     ReactDOM.unmountComponentAtNode(this.toolbar);
     this.unsubscribeStore();
 
     this._destroyPanes();
   },
 
   /**
    * Initializes the UI for all the displayed panes.
@@ -144,124 +138,50 @@ var NetMonitorView = {
       gStore.dispatch(Actions.openSidebar(false));
     }
 
     if (tabIndex !== undefined) {
       $("#event-details-pane").selectedIndex = tabIndex;
     }
   },
 
-  /**
-   * Gets the current mode for this tool.
-   * @return string (e.g, "network-inspector-view" or "network-statistics-view")
-   */
   get currentFrontendMode() {
     // The getter may be called from a timeout after the panel is destroyed.
     if (!this._body.selectedPanel) {
       return null;
     }
     return this._body.selectedPanel.id;
   },
 
-  /**
-   * Toggles between the frontend view modes ("Inspector" vs. "Statistics").
-   */
   toggleFrontendMode: function () {
     if (gStore.getState().ui.statisticsOpen) {
       this.showNetworkStatisticsView();
     } else {
       this.showNetworkInspectorView();
     }
   },
 
-  /**
-   * Switches to the "Inspector" frontend view mode.
-   */
   showNetworkInspectorView: function () {
-    this._body.selectedPanel = $("#network-inspector-view");
+    this._body.selectedPanel = $("#inspector-panel");
   },
 
-  /**
-   * Switches to the "Statistics" frontend view mode.
-   */
   showNetworkStatisticsView: function () {
-    this._body.selectedPanel = $("#network-statistics-view");
-
-    let controller = NetMonitorController;
-    let requestsView = this.RequestsMenu;
-    let statisticsView = this.Statistics;
-
-    Task.spawn(function* () {
-      statisticsView.displayPlaceholderCharts();
-      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, [
-          "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.valueSeq();
-      statisticsView.createPrimedCacheChart(requests);
-      statisticsView.createEmptyCacheChart(requests);
-    });
+    this._body.selectedPanel = $("#statistics-panel");
+    NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
   },
 
   reloadPage: function () {
     NetMonitorController.triggerActivity(
       ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT);
   },
 
   _body: null,
   _detailsPane: null,
 };
 
-/**
- * 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 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
-        )
-      );
-
-      if (allFieldsPresent) {
-        clearInterval(interval);
-        clearTimeout(timer);
-        resolve();
-      }
-    }, WDA_DEFAULT_VERIFY_INTERVAL);
-
-    let timer = setTimeout(() => {
-      clearInterval(interval);
-      reject(new Error("Timed out while waiting for data"));
-    }, WDA_DEFAULT_GIVE_UP_TIMEOUT);
-  });
-}
-
 // A smart store watcher to notify store changes as necessary
 function storeWatcher(initialValue, reduceValue, onChange) {
   let currentValue = initialValue;
 
   return () => {
     const newValue = reduceValue();
     if (newValue !== currentValue) {
       onChange();
@@ -271,11 +191,10 @@ function storeWatcher(initialValue, redu
 }
 
 /**
  * Preliminary setup for the NetMonitorView object.
  */
 NetMonitorView.Sidebar = new SidebarView();
 NetMonitorView.RequestsMenu = new RequestsMenuView();
 NetMonitorView.CustomRequest = new CustomRequestView();
-NetMonitorView.Statistics = new StatisticsView();
 
 exports.NetMonitorView = NetMonitorView;
--- a/devtools/client/netmonitor/netmonitor.xul
+++ b/devtools/client/netmonitor/netmonitor.xul
@@ -14,17 +14,17 @@
           src="chrome://devtools/content/shared/theme-switching.js"/>
   <script type="text/javascript" src="netmonitor.js"/>
 
   <deck id="body"
         class="theme-sidebar"
         flex="1"
         data-localization-bundle="devtools/client/locales/netmonitor.properties">
 
-    <vbox id="network-inspector-view" flex="1">
+    <vbox id="inspector-panel" 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">
@@ -96,25 +96,14 @@
           </vbox>
           <html:div xmlns="http://www.w3.org/1999/xhtml"
                     id="react-details-panel-hook"/>
         </deck>
       </hbox>
 
     </vbox>
 
-    <html:div id="network-statistics-view">
-      <html:div id="network-statistics-toolbar"
-                class="devtools-toolbar">
-        <html:div xmlns="http://www.w3.org/1999/xhtml"
-                  id="react-statistics-back-hook"/>
-      </html:div>
-      <html:div id="network-statistics-charts"
-                class="devtools-responsive-container">
-        <html:div id="primed-cache-chart"/>
-        <html:div id="network-statistics-view-splitter"
-                  class="split-box devtools-side-splitter"/>
-        <html:div id="empty-cache-chart"/>
-      </html:div>
+    <html:div xmlns="http://www.w3.org/1999/xhtml"
+              id="statistics-panel">
     </html:div>
   </deck>
 
 </window>
deleted file mode 100644
--- a/devtools/client/netmonitor/statistics-view.js
+++ /dev/null
@@ -1,288 +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/. */
-
-/* eslint-disable mozilla/reject-some-requires */
-/* globals $, window, document */
-
-"use strict";
-
-const { PluralForm } = require("devtools/shared/plural-form");
-const { Filters } = require("./filter-predicates");
-const { L10N } = require("./l10n");
-const { EVENTS } = require("./events");
-const { DOM } = require("devtools/client/shared/vendor/react");
-const { button } = DOM;
-const ReactDOM = require("devtools/client/shared/vendor/react-dom");
-const Actions = require("./actions/index");
-const { Chart } = require("devtools/client/shared/widgets/Chart");
-const {
-  getSizeWithDecimals,
-  getTimeWithDecimals
-} = require("./utils/format-utils");
-
-// px
-const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200;
-
-/**
- * Functions handling the performance statistics view.
- */
-function StatisticsView() {
-}
-
-StatisticsView.prototype = {
-  /**
-   * Initialization function, called when the statistics view is started.
-   */
-  initialize: function (store) {
-    this.store = store;
-    this.Chart = Chart;
-    this._backButton = $("#react-statistics-back-hook");
-
-    let backStr = L10N.getStr("netmonitor.backButton");
-    ReactDOM.render(button({
-      id: "network-statistics-back-button",
-      className: "devtools-toolbarbutton",
-      "data-text-only": "true",
-      title: backStr,
-      onClick: () => {
-        this.store.dispatch(Actions.openStatistics(false));
-      },
-    }, backStr), this._backButton);
-  },
-
-  /**
-    * Destruction function, called when the statistics view is closed.
-    */
-  destroy: function () {
-    ReactDOM.unmountComponentAtNode(this._backButton);
-  },
-
-  /**
-   * Initializes and displays empty charts in this container.
-   */
-  displayPlaceholderCharts: function () {
-    this._createChart({
-      id: "#primed-cache-chart",
-      title: "charts.cacheEnabled"
-    });
-    this._createChart({
-      id: "#empty-cache-chart",
-      title: "charts.cacheDisabled"
-    });
-    window.emit(EVENTS.PLACEHOLDER_CHARTS_DISPLAYED);
-  },
-
-  /**
-   * Populates and displays the primed cache chart in this container.
-   *
-   * @param array items
-   *        @see this._sanitizeChartDataSource
-   */
-  createPrimedCacheChart: function (items) {
-    this._createChart({
-      id: "#primed-cache-chart",
-      title: "charts.cacheEnabled",
-      data: this._sanitizeChartDataSource(items),
-      strings: this._commonChartStrings,
-      totals: this._commonChartTotals,
-      sorted: true
-    });
-    window.emit(EVENTS.PRIMED_CACHE_CHART_DISPLAYED);
-  },
-
-  /**
-   * Populates and displays the empty cache chart in this container.
-   *
-   * @param array items
-   *        @see this._sanitizeChartDataSource
-   */
-  createEmptyCacheChart: function (items) {
-    this._createChart({
-      id: "#empty-cache-chart",
-      title: "charts.cacheDisabled",
-      data: this._sanitizeChartDataSource(items, true),
-      strings: this._commonChartStrings,
-      totals: this._commonChartTotals,
-      sorted: true
-    });
-    window.emit(EVENTS.EMPTY_CACHE_CHART_DISPLAYED);
-  },
-
-  /**
-   * Common stringifier predicates used for items and totals in both the
-   * "primed" and "empty" cache charts.
-   */
-  _commonChartStrings: {
-    size: value => {
-      let string = getSizeWithDecimals(value / 1024);
-      return L10N.getFormatStr("charts.sizeKB", string);
-    },
-    time: value => {
-      let string = getTimeWithDecimals(value / 1000);
-      return L10N.getFormatStr("charts.totalS", string);
-    }
-  },
-  _commonChartTotals: {
-    size: total => {
-      let string = getSizeWithDecimals(total / 1024);
-      return L10N.getFormatStr("charts.totalSize", string);
-    },
-    time: total => {
-      let seconds = total / 1000;
-      let string = getTimeWithDecimals(seconds);
-      return PluralForm.get(seconds,
-        L10N.getStr("charts.totalSeconds")).replace("#1", string);
-    },
-    cached: total => {
-      return L10N.getFormatStr("charts.totalCached", total);
-    },
-    count: total => {
-      return L10N.getFormatStr("charts.totalCount", total);
-    }
-  },
-
-  /**
-   * Adds a specific chart to this container.
-   *
-   * @param object
-   *        An object containing all or some the following properties:
-   *          - id: either "#primed-cache-chart" or "#empty-cache-chart"
-   *          - title/data/strings/totals/sorted: @see Chart.js for details
-   */
-  _createChart: function ({ id, title, data, strings, totals, sorted }) {
-    let container = $(id);
-
-    // Nuke all existing charts of the specified type.
-    while (container.hasChildNodes()) {
-      container.firstChild.remove();
-    }
-
-    // Create a new chart.
-    let chart = this.Chart.PieTable(document, {
-      diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
-      title: L10N.getStr(title),
-      data: data,
-      strings: strings,
-      totals: totals,
-      sorted: sorted
-    });
-
-    chart.on("click", (_, item) => {
-      // Reset FilterButtons and enable one filter exclusively
-      this.store.dispatch(Actions.enableRequestFilterTypeOnly(item.label));
-      this.store.dispatch(Actions.openStatistics(false));
-    });
-
-    container.appendChild(chart.node);
-  },
-
-  /**
-   * Sanitizes the data source used for creating charts, to follow the
-   * data format spec defined in Chart.js.
-   *
-   * @param array items
-   *        A collection of request items used as the data source for the chart.
-   * @param boolean emptyCache
-   *        True if the cache is considered enabled, false for disabled.
-   */
-  _sanitizeChartDataSource: function (items, emptyCache) {
-    let data = [
-      "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "ws", "other"
-    ].map(e => ({
-      cached: 0,
-      count: 0,
-      label: e,
-      size: 0,
-      time: 0
-    }));
-
-    for (let requestItem of items) {
-      let details = requestItem;
-      let type;
-
-      if (Filters.html(details)) {
-        // "html"
-        type = 0;
-      } else if (Filters.css(details)) {
-        // "css"
-        type = 1;
-      } else if (Filters.js(details)) {
-        // "js"
-        type = 2;
-      } else if (Filters.fonts(details)) {
-        // "fonts"
-        type = 4;
-      } else if (Filters.images(details)) {
-        // "images"
-        type = 5;
-      } else if (Filters.media(details)) {
-        // "media"
-        type = 6;
-      } else if (Filters.flash(details)) {
-        // "flash"
-        type = 7;
-      } else if (Filters.ws(details)) {
-        // "ws"
-        type = 8;
-      } else if (Filters.xhr(details)) {
-        // Verify XHR last, to categorize other mime types in their own blobs.
-        // "xhr"
-        type = 3;
-      } else {
-        // "other"
-        type = 9;
-      }
-
-      if (emptyCache || !responseIsFresh(details)) {
-        data[type].time += details.totalTime || 0;
-        data[type].size += details.contentSize || 0;
-      } else {
-        data[type].cached++;
-      }
-      data[type].count++;
-    }
-
-    return data.filter(e => e.count > 0);
-  },
-};
-
-/**
- * Checks if the "Expiration Calculations" defined in section 13.2.4 of the
- * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers.
- *
- * @param object
- *        An object containing the { responseHeaders, status } properties.
- * @return boolean
- *         True if the response is fresh and loaded from cache.
- */
-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");
-
-  // 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) {
-      return true;
-    }
-  }
-
-  // Check the "Expires" header for a valid date.
-  if (expires && Date.parse(expires.value)) {
-    return true;
-  }
-
-  return false;
-}
-
-exports.StatisticsView = StatisticsView;
--- a/devtools/client/netmonitor/test/browser_net_charts-01.js
+++ b/devtools/client/netmonitor/test/browser_net_charts-01.js
@@ -6,18 +6,18 @@
 /**
  * Makes sure Pie Charts have the right internal structure.
  */
 
 add_task(function* () {
   let { monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { document, NetMonitorView } = monitor.panelWin;
-  const { Chart } = NetMonitorView.Statistics;
+  let { document, windowRequire } = monitor.panelWin;
+  let { Chart } = windowRequire("devtools/client/shared/widgets/Chart");
 
   let pie = Chart.Pie(document, {
     width: 100,
     height: 100,
     data: [{
       size: 1,
       label: "foo"
     }, {
--- a/devtools/client/netmonitor/test/browser_net_charts-02.js
+++ b/devtools/client/netmonitor/test/browser_net_charts-02.js
@@ -9,18 +9,18 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { document, NetMonitorView } = monitor.panelWin;
-  let { Chart } = NetMonitorView.Statistics;
+  let { document, windowRequire } = monitor.panelWin;
+  let { Chart } = windowRequire("devtools/client/shared/widgets/Chart");
 
   let pie = Chart.Pie(document, {
     data: null,
     width: 100,
     height: 100
   });
 
   let node = pie.node;
--- a/devtools/client/netmonitor/test/browser_net_charts-03.js
+++ b/devtools/client/netmonitor/test/browser_net_charts-03.js
@@ -8,18 +8,18 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { document, NetMonitorView } = monitor.panelWin;
-  let { Chart } = NetMonitorView.Statistics;
+  let { document, windowRequire } = monitor.panelWin;
+  let { Chart } = windowRequire("devtools/client/shared/widgets/Chart");
 
   let table = Chart.Table(document, {
     title: "Table title",
     data: [{
       label1: 1,
       label2: 11.1
     }, {
       label1: 2,
--- a/devtools/client/netmonitor/test/browser_net_charts-04.js
+++ b/devtools/client/netmonitor/test/browser_net_charts-04.js
@@ -9,18 +9,18 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { document, NetMonitorView } = monitor.panelWin;
-  let { Chart } = NetMonitorView.Statistics;
+  let { document, windowRequire } = monitor.panelWin;
+  let { Chart } = windowRequire("devtools/client/shared/widgets/Chart");
 
   let table = Chart.Table(document, {
     title: "Table title",
     data: null,
     totals: {
       label1: value => "Hello " + L10N.numberWithDecimals(value, 2),
       label2: value => "World " + L10N.numberWithDecimals(value, 2)
     }
--- a/devtools/client/netmonitor/test/browser_net_charts-05.js
+++ b/devtools/client/netmonitor/test/browser_net_charts-05.js
@@ -8,18 +8,18 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { document, NetMonitorView } = monitor.panelWin;
-  let { Chart } = NetMonitorView.Statistics;
+  let { document, windowRequire } = monitor.panelWin;
+  let { Chart } = windowRequire("devtools/client/shared/widgets/Chart");
 
   let chart = Chart.PieTable(document, {
     title: "Table title",
     data: [{
       size: 1,
       label: 11.1
     }, {
       size: 2,
--- a/devtools/client/netmonitor/test/browser_net_charts-06.js
+++ b/devtools/client/netmonitor/test/browser_net_charts-06.js
@@ -8,18 +8,18 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { document, NetMonitorView } = monitor.panelWin;
-  let { Chart } = NetMonitorView.Statistics;
+  let { document, windowRequire } = monitor.panelWin;
+  let { Chart } = windowRequire("devtools/client/shared/widgets/Chart");
 
   let pie = Chart.Pie(document, {
     data: [],
     width: 100,
     height: 100
   });
 
   let node = pie.node;
--- a/devtools/client/netmonitor/test/browser_net_charts-07.js
+++ b/devtools/client/netmonitor/test/browser_net_charts-07.js
@@ -8,18 +8,18 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  let { document, NetMonitorView } = monitor.panelWin;
-  let { Chart } = NetMonitorView.Statistics;
+  let { document, windowRequire } = monitor.panelWin;
+  let { Chart } = windowRequire("devtools/client/shared/widgets/Chart");
 
   let table = Chart.Table(document, {
     data: [],
     totals: {
       label1: value => "Hello " + L10N.numberWithDecimals(value, 2),
       label2: value => "World " + L10N.numberWithDecimals(value, 2)
     }
   });
--- a/devtools/client/netmonitor/test/browser_net_statistics-01.js
+++ b/devtools/client/netmonitor/test/browser_net_statistics-01.js
@@ -7,59 +7,50 @@
  * Tests if the statistics view is populated correctly.
  */
 
 add_task(function* () {
   let { monitor } = yield initNetMonitor(STATISTICS_URL);
   info("Starting test... ");
 
   let panel = monitor.panelWin;
-  let { $, $all, EVENTS, NetMonitorView, gStore, windowRequire } = panel;
+  let { document, gStore, windowRequire } = panel;
   let Actions = windowRequire("devtools/client/netmonitor/actions/index");
 
-  is(NetMonitorView.currentFrontendMode, "network-inspector-view",
-   "The initial frontend mode is correct.");
+  let body = document.querySelector("#body");
 
-  is($("#primed-cache-chart").childNodes.length, 0,
-    "There should be no primed cache chart created yet.");
-  is($("#empty-cache-chart").childNodes.length, 0,
-    "There should be no empty cache chart created yet.");
-
-  let onChartDisplayed = Promise.all([
-    panel.once(EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
-    panel.once(EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
-  ]);
-  let onPlaceholderDisplayed = panel.once(EVENTS.PLACEHOLDER_CHARTS_DISPLAYED);
+  is(body.selectedPanel.id, "inspector-panel",
+    "The current main panel is correct.");
 
   info("Displaying statistics view");
   gStore.dispatch(Actions.openStatistics(true));
-  is(NetMonitorView.currentFrontendMode, "network-statistics-view",
-   "The current frontend mode is correct.");
+  is(body.selectedPanel.id, "statistics-panel",
+    "The current main panel is correct.");
 
   info("Waiting for placeholder to display");
-  yield onPlaceholderDisplayed;
-  is($("#primed-cache-chart").childNodes.length, 1,
+
+  is(document.querySelector(".primed-cache-chart").childNodes.length, 1,
     "There should be a placeholder primed cache chart created now.");
-  is($("#empty-cache-chart").childNodes.length, 1,
+  is(document.querySelector(".empty-cache-chart").childNodes.length, 1,
     "There should be a placeholder empty cache chart created now.");
 
-  is($all(".pie-chart-container[placeholder=true]").length, 2,
+  is(document.querySelectorAll(".pie-chart-container[placeholder=true]").length, 2,
     "Two placeholder pie chart appear to be rendered correctly.");
-  is($all(".table-chart-container[placeholder=true]").length, 2,
+  is(document.querySelectorAll(".table-chart-container[placeholder=true]").length, 2,
     "Two placeholder table chart appear to be rendered correctly.");
 
   info("Waiting for chart to display");
-  yield onChartDisplayed;
-  is($("#primed-cache-chart").childNodes.length, 1,
+
+  is(document.querySelector(".primed-cache-chart").childNodes.length, 1,
     "There should be a real primed cache chart created now.");
-  is($("#empty-cache-chart").childNodes.length, 1,
+  is(document.querySelector(".empty-cache-chart").childNodes.length, 1,
     "There should be a real empty cache chart created now.");
 
   yield waitUntil(
-    () => $all(".pie-chart-container:not([placeholder=true])").length == 2);
+    () => document.querySelectorAll(".pie-chart-container:not([placeholder=true])").length == 2);
   ok(true, "Two real pie charts appear to be rendered correctly.");
 
   yield waitUntil(
-    () => $all(".table-chart-container:not([placeholder=true])").length == 2);
+    () => document.querySelectorAll(".table-chart-container:not([placeholder=true])").length == 2);
   ok(true, "Two real table charts appear to be rendered correctly.");
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_statistics-02.js
+++ b/devtools/client/netmonitor/test/browser_net_statistics-02.js
@@ -8,39 +8,46 @@
  * the performance analysis view.
  */
 
 add_task(function* () {
   let { monitor } = yield initNetMonitor(FILTERING_URL);
   info("Starting test... ");
 
   let panel = monitor.panelWin;
-  let { $, $all, EVENTS, NetMonitorView, gStore, windowRequire } = panel;
+  let { document, gStore, windowRequire } = panel;
   let Actions = windowRequire("devtools/client/netmonitor/actions/index");
 
-  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"));
+  EventUtils.sendMouseEvent({ type: "click" },
+    document.querySelector("#requests-menu-filter-html-button"));
+  EventUtils.sendMouseEvent({ type: "click" },
+    document.querySelector("#requests-menu-filter-css-button"));
+  EventUtils.sendMouseEvent({ type: "click" },
+    document.querySelector("#requests-menu-filter-js-button"));
+  EventUtils.sendMouseEvent({ type: "click" },
+    document.querySelector("#requests-menu-filter-ws-button"));
+  EventUtils.sendMouseEvent({ type: "click" },
+    document.querySelector("#requests-menu-filter-other-button"));
   testFilterButtonsCustom(monitor, [0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1]);
   info("The correct filtering predicates are used before entering perf. analysis mode.");
 
-  let onEvents = promise.all([
-    panel.once(EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
-    panel.once(EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
-  ]);
   gStore.dispatch(Actions.openStatistics(true));
-  yield onEvents;
+
+  let body = document.querySelector("#body");
+
+  is(body.selectedPanel.id, "statistics-panel",
+    "The main panel is switched to the statistics panel.");
 
-  is(NetMonitorView.currentFrontendMode, "network-statistics-view",
-    "The frontend mode is switched to the statistics view.");
+  yield waitUntil(
+    () => document.querySelectorAll(".pie-chart-container:not([placeholder=true])").length == 2);
+  ok(true, "Two real pie charts appear to be rendered correctly.");
 
-  EventUtils.sendMouseEvent({ type: "click" }, $(".pie-chart-slice"));
+  EventUtils.sendMouseEvent({ type: "click" },
+    document.querySelector(".pie-chart-slice"));
 
-  is(NetMonitorView.currentFrontendMode, "network-inspector-view",
-    "The frontend mode is switched back to the inspector view.");
+  is(body.selectedPanel.id, "inspector-panel",
+    "The main panel is switched back to the inspector panel.");
 
   testFilterButtons(monitor, "html");
   info("The correct filtering predicate is used when exiting perf. analysis mode.");
 
   yield teardown(monitor);
 });
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -102,23 +102,16 @@
   --timing-send-color: rgba(70, 175, 227, 0.8); /* light blue */
   --timing-wait-color: rgba(94, 136, 176, 0.8); /* blue grey */
   --timing-receive-color: rgba(112, 191, 83, 0.8); /* green */
 
   --sort-ascending-image: url(chrome://devtools/skin/images/firebug/arrow-up.svg);
   --sort-descending-image: url(chrome://devtools/skin/images/firebug/arrow-down.svg);
 }
 
-#network-table {
-  display: -moz-box;
-  -moz-box-orient: vertical;
-  -moz-box-flex: 1;
-  overflow: hidden;
-}
-
 .request-list-container {
   display: -moz-box;
   -moz-box-orient: vertical;
   -moz-box-flex: 1;
 }
 
 .request-list-empty-notice {
   margin: 0;
@@ -840,57 +833,55 @@
 
 #requests-menu-network-summary-button:hover > .summary-info-icon,
 #requests-menu-network-summary-button:hover > .summary-info-text {
   opacity: 1;
 }
 
 /* Performance analysis view */
 
-#network-statistics-view {
-  display: -moz-box;
+.statistics-panel {
+  display: flex;
+  height: 100vh;
 }
 
-#network-statistics-toolbar {
-  border: none;
-  margin: 0;
-  padding: 0;
-}
-
-#network-statistics-back-button {
+.statistics-panel .devtools-toolbarbutton.back-button {
   min-width: 4em;
-  min-height: 100vh;
   margin: 0;
   padding: 0;
   border-radius: 0;
   border-top: none;
   border-bottom: none;
   border-inline-start: none;
 }
 
-#network-statistics-view-splitter {
+.statistics-panel .splitter {
   border-color: rgba(0,0,0,0.2);
   cursor: default;
   pointer-events: none;
+  height: 100vh;
 }
 
-#network-statistics-charts {
-  min-height: 1px;
+.statistics-panel .charts-container {
+  display: flex;
+  width: 100%;
 }
 
-#network-statistics-charts {
-  background-color: var(--theme-sidebar-background);
+.statistics-panel .charts,
+.statistics-panel .pie-table-chart-container {
+  width: 100%;
+  height: 100%;
 }
 
-#network-statistics-charts .pie-chart-container {
+.statistics-panel .pie-chart-container {
   margin-inline-start: 3vw;
   margin-inline-end: 1vw;
 }
 
-#network-statistics-charts .table-chart-container {
+.statistics-panel .table-chart-container {
   margin-inline-start: 1vw;
   margin-inline-end: 3vw;
 }
 
 .chart-colored-blob[name=html] {
   fill: var(--theme-highlight-bluegrey);
   background: var(--theme-highlight-bluegrey);
 }
@@ -1045,16 +1036,25 @@
   .requests-menu-size {
     max-width: none;
     width: 10vw;
   }
 
   .requests-menu-waterfall {
     display: none;
   }
+
+  .statistics-panel .charts-container {
+    flex-direction: column;
+  }
+
+  .statistics-panel .splitter {
+    width: 100vw;
+    height: 0;
+  }
 }
 
 /* Platform overrides (copied in from the old platform specific files) */
 :root[platform="win"] .requests-menu-header-button > .button-box {
   padding: 0;
 }
 
 :root[platform="win"] .requests-menu-timings-division {
@@ -1285,20 +1285,26 @@
   width: 100%;
   height: 100%;
 }
 
 /*
  * FIXME: normal html block element cannot fill outer XUL element
  * This workaround should be removed after netmonitor is migrated to react
  */
+#network-table {
+  display: -moz-box;
+  -moz-box-orient: vertical;
+  -moz-box-flex: 1;
+  overflow: hidden;
+}
 
+#statistics-panel,
 #react-details-panel-hook {
   display: flex;
   flex-direction: column;
 }
 
-#network-statistics-charts,
 #primed-cache-chart,
 #empty-cache-chart {
   display: -moz-box;
   -moz-box-flex: 1;
 }