merge m-c into fx-team
authorGavin Sharp <gavin@gavinsharp.com>
Fri, 31 Jan 2014 22:42:02 -0800
changeset 182458 b8b35ef1ea88e46d3b618f59657b3b005cf67c37
parent 182441 8840c133115a9c261fddfcfa91b8ff64ef92363d (current diff)
parent 182457 b3a41a5f1972abd1e0b0b1afb61759e811c25637 (diff)
child 182459 6a6aaf1887feae3e70647d3b5eff32e69af92a8d
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge m-c into fx-team
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1136,16 +1136,17 @@ pref("devtools.profiler.enabled", true);
 pref("devtools.profiler.ui.show-platform-data", false);
 
 // Enable the Network Monitor
 pref("devtools.netmonitor.enabled", true);
 
 // The default Network Monitor UI settings
 pref("devtools.netmonitor.panes-network-details-width", 450);
 pref("devtools.netmonitor.panes-network-details-height", 450);
+pref("devtools.netmonitor.statistics", true);
 
 // Enable the Tilt inspector
 pref("devtools.tilt.enabled", true);
 pref("devtools.tilt.intro_transition", true);
 pref("devtools.tilt.outro_transition", true);
 
 // Scratchpad settings
 // - recentFileMax: The maximum number of recently-opened files
--- a/browser/devtools/netmonitor/netmonitor-controller.js
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -52,45 +52,74 @@ const EVENTS = {
   RECEIVED_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdated:ResponseContent",
 
   // When the request post params are displayed in the UI.
   REQUEST_POST_PARAMS_DISPLAYED: "NetMonitor:RequestPostParamsAvailable",
 
   // When the response body is displayed in the UI.
   RESPONSE_BODY_DISPLAYED: "NetMonitor:ResponseBodyAvailable",
 
-  // When `onTabSelect` is fired and subsequently rendered
+  // When `onTabSelect` is fired and subsequently rendered.
   TAB_UPDATED: "NetMonitor:TabUpdated",
 
-  // Fired when Sidebar is finished being populated
+  // Fired when Sidebar has finished being populated.
   SIDEBAR_POPULATED: "NetMonitor:SidebarPopulated",
 
-  // Fired when NetworkDetailsView is finished being populated
+  // Fired when NetworkDetailsView has finished being populated.
   NETWORKDETAILSVIEW_POPULATED: "NetMonitor:NetworkDetailsViewPopulated",
 
-  // Fired when NetworkDetailsView is finished being populated
-  CUSTOMREQUESTVIEW_POPULATED: "NetMonitor:CustomRequestViewPopulated"
+  // Fired when CustomRequestView has finished being populated.
+  CUSTOMREQUESTVIEW_POPULATED: "NetMonitor:CustomRequestViewPopulated",
+
+  // Fired when charts have been displayed in the PerformanceStatisticsView.
+  PLACEHOLDER_CHARTS_DISPLAYED: "NetMonitor:PlaceholderChartsDisplayed",
+  PRIMED_CACHE_CHART_DISPLAYED: "NetMonitor:PrimedChartsDisplayed",
+  EMPTY_CACHE_CHART_DISPLAYED: "NetMonitor:EmptyChartsDisplayed"
+};
+
+// Descriptions for what this frontend is currently doing.
+const ACTIVITY_TYPE = {
+  // Standing by and handling requests normally.
+  NONE: 0,
+
+  // Forcing the target to reload with cache enabled or disabled.
+  RELOAD: {
+    WITH_CACHE_ENABLED: 1,
+    WITH_CACHE_DISABLED: 2
+  },
+
+  // Enabling or disabling the cache without triggering a reload.
+  ENABLE_CACHE: 3,
+  DISABLE_CACHE: 4
 };
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
 Cu.import("resource:///modules/devtools/VariablesView.jsm");
 Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
-const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 
 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+const EventEmitter = require("devtools/shared/event-emitter");
 const Editor = require("devtools/sourceeditor/editor");
 
+XPCOMUtils.defineLazyModuleGetter(this, "Chart",
+  "resource:///modules/devtools/Chart.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
   "resource://gre/modules/PluralForm.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils",
+  "resource://gre/modules/devtools/DevToolsUtils.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
   "resource://gre/modules/devtools/Loader.jsm");
 
 Object.defineProperty(this, "NetworkHelper", {
   get: function() {
     return devtools.require("devtools/toolkit/webconsole/network-helper");
   },
   configurable: true,
@@ -250,19 +279,91 @@ let NetMonitorController = {
 
         if (aCallback) {
           aCallback();
         }
       });
     });
   },
 
+  /**
+   * Gets the activity currently performed by the frontend.
+   * @return number
+   */
+  getCurrentActivity: function() {
+    return this._currentActivity || ACTIVITY_TYPE.NONE;
+  },
+
+  /**
+   * Triggers a specific "activity" to be performed by the frontend. This can be,
+   * for example, triggering reloads or enabling/disabling cache.
+   *
+   * @param number aType
+   *        The activity type. See the ACTIVITY_TYPE const.
+   * @return object
+   *         A promise resolved once the activity finishes and the frontend
+   *         is back into "standby" mode.
+   */
+  triggerActivity: function(aType) {
+    // Puts the frontend into "standby" (when there's no particular activity).
+    let standBy = () => {
+      this._currentActivity = ACTIVITY_TYPE.NONE;
+    };
+
+    // Waits for a series of "navigation start" and "navigation stop" events.
+    let waitForNavigation = () => {
+      let deferred = promise.defer();
+      this._target.once("will-navigate", () => {
+        this._target.once("navigate", () => {
+          deferred.resolve();
+        });
+      });
+      return deferred.promise;
+    };
+
+    // Reconfigures the tab, optionally triggering a reload.
+    let reconfigureTab = aOptions => {
+      let deferred = promise.defer();
+      this._target.activeTab.reconfigure(aOptions, deferred.resolve);
+      return deferred.promise;
+    };
+
+    // Reconfigures the tab and waits for the target to finish navigating.
+    let reconfigureTabAndWaitForNavigation = aOptions => {
+      aOptions.performReload = true;
+      let navigationFinished = waitForNavigation();
+      return reconfigureTab(aOptions).then(() => navigationFinished);
+    }
+
+    if (aType == ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED) {
+      this._currentActivity = ACTIVITY_TYPE.ENABLE_CACHE;
+      this._target.once("will-navigate", () => this._currentActivity = aType);
+      return reconfigureTabAndWaitForNavigation({ cacheEnabled: true }).then(standBy);
+    }
+    if (aType == ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED) {
+      this._currentActivity = ACTIVITY_TYPE.DISABLE_CACHE;
+      this._target.once("will-navigate", () => this._currentActivity = aType);
+      return reconfigureTabAndWaitForNavigation({ cacheEnabled: false }).then(standBy);
+    }
+    if (aType == ACTIVITY_TYPE.ENABLE_CACHE) {
+      this._currentActivity = aType;
+      return reconfigureTab({ cacheEnabled: true, performReload: false }).then(standBy);
+    }
+    if (aType == ACTIVITY_TYPE.DISABLE_CACHE) {
+      this._currentActivity = aType;
+      return reconfigureTab({ cacheEnabled: false, performReload: false }).then(standBy);
+    }
+    this._currentActivity = ACTIVITY_TYPE.NONE;
+    return promise.reject(new Error("Invalid activity type"));
+  },
+
   _startup: null,
   _shutdown: null,
   _connection: null,
+  _currentActivity: null,
   client: null,
   tabClient: null,
   webConsoleClient: null
 };
 
 /**
  * Functions handling target-related lifetime events.
  */
@@ -309,16 +410,21 @@ TargetEventsHandler.prototype = {
   _onTabNavigated: function(aType, aPacket) {
     switch (aType) {
       case "will-navigate": {
         // Reset UI.
         NetMonitorView.RequestsMenu.reset();
         NetMonitorView.Sidebar.reset();
         NetMonitorView.NetworkDetails.reset();
 
+        // Switch to the default network traffic inspector view.
+        if (NetMonitorController.getCurrentActivity() == ACTIVITY_TYPE.NONE) {
+          NetMonitorView.showNetworkInspectorView();
+        }
+
         window.emit(EVENTS.TARGET_WILL_NAVIGATE);
         break;
       }
       case "navigate": {
         window.emit(EVENTS.TARGET_DID_NAVIGATE);
         break;
       }
     }
@@ -378,17 +484,16 @@ NetworkEventsHandler.prototype = {
    * @param string aType
    *        Message type.
    * @param object aPacket
    *        The message received from the server.
    */
   _onNetworkEvent: function(aType, aPacket) {
     let { actor, startedDateTime, method, url, isXHR } = aPacket.eventActor;
     NetMonitorView.RequestsMenu.addRequest(actor, startedDateTime, method, url, isXHR);
-
     window.emit(EVENTS.NETWORK_EVENT);
   },
 
   /**
    * The "networkEventUpdate" message type handler.
    *
    * @param string aType
    *        Message type.
@@ -580,17 +685,18 @@ NetworkEventsHandler.prototype = {
  */
 let L10N = new ViewHelpers.L10N(NET_STRINGS_URI);
 
 /**
  * Shortcuts for accessing various network monitor preferences.
  */
 let Prefs = new ViewHelpers.Prefs("devtools.netmonitor", {
   networkDetailsWidth: ["Int", "panes-network-details-width"],
-  networkDetailsHeight: ["Int", "panes-network-details-height"]
+  networkDetailsHeight: ["Int", "panes-network-details-height"],
+  statistics: ["Bool", "statistics"]
 });
 
 /**
  * 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";
@@ -612,16 +718,49 @@ NetMonitorController.NetworkEventsHandle
  */
 Object.defineProperties(window, {
   "gNetwork": {
     get: function() NetMonitorController.NetworkEventsHandler
   }
 });
 
 /**
+ * Makes sure certain properties are available on all objects in a data store.
+ *
+ * @param array aDataStore
+ *        A list of objects for which to check the availability of properties.
+ * @param array aMandatoryFields
+ *        A list of strings representing properties of objects in aDataStore.
+ * @return object
+ *         A promise resolved when all objects in aDataStore contain the
+ *         properties defined in aMandatoryFields.
+ */
+function whenDataAvailable(aDataStore, aMandatoryFields) {
+  let deferred = promise.defer();
+
+  let interval = setInterval(() => {
+    if (aDataStore.every(item => aMandatoryFields.every(field => field in item))) {
+      clearInterval(interval);
+      clearTimeout(timer);
+      deferred.resolve();
+    }
+  }, WDA_DEFAULT_VERIFY_INTERVAL);
+
+  let timer = setTimeout(() => {
+    clearInterval(interval);
+    deferred.reject(new Error("Timed out while waiting for data"));
+  }, WDA_DEFAULT_GIVE_UP_TIMEOUT);
+
+  return deferred.promise;
+};
+
+const WDA_DEFAULT_VERIFY_INTERVAL = 50; // ms
+const WDA_DEFAULT_GIVE_UP_TIMEOUT = 2000; // ms
+
+/**
  * Helper method for debugging.
  * @param string
  */
 function dumpn(str) {
   if (wantLogging) {
     dump("NET-FRONTEND: " + str + "\n");
   }
 }
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -16,16 +16,17 @@ const REQUESTS_WATERFALL_HEADER_TICKS_MU
 const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
 const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
 const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3;
 const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
 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; // byte
 const DEFAULT_HTTP_VERSION = "HTTP/1.1";
+const REQUEST_TIME_DECIMALS = 2;
 const HEADERS_SIZE_DECIMALS = 3;
 const CONTENT_SIZE_DECIMALS = 2;
 const CONTENT_MIME_TYPE_ABBREVIATIONS = {
   "ecmascript": "js",
   "javascript": "js",
   "x-javascript": "js"
 };
 const CONTENT_MIME_TYPE_MAPPINGS = {
@@ -52,16 +53,17 @@ const GENERIC_VARIABLES_VIEW_SETTINGS = 
   searchEnabled: true,
   editableValueTooltip: "",
   editableNameTooltip: "",
   preventDisableOnChange: true,
   preventDescriptorModifiers: true,
   eval: () => {},
   switch: () => {}
 };
+const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200; // px
 
 /**
  * Object defining the network monitor view components.
  */
 let NetMonitorView = {
   /**
    * Initializes the network monitor view.
    */
@@ -97,16 +99,24 @@ let NetMonitorView = {
     this._detailsPaneToggleButton = $("#details-pane-toggle");
 
     this._collapsePaneString = L10N.getStr("collapseDetailsPane");
     this._expandPaneString = L10N.getStr("expandDetailsPane");
 
     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;
+      $("#requests-menu-network-summary-button").hidden = true;
+      $("#requests-menu-network-summary-label").hidden = true;
+    }
   },
 
   /**
    * Destroys the UI for all the displayed panes.
    */
   _destroyPanes: function() {
     dumpn("Destroying the NetMonitorView panes");
 
@@ -116,18 +126,19 @@ let NetMonitorView = {
     this._detailsPane = null;
     this._detailsPaneToggleButton = null;
   },
 
   /**
    * Gets the visibility state of the network details pane.
    * @return boolean
    */
-  get detailsPaneHidden()
-    this._detailsPane.hasAttribute("pane-collapsed"),
+  get detailsPaneHidden() {
+    return this._detailsPane.hasAttribute("pane-collapsed");
+  },
 
   /**
    * Sets the network details pane hidden or visible.
    *
    * @param object aFlags
    *        An object containing some of the following properties:
    *        - visible: true if the pane should be shown, false to hide
    *        - animated: true to display an animation on toggle
@@ -153,16 +164,76 @@ let NetMonitorView = {
     }
 
     if (aTabIndex !== undefined) {
       $("#event-details-pane").selectedIndex = aTabIndex;
     }
   },
 
   /**
+   * Gets the current mode for this tool.
+   * @return string (e.g, "network-inspector-view" or "network-statistics-view")
+   */
+  get currentFrontendMode() {
+    return this._body.selectedPanel.id;
+  },
+
+  /**
+   * Toggles between the frontend view modes ("Inspector" vs. "Statistics").
+   */
+  toggleFrontendMode: function() {
+    if (this.currentFrontendMode != "network-inspector-view") {
+      this.showNetworkInspectorView();
+    } else {
+      this.showNetworkStatisticsView();
+    }
+  },
+
+  /**
+   * 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");
+
+    let controller = NetMonitorController;
+    let requestsView = this.RequestsMenu;
+    let statisticsView = this.PerformanceStatistics;
+
+    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.attachments, [
+          "responseHeaders", "status", "contentSize", "mimeType", "totalTime"
+        ]);
+      } catch (ex) {
+        // Timed out while waiting for data. Continue with what we have.
+        DevToolsUtils.reportException("showNetworkStatisticsView", ex);
+      }
+
+      statisticsView.createPrimedCacheChart(requestsView.items);
+      statisticsView.createEmptyCacheChart(requestsView.items);
+    });
+  },
+
+  /**
    * Lazily initializes and returns a promise for a Editor instance.
    *
    * @param string aId
    *        The id of the editor placeholder node.
    * @return object
    *         A promise that is resolved when the editor is available.
    */
   editor: function(aId) {
@@ -258,46 +329,53 @@ function RequestsMenuView() {
 RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
   /**
    * Initialization function, called when the network monitor is started.
    */
   initialize: function() {
     dumpn("Initializing the RequestsMenuView");
 
     this.widget = new SideMenuWidget($("#requests-menu-contents"));
-    this._splitter = $('#splitter');
-    this._summary = $("#request-menu-network-summary");
+    this._splitter = $("#network-inspector-view-splitter");
+    this._summary = $("#requests-menu-network-summary-label");
+    this._summary.setAttribute("value", L10N.getStr("networkMenu.empty"));
 
     this.allowFocusOnRightClick = true;
     this.widget.maintainSelectionVisible = false;
     this.widget.autoscrollWithAppendedItems = true;
 
     this.widget.addEventListener("select", this._onSelect, false);
     this._splitter.addEventListener("mousemove", this._onResize, false);
     window.addEventListener("resize", this._onResize, false);
 
     this.requestsMenuSortEvent = getKeyWithEvent(this.sortBy.bind(this));
     this.requestsMenuFilterEvent = getKeyWithEvent(this.filterOn.bind(this));
-    this.clearEvent = this.clear.bind(this);
+    this.reqeustsMenuClearEvent = this.clear.bind(this);
     this._onContextShowing = this._onContextShowing.bind(this);
     this._onContextNewTabCommand = this.openRequestInTab.bind(this);
     this._onContextCopyUrlCommand = this.copyUrl.bind(this);
     this._onContextResendCommand = this.cloneSelectedRequest.bind(this);
+    this._onContextPerfCommand = () => NetMonitorView.toggleFrontendMode();
 
     this.sendCustomRequestEvent = this.sendCustomRequest.bind(this);
     this.closeCustomRequestEvent = this.closeCustomRequest.bind(this);
     this.cloneSelectedRequestEvent = this.cloneSelectedRequest.bind(this);
 
     $("#toolbar-labels").addEventListener("click", this.requestsMenuSortEvent, false);
     $("#requests-menu-footer").addEventListener("click", this.requestsMenuFilterEvent, false);
-    $("#requests-menu-clear-button").addEventListener("click", this.clearEvent, false);
+    $("#requests-menu-clear-button").addEventListener("click", this.reqeustsMenuClearEvent, false);
     $("#network-request-popup").addEventListener("popupshowing", this._onContextShowing, false);
     $("#request-menu-context-newtab").addEventListener("command", this._onContextNewTabCommand, false);
     $("#request-menu-context-copy-url").addEventListener("command", this._onContextCopyUrlCommand, false);
     $("#request-menu-context-resend").addEventListener("command", this._onContextResendCommand, false);
+    $("#request-menu-context-perf").addEventListener("command", this._onContextPerfCommand, false);
+
+    $("#requests-menu-perf-notice-button").addEventListener("command", this._onContextPerfCommand, false);
+    $("#requests-menu-network-summary-button").addEventListener("command", this._onContextPerfCommand, false);
+    $("#requests-menu-network-summary-label").addEventListener("click", this._onContextPerfCommand, 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);
   },
 
   /**
    * Destruction function, called when the network monitor is closed.
@@ -306,32 +384,38 @@ RequestsMenuView.prototype = Heritage.ex
     dumpn("Destroying the SourcesView");
 
     this.widget.removeEventListener("select", this._onSelect, false);
     this._splitter.removeEventListener("mousemove", this._onResize, false);
     window.removeEventListener("resize", this._onResize, false);
 
     $("#toolbar-labels").removeEventListener("click", this.requestsMenuSortEvent, false);
     $("#requests-menu-footer").removeEventListener("click", this.requestsMenuFilterEvent, false);
-    $("#requests-menu-clear-button").removeEventListener("click", this.clearEvent, false);
+    $("#requests-menu-clear-button").removeEventListener("click", this.reqeustsMenuClearEvent, false);
     $("#network-request-popup").removeEventListener("popupshowing", this._onContextShowing, false);
     $("#request-menu-context-newtab").removeEventListener("command", this._onContextNewTabCommand, false);
     $("#request-menu-context-copy-url").removeEventListener("command", this._onContextCopyUrlCommand, false);
     $("#request-menu-context-resend").removeEventListener("command", this._onContextResendCommand, false);
+    $("#request-menu-context-perf").removeEventListener("command", this._onContextPerfCommand, false);
+
+    $("#requests-menu-perf-notice-button").removeEventListener("command", this._onContextPerfCommand, false);
+    $("#requests-menu-network-summary-button").removeEventListener("command", this._onContextPerfCommand, false);
+    $("#requests-menu-network-summary-label").removeEventListener("click", 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);
   },
 
   /**
    * Resets this container (removes all the networking information).
    */
   reset: function() {
     this.empty();
+    this.filterOn("all");
     this._firstRequestStartedMillis = -1;
     this._lastRequestEndedMillis = -1;
   },
 
   /**
    * Specifies if this view may be updated lazily.
    */
   lazyUpdate: true,
@@ -389,16 +473,17 @@ RequestsMenuView.prototype = Heritage.ex
    * the currently selected request.
    */
   cloneSelectedRequest: function() {
     let selected = this.selectedItem.attachment;
 
     // Create the element node for the network request item.
     let menuView = this._createMenuView(selected.method, selected.url);
 
+    // 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;
@@ -452,17 +537,17 @@ RequestsMenuView.prototype = Heritage.ex
     NetMonitorView.Sidebar.toggle(false);
    },
 
   /**
    * Filters all network requests in this container by a specified type.
    *
    * @param string aType
    *        Either "all", "html", "css", "js", "xhr", "fonts", "images", "media"
-   *        or "flash".
+   *        "flash" or "other".
    */
   filterOn: function(aType = "all") {
     let target = $("#requests-menu-filter-" + aType + "-button");
     let buttons = document.querySelectorAll(".requests-menu-footer-button");
 
     for (let button of buttons) {
       if (button != target) {
         button.removeAttribute("checked");
@@ -472,38 +557,41 @@ RequestsMenuView.prototype = Heritage.ex
     }
 
     // Filter on whatever was requested.
     switch (aType) {
       case "all":
         this.filterContents(() => true);
         break;
       case "html":
-        this.filterContents(this._onHtml);
+        this.filterContents(e => this.isHtml(e));
         break;
       case "css":
-        this.filterContents(this._onCss);
+        this.filterContents(e => this.isCss(e));
         break;
       case "js":
-        this.filterContents(this._onJs);
+        this.filterContents(e => this.isJs(e));
         break;
       case "xhr":
-        this.filterContents(this._onXhr);
+        this.filterContents(e => this.isXHR(e));
         break;
       case "fonts":
-        this.filterContents(this._onFonts);
+        this.filterContents(e => this.isFont(e));
         break;
       case "images":
-        this.filterContents(this._onImages);
+        this.filterContents(e => this.isImage(e));
         break;
       case "media":
-        this.filterContents(this._onMedia);
+        this.filterContents(e => this.isMedia(e));
         break;
       case "flash":
-        this.filterContents(this._onFlash);
+        this.filterContents(e => this.isFlash(e));
+        break;
+      case "other":
+        this.filterContents(e => this.isOther(e));
         break;
     }
 
     this.refreshSummary();
     this.refreshZebra();
   },
 
   /**
@@ -606,56 +694,60 @@ RequestsMenuView.prototype = Heritage.ex
   /**
    * Predicates used when filtering items.
    *
    * @param object aItem
    *        The filtered item.
    * @return boolean
    *         True if the item should be visible, false otherwise.
    */
-  _onHtml: function({ attachment: { mimeType } })
+  isHtml: function({ attachment: { mimeType } })
     mimeType && mimeType.contains("/html"),
 
-  _onCss: function({ attachment: { mimeType } })
+  isCss: function({ attachment: { mimeType } })
     mimeType && mimeType.contains("/css"),
 
-  _onJs: function({ attachment: { mimeType } })
+  isJs: function({ attachment: { mimeType } })
     mimeType && (
       mimeType.contains("/ecmascript") ||
       mimeType.contains("/javascript") ||
       mimeType.contains("/x-javascript")),
 
-  _onXhr: function({ attachment: { isXHR } })
+  isXHR: function({ attachment: { isXHR } })
     isXHR,
 
-  _onFonts: function({ attachment: { url, mimeType } }) // Fonts are a mess.
+  isFont: function({ attachment: { url, mimeType } }) // Fonts are a mess.
     (mimeType && (
       mimeType.contains("font/") ||
       mimeType.contains("/font"))) ||
     url.contains(".eot") ||
     url.contains(".ttf") ||
     url.contains(".otf") ||
     url.contains(".woff"),
 
-  _onImages: function({ attachment: { mimeType } })
+  isImage: function({ attachment: { mimeType } })
     mimeType && mimeType.contains("image/"),
 
-  _onMedia: function({ attachment: { mimeType } }) // Not including images.
+  isMedia: function({ attachment: { mimeType } }) // Not including images.
     mimeType && (
       mimeType.contains("audio/") ||
       mimeType.contains("video/") ||
       mimeType.contains("model/")),
 
-  _onFlash: function({ attachment: { url, mimeType } }) // Flash is a mess.
+  isFlash: function({ attachment: { url, mimeType } }) // Flash is a mess.
     (mimeType && (
       mimeType.contains("/x-flv") ||
       mimeType.contains("/x-shockwave-flash"))) ||
     url.contains(".swf") ||
     url.contains(".flv"),
 
+  isOther: function(e)
+    !this.isHtml(e) && !this.isCss(e) && !this.isJs(e) && !this.isXHR(e) &&
+    !this.isFont(e) && !this.isImage(e) && !this.isMedia(e) && !this.isFlash(e),
+
   /**
    * Predicates used when sorting items.
    *
    * @param object aFirst
    *        The first item used in the comparison.
    * @param object aSecond
    *        The second item used in the comparison.
    * @return number
@@ -719,18 +811,18 @@ RequestsMenuView.prototype = Heritage.ex
     let totalMillis =
       this._getNewestRequest(visibleItems).attachment.endedMillis -
       this._getOldestRequest(visibleItems).attachment.startedMillis;
 
     // https://developer.mozilla.org/en-US/docs/Localization_and_Plurals
     let str = PluralForm.get(visibleRequestsCount, L10N.getStr("networkMenu.summary"));
     this._summary.setAttribute("value", str
       .replace("#1", visibleRequestsCount)
-      .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, 2))
-      .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, 2))
+      .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, CONTENT_SIZE_DECIMALS))
+      .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, REQUEST_TIME_DECIMALS))
     );
   },
 
   /**
    * Adds odd/even attributes to all the visible items in this container.
    */
   refreshZebra: function() {
     let visibleItems = this.visibleItems;
@@ -833,16 +925,22 @@ RequestsMenuView.prototype = Heritage.ex
             this.updateMenuView(requestItem, key, value);
             break;
           case "mimeType":
             requestItem.attachment.mimeType = value;
             this.updateMenuView(requestItem, key, value);
             break;
           case "responseContent":
             requestItem.attachment.responseContent = value;
+            // 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");
+            }
             break;
           case "totalTime":
             requestItem.attachment.totalTime = value;
             requestItem.attachment.endedMillis = requestItem.attachment.startedMillis + value;
             this.updateMenuView(requestItem, key, value);
             this._registerLastRequestEnd(requestItem.attachment.endedMillis);
             break;
           case "eventTimings":
@@ -1016,16 +1114,21 @@ RequestsMenuView.prototype = Heritage.ex
       }
     }
 
     // Since at least one timing box should've been rendered, unhide the
     // start and end timing cap nodes.
     startCapNode.hidden = false;
     endCapNode.hidden = false;
 
+    // Don't paint things while the waterfall view isn't even visible.
+    if (NetMonitorView.currentFrontendMode != "network-inspector-view") {
+      return;
+    }
+
     // Rescale all the waterfalls so that everything is visible at once.
     this._flushWaterfallViews();
   },
 
   /**
    * Rescales and redraws all the waterfall views in this container.
    *
    * @param boolean aReset
@@ -1129,17 +1232,17 @@ RequestsMenuView.prototype = Heritage.ex
           normalizedTime /= 1000;
           divisionScale = "second";
         }
 
         // Showing too many decimals is bad UX.
         if (divisionScale == "millisecond") {
           normalizedTime |= 0;
         } else {
-          normalizedTime = L10N.numberWithDecimals(normalizedTime, 2);
+          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;
 
@@ -1258,16 +1361,21 @@ RequestsMenuView.prototype = Heritage.ex
       NetMonitorView.Sidebar.toggle(false);
     }
   },
 
   /**
    * The resize listener for this container's window.
    */
   _onResize: function(e) {
+    // Don't paint things while the waterfall view isn't even visible.
+    if (NetMonitorView.currentFrontendMode != "network-inspector-view") {
+      return;
+    }
+
     // Allow requests to settle down first.
     setNamedTimeout(
       "resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
   },
 
   /**
    * Handle the context menu opening. Hide items if no request is selected.
    */
@@ -1448,17 +1556,17 @@ SidebarView.prototype = {
   populate: function(aData) {
     let isCustom = aData.isCustom;
     let view = isCustom ?
       NetMonitorView.CustomRequest :
       NetMonitorView.NetworkDetails;
 
     return view.populate(aData).then(() => {
       $("#details-pane").selectedIndex = isCustom ? 0 : 1
-      window.emit(EVENTS.SIDEBAR_POPULATED)
+      window.emit(EVENTS.SIDEBAR_POPULATED);
     });
   },
 
   /**
    * Hides this container.
    */
   reset: function() {
     this.toggle(false);
@@ -1475,17 +1583,16 @@ function CustomRequestView() {
 CustomRequestView.prototype = {
   /**
    * Initialization function, called when the network monitor is started.
    */
   initialize: function() {
     dumpn("Initializing the CustomRequestView");
 
     this.updateCustomRequestEvent = getKeyWithEvent(this.onUpdate.bind(this));
-
     $("#custom-pane").addEventListener("input", this.updateCustomRequestEvent, false);
   },
 
   /**
    * Destruction function, called when the network monitor is closed.
    */
   destroy: function() {
     dumpn("Destroying the CustomRequestView");
@@ -1550,28 +1657,22 @@ CustomRequestView.prototype = {
         let query = $("#custom-query-value").value;
         this.updateCustomUrl(query);
         field = 'url';
         value = $("#custom-url-value").value
         selectedItem.attachment.url = value;
         break;
       case 'body':
         value = $("#custom-postdata-value").value;
-        selectedItem.attachment.requestPostData = {
-          postData: {
-            text: value
-          }
-        };
+        selectedItem.attachment.requestPostData = { postData: { text: value } };
         break;
       case 'headers':
         let headersText = $("#custom-headers-value").value;
         value = parseHeaderText(headersText);
-        selectedItem.attachment.requestHeaders = {
-          headers: value
-        };
+        selectedItem.attachment.requestHeaders = { headers: value };
         break;
     }
 
     NetMonitorView.RequestsMenu.updateMenuView(selectedItem, field, value);
   },
 
   /**
    * Update the query string field based on the url.
@@ -2168,16 +2269,180 @@ NetworkDetailsView.prototype = {
   _paramsPostPayload: "",
   _requestHeaders: "",
   _responseHeaders: "",
   _requestCookies: "",
   _responseCookies: ""
 };
 
 /**
+ * Functions handling the performance statistics view.
+ */
+function PerformanceStatisticsView() {
+}
+
+PerformanceStatisticsView.prototype = {
+  /**
+   * 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 aItems
+   *        @see this._sanitizeChartDataSource
+   */
+  createPrimedCacheChart: function(aItems) {
+    this._createChart({
+      id: "#primed-cache-chart",
+      title: "charts.cacheEnabled",
+      data: this._sanitizeChartDataSource(aItems),
+      sorted: true,
+      totals: {
+        size: L10N.getStr("charts.totalSize"),
+        time: L10N.getStr("charts.totalTime"),
+        cached: L10N.getStr("charts.totalCached"),
+        count: L10N.getStr("charts.totalCount")
+      }
+    });
+    window.emit(EVENTS.PRIMED_CACHE_CHART_DISPLAYED);
+  },
+
+  /**
+   * Populates and displays the empty cache chart in this container.
+   *
+   * @param array aItems
+   *        @see this._sanitizeChartDataSource
+   */
+  createEmptyCacheChart: function(aItems) {
+    this._createChart({
+      id: "#empty-cache-chart",
+      title: "charts.cacheDisabled",
+      data: this._sanitizeChartDataSource(aItems, true),
+      sorted: true,
+      totals: {
+        size: L10N.getStr("charts.totalSize"),
+        time: L10N.getStr("charts.totalTime"),
+        cached: L10N.getStr("charts.totalCached"),
+        count: L10N.getStr("charts.totalCount")
+      }
+    });
+    window.emit(EVENTS.EMPTY_CACHE_CHART_DISPLAYED);
+  },
+
+  /**
+   * 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/sorted/totals: @see Chart.jsm for details
+   */
+  _createChart: function({ id, title, data, sorted, totals }) {
+    let container = $(id);
+
+    // Nuke all existing charts of the specified type.
+    while (container.hasChildNodes()) {
+      container.firstChild.remove();
+    }
+
+    // Create a new chart.
+    let chart = Chart.PieTable(document, {
+      diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
+      title: L10N.getStr(title),
+      data: data,
+      sorted: sorted,
+      totals: totals
+    });
+
+    chart.on("click", (_, item) => {
+      NetMonitorView.RequestsMenu.filterOn(item.label);
+      NetMonitorView.showNetworkInspectorView();
+    });
+
+    container.appendChild(chart.node);
+  },
+
+  /**
+   * Sanitizes the data source used for creating charts, to follow the
+   * data format spec defined in Chart.jsm.
+   *
+   * @param array aItems
+   *        A collection of request items used as the data source for the chart.
+   * @param boolean aEmptyCache
+   *        True if the cache is considered enabled, false for disabled.
+   */
+  _sanitizeChartDataSource: function(aItems, aEmptyCache) {
+    let data = [
+      "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "other"
+    ].map(e => ({
+      cached: 0,
+      count: 0,
+      label: e,
+      size: 0,
+      time: 0
+    }));
+
+    for (let requestItem of aItems) {
+      let details = requestItem.attachment;
+      let type;
+
+      if (RequestsMenuView.prototype.isHtml(requestItem)) {
+        type = 0; // "html"
+      } else if (RequestsMenuView.prototype.isCss(requestItem)) {
+        type = 1; // "css"
+      } else if (RequestsMenuView.prototype.isJs(requestItem)) {
+        type = 2; // "js"
+      } else if (RequestsMenuView.prototype.isFont(requestItem)) {
+        type = 4; // "fonts"
+      } else if (RequestsMenuView.prototype.isImage(requestItem)) {
+        type = 5; // "images"
+      } else if (RequestsMenuView.prototype.isMedia(requestItem)) {
+        type = 6; // "media"
+      } else if (RequestsMenuView.prototype.isFlash(requestItem)) {
+        type = 7; // "flash"
+      } else if (RequestsMenuView.prototype.isXHR(requestItem)) {
+        // Verify XHR last, to categorize other mime types in their own blobs.
+        type = 3; // "xhr"
+      } else {
+        type = 8; // "other"
+      }
+
+      if (aEmptyCache || !responseIsFresh(details)) {
+        data[type].time += details.totalTime || 0;
+        data[type].size += details.contentSize || 0;
+      } else {
+        data[type].cached++;
+      }
+      data[type].count++;
+    }
+
+    for (let chartItem of data) {
+      let size = L10N.numberWithDecimals(chartItem.size / 1024, CONTENT_SIZE_DECIMALS);
+      let time = L10N.numberWithDecimals(chartItem.time / 1000, REQUEST_TIME_DECIMALS);
+      chartItem.size = L10N.getFormatStr("charts.sizeKB", size);
+      chartItem.time = L10N.getFormatStr("charts.totalMS", time);
+    }
+
+    return data.filter(e => e.count > 0);
+  },
+};
+
+/**
  * DOM query helper.
  */
 function $(aSelector, aTarget = document) aTarget.querySelector(aSelector);
 function $all(aSelector, aTarget = document) aTarget.querySelectorAll(aSelector);
 
 /**
  * Helper for getting an nsIURL instance out of a string.
  */
@@ -2189,18 +2454,18 @@ function nsIURL(aUrl, aStore = nsIURL.st
   aStore.set(aUrl, uri);
   return uri;
 }
 nsIURL.store = new Map();
 
 /**
  * Parse a url's query string into its components
  *
- * @param  string aQueryString
- *         The query part of a url
+ * @param string aQueryString
+ *        The query part of a url
  * @return array
  *         Array of query params {name, value}
  */
 function parseQueryString(aQueryString) {
   // Make sure there's at least one param available.
   if (!aQueryString || !aQueryString.contains("=")) {
     return;
   }
@@ -2211,43 +2476,43 @@ function parseQueryString(aQueryString) 
       value: NetworkHelper.convertToUnicode(unescape(param[1]))
     });
   return paramsArray;
 }
 
 /**
  * Parse text representation of HTTP headers.
  *
- * @param  string aText
- *         Text of headers
+ * @param string aText
+ *        Text of headers
  * @return array
  *         Array of headers info {name, value}
  */
 function parseHeaderText(aText) {
   return parseRequestText(aText, ":");
 }
 
 /**
  * Parse readable text list of a query string.
  *
- * @param  string aText
- *         Text of query string represetation
+ * @param string aText
+ *        Text of query string represetation
  * @return array
  *         Array of query params {name, value}
  */
 function parseQueryText(aText) {
   return parseRequestText(aText, "=");
 }
 
 /**
  * Parse a text representation of a name:value list with
  * the given name:value divider character.
  *
- * @param  string aText
- *         Text of list
+ * @param string aText
+ *        Text of list
  * @return array
  *         Array of headers info {name, value}
  */
 function parseRequestText(aText, aDivider) {
   let regex = new RegExp("(.+?)\\" + aDivider + "\\s*(.+)");
   let pairs = [];
   for (let line of aText.split("\n")) {
     let matches;
@@ -2291,16 +2556,54 @@ function writeQueryText(aParams) {
  * @return string
  *         Query string that can be appended to a url.
  */
 function writeQueryString(aParams) {
   return [(name + "=" + value) for ({name, value} of aParams)].join("&");
 }
 
 /**
+ * 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.filter(e => 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) {
+      return true;
+    }
+  }
+
+  // Check the "Expires" header for a valid date.
+  if (expires && Date.parse(expires.value)) {
+    return true;
+  }
+
+  return false;
+}
+
+/**
  * Helper method to get a wrapped function which can be bound to as an event listener directly and is executed only when data-key is present in event.target.
  *
  * @param function callback
  *          Function to execute execute when data-key is present in event.target.
  * @return function
  *          Wrapped function with the target data-key as the first argument.
  */
 function getKeyWithEvent(callback) {
@@ -2315,8 +2618,9 @@ function getKeyWithEvent(callback) {
 /**
  * Preliminary setup for the NetMonitorView object.
  */
 NetMonitorView.Toolbar = new ToolbarView();
 NetMonitorView.RequestsMenu = new RequestsMenuView();
 NetMonitorView.Sidebar = new SidebarView();
 NetMonitorView.CustomRequest = new CustomRequestView();
 NetMonitorView.NetworkDetails = new NetworkDetailsView();
+NetMonitorView.PerformanceStatistics = new PerformanceStatisticsView();
--- a/browser/devtools/netmonitor/netmonitor.css
+++ b/browser/devtools/netmonitor/netmonitor.css
@@ -7,51 +7,51 @@
   overflow: hidden;
 }
 
 #details-pane-toggle[disabled] {
   /* Don't use display: none; to avoid collapsing #requests-menu-toolbar */
   visibility: hidden;
 }
 
-#response-content-image-box {
+#custom-pane {
   overflow: auto;
 }
 
-#custom-pane {
+#response-content-image-box {
   overflow: auto;
 }
 
 #timings-summary-blocked {
   display: none; /* This doesn't work yet. */
 }
 
+#network-statistics-charts {
+  overflow: auto;
+}
+
 /* Responsive sidebar */
 @media (max-width: 700px) {
   #toolbar-spacer,
   #details-pane-toggle,
   #details-pane[pane-collapsed],
   .requests-menu-waterfall,
   .requests-menu-footer-label {
     display: none;
   }
 }
 
-@media (min-width: 701px) and (max-width: 1024px) {
-  #body:not([pane-collapsed]) .requests-menu-footer-button,
+@media (min-width: 701px) and (max-width: 1280px) {
+  #body:not([pane-collapsed]) .requests-menu-filter-button,
   #body:not([pane-collapsed]) .requests-menu-footer-spacer {
     display: none;
   }
 }
 
 @media (min-width: 701px) {
-  #requests-menu-spacer-start {
-    display: none;
-  }
-
   #network-table[waterfall-overflows] .requests-menu-waterfall {
     display: none;
   }
 
   #network-table[size-overflows] .requests-menu-size {
     display: none;
   }
 
--- a/browser/devtools/netmonitor/netmonitor.xul
+++ b/browser/devtools/netmonitor/netmonitor.xul
@@ -26,22 +26,26 @@
                 label="&netmonitorUI.context.newTab;"
                 accesskey="&netmonitorUI.context.newTab.accesskey;"/>
       <menuitem id="request-menu-context-copy-url"
                 label="&netmonitorUI.context.copyUrl;"
                 accesskey="&netmonitorUI.context.copyUrl.accesskey;"/>
       <menuitem id="request-menu-context-resend"
                 label="&netmonitorUI.summary.editAndResend;"
                 accesskey="&netmonitorUI.summary.editAndResend.accesskey;"/>
+      <menuseparator/>
+      <menuitem id="request-menu-context-perf"
+                label="&netmonitorUI.context.perfTools;"
+                accesskey="&netmonitorUI.context.perfTools.accesskey;"/>
     </menupopup>
   </popupset>
 
-  <box id="body"
-       class="devtools-responsive-container theme-sidebar"
-       flex="1">
+  <deck id="body" class="theme-sidebar" flex="1">
+
+  <box id="network-inspector-view" class="devtools-responsive-container">
     <vbox id="network-table" flex="1">
       <toolbar id="requests-menu-toolbar"
                class="devtools-toolbar"
                align="center">
         <hbox id="toolbar-labels" flex="1">
           <hbox id="requests-menu-status-and-method-header-box"
                 class="requests-menu-header requests-menu-status-and-method"
                 align="center">
@@ -113,19 +117,30 @@
           </hbox>
         </hbox>
         <toolbarbutton id="details-pane-toggle"
                        class="devtools-toolbarbutton"
                        tooltiptext="&netmonitorUI.panesButton.tooltip;"
                        disabled="true"
                        tabindex="0"/>
       </toolbar>
-      <label id="requests-menu-empty-notice"
-             class="side-menu-widget-empty-text"
-             value="&netmonitorUI.emptyNotice2;"/>
+
+      <vbox id="requests-menu-empty-notice"
+            class="side-menu-widget-empty-text">
+        <hbox id="notice-perf-message" align="center">
+          <label value="&netmonitorUI.perfNotice1;"/>
+          <button id="requests-menu-perf-notice-button"
+                  class="devtools-toolbarbutton"/>
+          <label value="&netmonitorUI.perfNotice2;"/>
+        </hbox>
+        <hbox id="notice-reload-message" align="center">
+          <label value="&netmonitorUI.emptyNotice3;"/>
+        </hbox>
+      </vbox>
+
       <vbox id="requests-menu-contents" flex="1" context="network-request-popup">
         <hbox id="requests-menu-item-template" hidden="true">
           <hbox class="requests-menu-subitem requests-menu-status-and-method"
                 align="center">
             <box class="requests-menu-status"/>
             <label class="plain requests-menu-method"
                    crop="end"
                    flex="1"/>
@@ -146,80 +161,80 @@
               <hbox class="start requests-menu-timings-cap" hidden="true"/>
               <hbox class="end requests-menu-timings-cap" hidden="true"/>
               <label class="plain requests-menu-timings-total"/>
             </hbox>
           </hbox>
         </hbox>
       </vbox>
       <hbox id="requests-menu-footer">
-        <spacer id="requests-menu-spacer-start"
-                class="requests-menu-footer-spacer"
-                flex="100"/>
         <button id="requests-menu-filter-all-button"
-                class="requests-menu-footer-button"
+                class="requests-menu-filter-button requests-menu-footer-button"
                 checked="true"
                 data-key="all"
                 label="&netmonitorUI.footer.filterAll;">
         </button>
         <button id="requests-menu-filter-html-button"
-                class="requests-menu-footer-button"
+                class="requests-menu-filter-button requests-menu-footer-button"
                 data-key="html"
                 label="&netmonitorUI.footer.filterHTML;">
         </button>
         <button id="requests-menu-filter-css-button"
-                class="requests-menu-footer-button"
+                class="requests-menu-filter-button requests-menu-footer-button"
                 data-key="css"
                 label="&netmonitorUI.footer.filterCSS;">
         </button>
         <button id="requests-menu-filter-js-button"
-                class="requests-menu-footer-button"
+                class="requests-menu-filter-button requests-menu-footer-button"
                 data-key="js"
                 label="&netmonitorUI.footer.filterJS;">
         </button>
         <button id="requests-menu-filter-xhr-button"
-                class="requests-menu-footer-button"
+                class="requests-menu-filter-button requests-menu-footer-button"
                 data-key="xhr"
                 label="&netmonitorUI.footer.filterXHR;">
         </button>
         <button id="requests-menu-filter-fonts-button"
-                class="requests-menu-footer-button"
+                class="requests-menu-filter-button requests-menu-footer-button"
                 data-key="fonts"
                 label="&netmonitorUI.footer.filterFonts;">
         </button>
         <button id="requests-menu-filter-images-button"
-                class="requests-menu-footer-button"
+                class="requests-menu-filter-button requests-menu-footer-button"
                 data-key="images"
                 label="&netmonitorUI.footer.filterImages;">
         </button>
         <button id="requests-menu-filter-media-button"
-                class="requests-menu-footer-button"
+                class="requests-menu-filter-button requests-menu-footer-button"
                 data-key="media"
                 label="&netmonitorUI.footer.filterMedia;">
         </button>
         <button id="requests-menu-filter-flash-button"
-                class="requests-menu-footer-button"
+                class="requests-menu-filter-button requests-menu-footer-button"
                 data-key="flash"
                 label="&netmonitorUI.footer.filterFlash;">
         </button>
-        <spacer id="requests-menu-spacer-end"
+        <spacer id="requests-menu-spacer"
                 class="requests-menu-footer-spacer"
                 flex="100"/>
-        <label id="request-menu-network-summary"
+        <button id="requests-menu-network-summary-button"
+                class="requests-menu-footer-button"
+                tooltiptext="&netmonitorUI.footer.perf;"/>
+        <label id="requests-menu-network-summary-label"
                class="plain requests-menu-footer-label"
-               flex="1"
-               crop="end"/>
+               crop="end"
+               tooltiptext="&netmonitorUI.footer.perf;"/>
         <button id="requests-menu-clear-button"
-               class="requests-menu-footer-button"
-               label="&netmonitorUI.footer.clear;">
-        </button>
+                class="requests-menu-footer-button"
+                label="&netmonitorUI.footer.clear;"/>
       </hbox>
     </vbox>
 
-    <splitter id="splitter" class="devtools-side-splitter"/>
+    <splitter id="network-inspector-view-splitter"
+              class="devtools-side-splitter"/>
 
     <deck id="details-pane"
           hidden="true">
       <vbox id="custom-pane"
             class="tabpanel-content">
         <hbox align="baseline">
           <label value="&netmonitorUI.custom.newRequest;"
                  class="plain tabpanel-summary-label
@@ -455,9 +470,29 @@
               </hbox>
             </vbox>
           </tabpanel>
         </tabpanels>
       </tabbox>
     </deck>
   </box>
 
+  <box id="network-statistics-view">
+    <toolbar id="network-statistics-toolbar"
+             class="devtools-toolbar">
+      <button id="network-statistics-back-button"
+              class="devtools-toolbarbutton"
+              onclick="NetMonitorView.toggleFrontendMode()"
+              label="&netmonitorUI.backButton;"/>
+    </toolbar>
+    <box id="network-statistics-charts"
+         class="devtools-responsive-container"
+         flex="1">
+      <vbox id="primed-cache-chart" pack="center" flex="1"/>
+      <splitter id="network-statistics-view-splitter"
+                class="devtools-side-splitter"/>
+      <vbox id="empty-cache-chart" pack="center" flex="1"/>
+    </box>
+  </box>
+
+  </deck>
+
 </window>
--- a/browser/devtools/netmonitor/test/browser.ini
+++ b/browser/devtools/netmonitor/test/browser.ini
@@ -10,27 +10,33 @@ support-files =
   html_json-long-test-page.html
   html_json-malformed-test-page.html
   html_jsonp-test-page.html
   html_navigate-test-page.html
   html_post-data-test-page.html
   html_post-raw-test-page.html
   html_simple-test-page.html
   html_sorting-test-page.html
+  html_statistics-test-page.html
   html_status-codes-test-page.html
   sjs_content-type-test-server.sjs
   sjs_simple-test-server.sjs
   sjs_sorting-test-server.sjs
   sjs_status-codes-test-server.sjs
   test-image.png
 
 [browser_net_aaa_leaktest.js]
 [browser_net_accessibility-01.js]
 [browser_net_accessibility-02.js]
 [browser_net_autoscroll.js]
+[browser_net_charts-01.js]
+[browser_net_charts-02.js]
+[browser_net_charts-03.js]
+[browser_net_charts-04.js]
+[browser_net_charts-05.js]
 [browser_net_clear.js]
 [browser_net_content-type.js]
 [browser_net_copy_url.js]
 [browser_net_cyrillic-01.js]
 [browser_net_cyrillic-02.js]
 [browser_net_filter-01.js]
 [browser_net_filter-02.js]
 [browser_net_filter-03.js]
@@ -52,11 +58,13 @@ support-files =
 [browser_net_resend.js]
 [browser_net_simple-init.js]
 [browser_net_simple-request-data.js]
 [browser_net_simple-request-details.js]
 [browser_net_simple-request.js]
 [browser_net_sort-01.js]
 [browser_net_sort-02.js]
 [browser_net_sort-03.js]
+[browser_net_statistics-01.js]
+[browser_net_statistics-02.js]
 [browser_net_status-codes.js]
 [browser_net_timeline_ticks.js]
 [browser_net_timing-division.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_charts-01.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Makes sure Pie Charts have the right internal structure.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, Chart } = aMonitor.panelWin;
+    let container = document.createElement("box");
+
+    let pie = Chart.Pie(document, {
+      width: 100,
+      height: 100,
+      data: [{
+        size: 1,
+        label: "foo"
+      }, {
+        size: 2,
+        label: "bar"
+      }, {
+        size: 3,
+        label: "baz"
+      }]
+    });
+
+    let node = pie.node;
+    let slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob");
+    let labels = node.querySelectorAll(".pie-chart-label");
+
+    ok(node.classList.contains("pie-chart-container") &&
+       node.classList.contains("generic-chart-container"),
+      "A pie chart container was created successfully.");
+
+    is(slices.length, 3,
+      "There should be 3 pie chart slices created.");
+    ok(slices[0].getAttribute("d").match(/\s*M 50,50 L 49\.\d+,97\.\d+ A 47\.5,47\.5 0 0 1 49\.\d+,2\.5\d* Z/),
+      "The first slice has the correct data.");
+    ok(slices[1].getAttribute("d").match(/\s*M 50,50 L 91\.\d+,26\.\d+ A 47\.5,47\.5 0 0 1 49\.\d+,97\.\d+ Z/),
+      "The second slice has the correct data.");
+    ok(slices[2].getAttribute("d").match(/\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 0 1 91\.\d+,26\.\d+ Z/),
+      "The third slice has the correct data.");
+
+    ok(slices[0].hasAttribute("largest"),
+      "The first slice should be the largest one.");
+    ok(slices[2].hasAttribute("smallest"),
+      "The third slice should be the smallest one.");
+
+    ok(slices[0].getAttribute("name"), "baz",
+      "The first slice's name is correct.");
+    ok(slices[1].getAttribute("name"), "bar",
+      "The first slice's name is correct.");
+    ok(slices[2].getAttribute("name"), "foo",
+      "The first slice's name is correct.");
+
+    is(labels.length, 3,
+      "There should be 3 pie chart labels created.");
+    is(labels[0].textContent, "baz",
+      "The first label's text is correct.");
+    is(labels[1].textContent, "bar",
+      "The first label's text is correct.");
+    is(labels[2].textContent, "foo",
+      "The first label's text is correct.");
+
+    teardown(aMonitor).then(finish);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_charts-02.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Makes sure Pie Charts have the right internal structure when
+ * initialized with empty data.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, L10N, Chart } = aMonitor.panelWin;
+    let container = document.createElement("box");
+
+    let pie = Chart.Pie(document, {
+      data: null,
+      width: 100,
+      height: 100
+    });
+
+    let node = pie.node;
+    let slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob");
+    let labels = node.querySelectorAll(".pie-chart-label");
+
+    ok(node.classList.contains("pie-chart-container") &&
+       node.classList.contains("generic-chart-container"),
+      "A pie chart container was created successfully.");
+
+    is(slices.length, 1,
+      "There should be 1 pie chart slice created.");
+    ok(slices[0].getAttribute("d").match(/\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 1 1 49\.\d+,2\.5\d* Z/),
+      "The first slice has the correct data.");
+
+    ok(slices[0].hasAttribute("largest"),
+      "The first slice should be the largest one.");
+    ok(slices[0].hasAttribute("smallest"),
+      "The first slice should also be the smallest one.");
+    ok(slices[0].getAttribute("name"), L10N.getStr("pieChart.empty"),
+      "The first slice's name is correct.");
+
+    is(labels.length, 1,
+      "There should be 1 pie chart label created.");
+    is(labels[0].textContent, "Loading",
+      "The first label's text is correct.");
+
+    teardown(aMonitor).then(finish);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_charts-03.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Makes sure Table Charts have the right internal structure.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, Chart } = aMonitor.panelWin;
+    let container = document.createElement("box");
+
+    let table = Chart.Table(document, {
+      title: "Table title",
+      data: [{
+        label1: 1,
+        label2: "11.1foo"
+      }, {
+        label1: 2,
+        label2: "12.2bar"
+      }, {
+        label1: 3,
+        label2: "13.3baz"
+      }],
+      totals: {
+        label1: "Hello %S",
+        label2: "World %S"
+      }
+    });
+
+    let node = table.node;
+    let title = node.querySelector(".table-chart-title");
+    let grid = node.querySelector(".table-chart-grid");
+    let totals = node.querySelector(".table-chart-totals");
+    let rows = grid.querySelectorAll(".table-chart-row");
+    let sums = node.querySelectorAll(".table-chart-summary-label");
+
+    ok(node.classList.contains("table-chart-container") &&
+       node.classList.contains("generic-chart-container"),
+      "A table chart container was created successfully.");
+
+    ok(title,
+      "A title node was created successfully.");
+    is(title.getAttribute("value"), "Table title",
+      "The title node displays the correct text.");
+
+    is(rows.length, 3,
+      "There should be 3 table chart rows created.");
+
+    ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"),
+      "A colored blob exists for the firt row.");
+    is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "label1",
+      "The first column of the first row exists.");
+    is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label2",
+      "The second column of the first row exists.");
+    is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "1",
+      "The first column of the first row displays the correct text.");
+    is(rows[0].querySelectorAll("label")[1].getAttribute("value"), "11.1foo",
+      "The second column of the first row displays the correct text.");
+
+    ok(rows[1].querySelector(".table-chart-row-box.chart-colored-blob"),
+      "A colored blob exists for the second row.");
+    is(rows[1].querySelectorAll("label")[0].getAttribute("name"), "label1",
+      "The first column of the second row exists.");
+    is(rows[1].querySelectorAll("label")[1].getAttribute("name"), "label2",
+      "The second column of the second row exists.");
+    is(rows[1].querySelectorAll("label")[0].getAttribute("value"), "2",
+      "The first column of the second row displays the correct text.");
+    is(rows[1].querySelectorAll("label")[1].getAttribute("value"), "12.2bar",
+      "The second column of the first row displays the correct text.");
+
+    ok(rows[2].querySelector(".table-chart-row-box.chart-colored-blob"),
+      "A colored blob exists for the third row.");
+    is(rows[2].querySelectorAll("label")[0].getAttribute("name"), "label1",
+      "The first column of the third row exists.");
+    is(rows[2].querySelectorAll("label")[1].getAttribute("name"), "label2",
+      "The second column of the third row exists.");
+    is(rows[2].querySelectorAll("label")[0].getAttribute("value"), "3",
+      "The first column of the third row displays the correct text.");
+    is(rows[2].querySelectorAll("label")[1].getAttribute("value"), "13.3baz",
+      "The second column of the third row displays the correct text.");
+
+    is(sums.length, 2,
+      "There should be 2 total summaries created.");
+
+    is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("name"), "label1",
+      "The first sum's type is correct.");
+    is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("value"), "Hello 6",
+      "The first sum's value is correct.");
+
+    is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("name"), "label2",
+      "The second sum's type is correct.");
+    is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("value"), "World 36.60",
+      "The second sum's value is correct.");
+
+    teardown(aMonitor).then(finish);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_charts-04.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Makes sure Pie Charts have the right internal structure when
+ * initialized with empty data.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, L10N, Chart } = aMonitor.panelWin;
+    let container = document.createElement("box");
+
+    let table = Chart.Table(document, {
+      title: "Table title",
+      data: null,
+      totals: {
+        label1: "Hello %S",
+        label2: "World %S"
+      }
+    });
+
+    let node = table.node;
+    let title = node.querySelector(".table-chart-title");
+    let grid = node.querySelector(".table-chart-grid");
+    let totals = node.querySelector(".table-chart-totals");
+    let rows = grid.querySelectorAll(".table-chart-row");
+    let sums = node.querySelectorAll(".table-chart-summary-label");
+
+    ok(node.classList.contains("table-chart-container") &&
+       node.classList.contains("generic-chart-container"),
+      "A table chart container was created successfully.");
+
+    ok(title,
+      "A title node was created successfully.");
+    is(title.getAttribute("value"), "Table title",
+      "The title node displays the correct text.");
+
+    is(rows.length, 1,
+      "There should be 1 table chart row created.");
+
+    ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"),
+      "A colored blob exists for the firt row.");
+    is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "size",
+      "The first column of the first row exists.");
+    is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label",
+      "The second column of the first row exists.");
+    is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "",
+      "The first column of the first row displays the correct text.");
+    is(rows[0].querySelectorAll("label")[1].getAttribute("value"), L10N.getStr("tableChart.empty"),
+      "The second column of the first row displays the correct text.");
+
+    is(sums.length, 2,
+      "There should be 2 total summaries created.");
+
+    is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("name"), "label1",
+      "The first sum's type is correct.");
+    is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("value"), "Hello 0",
+      "The first sum's value is correct.");
+
+    is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("name"), "label2",
+      "The second sum's type is correct.");
+    is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("value"), "World 0",
+      "The second sum's value is correct.");
+
+    teardown(aMonitor).then(finish);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_charts-05.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Makes sure Pie+Table Charts have the right internal structure.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, Chart } = aMonitor.panelWin;
+    let container = document.createElement("box");
+
+    let chart = Chart.PieTable(document, {
+      title: "Table title",
+      data: [{
+        size: 1,
+        label: "11.1foo"
+      }, {
+        size: 2,
+        label: "12.2bar"
+      }, {
+        size: 3,
+        label: "13.3baz"
+      }],
+      totals: {
+        size: "Hello %S",
+        label: "World %S"
+      }
+    });
+
+    ok(chart.pie, "The pie chart proxy is accessible.");
+    ok(chart.table, "The table chart proxy is accessible.");
+
+    let node = chart.node;
+    let slices = node.querySelectorAll(".pie-chart-slice");
+    let rows = node.querySelectorAll(".table-chart-row");
+    let sums = node.querySelectorAll(".table-chart-summary-label");
+
+    ok(node.classList.contains("pie-table-chart-container"),
+      "A pie+table chart container was created successfully.");
+
+    ok(node.querySelector(".table-chart-title"),
+      "A title node was created successfully.");
+    ok(node.querySelector(".pie-chart-container"),
+      "A pie chart was created successfully.");
+    ok(node.querySelector(".table-chart-container"),
+      "A table chart was created successfully.");
+
+    is(rows.length, 3,
+      "There should be 3 pie chart slices created.");
+    is(rows.length, 3,
+      "There should be 3 table chart rows created.");
+    is(sums.length, 2,
+      "There should be 2 total summaries created.");
+
+    teardown(aMonitor).then(finish);
+  });
+}
--- a/browser/devtools/netmonitor/test/browser_net_footer-summary.js
+++ b/browser/devtools/netmonitor/test/browser_net_footer-summary.js
@@ -75,32 +75,26 @@ function test() {
 
       EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
       testStatus();
 
       teardown(aMonitor).then(finish);
     })
 
     function testStatus() {
-      let summary = $("#request-menu-network-summary");
+      let summary = $("#requests-menu-network-summary-label");
       let value = summary.getAttribute("value");
       info("Current summary: " + value);
 
       let visibleItems = RequestsMenu.visibleItems;
       let visibleRequestsCount = visibleItems.length;
       let totalRequestsCount = RequestsMenu.itemCount;
       info("Current requests: " + visibleRequestsCount + " of " + totalRequestsCount + ".");
 
-      if (!totalRequestsCount) {
-        is(value, "",
-          "The current summary text is incorrect, expected an empty string.");
-        return;
-      }
-
-      if (!visibleRequestsCount) {
+      if (!totalRequestsCount || !visibleRequestsCount) {
         is(value, L10N.getStr("networkMenu.empty"),
           "The current summary text is incorrect, expected an 'empty' label.");
         return;
       }
 
       let totalBytes = RequestsMenu._getTotalBytesOfRequests(visibleItems);
       let totalMillis =
         RequestsMenu._getNewestRequest(visibleItems).attachment.endedMillis -
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_statistics-01.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the statistics view is populated correctly.
+ */
+
+function test() {
+  initNetMonitor(STATISTICS_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let panel = aMonitor.panelWin;
+    let { document, $, $all, EVENTS, NetMonitorView } = panel;
+
+    is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+      "The initial frontend mode is correct.");
+
+    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.");
+
+    waitFor(panel, EVENTS.PLACEHOLDER_CHARTS_DISPLAYED).then(() => {
+      is($("#primed-cache-chart").childNodes.length, 1,
+        "There should be a placeholder primed cache chart created now.");
+      is($("#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,
+        "Two placeholder pie chart appear to be rendered correctly.");
+      is($all(".table-chart-container[placeholder=true]").length, 2,
+        "Two placeholder table chart appear to be rendered correctly.");
+
+      promise.all([
+        waitFor(panel, EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
+        waitFor(panel, EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
+      ]).then(() => {
+        is($("#primed-cache-chart").childNodes.length, 1,
+          "There should be a real primed cache chart created now.");
+        is($("#empty-cache-chart").childNodes.length, 1,
+          "There should be a real empty cache chart created now.");
+
+        is($all(".pie-chart-container:not([placeholder=true])").length, 2,
+          "Two real pie chart appear to be rendered correctly.");
+        is($all(".table-chart-container:not([placeholder=true])").length, 2,
+          "Two real table chart appear to be rendered correctly.");
+
+        teardown(aMonitor).then(finish);
+      });
+    });
+
+    NetMonitorView.toggleFrontendMode();
+
+    is(NetMonitorView.currentFrontendMode, "network-statistics-view",
+      "The current frontend mode is correct.");
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_statistics-02.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the network inspector view is shown when the target navigates
+ * away while in the statistics view.
+ */
+
+function test() {
+  initNetMonitor(STATISTICS_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let panel = aMonitor.panelWin;
+    let { document, EVENTS, NetMonitorView } = panel;
+
+    is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+      "The initial frontend mode is correct.");
+
+    promise.all([
+      waitFor(panel, EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
+      waitFor(panel, EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
+    ]).then(() => {
+      is(NetMonitorView.currentFrontendMode, "network-statistics-view",
+        "The frontend mode is currently in the statistics view.");
+
+      waitFor(panel, EVENTS.TARGET_WILL_NAVIGATE).then(() => {
+        is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+          "The frontend mode switched back to the inspector view.");
+
+        waitFor(panel, EVENTS.TARGET_DID_NAVIGATE).then(() => {
+          is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+            "The frontend mode is still in the inspector view.");
+
+          teardown(aMonitor).then(finish);
+        });
+      });
+
+      aDebuggee.location.reload();
+    });
+
+    NetMonitorView.toggleFrontendMode();
+  });
+}
--- a/browser/devtools/netmonitor/test/head.js
+++ b/browser/devtools/netmonitor/test/head.js
@@ -24,16 +24,17 @@ const POST_RAW_URL = EXAMPLE_URL + "html
 const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html";
 const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html";
 const JSON_MALFORMED_URL = EXAMPLE_URL + "html_json-malformed-test-page.html";
 const JSON_CUSTOM_MIME_URL = EXAMPLE_URL + "html_json-custom-mime-test-page.html";
 const SORTING_URL = EXAMPLE_URL + "html_sorting-test-page.html";
 const FILTERING_URL = EXAMPLE_URL + "html_filter-test-page.html";
 const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html";
 const CUSTOM_GET_URL = EXAMPLE_URL + "html_custom-get-page.html";
+const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html";
 
 const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
 const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";
 const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs";
 const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs";
 
 const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_statistics-test-page.html
@@ -0,0 +1,37 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Network Monitor test page</title>
+  </head>
+
+  <body>
+    <p>Statistics test</p>
+
+    <script type="text/javascript">
+      function get(aAddress) {
+        var xhr = new XMLHttpRequest();
+        xhr.open("GET", aAddress, true);
+        xhr.send(null);
+      }
+
+      get("sjs_content-type-test-server.sjs?sts=304&fmt=txt");
+      get("sjs_content-type-test-server.sjs?sts=304&fmt=xml");
+      get("sjs_content-type-test-server.sjs?sts=304&fmt=html");
+      get("sjs_content-type-test-server.sjs?sts=304&fmt=css");
+      get("sjs_content-type-test-server.sjs?sts=304&fmt=js");
+      get("sjs_content-type-test-server.sjs?sts=304&fmt=json");
+      get("sjs_content-type-test-server.sjs?sts=304&fmt=jsonp");
+      get("sjs_content-type-test-server.sjs?sts=304&fmt=font");
+      get("sjs_content-type-test-server.sjs?sts=304&fmt=image");
+      get("sjs_content-type-test-server.sjs?sts=304&fmt=audio");
+      get("sjs_content-type-test-server.sjs?sts=304&fmt=video");
+      get("sjs_content-type-test-server.sjs?sts=304&fmt=flash");
+      get("test-image.png");
+    </script>
+  </body>
+
+</html>
--- a/browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs
+++ b/browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs
@@ -2,128 +2,161 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const { classes: Cc, interfaces: Ci } = Components;
 
 function handleRequest(request, response) {
   response.processAsync();
 
   let params = request.queryString.split("&");
-  let format = params.filter((s) => s.contains("fmt="))[0].split("=")[1];
+  let format = (params.filter((s) => s.contains("fmt="))[0] || "").split("=")[1];
+  let status = (params.filter((s) => s.contains("sts="))[0] || "").split("=")[1] || 200;
+
+  let cachedCount = 0;
+  let cacheExpire = 60; // seconds
+
+  function maybeMakeCached() {
+    if (status != 304) {
+      return;
+    }
+    // Spice things up a little!
+    if (cachedCount % 2) {
+      response.setHeader("Cache-Control", "max-age=" + cacheExpire, false);
+    } else {
+      response.setHeader("Expires", Date(Date.now() + cacheExpire * 1000), false);
+    }
+    cachedCount++;
+  }
 
   Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer).initWithCallback(() => {
     switch (format) {
       case "txt": {
-        response.setStatusLine(request.httpVersion, 200, "DA DA DA");
+        response.setStatusLine(request.httpVersion, status, "DA DA DA");
         response.setHeader("Content-Type", "text/plain", false);
+        maybeMakeCached();
         response.write("Братан, ты вообще качаешься?");
         response.finish();
         break;
       }
       case "xml": {
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/xml; charset=utf-8", false);
+        maybeMakeCached();
         response.write("<label value='greeting'>Hello XML!</label>");
         response.finish();
         break;
       }
       case "html": {
         let content = params.filter((s) => s.contains("res="))[0].split("=")[1];
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+        maybeMakeCached();
         response.write(content || "<p>Hello HTML!</p>");
         response.finish();
         break;
       }
       case "html-long": {
         let str = new Array(102400 /* 100 KB in bytes */).join(".");
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+        maybeMakeCached();
         response.write("<p>" + str + "</p>");
         response.finish();
         break;
       }
       case "css": {
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/css; charset=utf-8", false);
+        maybeMakeCached();
         response.write("body:pre { content: 'Hello CSS!' }");
         response.finish();
         break;
       }
       case "js": {
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "application/javascript; charset=utf-8", false);
+        maybeMakeCached();
         response.write("function() { return 'Hello JS!'; }");
         response.finish();
         break;
       }
       case "json": {
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "application/json; charset=utf-8", false);
+        maybeMakeCached();
         response.write("{ \"greeting\": \"Hello JSON!\" }");
         response.finish();
         break;
       }
       case "jsonp": {
         let fun = params.filter((s) => s.contains("jsonp="))[0].split("=")[1];
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/json; charset=utf-8", false);
+        maybeMakeCached();
         response.write(fun + "({ \"greeting\": \"Hello JSONP!\" })");
         response.finish();
         break;
       }
       case "json-long": {
         let str = "{ \"greeting\": \"Hello long string JSON!\" },";
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/json; charset=utf-8", false);
+        maybeMakeCached();
         response.write("[" + new Array(2048).join(str).slice(0, -1) + "]");
         response.finish();
         break;
       }
       case "json-malformed": {
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/json; charset=utf-8", false);
+        maybeMakeCached();
         response.write("{ \"greeting\": \"Hello malformed JSON!\" },");
         response.finish();
         break;
       }
       case "json-custom-mime": {
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/x-bigcorp-json; charset=utf-8", false);
+        maybeMakeCached();
         response.write("{ \"greeting\": \"Hello oddly-named JSON!\" }");
         response.finish();
         break;
       }
       case "font": {
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "font/woff", false);
+        maybeMakeCached();
         response.finish();
         break;
       }
       case "image": {
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "image/png", false);
+        maybeMakeCached();
         response.finish();
         break;
       }
       case "audio": {
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "audio/ogg", false);
+        maybeMakeCached();
         response.finish();
         break;
       }
       case "video": {
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "video/webm", false);
+        maybeMakeCached();
         response.finish();
         break;
       }
       case "flash": {
-        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "application/x-shockwave-flash", false);
+        maybeMakeCached();
         response.finish();
         break;
       }
       default: {
         response.setStatusLine(request.httpVersion, 404, "Not Found");
         response.setHeader("Content-Type", "text/html; charset=utf-8", false);
         response.write("<blink>Not Found</blink>");
         response.finish();
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/widgets/Chart.jsm
@@ -0,0 +1,422 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const NET_STRINGS_URI = "chrome://browser/locale/devtools/netmonitor.properties";
+const SVG_NS = "http://www.w3.org/2000/svg";
+const PI = Math.PI;
+const TAU = PI * 2;
+const EPSILON = 0.0000001;
+const NAMED_SLICE_MIN_ANGLE = TAU / 8;
+const NAMED_SLICE_TEXT_DISTANCE_RATIO = 1.9;
+const HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO = 20;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+
+this.EXPORTED_SYMBOLS = ["Chart"];
+
+/**
+ * Localization convenience methods.
+ */
+let L10N = new ViewHelpers.L10N(NET_STRINGS_URI);
+
+/**
+ * A factory for creating charts.
+ * Example usage: let myChart = Chart.Pie(document, { ... });
+ */
+let Chart = {
+  Pie: createPieChart,
+  Table: createTableChart,
+  PieTable: createPieTableChart
+};
+
+/**
+ * A simple pie chart proxy for the underlying view.
+ * Each item in the `slices` property represents a [data, node] pair containing
+ * the data used to create the slice and the nsIDOMNode displaying it.
+ *
+ * @param nsIDOMNode node
+ *        The node representing the view for this chart.
+ */
+function PieChart(node) {
+  this.node = node;
+  this.slices = new WeakMap();
+  EventEmitter.decorate(this);
+}
+
+/**
+ * A simple table chart proxy for the underlying view.
+ * Each item in the `rows` property represents a [data, node] pair containing
+ * the data used to create the row and the nsIDOMNode displaying it.
+ *
+ * @param nsIDOMNode node
+ *        The node representing the view for this chart.
+ */
+function TableChart(node) {
+  this.node = node;
+  this.rows = new WeakMap();
+  EventEmitter.decorate(this);
+}
+
+/**
+ * A simple pie+table chart proxy for the underlying view.
+ *
+ * @param nsIDOMNode node
+ *        The node representing the view for this chart.
+ * @param PieChart pie
+ *        The pie chart proxy.
+ * @param TableChart table
+ *        The table chart proxy.
+ */
+function PieTableChart(node, pie, table) {
+  this.node = node;
+  this.pie = pie;
+  this.table = table;
+  EventEmitter.decorate(this);
+}
+
+/**
+ * Creates the DOM for a pie+table chart.
+ *
+ * @param nsIDocument document
+ *        The document responsible with creating the DOM.
+ * @param object
+ *        An object containing all or some of the following properties:
+ *          - title: a string displayed as the table chart's (description)/local
+ *          - diameter: the diameter of the pie chart, in pixels
+ *          - data: an array of items used to display each slice in the pie
+ *                  and each row in the table;
+ *                  @see `createPieChart` and `createTableChart` for details.
+ *          - sorted: a flag specifying if the `data` should be sorted
+ *                    ascending by `size`.
+ *          - totals: @see `createTableChart` for details.
+ * @return PieTableChart
+ *         A pie+table chart proxy instance, which emits the following events:
+ *           - "mouseenter", when the mouse enters a slice or a row
+ *           - "mouseleave", when the mouse leaves a slice or a row
+ *           - "click", when the mouse enters a slice or a row
+ */
+function createPieTableChart(document, { sorted, title, diameter, data, totals }) {
+  if (sorted) {
+    data = data.slice().sort((a, b) => +(parseFloat(a.size) < parseFloat(b.size)));
+  }
+
+  let pie = Chart.Pie(document, {
+    width: diameter,
+    data: data
+  });
+
+  let table = Chart.Table(document, {
+    title: title,
+    data: data,
+    totals: totals
+  });
+
+  let container = document.createElement("hbox");
+  container.className = "pie-table-chart-container";
+  container.appendChild(pie.node);
+  container.appendChild(table.node);
+
+  let proxy = new PieTableChart(container, pie, table);
+
+  pie.on("click", (event, item) => {
+    proxy.emit(event, item)
+  });
+
+  table.on("click", (event, item) => {
+    proxy.emit(event, item)
+  });
+
+  pie.on("mouseenter", (event, item) => {
+    proxy.emit(event, item);
+    if (table.rows.has(item)) {
+      table.rows.get(item).setAttribute("focused", "");
+    }
+  });
+
+  pie.on("mouseleave", (event, item) => {
+    proxy.emit(event, item);
+    if (table.rows.has(item)) {
+      table.rows.get(item).removeAttribute("focused");
+    }
+  });
+
+  table.on("mouseenter", (event, item) => {
+    proxy.emit(event, item);
+    if (pie.slices.has(item)) {
+      pie.slices.get(item).setAttribute("focused", "");
+    }
+  });
+
+  table.on("mouseleave", (event, item) => {
+    proxy.emit(event, item);
+    if (pie.slices.has(item)) {
+      pie.slices.get(item).removeAttribute("focused");
+    }
+  });
+
+  return proxy;
+}
+
+/**
+ * Creates the DOM for a pie chart based on the specified properties.
+ *
+ * @param nsIDocument document
+ *        The document responsible with creating the DOM.
+ * @param object
+ *        An object containing all or some of the following properties:
+ *          - data: an array of items used to display each slice; all the items
+ *                  should be objects containing a `size` and a `label` property.
+ *                  e.g: [{
+ *                    size: 1,
+ *                    label: "foo"
+ *                  }, {
+ *                    size: 2,
+ *                    label: "bar"
+ *                  }];
+ *          - width: the width of the chart, in pixels
+ *          - height: optional, the height of the chart, in pixels.
+ *          - centerX: optional, the X-axis center of the chart, in pixels.
+ *          - centerY: optional, the Y-axis center of the chart, in pixels.
+ *          - radius: optional, the radius of the chart, in pixels.
+ * @return PieChart
+ *         A pie chart proxy instance, which emits the following events:
+ *           - "mouseenter", when the mouse enters a slice
+ *           - "mouseleave", when the mouse leaves a slice
+ *           - "click", when the mouse clicks a slice
+ */
+function createPieChart(document, { data, width, height, centerX, centerY, radius }) {
+  height = height || width;
+  centerX = centerX || width / 2;
+  centerY = centerY || height / 2;
+  radius = radius || (width + height) / 4;
+  let isPlaceholder = false;
+
+  // Filter out very small sizes, as they'll just render invisible slices.
+  data = data ? data.filter(e => parseFloat(e.size) > EPSILON) : null;
+
+  // If there's no data available, display an empty placeholder.
+  if (!data || !data.length) {
+    data = emptyPieChartData;
+    isPlaceholder = true;
+  }
+
+  let container = document.createElementNS(SVG_NS, "svg");
+  container.setAttribute("class", "generic-chart-container pie-chart-container");
+  container.setAttribute("pack", "center");
+  container.setAttribute("flex", "1");
+  container.setAttribute("width", width);
+  container.setAttribute("height", height);
+  container.setAttribute("viewBox", "0 0 " + width + " " + height);
+  container.setAttribute("slices", data.length);
+  container.setAttribute("placeholder", isPlaceholder);
+
+  let proxy = new PieChart(container);
+
+  let total = data.reduce((acc, e) => acc + parseFloat(e.size), 0);
+  let angles = data.map(e => parseFloat(e.size) / total * (TAU - EPSILON));
+  let largest = data.reduce((a, b) => parseFloat(a.size) > parseFloat(b.size) ? a : b);
+  let smallest = data.reduce((a, b) => parseFloat(a.size) < parseFloat(b.size) ? a : b);
+
+  let textDistance = radius / NAMED_SLICE_TEXT_DISTANCE_RATIO;
+  let translateDistance = radius / HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO;
+  let startAngle = TAU;
+  let endAngle = 0;
+  let midAngle = 0;
+  radius -= translateDistance;
+
+  for (let i = data.length - 1; i >= 0; i--) {
+    let sliceInfo = data[i];
+    let sliceAngle = angles[i];
+    if (!sliceInfo.size || sliceAngle < EPSILON) {
+      continue;
+    }
+
+    endAngle = startAngle - sliceAngle;
+    midAngle = (startAngle + endAngle) / 2;
+
+    let x1 = centerX + radius * Math.sin(startAngle);
+    let y1 = centerY - radius * Math.cos(startAngle);
+    let x2 = centerX + radius * Math.sin(endAngle);
+    let y2 = centerY - radius * Math.cos(endAngle);
+    let largeArcFlag = Math.abs(startAngle - endAngle) > PI ? 1 : 0;
+
+    let pathNode = document.createElementNS(SVG_NS, "path");
+    pathNode.setAttribute("class", "pie-chart-slice chart-colored-blob");
+    pathNode.setAttribute("name", sliceInfo.label);
+    pathNode.setAttribute("d",
+      " M " + centerX + "," + centerY +
+      " L " + x2 + "," + y2 +
+      " A " + radius + "," + radius +
+      " 0 " + largeArcFlag +
+      " 1 " + x1 + "," + y1 +
+      " Z");
+
+    if (sliceInfo == largest) {
+      pathNode.setAttribute("largest", "");
+    }
+    if (sliceInfo == smallest) {
+      pathNode.setAttribute("smallest", "");
+    }
+
+    let hoverX = translateDistance * Math.sin(midAngle);
+    let hoverY = -translateDistance * Math.cos(midAngle);
+    let hoverTransform = "transform: translate(" + hoverX + "px, " + hoverY + "px)";
+    pathNode.setAttribute("style", hoverTransform);
+
+    proxy.slices.set(sliceInfo, pathNode);
+    delegate(proxy, ["click", "mouseenter", "mouseleave"], pathNode, sliceInfo);
+    container.appendChild(pathNode);
+
+    if (sliceInfo.label && sliceAngle > NAMED_SLICE_MIN_ANGLE) {
+      let textX = centerX + textDistance * Math.sin(midAngle);
+      let textY = centerY - textDistance * Math.cos(midAngle);
+      let label = document.createElementNS(SVG_NS, "text");
+      label.appendChild(document.createTextNode(sliceInfo.label));
+      label.setAttribute("class", "pie-chart-label");
+      label.setAttribute("style", data.length > 1 ? hoverTransform : "");
+      label.setAttribute("x", data.length > 1 ? textX : centerX);
+      label.setAttribute("y", data.length > 1 ? textY : centerY);
+      container.appendChild(label);
+    }
+
+    startAngle = endAngle;
+  }
+
+  return proxy;
+}
+
+/**
+ * Creates the DOM for a table chart based on the specified properties.
+ *
+ * @param nsIDocument document
+ *        The document responsible with creating the DOM.
+ * @param object
+ *        An object containing all or some of the following properties:
+ *          - title: a string displayed as the chart's (description)/local
+ *          - data: an array of items used to display each row; all the items
+ *                  should be objects representing columns, for which the
+ *                  properties' values will be displayed in each cell of a row.
+ *                  e.g: [{
+ *                    size: 1,
+ *                    label2: "1foo",
+ *                    label3: "2yolo"
+ *                  }, {
+ *                    size: 2,
+ *                    label2: "3bar",
+ *                    label3: "4swag"
+ *                  }];
+ *          - totals: an object specifying for which rows in the `data` array
+ *                    the sum of their cells is to be displayed in the chart;
+ *                    e.g: {
+ *                      label1: "Total size: %S",
+ *                      label3: "Total lolz: %S"
+ *                    }
+ * @return TableChart
+ *         A table chart proxy instance, which emits the following events:
+ *           - "mouseenter", when the mouse enters a row
+ *           - "mouseleave", when the mouse leaves a row
+ *           - "click", when the mouse clicks a row
+ */
+function createTableChart(document, { data, totals, title }) {
+  let isPlaceholder = false;
+
+  // If there's no data available, display an empty placeholder.
+  if (!data || !data.length) {
+    data = emptyTableChartData;
+    isPlaceholder = true;
+  }
+
+  let container = document.createElement("vbox");
+  container.className = "generic-chart-container table-chart-container";
+  container.setAttribute("pack", "center");
+  container.setAttribute("flex", "1");
+  container.setAttribute("rows", data.length);
+  container.setAttribute("placeholder", isPlaceholder);
+
+  let proxy = new TableChart(container);
+
+  let titleNode = document.createElement("label");
+  titleNode.className = "plain table-chart-title";
+  titleNode.setAttribute("value", title);
+  container.appendChild(titleNode);
+
+  let tableNode = document.createElement("vbox");
+  tableNode.className = "plain table-chart-grid";
+  container.appendChild(tableNode);
+
+  for (let rowInfo of data) {
+    let rowNode = document.createElement("hbox");
+    rowNode.className = "table-chart-row";
+    rowNode.setAttribute("align", "center");
+
+    let boxNode = document.createElement("hbox");
+    boxNode.className = "table-chart-row-box chart-colored-blob";
+    boxNode.setAttribute("name", rowInfo.label);
+    rowNode.appendChild(boxNode);
+
+    for (let [key, value] in Iterator(rowInfo)) {
+      let labelNode = document.createElement("label");
+      labelNode.className = "plain table-chart-row-label";
+      labelNode.setAttribute("name", key);
+      labelNode.setAttribute("value", value);
+      rowNode.appendChild(labelNode);
+    }
+
+    proxy.rows.set(rowInfo, rowNode);
+    delegate(proxy, ["click", "mouseenter", "mouseleave"], rowNode, rowInfo);
+    tableNode.appendChild(rowNode);
+  }
+
+  let totalsNode = document.createElement("vbox");
+  totalsNode.className = "table-chart-totals";
+
+  for (let [key, value] in Iterator(totals || {})) {
+    let total = data.reduce((acc, e) => acc + parseFloat(e[key]), 0);
+    let formatted = !isNaN(total) ? L10N.numberWithDecimals(total, 2) : 0;
+    let labelNode = document.createElement("label");
+    labelNode.className = "plain table-chart-summary-label";
+    labelNode.setAttribute("name", key);
+    labelNode.setAttribute("value", value.replace("%S", formatted));
+    totalsNode.appendChild(labelNode);
+  }
+
+  container.appendChild(totalsNode);
+
+  return proxy;
+}
+
+XPCOMUtils.defineLazyGetter(this, "emptyPieChartData", () => {
+  return [{ size: 1, label: L10N.getStr("pieChart.empty") }];
+});
+
+XPCOMUtils.defineLazyGetter(this, "emptyTableChartData", () => {
+  return [{ size: "", label: L10N.getStr("tableChart.empty") }];
+});
+
+/**
+ * Delegates DOM events emitted by an nsIDOMNode to an EventEmitter proxy.
+ *
+ * @param EventEmitter emitter
+ *        The event emitter proxy instance.
+ * @param array events
+ *        An array of events, e.g. ["mouseenter", "mouseleave"].
+ * @param nsIDOMNode node
+ *        The element firing the DOM events.
+ * @param any args
+ *        The arguments passed when emitting events through the proxy.
+ */
+function delegate(emitter, events, node, args) {
+  for (let event of events) {
+    node.addEventListener(event, emitter.emit.bind(emitter, event, args));
+  }
+}
--- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
@@ -6,19 +6,24 @@
 <!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
 
 <!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
   - keep it in English, or another language commonly spoken among web developers.
   - You want to make that choice consistent across the developer tools.
   - A good criteria is the language in which you'd find the best
   - documentation on web development on the web. -->
 
-<!-- LOCALIZATION NOTE (netmonitorUI.emptyNotice2): This is the label displayed
+<!-- LOCALIZATION NOTE (netmonitorUI.perfNotice1/2): These are the labels displayed
+  -  in the network table when empty to start performance analysis. -->
+<!ENTITY netmonitorUI.perfNotice1         "• Click on the">
+<!ENTITY netmonitorUI.perfNotice2         "button to start performance analysis.">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.emptyNotice3): This is the label displayed
   -  in the network table when empty. -->
-<!ENTITY netmonitorUI.emptyNotice2        "Perform a request or reload the page to see detailed information about network activity.">
+<!ENTITY netmonitorUI.emptyNotice3        "• Perform a request or reload the page to see detailed information about network activity.">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.status2): This is the label displayed
   -  in the network table toolbar, above the "status" column. -->
 <!ENTITY netmonitorUI.toolbar.status2     "✓">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.method): This is the label displayed
   -  in the network table toolbar, above the "method" column. -->
 <!ENTITY netmonitorUI.toolbar.method      "Method">
@@ -94,20 +99,28 @@
 <!-- LOCALIZATION NOTE (debuggerUI.footer.filterMedia): This is the label displayed
   -  in the network details footer for the "Media" filtering button. -->
 <!ENTITY netmonitorUI.footer.filterMedia  "Media">
 
 <!-- LOCALIZATION NOTE (debuggerUI.footer.filterFlash): This is the label displayed
   -  in the network details footer for the "Flash" filtering button. -->
 <!ENTITY netmonitorUI.footer.filterFlash  "Flash">
 
+<!-- LOCALIZATION NOTE (debuggerUI.footer.filterOther): This is the label displayed
+  -  in the network details footer for the "Other" filtering button. -->
+<!ENTITY netmonitorUI.footer.filterOther  "Other">
+
 <!-- LOCALIZATION NOTE (debuggerUI.footer.clear): This is the label displayed
   -  in the network details footer for the "Clear" button. -->
 <!ENTITY netmonitorUI.footer.clear  "Clear">
 
+<!-- LOCALIZATION NOTE (debuggerUI.footer.clear): This is the label displayed
+  -  in the network details footer for the performance analysis button. -->
+<!ENTITY netmonitorUI.footer.perf   "Toggle performance analysis...">
+
 <!-- LOCALIZATION NOTE (debuggerUI.panesButton.tooltip): This is the tooltip for
   -  the button that toggles the panes visible or hidden in the netmonitor UI. -->
 <!ENTITY netmonitorUI.panesButton.tooltip "Toggle network info">
 
 <!-- LOCALIZATION NOTE (debuggerUI.summary.url): This is the label displayed
   -  in the network details headers tab identifying the URL. -->
 <!ENTITY netmonitorUI.summary.url         "Request URL:">
 
@@ -168,42 +181,50 @@
   -  in a "wait" state. -->
 <!ENTITY netmonitorUI.timings.wait        "Waiting:">
 
 <!-- LOCALIZATION NOTE (debuggerUI.timings.receive): This is the label displayed
   -  in the network details timings tab identifying the amount of time spent
   -  in a "receive" state. -->
 <!ENTITY netmonitorUI.timings.receive     "Receiving:">
 
+<!-- LOCALIZATION NOTE (netmonitorUI.context.perfTools): This is the label displayed
+  -  on the context menu that shows the performance analysis tools -->
+<!ENTITY netmonitorUI.context.perfTools   "Start Performance Analysis...">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.context.perfTools.accesskey): This is the access key
+  -  for the performance analysis menu item displayed in the context menu for a request -->
+<!ENTITY netmonitorUI.context.perfTools.accesskey "S">
+
 <!-- LOCALIZATION NOTE (netmonitorUI.context.copyUrl): This is the label displayed
   -  on the context menu that copies the selected request's url -->
-<!ENTITY netmonitorUI.context.copyUrl      "Copy URL">
+<!ENTITY netmonitorUI.context.copyUrl     "Copy URL">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.context.copyUrl.accesskey): This is the access key
   -  for the Copy URL menu item displayed in the context menu for a request -->
-<!ENTITY netmonitorUI.context.copyUrl.accesskey  "C">
+<!ENTITY netmonitorUI.context.copyUrl.accesskey "C">
 
 <!-- LOCALIZATION NOTE (debuggerUI.summary.editAndResend): This is the label displayed
   -  on the button in the headers tab that opens a form to edit and resend the currently
      displayed request -->
-<!ENTITY netmonitorUI.summary.editAndResend      "Edit and Resend">
+<!ENTITY netmonitorUI.summary.editAndResend "Edit and Resend">
 
 <!-- LOCALIZATION NOTE (debuggerUI.summary.editAndResend.accesskey): This is the access key
   -  for the "Edit and Resend" menu item displayed in the context menu for a request -->
-<!ENTITY netmonitorUI.summary.editAndResend.accesskey  "R">
+<!ENTITY netmonitorUI.summary.editAndResend.accesskey "R">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.context.newTab):  This is the label
   -  for the Open in New Tab menu item displayed in the context menu of the
   -  network container  -->
 <!ENTITY netmonitorUI.context.newTab      "Open in New Tab">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.context.newTab.accesskey): This is the access key
   -  for the Open in New Tab menu item displayed in the context menu of the
   -  network container -->
-<!ENTITY netmonitorUI.context.newTab.accesskey  "O">
+<!ENTITY netmonitorUI.context.newTab.accesskey "O">
 
 <!-- LOCALIZATION NOTE (debuggerUI.custom.newRequest): This is the label displayed
   -  as the title of the new custom request form -->
 <!ENTITY netmonitorUI.custom.newRequest   "New Request">
 
 <!-- LOCALIZATION NOTE (debuggerUI.custom.query): This is the label displayed
   -  above the query string entry in the custom request form -->
 <!ENTITY netmonitorUI.custom.query        "Query String:">
@@ -218,8 +239,12 @@
 
 <!-- LOCALIZATION NOTE (debuggerUI.custom.send): This is the label displayed
   -  on the button which sends the custom request -->
 <!ENTITY netmonitorUI.custom.send         "Send">
 
 <!-- LOCALIZATION NOTE (debuggerUI.custom.cancel): This is the label displayed
   -  on the button which cancels and closes the custom request form -->
 <!ENTITY netmonitorUI.custom.cancel       "Cancel">
+
+<!-- LOCALIZATION NOTE (debuggerUI.backButton): This is the label displayed
+  -  on the button which exists the performance statistics view -->
+<!ENTITY netmonitorUI.backButton          "Back">
--- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.properties
@@ -130,8 +130,54 @@ networkMenu.millisecond=%S ms
 
 # LOCALIZATION NOTE (networkMenu.second): This is the label displayed
 # in the network menu specifying timing interval divisions (in seconds).
 networkMenu.second=%S s
 
 # LOCALIZATION NOTE (networkMenu.minute): This is the label displayed
 # in the network menu specifying timing interval divisions (in minutes).
 networkMenu.minute=%S min
+
+# LOCALIZATION NOTE (networkMenu.minute): This is the label displayed
+# in the network menu specifying timing interval divisions (in minutes).
+networkMenu.minute=%S min
+
+# LOCALIZATION NOTE (pieChart.empty): This is the label displayed
+# for pie charts (e.g., in the performance analysis view) when there is
+# no data available yet.
+pieChart.empty=Loading
+
+# LOCALIZATION NOTE (tableChart.empty): This is the label displayed
+# for table charts (e.g., in the performance analysis view) when there is
+# no data available yet.
+tableChart.empty=Please wait…
+
+# LOCALIZATION NOTE (charts.sizeKB): This is the label displayed
+# in pie or table charts specifying the size of a request (in kilobytes).
+charts.sizeKB=%S KB
+
+# LOCALIZATION NOTE (charts.totalMS): This is the label displayed
+# in pie or table charts specifying the time for a request to finish (in milliseconds).
+charts.totalMS=%S ms
+
+# LOCALIZATION NOTE (charts.cacheEnabled): This is the label displayed
+# in the performance analysis view for "cache enabled" charts.
+charts.cacheEnabled=Primed cache
+
+# LOCALIZATION NOTE (charts.cacheDisabled): This is the label displayed
+# in the performance analysis view for "cache disabled" charts.
+charts.cacheDisabled=Empty cache
+
+# LOCALIZATION NOTE (charts.totalSize): This is the label displayed
+# in the performance analysis view for total requests size, in kilobytes.
+charts.totalSize=Size: %S KB
+
+# LOCALIZATION NOTE (charts.totalTime): This is the label displayed
+# in the performance analysis view for total requests time, in milliseconds.
+charts.totalTime=Time: %S ms
+
+# LOCALIZATION NOTE (charts.totalCached): This is the label displayed
+# in the performance analysis view for total cached responses.
+charts.totalCached=Cached responses: %S
+
+# LOCALIZATION NOTE (charts.totalCount): This is the label displayed
+# in the performance analysis view for total requests.
+charts.totalCount=Total requests: %S
--- a/browser/themes/shared/devtools/netmonitor.inc.css
+++ b/browser/themes/shared/devtools/netmonitor.inc.css
@@ -1,27 +1,39 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
 #requests-menu-empty-notice {
   margin: 0;
   padding: 12px;
-  font-size: 110%;
+  font-size: 120%;
 }
 
 .theme-dark #requests-menu-empty-notice {
   color: #f5f7fa; /* Light foreground text */
 }
 
 .theme-light #requests-menu-empty-notice {
   color: #585959; /* Grey foreground text */
 }
 
+#requests-menu-perf-notice-button {
+  min-width: 30px;
+  min-height: 28px;
+  margin: 0;
+  list-style-image: url(profiler-stopwatch.png);
+  -moz-image-region: rect(0px,16px,16px,0px);
+}
+
+#requests-menu-perf-notice-button .button-text {
+  display: none;
+}
+
 %filter substitution
 %define table_itemDarkStartBorder rgba(0,0,0,0.2)
 %define table_itemDarkEndBorder rgba(128,128,128,0.15)
 %define table_itemLightStartBorder rgba(128,128,128,0.25)
 %define table_itemLightEndBorder transparent
 
 /* Network requests table */
 
@@ -470,22 +482,21 @@ box.requests-menu-status {
 
 #details-pane-toggle:active {
   -moz-image-region: rect(0px,32px,16px,16px);
 }
 
 /* Network request details tabpanels */
 
 .theme-dark .tabpanel-content {
+  background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
   color: #f5f7fa; /* Light foreground text */
 }
 
-.theme-dark .tabpanel-summary-label {
-  color: #f5f7fa; /* Dark foreground text */
-}
+/* Summary tabpanel */
 
 .tabpanel-summary-container {
   padding: 1px;
 }
 
 .tabpanel-summary-label {
   -moz-padding-start: 4px;
   -moz-padding-end: 3px;
@@ -573,41 +584,41 @@ box.requests-menu-status {
   border-top: solid 1px hsla(210,5%,5%,.3);
 }
 
 .requests-menu-footer-button,
 .requests-menu-footer-label {
   min-width: 1em;
   margin: 0;
   border: none;
-  padding: 2px 1.5vw;
+  padding: 2px 0.75vw;
 }
 
 .theme-dark .requests-menu-footer-button,
 .theme-dark .requests-menu-footer-label {
   color: #f5f7fa; /* Light foreground text */
 }
 
 .theme-light .requests-menu-footer-button,
 .theme-light .requests-menu-footer-label {
   color: #18191a; /* Dark foreground text */
 }
 
 .requests-menu-footer-spacer {
   min-width: 2px;
 }
 
-.theme-dark .requests-menu-footer-spacer:not(:first-of-type),
-.theme-dark .requests-menu-footer-button:not(:first-of-type) {
+.theme-dark .requests-menu-footer-spacer:not(:first-child),
+.theme-dark .requests-menu-footer-button:not(:first-child) {
   -moz-border-start: 1px solid @table_itemDarkStartBorder@;
   box-shadow: -1px 0 0 @table_itemDarkEndBorder@;
 }
 
-.theme-light .requests-menu-footer-spacer:not(:first-of-type),
-.theme-light .requests-menu-footer-button:not(:first-of-type) {
+.theme-light .requests-menu-footer-spacer:not(:first-child),
+.theme-light .requests-menu-footer-button:not(:first-child) {
   -moz-border-start: 1px solid @table_itemLightStartBorder@;
   box-shadow: -1px 0 0 @table_itemLightEndBorder@;
 }
 
 .requests-menu-footer-button {
   -moz-appearance: none;
   background: rgba(0,0,0,0.025);
 }
@@ -623,33 +634,202 @@ box.requests-menu-status {
 .requests-menu-footer-button:not(:active)[checked] {
   background-color: rgba(0,0,0,0.25);
   background-image: radial-gradient(farthest-side at center top, hsla(200,100%,70%,.7), hsla(200,100%,70%,0.3));
   background-size: 100% 1px;
   background-repeat: no-repeat;
 }
 
 .requests-menu-footer-label {
-  padding-top: 2px;
+  padding-top: 3px;
   font-weight: 600;
 }
 
+/* Performance analysis buttons */
+
+#requests-menu-network-summary-button {
+  background: none;
+  box-shadow: none;
+  border-color: transparent;
+  list-style-image: url(profiler-stopwatch.png);
+  -moz-image-region: rect(0px,16px,16px,0px);
+  -moz-padding-end: 0;
+  cursor: pointer;
+}
+
+#requests-menu-network-summary-label {
+  -moz-padding-start: 0;
+  cursor: pointer;
+}
+
+#requests-menu-network-summary-label:hover {
+  text-decoration: underline;
+}
+
+/* Performance analysis view */
+
+#network-statistics-toolbar {
+  border: none;
+  margin: 0;
+  padding: 0;
+}
+
+#network-statistics-back-button {
+  min-width: 4em;
+  min-height: 100vh;
+  margin: 0;
+  padding: 0;
+  border-radius: 0;
+  border-top: none;
+  border-bottom: none;
+  -moz-border-start: none;
+}
+
+#network-statistics-view-splitter {
+  border-color: rgba(0,0,0,0.2);
+  cursor: default;
+  pointer-events: none;
+}
+
+#network-statistics-charts {
+  min-height: 1px;
+}
+
+.theme-dark #network-statistics-charts {
+  background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
+}
+
+.theme-light #network-statistics-charts {
+  background: url(background-noise-toolbar.png), #f0f1f2; /* Toolbars */
+}
+
+#network-statistics-charts .pie-chart-container {
+  -moz-margin-start: 3vw;
+  -moz-margin-end: 1vw;
+}
+
+#network-statistics-charts .table-chart-container {
+  -moz-margin-start: 1vw;
+  -moz-margin-end: 3vw;
+}
+
+.theme-dark .chart-colored-blob[name=html] {
+  fill: #5e88b0; /* Blue-Grey highlight */
+  background: #5e88b0;
+}
+
+.theme-light .chart-colored-blob[name=html] {
+  fill: #5f88b0; /* Blue-Grey highlight */
+  background: #5f88b0;
+}
+
+.theme-dark .chart-colored-blob[name=css] {
+  fill: #46afe3; /* Blue highlight */
+  background: #46afe3;
+}
+
+.theme-light .chart-colored-blob[name=css] {
+  fill: #0088cc; /* Blue highlight */
+  background: #0088cc;
+}
+
+.theme-dark .chart-colored-blob[name=js] {
+  fill: #d99b28; /* Light Orange highlight */
+  background: #d99b28;
+}
+
+.theme-light .chart-colored-blob[name=js] {
+  fill: #d97e00; /* Light Orange highlight */
+  background: #d97e00;
+}
+
+.theme-dark .chart-colored-blob[name=xhr] {
+  fill: #d96629; /* Orange highlight */
+  background: #d96629;
+}
+
+.theme-light .chart-colored-blob[name=xhr] {
+  fill: #f13c00; /* Orange highlight */
+  background: #f13c00;
+}
+
+.theme-dark .chart-colored-blob[name=fonts] {
+  fill: #6b7abb; /* Purple highlight */
+  background: #6b7abb;
+}
+
+.theme-light .chart-colored-blob[name=fonts] {
+  fill: #5b5fff; /* Purple highlight */
+  background: #5b5fff;
+}
+
+.theme-dark .chart-colored-blob[name=images] {
+  fill: #df80ff; /* Pink highlight */
+  background: #df80ff;
+}
+
+.theme-light .chart-colored-blob[name=images] {
+  fill: #b82ee5; /* Pink highlight */
+  background: #b82ee5;
+}
+
+.theme-dark .chart-colored-blob[name=media] {
+  fill: #70bf53; /* Green highlight */
+  background: #70bf53;
+}
+
+.theme-light .chart-colored-blob[name=media] {
+  fill: #2cbb0f; /* Green highlight */
+  background: #2cbb0f;
+}
+
+.theme-dark .chart-colored-blob[name=flash] {
+  fill: #eb5368; /* Red highlight */
+  background: #eb5368;
+}
+
+.theme-light .chart-colored-blob[name=flash] {
+  fill: #ed2655; /* Red highlight */
+  background: #ed2655;
+}
+
+.table-chart-row-label[name=cached] {
+  display: none;
+}
+
+.table-chart-row-label[name=count] {
+  width: 3em;
+  text-align: end;
+}
+
+.table-chart-row-label[name=label] {
+  width: 7em;
+}
+
+.table-chart-row-label[name=size] {
+  width: 7em;
+}
+
+.table-chart-row-label[name=time] {
+  width: 7em;
+}
+
 /* Responsive sidebar */
 @media (max-width: 700px) {
   #requests-menu-toolbar {
     height: 22px;
   }
 
   .requests-menu-header-button {
     min-height: 20px;
   }
 
   .requests-menu-footer-button,
   .requests-menu-footer-label {
-    padding: 2px 2vw;
+    padding: 2px 1vw;
   }
 
   #details-pane {
     max-width: none;
     margin: 0 !important;
     /* To prevent all the margin hacks to hide the sidebar. */
   }
 
--- a/browser/themes/shared/devtools/toolbars.inc.css
+++ b/browser/themes/shared/devtools/toolbars.inc.css
@@ -718,17 +718,19 @@
 .theme-light .command-button > image,
 .theme-light .command-button:active > image,
 .theme-light .devtools-closebutton > image,
 .theme-light .devtools-toolbarbutton > image,
 .theme-light .devtools-option-toolbarbutton > image,
 .theme-light #breadcrumb-separator-normal,
 .theme-light .scrollbutton-up > .toolbarbutton-icon,
 .theme-light .scrollbutton-down > .toolbarbutton-icon,
-.theme-light #black-boxed-message-button .button-icon {
+.theme-light #black-boxed-message-button .button-icon,
+.theme-light #requests-menu-perf-notice-button .button-icon,
+.theme-light #requests-menu-network-summary-button .button-icon {
   filter: url(filters.svg#invert);
 }
 
 /* Since selected backgrounds are blue, we want to use the normal
  * (light) icons. */
 .theme-light .command-button[checked=true]:not(:active) > image,
 .theme-light .devtools-tab[selected] > image,
 .theme-light .devtools-tab[highlighted] > image,
--- a/browser/themes/shared/devtools/widgets.inc.css
+++ b/browser/themes/shared/devtools/widgets.inc.css
@@ -787,9 +787,150 @@
 .arrow[open] {
   -moz-appearance: treetwistyopen;
 }
 
 .arrow[invisible] {
   visibility: hidden;
 }
 
+/* Charts */
+
+.generic-chart-container {
+  /* Hack: force hardware acceleration */
+  transform: translateZ(1px);
+}
+
+.theme-dark .generic-chart-container {
+  color: #f5f7fa; /* Light foreground text */
+}
+
+.theme-light .generic-chart-container {
+  color: #585959; /* Grey foreground text */
+}
+
+.theme-dark .chart-colored-blob {
+  fill: #b8c8d9; /* Light content text */
+  background: #b8c8d9;
+}
+
+.theme-light .chart-colored-blob {
+  fill: #8fa1b2; /* Grey content text */
+  background: #8fa1b2;
+}
+
+/* Charts: Pie */
+
+.pie-chart-slice {
+  stroke-width: 1px;
+  cursor: pointer;
+}
+
+.theme-dark .pie-chart-slice {
+  stroke: rgba(0,0,0,0.2);
+}
+
+.theme-light .pie-chart-slice {
+  stroke: rgba(255,255,255,0.8);
+}
+
+.theme-dark .pie-chart-slice[largest] {
+  stroke-width: 2px;
+  stroke: #fff;
+}
+
+.theme-light .pie-chart-slice[largest] {
+  stroke: #000;
+}
+
+.pie-chart-label {
+  text-anchor: middle;
+  dominant-baseline: middle;
+  pointer-events: none;
+}
+
+.theme-dark .pie-chart-label {
+  fill: #000;
+}
+
+.theme-light .pie-chart-label {
+  fill: #fff;
+}
+
+.pie-chart-container[slices="1"] > .pie-chart-slice {
+  stroke-width: 0px;
+}
+
+.pie-chart-slice,
+.pie-chart-label {
+  transition: all 0.1s ease-out;
+}
+
+.pie-chart-slice:not(:hover):not([focused]),
+.pie-chart-slice:not(:hover):not([focused]) + .pie-chart-label {
+  transform: none !important;
+}
+
+/* Charts: Table */
+
+.table-chart-title {
+  padding-bottom: 10px;
+  font-size: 120%;
+  font-weight: 600;
+}
+
+.table-chart-row {
+  margin-top: 1px;
+  cursor: pointer;
+}
+
+.table-chart-grid:hover > .table-chart-row {
+  transition: opacity 0.1s ease-in-out;
+}
+
+.table-chart-grid:not(:hover) > .table-chart-row {
+  transition: opacity 0.2s ease-in-out;
+}
+
+.generic-chart-container:hover > .table-chart-grid:hover > .table-chart-row:not(:hover),
+.generic-chart-container:hover ~ .table-chart-container > .table-chart-grid > .table-chart-row:not([focused]) {
+  opacity: 0.4;
+}
+
+.table-chart-row-box {
+  width: 8px;
+  height: 1.5em;
+  -moz-margin-end: 10px;
+}
+
+.table-chart-row-label {
+  width: 8em;
+  -moz-padding-end: 6px;
+  cursor: inherit;
+}
+
+.table-chart-totals {
+  margin-top: 8px;
+  padding-top: 6px;
+}
+
+.theme-dark .table-chart-totals {
+  border-top: 1px solid #b6babf; /* Grey foreground text */
+}
+
+.theme-light .table-chart-totals {
+  border-top: 1px solid #585959; /* Grey foreground text */
+}
+
+.table-chart-summary-label {
+  font-weight: 600;
+  padding: 1px 0px;
+}
+
+.theme-dark .table-chart-summary-label {
+  color: #f5f7fa; /* Light foreground text */
+}
+
+.theme-light .table-chart-summary-label {
+  color: #18191a; /* Dark foreground text */
+}
+
 %include ../../shared/devtools/app-manager/manifest-editor.inc.css
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -776,17 +776,22 @@ BrowserTabActor.prototype = {
       reload = true;
     }
     if (typeof options.cacheEnabled !== "undefined" &&
         options.cacheEnabled !== this._getCacheEnabled()) {
       this._setCacheEnabled(options.cacheEnabled);
       reload = true;
     }
 
-    if (reload) {
+    // Reload if:
+    //  - there's an explicit `performReload` flag and it's true
+    //  - there's no `performReload` flag, but it makes sense to do so
+    let hasExplicitReloadFlag = "performReload" in options;
+    if ((hasExplicitReloadFlag && options.performReload) ||
+       (!hasExplicitReloadFlag && reload)) {
       this.onReload();
     }
   },
 
   /**
    * Disable or enable the cache via docShell.
    */
   _setCacheEnabled: function(allow) {