Merge f-t to m-c
authorPhil Ringnalda <philringnalda@gmail.com>
Sun, 02 Feb 2014 09:10:57 -0800
changeset 166469 2918a9e625b4afb867d4afc860eee18932514232
parent 166446 3e40f7389d1b9a6d81427e0bcae6666a9963bddc (current diff)
parent 166468 463bae14bef347a349f6026a843a298371f478c0 (diff)
child 166539 5f88d54c28e03d8cab01968ff2d54d85ab521ef1
push id26127
push userphilringnalda@gmail.com
push dateSun, 02 Feb 2014 17:11:12 +0000
treeherdermozilla-central@2918a9e625b4 [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 f-t to m-c
browser/themes/linux/devtools/vview-delete.png
browser/themes/linux/devtools/vview-edit.png
browser/themes/osx/devtools/vview-delete.png
browser/themes/osx/devtools/vview-edit.png
browser/themes/windows/devtools/vview-delete.png
browser/themes/windows/devtools/vview-edit.png
--- a/addon-sdk/source/app-extension/install.rdf
+++ b/addon-sdk/source/app-extension/install.rdf
@@ -13,17 +13,17 @@
     <em:bootstrap>true</em:bootstrap>
     <em:unpack>false</em:unpack>
 
     <!-- Firefox -->
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>21.0</em:minVersion>
-        <em:maxVersion>25.0a1</em:maxVersion>
+        <em:maxVersion>29.0a1</em:maxVersion>
       </Description>
     </em:targetApplication>
 
     <!-- Front End MetaData -->
     <em:name>Test App</em:name>
     <em:description>Harness for tests.</em:description>
     <em:creator>Mozilla Corporation</em:creator>
     <em:homepageURL></em:homepageURL>
--- a/addon-sdk/source/lib/sdk/event/utils.js
+++ b/addon-sdk/source/lib/sdk/event/utils.js
@@ -261,15 +261,15 @@ exports.Reactor = Reactor;
  * used to be called `require('sdk/event/core').setListeners` on.
  * This strips all keys that would trigger a listener to be set.
  * 
  * @params {Object} object
  * @return {Object}
  */
 
 function stripListeners (object) {
-  return Object.keys(object).reduce((agg, key) => {
+  return Object.keys(object || {}).reduce((agg, key) => {
     if (!EVENT_TYPE_PATTERN.test(key))
       agg[key] = object[key];
     return agg;
   }, {});
 }
 exports.stripListeners = stripListeners;
--- a/addon-sdk/source/lib/sdk/l10n/prefs.js
+++ b/addon-sdk/source/lib/sdk/l10n/prefs.js
@@ -4,17 +4,17 @@
 "use strict";
 
 const { on } = require("../system/events");
 const core = require("./core");
 const { id: jetpackId} = require('../self');
 
 const OPTIONS_DISPLAYED = "addon-options-displayed";
 
-function onOptionsDisplayed({ subjec: document, data: addonId }) {
+function onOptionsDisplayed({ subject: document, data: addonId }) {
   if (addonId !== jetpackId)
     return;
   let query = 'setting[data-jetpack-id="' + jetpackId + '"][pref-name], ' +
               'button[data-jetpack-id="' + jetpackId + '"][pref-name]';
   let nodes = document.querySelectorAll(query);
   for (let node of nodes) {
     let name = node.getAttribute("pref-name");
     if (node.tagName == "setting") {
--- a/addon-sdk/source/lib/toolkit/loader.js
+++ b/addon-sdk/source/lib/toolkit/loader.js
@@ -688,17 +688,19 @@ exports.unload = unload;
 //   If `resolve` does not returns `uri` string exception will be thrown by
 //   an associated `require` call.
 const Loader = iced(function Loader(options) {
   let {
     modules, globals, resolve, paths, rootURI, manifest, requireMap, isNative
   } = override({
     paths: {},
     modules: {},
-    globals: {},
+    globals: {
+      console: console
+    },
     resolve: options.isNative ?
       exports.nodeResolve :
       exports.resolve,
   }, options);
 
   // We create an identity object that will be dispatched on an unload
   // event as subject. This way unload listeners will be able to assert
   // which loader is unloaded. Please note that we intentionally don't
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/simple-prefs-l10n/locale/en.properties
@@ -0,0 +1,5 @@
+# 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/.
+
+somePreference_title=A
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/simple-prefs-l10n/main.js
@@ -0,0 +1,57 @@
+/* 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 { Cu } = require('chrome');
+const sp = require('sdk/simple-prefs');
+const app = require('sdk/system/xul-app');
+const self = require('sdk/self');
+const tabs = require('sdk/tabs');
+const { preferencesBranch } = require('sdk/self');
+
+const { AddonManager } = Cu.import('resource://gre/modules/AddonManager.jsm', {});
+
+exports.testAOMLocalization = function(assert, done) {
+    tabs.open({
+    	url: 'about:addons',
+    	onReady: function(tab) {
+        tab.attach({
+          contentScriptWhen: 'end',
+        	contentScript: 'function onLoad() {\n' +
+                           'unsafeWindow.removeEventListener("load", onLoad, false);\n' +
+                           'AddonManager.getAddonByID("' + self.id + '", function(aAddon) {\n' +
+                             'unsafeWindow.gViewController.viewObjects.detail.node.addEventListener("ViewChanged", function whenViewChanges() {\n' +
+                               'unsafeWindow.gViewController.viewObjects.detail.node.removeEventListener("ViewChanged", whenViewChanges, false);\n' +
+                               'setTimeout(function() {\n' + // TODO: figure out why this is necessary..
+                                   'self.postMessage({\n' +
+                                     'somePreference: getAttributes(unsafeWindow.document.querySelector("setting[data-jetpack-id=\'' + self.id + '\']"))\n' +
+                                   '});\n' +
+                               '}, 250);\n' +
+                             '}, false);\n' +
+                             'unsafeWindow.gViewController.commands.cmd_showItemDetails.doCommand(aAddon, true);\n' +
+                           '});\n' +
+                           'function getAttributes(ele) {\n' +
+                             'if (!ele) return {};\n' +
+                             'return {\n' +
+                               'title: ele.getAttribute("title")\n' +
+                             '}\n' +
+                           '}\n' +
+                         '}\n' +
+                         // Wait for the load event ?
+                         'if (document.readyState == "complete") {\n' +
+                           'onLoad()\n' +
+                         '} else {\n' +
+                           'unsafeWindow.addEventListener("load", onLoad, false);\n' +
+                         '}\n',
+          onMessage: function(msg) {
+            // test somePreference
+            assert.equal(msg.somePreference.title, 'A', 'somePreference title is correct');
+            tab.close(done);
+          }
+        });
+    	}
+    });
+}
+
+require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/simple-prefs-l10n/package.json
@@ -0,0 +1,10 @@
+{
+  "id": "test-simple-prefs-l10n",
+  "preferences": [{
+    "name": "somePreference",
+    "title": "some-title",
+    "description": "Some short description for the preference",
+    "type": "string",
+    "value": "TEST"
+  }]
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/loader/globals/main.js
@@ -0,0 +1,6 @@
+/* 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';
+exports.console = console;
--- a/addon-sdk/source/test/test-loader.js
+++ b/addon-sdk/source/test/test-loader.js
@@ -322,9 +322,25 @@ exports['test invisibleToDebugger: true'
   try {
     dbg.addDebuggee(sandbox);
     assert.fail('debugger added invisible value');
   } catch(e) {
     assert.ok(true, 'debugger did not add invisible value');
   }
 };
 
+exports['test console global by default'] = function (assert) {
+  let uri = root + '/fixtures/loader/globals/';
+  let loader = Loader({ paths: { '': uri }});
+  let program = main(loader, 'main');
+ 
+  assert.ok(typeof program.console === 'object', 'global `console` exists');
+  assert.ok(typeof program.console.log === 'function', 'global `console.log` exists');
+
+  let loader2 = Loader({ paths: { '': uri }, globals: { console: fakeConsole }});
+  let program2 = main(loader2, 'main');
+
+  assert.equal(program2.console, fakeConsole,
+    'global console can be overridden with Loader options');
+  function fakeConsole () {};
+};
+
 require('test').run(exports);
--- a/addon-sdk/source/test/test-panel.js
+++ b/addon-sdk/source/test/test-panel.js
@@ -960,16 +960,23 @@ exports['test emits on url changes'] = f
   panel.port.emit('hi', 'hi')
   panel.port.on('bye', function(uri) {
     assert.equal(uri, uriB, 'message was delivered to new uri');
     loader.unload();
     done();
   });
 };
 
+exports['test panel can be constructed without any arguments'] = function (assert) {
+  const { Panel } = require('sdk/panel');
+
+  let panel = Panel();
+  assert.ok(true, "Creating a panel with no arguments does not throw");
+};
+
 if (isWindowPBSupported) {
   exports.testGetWindow = function(assert, done) {
     let activeWindow = getMostRecentBrowserWindow();
     open(null, { features: {
       toolbar: true,
       chrome: true,
       private: true
     } }).then(function(window) {
--- 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/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -994,17 +994,17 @@
                      class="toolbarbutton-1 chromeclass-toolbar-additional"
                      label="&syncToolbarButton.label;"
                      oncommand="gSyncUI.handleToolbarButton()"/>
 #endif
 
       <toolbarbutton id="tabview-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
                      label="&tabGroupsButton.label;"
                      command="Browser:ToggleTabView"
-                     tooltiptext="&tabGroupsButton.tooltip;"
+                     tooltip="dynamic-shortcut-tooltip"
                      observes="tabviewGroupsNumber"/>
     </toolbarpalette>
   </toolbox>
 
   <hbox id="fullscr-toggler" collapsed="true"/>
 
   <deck id="content-deck" flex="1">
     <hbox flex="1" id="browser">
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -1309,23 +1309,39 @@ let CustomizableUIInternal = {
       }
       let isInteractive = this._isOnInteractiveElement(aEvent);
       LOG("maybeAutoHidePanel: interactive ? " + isInteractive);
       if (isInteractive) {
         return;
       }
     }
 
-    if (aEvent.originalTarget.getAttribute("closemenu") == "none" ||
-        aEvent.originalTarget.getAttribute("widget-type") == "view") {
+    // We can't use event.target because we might have passed a panelview
+    // anonymous content boundary as well, and so target points to the
+    // panelmultiview in that case. Unfortunately, this means we get
+    // anonymous child nodes instead of the real ones, so looking for the 
+    // 'stoooop, don't close me' attributes is more involved.
+    let target = aEvent.originalTarget;
+    let closemenu = "auto";
+    let widgetType = "button";
+    while (target.localName != "panel") {
+      closemenu = target.getAttribute("closemenu");
+      widgetType = target.getAttribute("widget-type");
+      if (closemenu == "none" || closemenu == "single" ||
+          widgetType == "view") {
+        break;
+      }
+      target = target.parentNode;
+    }
+    if (closemenu == "none" || widgetType == "view") {
       return;
     }
 
-    if (aEvent.originalTarget.getAttribute("closemenu") == "single") {
-      let panel = this._getPanelForNode(aEvent.originalTarget);
+    if (closemenu == "single") {
+      let panel = this._getPanelForNode(target);
       let multiview = panel.querySelector("panelmultiview");
       if (multiview.showingSubView) {
         multiview.showMainView();
         return;
       }
     }
 
     // If we get here, we can actually hide the popup:
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -20,20 +20,24 @@ skip-if = os == "mac"
 [browser_887438_currentset_shim.js]
 [browser_888817_currentset_updating.js]
 [browser_889120_customize_tab_merging.js]
 [browser_890140_orphaned_placeholders.js]
 [browser_890262_destroyWidget_after_add_to_panel.js]
 [browser_892955_isWidgetRemovable_for_removed_widgets.js]
 [browser_892956_destroyWidget_defaultPlacements.js]
 [browser_909779_overflow_toolbars_new_window.js]
+skip-if = os == "linux"
+
 [browser_901207_searchbar_in_panel.js]
 [browser_913972_currentset_overflow.js]
+skip-if = os == "linux"
 
 [browser_914138_widget_API_overflowable_toolbar.js]
+skip-if = os == "linux"
 
 [browser_914863_disabled_help_quit_buttons.js]
 [browser_918049_skipintoolbarset_dnd.js]
 [browser_923857_customize_mode_event_wrapping_during_reset.js]
 [browser_927717_customize_drag_empty_toolbar.js]
 [browser_932928_show_notice_when_palette_empty.js]
 
 [browser_934113_menubar_removable.js]
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -1725,49 +1725,54 @@ let SourceUtils = {
 /**
  * Functions handling the variables bubble UI.
  */
 function VariableBubbleView() {
   dumpn("VariableBubbleView was instantiated");
 
   this._onMouseMove = this._onMouseMove.bind(this);
   this._onMouseLeave = this._onMouseLeave.bind(this);
-  this._onMouseScroll = this._onMouseScroll.bind(this);
   this._onPopupHiding = this._onPopupHiding.bind(this);
 }
 
 VariableBubbleView.prototype = {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function() {
     dumpn("Initializing the VariableBubbleView");
 
-    this._tooltip = new Tooltip(document);
     this._editorContainer = document.getElementById("editor");
-
+    this._editorContainer.addEventListener("mousemove", this._onMouseMove, false);
+    this._editorContainer.addEventListener("mouseleave", this._onMouseLeave, false);
+
+    this._tooltip = new Tooltip(document, {
+      closeOnEvents: [{
+        emitter: DebuggerController._toolbox,
+        event: "select"
+      }, {
+        emitter: this._editorContainer,
+        event: "scroll",
+        useCapture: true
+      }]
+    });
     this._tooltip.defaultPosition = EDITOR_VARIABLE_POPUP_POSITION;
     this._tooltip.defaultShowDelay = EDITOR_VARIABLE_HOVER_DELAY;
-
     this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding);
-    this._editorContainer.addEventListener("mousemove", this._onMouseMove, false);
-    this._editorContainer.addEventListener("mouseleave", this._onMouseLeave, false);
-    this._editorContainer.addEventListener("scroll", this._onMouseScroll, true);
   },
 
   /**
    * Destruction function, called when the debugger is closed.
    */
   destroy: function() {
     dumpn("Destroying the VariableBubbleView");
 
     this._tooltip.panel.removeEventListener("popuphiding", this._onPopupHiding);
     this._editorContainer.removeEventListener("mousemove", this._onMouseMove, false);
     this._editorContainer.removeEventListener("mouseleave", this._onMouseLeave, false);
-    this._editorContainer.removeEventListener("scroll", this._onMouseScroll, true);
   },
 
   /**
    * Searches for an identifier underneath the specified position in the
    * source editor, and if found, opens a VariablesView inspection popup.
    *
    * @param number x, y
    *        The left/top coordinates where to look for an identifier.
@@ -1903,17 +1908,17 @@ VariableBubbleView.prototype = {
         }
       }, [{
         label: L10N.getStr("addWatchExpressionButton"),
         className: "dbg-expression-button",
         command: () => {
           DebuggerView.VariableBubble.hideContents();
           DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
         }
-      }]);
+      }], DebuggerController._toolbox);
     }
 
     this._tooltip.show(this._markedText.anchor);
   },
 
   /**
    * Hides the inspection popup.
    */
@@ -1970,23 +1975,16 @@ VariableBubbleView.prototype = {
   /**
    * The mouseleave listener for the source editor container node.
    */
   _onMouseLeave: function() {
     clearNamedTimeout("editor-mouse-move");
   },
 
   /**
-   * The mousescroll listener for the source editor container node.
-   */
-  _onMouseScroll: function() {
-    this.hideContents();
-  },
-
-  /**
    * Listener handling the popup hiding event.
    */
   _onPopupHiding: function({ target }) {
     if (this._tooltip.panel != target) {
       return;
     }
     if (this._markedText) {
       this._markedText.clear();
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -164,16 +164,20 @@ let DebuggerView = {
       searchEnabled: Prefs.variablesSearchboxVisible,
       eval: (variable, value) => {
         let string = variable.evaluationMacro(variable, value);
         DebuggerController.StackFrames.evaluate(string);
       },
       lazyEmpty: true
     });
 
+    // Attach the current toolbox to the VView so it can link DOMNodes to
+    // the inspector/highlighter
+    this.Variables.toolbox = DebuggerController._toolbox;
+
     // Attach a controller that handles interfacing with the debugger protocol.
     VariablesViewController.attach(this.Variables, {
       getEnvironmentClient: aObject => gThreadClient.environment(aObject),
       getObjectClient: aObject => {
         return aObject instanceof DebuggerController.Tracer.WrappedObject
           ? DebuggerController.Tracer.syncGripClient(aObject.object)
           : gThreadClient.pauseGrip(aObject)
       }
--- a/browser/devtools/debugger/panel.js
+++ b/browser/devtools/debugger/panel.js
@@ -15,16 +15,17 @@ function DebuggerPanel(iframeWindow, too
   this.panelWin = iframeWindow;
   this._toolbox = toolbox;
   this._destroyer = null;
 
   this._view = this.panelWin.DebuggerView;
   this._controller = this.panelWin.DebuggerController;
   this._view._hostType = this._toolbox.hostType;
   this._controller._target = this.target;
+  this._controller._toolbox = this._toolbox;
 
   this.handleHostChanged = this.handleHostChanged.bind(this);
   this.highlightWhenPaused = this.highlightWhenPaused.bind(this);
   this.unhighlightWhenResumed = this.unhighlightWhenResumed.bind(this);
 
   EventEmitter.decorate(this);
 };
 
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -31,16 +31,17 @@ support-files =
   doc_auto-pretty-print-01.html
   doc_auto-pretty-print-02.html
   doc_binary_search.html
   doc_blackboxing.html
   doc_closures.html
   doc_cmd-break.html
   doc_cmd-dbg.html
   doc_conditional-breakpoints.html
+  doc_domnode-variables.html
   doc_editor-mode.html
   doc_empty-tab-01.html
   doc_empty-tab-02.html
   doc_event-listeners.html
   doc_event-listeners-02.html
   doc_frame-parameters.html
   doc_function-display-name.html
   doc_function-search.html
@@ -242,16 +243,17 @@ support-files =
 [browser_dbg_variables-view-popup-05.js]
 [browser_dbg_variables-view-popup-06.js]
 [browser_dbg_variables-view-popup-07.js]
 [browser_dbg_variables-view-popup-08.js]
 [browser_dbg_variables-view-popup-09.js]
 [browser_dbg_variables-view-popup-10.js]
 [browser_dbg_variables-view-popup-11.js]
 [browser_dbg_variables-view-popup-12.js]
+[browser_dbg_variables-view-popup-13.js]
 [browser_dbg_variables-view-reexpand-01.js]
 [browser_dbg_variables-view-reexpand-02.js]
 [browser_dbg_variables-view-webidl.js]
 [browser_dbg_watch-expressions-01.js]
 [browser_dbg_watch-expressions-02.js]
 [browser_dbg_chrome-create.js]
 skip-if = os == "linux" # Bug 847558
 [browser_dbg_on-pause-raise.js]
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-01.js
@@ -95,17 +95,17 @@ function testExpandVariables() {
     ok(thisVar.get("window").target.querySelector(".value").className.contains("token-other"),
       "Should have the right token class for 'window'.");
 
     is(thisVar.get("document").target.querySelector(".name").getAttribute("value"), "document",
       "Should have the right property name for 'document'.");
     is(thisVar.get("document").target.querySelector(".value").getAttribute("value"),
       "HTMLDocument \u2192 doc_frame-parameters.html",
       "Should have the right property value for 'document'.");
-    ok(thisVar.get("document").target.querySelector(".value").className.contains("token-other"),
+    ok(thisVar.get("document").target.querySelector(".value").className.contains("token-domnode"),
       "Should have the right token class for 'document'.");
 
     let argsProps = argsVar.target.querySelectorAll(".variables-view-property");
     is(argsProps.length, 8,
       "The 'arguments' variable should contain 5 enumerable and 3 non-enumerable properties");
 
     is(argsProps[0].querySelector(".name").getAttribute("value"), "0",
       "Should have the right property name for '0'.");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-popup-13.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the variable inspection popup has inspector links for DOMNode
+ * properties and that the popup closes when the link is clicked
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_domnode-variables.html";
+
+function test() {
+  Task.spawn(function() {
+    let [tab, debuggee, panel] = yield initDebugger(TAB_URL);
+    let win = panel.panelWin;
+    let bubble = win.DebuggerView.VariableBubble;
+    let tooltip = bubble._tooltip.panel;
+    let toolbox = gDevTools.getToolbox(panel.target);
+
+    function getDomNodeInTooltip(propertyName) {
+      let domNodeProperties = tooltip.querySelectorAll(".token-domnode");
+      for (let prop of domNodeProperties) {
+        let propName = prop.parentNode.querySelector(".name");
+        if (propName.getAttribute("value") === propertyName) {
+          ok(true, "DOMNode " + propertyName + " was found in the tooltip");
+          return prop;
+        }
+      }
+      ok(false, "DOMNode " + propertyName + " wasn't found in the tooltip");
+    }
+
+    // Allow this generator function to yield first.
+    executeSoon(() => debuggee.start());
+    yield waitForSourceAndCaretAndScopes(panel, ".html", 19);
+
+    // Inspect the div DOM variable.
+    yield openVarPopup(panel, { line: 17, ch: 38 }, true);
+    let property = getDomNodeInTooltip("firstElementChild");
+
+    // Simulate mouseover on the property value
+    let highlighted = once(toolbox, "node-highlight");
+    EventUtils.sendMouseEvent({ type: "mouseover" }, property,
+      property.ownerDocument.defaultView);
+    yield highlighted;
+    ok(true, "The node-highlight event was fired on hover of the DOMNode");
+
+    // Simulate a click on the "select in inspector" button
+    let button = property.parentNode.querySelector(".variables-view-open-inspector");
+    ok(button, "The select-in-inspector button is present");
+    let inspectorSelected = once(toolbox, "inspector-selected");
+    EventUtils.sendMouseEvent({ type: "mousedown" }, button,
+      button.ownerDocument.defaultView);
+    yield inspectorSelected;
+    ok(true, "The inspector got selected when clicked on the select-in-inspector");
+
+    // Make sure the inspector's initialization is finalized before ending the test
+    // Listening to the event *after* triggering the switch to the inspector isn't
+    // a problem as the inspector is asynchronously loaded.
+    yield once(toolbox.getPanel("inspector"), "inspector-updated");
+
+    yield resumeDebuggerThenCloseAndFinish(panel);
+  });
+}
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-webidl.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-webidl.js
@@ -59,32 +59,32 @@ function performTest() {
   let buttonVar = globalScope.get("button");
   let buttonAsProtoVar = globalScope.get("buttonAsProto");
   let documentVar = globalScope.get("document");
 
   is(buttonVar.target.querySelector(".name").getAttribute("value"), "button",
     "Should have the right property name for 'button'.");
   is(buttonVar.target.querySelector(".value").getAttribute("value"), "<button>",
     "Should have the right property value for 'button'.");
-  ok(buttonVar.target.querySelector(".value").className.contains("token-other"),
+  ok(buttonVar.target.querySelector(".value").className.contains("token-domnode"),
     "Should have the right token class for 'button'.");
 
   is(buttonAsProtoVar.target.querySelector(".name").getAttribute("value"), "buttonAsProto",
     "Should have the right property name for 'buttonAsProto'.");
   is(buttonAsProtoVar.target.querySelector(".value").getAttribute("value"), "Object",
     "Should have the right property value for 'buttonAsProto'.");
   ok(buttonAsProtoVar.target.querySelector(".value").className.contains("token-other"),
     "Should have the right token class for 'buttonAsProto'.");
 
   is(documentVar.target.querySelector(".name").getAttribute("value"), "document",
     "Should have the right property name for 'document'.");
   is(documentVar.target.querySelector(".value").getAttribute("value"),
     "HTMLDocument \u2192 doc_frame-parameters.html",
     "Should have the right property value for 'document'.");
-  ok(documentVar.target.querySelector(".value").className.contains("token-other"),
+  ok(documentVar.target.querySelector(".value").className.contains("token-domnode"),
     "Should have the right token class for 'document'.");
 
   is(buttonVar.expanded, false,
     "The buttonVar should not be expanded at this point.");
   is(buttonAsProtoVar.expanded, false,
     "The buttonAsProtoVar should not be expanded at this point.");
   is(documentVar.expanded, false,
     "The documentVar should not be expanded at this point.");
@@ -142,17 +142,17 @@ function performTest() {
       "Should have the right property value for '__proto__'.");
     ok(buttonProtoVar.target.querySelector(".value").className.contains("token-other"),
       "Should have the right token class for '__proto__'.");
 
     is(buttonAsProtoProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__",
       "Should have the right property name for '__proto__'.");
     is(buttonAsProtoProtoVar.target.querySelector(".value").getAttribute("value"), "<button>",
       "Should have the right property value for '__proto__'.");
-    ok(buttonAsProtoProtoVar.target.querySelector(".value").className.contains("token-other"),
+    ok(buttonAsProtoProtoVar.target.querySelector(".value").className.contains("token-domnode"),
       "Should have the right token class for '__proto__'.");
 
     is(documentProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__",
       "Should have the right property name for '__proto__'.");
     is(documentProtoVar.target.querySelector(".value").getAttribute("value"), "HTMLDocumentPrototype",
       "Should have the right property value for '__proto__'.");
     ok(documentProtoVar.target.querySelector(".value").className.contains("token-other"),
       "Should have the right token class for '__proto__'.");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/doc_domnode-variables.html
@@ -0,0 +1,24 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Debugger test page</title>
+  </head>
+
+  <body>
+    <div>Look at this DIV! Just look at it!</div>
+
+    <script type="text/javascript">
+      function start() {
+        var theDiv = document.querySelector("div");
+        var theBody = document.body;
+        var manyDomNodes = [theDiv, theBody];
+        debugger;
+      }
+    </script>
+  </body>
+
+</html>
--- a/browser/devtools/framework/test/browser_keybindings.js
+++ b/browser/devtools/framework/test/browser_keybindings.js
@@ -59,32 +59,16 @@ function test()
 
     gDevTools.once("toolbox-ready", (e, toolbox) => {
       inspectorShouldBeOpenAndHighlighting(toolbox.getCurrentPanel(), toolbox)
     });
 
     keysetMap.inspector.synthesizeKey();
   }
 
-  function moveMouseOver(aElement, aInspector, cb)
-  {
-    EventUtils.synthesizeMouse(aElement, 2, 2, {type: "mousemove"},
-      aElement.ownerDocument.defaultView);
-    aInspector.toolbox.once("picker-node-hovered", () => {
-      executeSoon(cb);
-    });
-  }
-
-  function isHighlighting()
-  {
-    let outline = gBrowser.selectedBrowser.parentNode
-      .querySelector(".highlighter-container .highlighter-outline");
-    return outline && !outline.hasAttribute("hidden");
-  }
-
   function inspectorShouldBeOpenAndHighlighting(aInspector, aToolbox)
   {
     is (aToolbox.currentToolId, "inspector", "Correct tool has been loaded");
 
     aToolbox.once("picker-started", () => {
       ok(true, "picker-started event received, highlighter started");
       keysetMap.inspector.synthesizeKey();
 
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -68,17 +68,17 @@ function Toolbox(target, selectedTool, h
   this._toolPanels = new Map();
   this._telemetry = new Telemetry();
 
   this._toolRegistered = this._toolRegistered.bind(this);
   this._toolUnregistered = this._toolUnregistered.bind(this);
   this._refreshHostTitle = this._refreshHostTitle.bind(this);
   this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this)
   this.destroy = this.destroy.bind(this);
-  this.stopPicker = this.stopPicker.bind(this);
+  this.highlighterUtils = new ToolboxHighlighterUtils(this);
 
   this._target.on("close", this.destroy);
 
   if (!hostType) {
     hostType = Services.prefs.getCharPref(this._prefs.LAST_HOST);
   }
   if (!selectedTool) {
     selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
@@ -187,17 +187,17 @@ Toolbox.prototype = {
     return parseFloat(Services.prefs.getCharPref(ZOOM_PREF));
   },
 
   /**
    * Get the toolbox highlighter front. Note that it may not always have been
    * initialized first. Use `initInspector()` if needed.
    */
   get highlighter() {
-    if (this.isRemoteHighlightable) {
+    if (this.highlighterUtils.isRemoteHighlightable) {
       return this._highlighter;
     } else {
       return null;
     }
   },
 
   /**
    * Get the toolbox's inspector front. Note that it may not always have been
@@ -552,18 +552,18 @@ Toolbox.prototype = {
     this._pickerButton = this.doc.createElement("toolbarbutton");
     this._pickerButton.id = "command-button-pick";
     this._pickerButton.className = "command-button";
     this._pickerButton.setAttribute("tooltiptext", toolboxStrings("pickButton.tooltip"));
 
     let container = this.doc.querySelector("#toolbox-buttons");
     container.appendChild(this._pickerButton);
 
-    this.togglePicker = this.togglePicker.bind(this);
-    this._pickerButton.addEventListener("command", this.togglePicker, false);
+    this._togglePicker = this.highlighterUtils.togglePicker.bind(this.highlighterUtils);
+    this._pickerButton.addEventListener("command", this._togglePicker, false);
   },
 
   /**
    * Build a tab for one tool definition and add to the toolbox
    *
    * @param {string} toolDefinition
    *        Tool definition of the tool to build a tab for.
    */
@@ -1031,17 +1031,17 @@ Toolbox.prototype = {
   initInspector: function() {
     let deferred = promise.defer();
 
     if (!this._inspector) {
       this._inspector = InspectorFront(this._target.client, this._target.form);
       this._inspector.getWalker().then(walker => {
         this._walker = walker;
         this._selection = new Selection(this._walker);
-        if (this.isRemoteHighlightable) {
+        if (this.highlighterUtils.isRemoteHighlightable) {
           this._inspector.getHighlighter().then(highlighter => {
             this._highlighter = highlighter;
             deferred.resolve();
           });
         } else {
           deferred.resolve();
         }
       });
@@ -1081,108 +1081,16 @@ Toolbox.prototype = {
     } else {
       deferred.resolve();
     }
 
     return deferred.promise;
   },
 
   /**
-   * Start/stop the element picker on the debuggee target.
-   */
-  togglePicker: function() {
-    if (this._isPicking) {
-      return this.stopPicker();
-    } else {
-      return this.startPicker();
-    }
-  },
-
-  get isRemoteHighlightable() {
-    return this._target.client.traits.highlightable;
-  },
-
-  /**
-   * Start the element picker on the debuggee target.
-   * This will request the inspector actor to start listening for mouse/touch
-   * events on the target to highlight the hovered/picked element.
-   * Depending on the server-side capabilities, this may fire events when nodes
-   * are hovered.
-   * @return A promise that resolves when the picker has started
-   */
-  startPicker: function() {
-    let deferred = promise.defer();
-
-    let done = () => {
-      this.emit("picker-started");
-      this.on("select", this.stopPicker);
-      deferred.resolve();
-    };
-
-    promise.all([
-      this.initInspector(),
-      this.selectTool("inspector")
-    ]).then(() => {
-      this._isPicking = true;
-      this._pickerButton.setAttribute("checked", "true");
-
-      if (this.isRemoteHighlightable) {
-        this.highlighter.pick().then(done);
-
-        this._onPickerNodeHovered = res => {
-          this.emit("picker-node-hovered", res.node);
-        };
-        this.walker.on("picker-node-hovered", this._onPickerNodeHovered);
-
-        this._onPickerNodePicked = res => {
-          this.selection.setNodeFront(res.node, "picker-node-picked");
-          this.stopPicker();
-        };
-        this.walker.on("picker-node-picked", this._onPickerNodePicked);
-      } else {
-        this.walker.pick().then(node => {
-          this.selection.setNodeFront(node, "picker-node-picked");
-          this.stopPicker();
-        });
-        done();
-      }
-    });
-
-    return deferred.promise;
-  },
-
-  /**
-   * Stop the element picker
-   * @return A promise that resolves when the picker has stopped
-   */
-  stopPicker: function() {
-    let deferred = promise.defer();
-
-    let done = () => {
-      this.emit("picker-stopped");
-      this.off("select", this.stopPicker);
-      deferred.resolve();
-    };
-
-    this.initInspector().then(() => {
-      this._isPicking = false;
-      this._pickerButton.removeAttribute("checked");
-      if (this.isRemoteHighlightable) {
-        this.highlighter.cancelPick().then(done);
-        this.walker.off("picker-node-hovered", this._onPickerNodeHovered);
-        this.walker.off("picker-node-picked", this._onPickerNodePicked);
-      } else {
-        this.walker.cancelPick().then(done);
-      }
-    });
-
-    return deferred.promise;
-  },
-
-  /**
    * Get the toolbox's notification box
    *
    * @return The notification box element.
    */
   getNotificationBox: function() {
     return this.doc.getElementById("toolbox-notificationbox");
   },
 
@@ -1223,17 +1131,17 @@ Toolbox.prototype = {
         console.error("Panel " + id + ":", e);
       }
     }
 
     // Destroying the walker and inspector fronts
     outstanding.push(this.destroyInspector());
 
     // Removing buttons
-    this._pickerButton.removeEventListener("command", this.togglePicker, false);
+    this._pickerButton.removeEventListener("command", this._togglePicker, false);
     this._pickerButton = null;
     let container = this.doc.getElementById("toolbox-buttons");
     while (container.firstChild) {
       container.removeChild(container.firstChild);
     }
 
     outstanding.push(this.destroyHost());
 
@@ -1255,8 +1163,189 @@ Toolbox.prototype = {
       this.emit("destroyed");
       // Free _host after the call to destroyed in order to let a chance
       // to destroyed listeners to still query toolbox attributes
       this._host = null;
       this._toolPanels.clear();
     }).then(null, console.error);
   }
 };
+
+/**
+ * The ToolboxHighlighterUtils is what you should use for anything related to
+ * node highlighting and picking.
+ * It encapsulates the logic to connecting to the HighlighterActor.
+ */
+function ToolboxHighlighterUtils(toolbox) {
+  this.toolbox = toolbox;
+  this._onPickerNodeHovered = this._onPickerNodeHovered.bind(this);
+  this._onPickerNodePicked = this._onPickerNodePicked.bind(this);
+  this.stopPicker = this.stopPicker.bind(this);
+}
+
+ToolboxHighlighterUtils.prototype = {
+  /**
+   * Indicates whether the highlighter actor exists on the server.
+   */
+  get isRemoteHighlightable() {
+    return this.toolbox._target.client.traits.highlightable;
+  },
+
+  /**
+   * Start/stop the element picker on the debuggee target.
+   */
+  togglePicker: function() {
+    if (this._isPicking) {
+      return this.stopPicker();
+    } else {
+      return this.startPicker();
+    }
+  },
+
+  _onPickerNodeHovered: function(res) {
+    this.toolbox.emit("picker-node-hovered", res.node);
+  },
+
+  _onPickerNodePicked: function(res) {
+    this.toolbox.selection.setNodeFront(res.node, "picker-node-picked");
+    this.stopPicker();
+  },
+
+  /**
+   * Start the element picker on the debuggee target.
+   * This will request the inspector actor to start listening for mouse/touch
+   * events on the target to highlight the hovered/picked element.
+   * Depending on the server-side capabilities, this may fire events when nodes
+   * are hovered.
+   * @return A promise that resolves when the picker has started
+   */
+  startPicker: function() {
+    let deferred = promise.defer();
+
+    let done = () => {
+      this.toolbox.emit("picker-started");
+      this.toolbox.on("select", this.stopPicker);
+      deferred.resolve();
+    };
+
+    promise.all([
+      this.toolbox.initInspector(),
+      this.toolbox.selectTool("inspector")
+    ]).then(() => {
+      this._isPicking = true;
+      this.toolbox._pickerButton.setAttribute("checked", "true");
+
+      if (this.isRemoteHighlightable) {
+        this.toolbox.highlighter.pick().then(done);
+
+        this.toolbox.walker.on("picker-node-hovered", this._onPickerNodeHovered);
+        this.toolbox.walker.on("picker-node-picked", this._onPickerNodePicked);
+      } else {
+        this.toolbox.walker.pick().then(node => {
+          this.toolbox.selection.setNodeFront(node, "picker-node-picked");
+          this.stopPicker();
+        });
+        done();
+      }
+    });
+
+    return deferred.promise;
+  },
+
+  /**
+   * Stop the element picker
+   * @return A promise that resolves when the picker has stopped
+   */
+  stopPicker: function() {
+    let deferred = promise.defer();
+
+    let done = () => {
+      this.toolbox.emit("picker-stopped");
+      this.toolbox.off("select", this.stopPicker);
+      deferred.resolve();
+    };
+
+    this.toolbox.initInspector().then(() => {
+      this._isPicking = false;
+      this.toolbox._pickerButton.removeAttribute("checked");
+      if (this.isRemoteHighlightable) {
+        this.toolbox.highlighter.cancelPick().then(done);
+        this.toolbox.walker.off("picker-node-hovered", this._onPickerNodeHovered);
+        this.toolbox.walker.off("picker-node-picked", this._onPickerNodePicked);
+      } else {
+        this.toolbox.walker.cancelPick().then(done);
+      }
+    });
+
+    return deferred.promise;
+  },
+
+  /**
+   * Show the box model highlighter on a node, given its NodeFront (this type
+   * of front is normally returned by the WalkerActor).
+   * @return a promise that resolves to the nodeFront when the node has been
+   * highlit
+   */
+  highlightNodeFront: function(nodeFront, options={}) {
+    let deferred = promise.defer();
+
+    // If the remote highlighter exists on the target, use it
+    if (this.isRemoteHighlightable) {
+      this.toolbox.initInspector().then(() => {
+        this.toolbox.highlighter.showBoxModel(nodeFront, options).then(() => {
+          this.toolbox.emit("node-highlight", nodeFront);
+          deferred.resolve(nodeFront);
+        });
+      });
+    }
+    // Else, revert to the "older" version of the highlighter in the walker
+    // actor
+    else {
+      this.toolbox.walker.highlight(nodeFront).then(() => {
+        this.toolbox.emit("node-highlight", nodeFront);
+        deferred.resolve(nodeFront);
+      });
+    }
+
+    return deferred.promise;
+  },
+
+  /**
+   * This is a convenience method in case you don't have a nodeFront but a
+   * valueGrip. This is often the case with VariablesView properties.
+   * This method will simply translate the grip into a nodeFront and call
+   * highlightNodeFront
+   * @return a promise that resolves to the nodeFront when the node has been
+   * highlit
+   */
+  highlightDomValueGrip: function(valueGrip, options={}) {
+    return this._translateGripToNodeFront(valueGrip).then(nodeFront => {
+      if (nodeFront) {
+        return this.highlightNodeFront(nodeFront, options);
+      } else {
+        return promise.reject();
+      }
+    });
+  },
+
+  _translateGripToNodeFront: function(grip) {
+    return this.toolbox.initInspector().then(() => {
+      return this.toolbox.walker.getNodeActorFromObjectActor(grip.actor);
+    });
+  },
+
+  /**
+   * Hide the highlighter.
+   * @return a promise that resolves when the highlighter is hidden
+   */
+  unhighlight: function() {
+    if (this.isRemoteHighlightable) {
+      // If the remote highlighter exists on the target, use it
+      return this.toolbox.initInspector().then(() => {
+        return this.toolbox.highlighter.hideBoxModel();
+      });
+    } else {
+      // If not, no need to unhighlight as the older highlight method uses a
+      // setTimeout to hide itself
+      return promise.resolve();
+    }
+  }
+};
--- a/browser/devtools/inspector/test/browser_inspector_basic_highlighter.js
+++ b/browser/devtools/inspector/test/browser_inspector_basic_highlighter.js
@@ -42,17 +42,17 @@ function test() {
   }
 
   function hoverH1InMarkupView() {
     let deferred = promise.defer();
 
     let container = getContainerForRawNode(inspector.markup, doc.querySelector("h1"));
     EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"},
       inspector.markup.doc.defaultView);
-    inspector.markup.once("node-highlight", deferred.resolve);
+    inspector.toolbox.once("node-highlight", deferred.resolve);
 
     return deferred.promise;
   }
 
   function assertH1Highlighted() {
     ok(isHighlighting(), "The highlighter is shown on a markup container hover");
     is(getHighlitNode(), doc.querySelector("h1"), "The highlighter highlights the right node");
     return promise.resolve();
--- a/browser/devtools/inspector/test/browser_inspector_bug_674871.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_674871.js
@@ -53,17 +53,17 @@ function test()
         getHighlighterOutline().setAttribute("disable-transitions", "true");
         runTests();
       });
     });
   }
 
   function runTests()
   {
-    inspector.toolbox.startPicker().then(() => {
+    inspector.toolbox.highlighterUtils.startPicker().then(() => {
       moveMouseOver(iframeNode, 1, 1, isTheIframeHighlighted);
     });
   }
 
   function isTheIframeHighlighted()
   {
     let outlineRect = getHighlighterOutlineRect();
     let iframeRect = iframeNode.getBoundingClientRect();
@@ -81,17 +81,17 @@ function test()
   function isTheIframeContentHighlighted()
   {
     is(getHighlitNode(), iframeBodyNode, "highlighter shows the right node");
 
     // 184 == 200 + 11(border) + 13(padding) - 40(scroll)
     let outlineRect = getHighlighterOutlineRect();
     is(outlineRect.height, 184, "highlighter height");
 
-    inspector.toolbox.stopPicker().then(() => {
+    inspector.toolbox.highlighterUtils.stopPicker().then(() => {
       let target = TargetFactory.forTab(gBrowser.selectedTab);
       gDevTools.closeToolbox(target);
       finishUp();
     });
   }
 
   function finishUp()
   {
--- a/browser/devtools/inspector/test/browser_inspector_bug_699308_iframe_navigation.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_699308_iframe_navigation.js
@@ -10,17 +10,17 @@ function test() {
   function startTest() {
     openInspector(aInspector => {
       inspector = aInspector;
       runInspectorTests();
     });
   }
 
   function showHighlighter(cb) {
-    inspector.toolbox.startPicker().then(() => {
+    inspector.toolbox.highlighterUtils.startPicker().then(() => {
       EventUtils.synthesizeMouse(content.document.body, 1, 1,
         {type: "mousemove"}, content);
       inspector.toolbox.once("picker-node-hovered", () => {
         executeSoon(() => {
           getHighlighterOutline().setAttribute("disable-transitions", "true");
           cb();
         });
       });
@@ -58,17 +58,17 @@ function test() {
 
     finishTest();
   }
 
   function finishTest() {
     is(iframeLoads, 2, "iframe loads");
     ok(checksAfterLoads, "the Inspector tests got the chance to run after iframe reloads");
 
-    inspector.toolbox.stopPicker().then(() => {
+    inspector.toolbox.highlighterUtils.stopPicker().then(() => {
       iframe = null;
       gBrowser.removeCurrentTab();
       executeSoon(finish);
     });
   }
 
   waitForExplicitFinish();
 
--- a/browser/devtools/inspector/test/browser_inspector_bug_958169_switch_to_inspector_on_pick.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_958169_switch_to_inspector_on_pick.js
@@ -19,17 +19,17 @@ function test() {
 
   content.location = "data:text/html,<p>Switch to inspector on pick</p>";
 }
 
 function startTests() {
   Task.spawn(function() {
     yield openToolbox();
     yield startPickerAndAssertSwitchToInspector();
-    yield toolbox.stopPicker();
+    yield toolbox.highlighterUtils.stopPicker();
 
     finishTests();
   }).then(null, Cu.reportError);
 }
 
 function openToolbox() {
   let target = TargetFactory.forTab(gBrowser.selectedTab);
   return gDevTools.showToolbox(target, "webconsole").then(aToolbox => {
--- a/browser/devtools/inspector/test/browser_inspector_bug_958456_highlight_comments.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_958456_highlight_comments.js
@@ -82,17 +82,17 @@ function prepareHighlighter() {
   });
   return deferred.promise;
 }
 
 function hoverContainer(container) {
   let deferred = promise.defer();
   EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"},
       markupView.doc.defaultView);
-  inspector.markup.once("node-highlight", deferred.resolve);
+  inspector.toolbox.once("node-highlight", deferred.resolve);
   return deferred.promise;
 }
 
 function hoverElement(selector) {
   info("Hovering node " + selector + " in the markup view");
   let container = getContainerForRawNode(markupView, doc.querySelector(selector));
   return hoverContainer(container);
 }
--- a/browser/devtools/inspector/test/browser_inspector_bug_961771_picker_stops_on_tool_select.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_961771_picker_stops_on_tool_select.js
@@ -28,17 +28,17 @@ function test() {
       inspector = aInspector;
 
       toolbox.once("picker-stopped", () => {
         ok(true, "picker-stopped event fired after switch tools, so picker is closed");
         finishUp();
       });
 
       Task.spawn(function() {
-        yield toolbox.startPicker();
+        yield toolbox.highlighterUtils.startPicker();
         yield toolbox.selectNextTool();
       }).then(null, Cu.reportError);
     });
   }
 
   function finishUp() {
     inspector = doc = toolbox = null;
     gBrowser.removeCurrentTab();
--- a/browser/devtools/inspector/test/browser_inspector_highlighter.js
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter.js
@@ -47,17 +47,17 @@ function createDocument() {
   doc.body.appendChild(div2);
   doc.body.appendChild(div3);
 
   openInspector(aInspector => {
     inspector = aInspector;
     inspector.selection.setNode(div, null);
     inspector.once("inspector-updated", () => {
       getHighlighterOutline().setAttribute("disable-transitions", "true");
-      inspector.toolbox.startPicker().then(testMouseOverH1Highlights);
+      inspector.toolbox.highlighterUtils.startPicker().then(testMouseOverH1Highlights);
     });
   });
 }
 
 function testMouseOverH1Highlights() {
   inspector.toolbox.once("picker-node-hovered", () => {
     ok(isHighlighting(), "Highlighter is shown");
     is(getHighlitNode(), h1, "Highlighter's outline correspond to the selected node");
@@ -104,17 +104,17 @@ function testOutlineDimensions() {
     is(outlineWidth, h1Width, "outline width matches dimensions of element (zoomed)");
     is(outlineHeight, h1Height, "outline height matches dimensions of element (zoomed)");
 
     executeSoon(finishUp);
   }, 500);
 }
 
 function finishUp() {
-  inspector.toolbox.stopPicker().then(() => {
+  inspector.toolbox.highlighterUtils.stopPicker().then(() => {
     doc = h1 = inspector = null;
     let target = TargetFactory.forTab(gBrowser.selectedTab);
     gDevTools.closeToolbox(target);
     gBrowser.removeCurrentTab();
     finish();
   });
 }
 
--- a/browser/devtools/inspector/test/browser_inspector_iframeTest.js
+++ b/browser/devtools/inspector/test/browser_inspector_iframeTest.js
@@ -29,17 +29,17 @@ function createDocument() {
       iframe2.removeEventListener("load", arguments.callee, false);
 
       div2 = iframe2.contentDocument.createElement('div');
       div2.textContent = 'nested div';
       iframe2.contentDocument.body.appendChild(div2);
       // Open the inspector, start the picker mode, and start the tests
       openInspector(aInspector => {
         inspector = aInspector;
-        inspector.toolbox.startPicker().then(runTests);
+        inspector.toolbox.highlighterUtils.startPicker().then(runTests);
       });
     }, false);
 
     iframe2.src = 'data:text/html,nested iframe';
     iframe1.contentDocument.body.appendChild(iframe2);
   }, false);
 
   iframe1.src = 'data:text/html,little iframe';
@@ -70,33 +70,33 @@ function testDiv2Highlighter() {
   moveMouseOver(div2, () => {
     is(getHighlitNode(), div2, "highlighter matches selection");
     selectRoot();
   });
 }
 
 function selectRoot() {
   // Select the root document element to clear the breadcrumbs.
-  inspector.selection.setNode(doc.documentElement);
+  inspector.selection.setNode(doc.documentElement, null);
   inspector.once("inspector-updated", selectIframe);
 }
 
 function selectIframe() {
   // Directly select an element in an iframe (without navigating to it
   // with mousemoves).
-  inspector.selection.setNode(div2);
+  inspector.selection.setNode(div2, null);
   inspector.once("inspector-updated", () => {
     let breadcrumbs = inspector.breadcrumbs;
     is(breadcrumbs.nodeHierarchy.length, 9, "Should have 9 items");
     finishUp();
   });
 }
 
 function finishUp() {
-  inspector.toolbox.stopPicker().then(() => {
+  inspector.toolbox.highlighterUtils.stopPicker().then(() => {
     doc = div1 = div2 = iframe1 = iframe2 = inspector = null;
     let target = TargetFactory.forTab(gBrowser.selectedTab);
     gDevTools.closeToolbox(target);
     gBrowser.removeCurrentTab();
     finish();
   });
 }
 
--- a/browser/devtools/main.js
+++ b/browser/devtools/main.js
@@ -104,17 +104,17 @@ Tools.inspector = {
   icon: "chrome://browser/skin/devtools/tool-inspector.svg",
   url: "chrome://browser/content/devtools/inspector/inspector.xul",
   label: l10n("inspector.label", inspectorStrings),
   tooltip: l10n("inspector.tooltip", inspectorStrings),
   inMenu: true,
 
   preventClosingOnKey: true,
   onkey: function(panel) {
-    panel.toolbox.togglePicker();
+    panel.toolbox.highlighterUtils.togglePicker();
   },
 
   isTargetSupported: function(target) {
     return true;
   },
 
   build: function(iframeWindow, toolbox) {
     let panel = new InspectorPanel(iframeWindow, toolbox);
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -49,20 +49,16 @@ loader.lazyGetter(this, "AutocompletePop
 /**
  * The markup tree.  Manages the mapping of nodes to MarkupContainers,
  * updating based on mutations, and the undo/redo bindings.
  *
  * @param Inspector aInspector
  *        The inspector we're watching.
  * @param iframe aFrame
  *        An iframe in which the caller has kindly loaded markup-view.xhtml.
- *
- * Fires the following events:
- * - node-highlight: When a node in the markup-view is hovered and the
- *   corresponding node in the content gets highlighted
  */
 function MarkupView(aInspector, aFrame, aControllerWindow) {
   this._inspector = aInspector;
   this.walker = this._inspector.walker;
   this._frame = aFrame;
   this.doc = this._frame.contentDocument;
   this._elt = this.doc.querySelector("#root");
   this.htmlEditor = new HTMLEditor(this.doc);
@@ -169,51 +165,21 @@ MarkupView.prototype = {
     }
   },
 
   _onMouseLeave: function() {
     this._hideBoxModel();
   },
 
   _showBoxModel: function(nodeFront, options={}) {
-    let toolbox = this._inspector.toolbox;
-
-    // If the remote highlighter exists on the target, use it
-    if (toolbox.isRemoteHighlightable) {
-      toolbox.initInspector().then(() => {
-        toolbox.highlighter.showBoxModel(nodeFront, options).then(() => {
-          this.emit("node-highlight", nodeFront);
-        });
-      });
-    }
-    // Else, revert to the "older" version of the highlighter in the walker
-    // actor
-    else {
-      this.walker.highlight(nodeFront).then(() => {
-        this.emit("node-highlight", nodeFront);
-      });
-    }
+    this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
   },
 
   _hideBoxModel: function() {
-    let deferred = promise.defer();
-    let toolbox = this._inspector.toolbox;
-
-    // If the remote highlighter exists on the target, use it
-    if (toolbox.isRemoteHighlightable) {
-      toolbox.initInspector().then(() => {
-        toolbox.highlighter.hideBoxModel().then(deferred.resolve);
-      });
-    } else {
-      deferred.resolve();
-    }
-    // If not, no need to unhighlight as the older highlight method uses a
-    // setTimeout to hide itself
-
-    return deferred.promise;
+    this._inspector.toolbox.highlighterUtils.unhighlight();
   },
 
   _briefBoxModelTimer: null,
   _brieflyShowBoxModel: function(nodeFront, options) {
     let win = this._frame.contentWindow;
 
     if (this._briefBoxModelTimer) {
       win.clearTimeout(this._briefBoxModelTimer);
--- 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,55 @@ 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._onContextCopyImageAsDataUriCommand = this.copyImageAsDataUri.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-copy-image-as-data-uri").addEventListener("command", this._onContextCopyImageAsDataUriCommand, 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 +386,39 @@ 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-copy-image-as-data-uri").removeEventListener("command", this._onContextCopyImageAsDataUriCommand, 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 +476,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;
@@ -417,16 +505,28 @@ RequestsMenuView.prototype = Heritage.ex
    * Copy the request url from the currently selected item.
    */
   copyUrl: function() {
     let selected = this.selectedItem.attachment;
     clipboardHelper.copyString(selected.url, document);
   },
 
   /**
+   * Copy image as data uri.
+   */
+  copyImageAsDataUri: function() {
+    let selected = this.selectedItem.attachment;
+    let { mimeType, text, encoding } = selected.responseContent.content;
+    gNetwork.getString(text).then(aString => {
+      let data = "data:" + mimeType + ";" + encoding + "," + aString;
+      clipboardHelper.copyString(data, document);
+    });
+  },
+
+  /**
    * Send a new HTTP request using the data in the custom request form.
    */
   sendCustomRequest: function() {
     let selected = this.selectedItem.attachment;
 
     let data = Object.create(selected, {
       headers: { value: selected.requestHeaders.headers }
     });
@@ -452,17 +552,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 +572,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 +709,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 +826,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 +940,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 +1129,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 +1247,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,33 +1376,45 @@ 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.
    */
   _onContextShowing: function() {
+    let selectedItem = this.selectedItem;
+
     let resendElement = $("#request-menu-context-resend");
-    resendElement.hidden = !this.selectedItem || this.selectedItem.attachment.isCustom;
+    resendElement.hidden = !selectedItem || selectedItem.attachment.isCustom;
 
     let copyUrlElement = $("#request-menu-context-copy-url");
-    copyUrlElement.hidden = !this.selectedItem;
+    copyUrlElement.hidden = !selectedItem;
+
+    let copyImageAsDataUriElement = $("#request-menu-context-copy-image-as-data-uri");
+    copyImageAsDataUriElement.hidden = !selectedItem ||
+      !selectedItem.attachment.responseContent ||
+      !selectedItem.attachment.responseContent.content.mimeType.contains("image/");
 
     let newTabElement = $("#request-menu-context-newtab");
-    newTabElement.hidden = !this.selectedItem;
+    newTabElement.hidden = !selectedItem;
   },
 
   /**
    * Checks if the specified unix time is the first one to be known of,
    * and saves it if so.
    *
    * @param number aUnixTime
    *        The milliseconds to check and save.
@@ -1448,17 +1578,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 +1605,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 +1679,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 +2291,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 +2476,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 +2498,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 +2578,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 +2640,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
@@ -23,25 +23,32 @@
   <popupset id="networkPopupSet">
     <menupopup id="network-request-popup">
       <menuitem id="request-menu-context-newtab"
                 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-copy-image-as-data-uri"
+                label="&netmonitorUI.context.copyImageAsDataUri;"
+                accesskey="&netmonitorUI.context.copyImageAsDataUri.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 +120,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 +164,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 +473,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
@@ -1,39 +1,47 @@
 [DEFAULT]
 support-files =
   head.js
   html_content-type-test-page.html
+  html_content-type-without-cache-test-page.html
   html_custom-get-page.html
   html_cyrillic-test-page.html
   html_filter-test-page.html
   html_infinite-get-page.html
   html_json-custom-mime-test-page.html
   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_copy_image_as_data_uri.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]
 [browser_net_footer-summary.js]
 [browser_net_json-long.js]
 [browser_net_json-malformed.js]
@@ -52,11 +60,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);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_copy_image_as_data_uri.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if copying an image as data uri works.
+ */
+
+function test() {
+  initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu } = NetMonitorView;
+    RequestsMenu.lazyUpdate = false;
+    let imageDataUri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
+
+    waitForNetworkEvents(aMonitor, 6).then(() => {
+      let requestItem = RequestsMenu.getItemAtIndex(5);
+      RequestsMenu.selectedItem = requestItem;
+
+      waitForClipboard(imageDataUri, function setup() {
+        RequestsMenu.copyImageAsDataUri();
+      }, function onSuccess() {
+        ok(true, "Clipboard contains the currently selected image as data uri.");
+        cleanUp();
+      }, function onFailure() {
+        ok(false, "Copying the currently selected image as data uri was unsuccessful.");
+        cleanUp();
+      });
+    });
+
+    aDebuggee.performRequests();
+
+    function cleanUp(){
+      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
@@ -12,28 +12,30 @@ let { devtools } = Cu.import("resource:/
 let TargetFactory = devtools.TargetFactory;
 let Toolbox = devtools.Toolbox;
 
 const EXAMPLE_URL = "http://example.com/browser/browser/devtools/netmonitor/test/";
 
 const SIMPLE_URL = EXAMPLE_URL + "html_simple-test-page.html";
 const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html";
 const CONTENT_TYPE_URL = EXAMPLE_URL + "html_content-type-test-page.html";
+const CONTENT_TYPE_WITHOUT_CACHE_URL = EXAMPLE_URL + "html_content-type-without-cache-test-page.html";
 const CYRILLIC_URL = EXAMPLE_URL + "html_cyrillic-test-page.html";
 const STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html";
 const POST_DATA_URL = EXAMPLE_URL + "html_post-data-test-page.html";
 const POST_RAW_URL = EXAMPLE_URL + "html_post-raw-test-page.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_content-type-without-cache-test-page.html
@@ -0,0 +1,45 @@
+<!-- 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>Content type test</p>
+
+    <script type="text/javascript">
+      function get(aAddress, aCallback) {
+        var xhr = new XMLHttpRequest();
+        xhr.open("GET", aAddress, true);
+
+        xhr.onreadystatechange = function() {
+          if (this.readyState == this.DONE) {
+            aCallback();
+          }
+        };
+        xhr.send(null);
+      }
+
+      function performRequests() {
+        get("sjs_content-type-test-server.sjs?fmt=xml", function() {
+          get("sjs_content-type-test-server.sjs?fmt=css", function() {
+            get("sjs_content-type-test-server.sjs?fmt=js", function() {
+              get("sjs_content-type-test-server.sjs?fmt=json", function() {
+                get("sjs_content-type-test-server.sjs?fmt=bogus", function() {
+                  get("test-image.png?v=" + Math.random(), function() {
+                    // Done.
+                  });
+                });
+              });
+            });
+          });
+        });
+      }
+    </script>
+  </body>
+
+</html>
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();
--- a/browser/devtools/shared/autocomplete-popup.js
+++ b/browser/devtools/shared/autocomplete-popup.js
@@ -449,16 +449,27 @@ AutocompletePopup.prototype = {
    * Getter for the number of items in the popup.
    * @type number
    */
   get itemCount() {
     return this._list.childNodes.length;
   },
 
   /**
+   * Getter for the height of each item in the list.
+   *
+   * @private
+   *
+   * @type number
+   */
+  get _itemHeight() {
+    return this._list.selectedItem.clientHeight;
+  },
+
+  /**
    * Select the next item in the list.
    *
    * @return object
    *         The newly selected item object.
    */
   selectNextItem: function AP_selectNextItem()
   {
     if (this.selectedIndex < (this.itemCount - 1)) {
@@ -470,31 +481,64 @@ AutocompletePopup.prototype = {
 
     return this.selectedItem;
   },
 
   /**
    * Select the previous item in the list.
    *
    * @return object
-   *         The newly selected item object.
+   *         The newly-selected item object.
    */
   selectPreviousItem: function AP_selectPreviousItem()
   {
     if (this.selectedIndex > 0) {
       this.selectedIndex--;
     }
     else {
       this.selectedIndex = this.itemCount - 1;
     }
 
     return this.selectedItem;
   },
 
   /**
+   * Select the top-most item in the next page of items or
+   * the last item in the list.
+   *
+   * @return object
+   *         The newly-selected item object.
+   */
+  selectNextPageItem: function AP_selectNextPageItem()
+  {
+    let itemsPerPane = Math.floor(this._list.scrollHeight / this._itemHeight);
+    let nextPageIndex = this.selectedIndex + itemsPerPane + 1;
+    this.selectedIndex = nextPageIndex > this.itemCount - 1 ?
+      this.itemCount - 1 : nextPageIndex;
+
+    return this.selectedItem;
+  },
+
+  /**
+   * Select the bottom-most item in the previous page of items,
+   * or the first item in the list.
+   *
+   * @return object
+   *         The newly-selected item object.
+   */
+  selectPreviousPageItem: function AP_selectPreviousPageItem()
+  {
+    let itemsPerPane = Math.floor(this._list.scrollHeight / this._itemHeight);
+    let prevPageIndex = this.selectedIndex - itemsPerPane - 1;
+    this.selectedIndex = prevPageIndex < 0 ? 0 : prevPageIndex;
+
+    return this.selectedItem;
+  },
+
+  /**
    * Focuses the richlistbox.
    */
   focus: function AP_focus()
   {
     this._list.focus();
   },
 
   /**
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/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -29,16 +29,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 const GRADIENT_RE = /\b(repeating-)?(linear|radial)-gradient\(((rgb|hsl)a?\(.+?\)|[^\)])+\)/gi;
 const BORDERCOLOR_RE = /^border-[-a-z]*color$/ig;
 const BORDER_RE = /^border(-(top|bottom|left|right))?$/ig;
 const BACKGROUND_IMAGE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/;
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 const SPECTRUM_FRAME = "chrome://browser/content/devtools/spectrum-frame.xhtml";
 const ESCAPE_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE;
 const ENTER_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_RETURN;
+const POPUP_EVENTS = ["shown", "hidden", "showing", "hiding"];
 
 /**
  * Tooltip widget.
  *
  * This widget is intended at any tool that may need to show rich content in the
  * form of floating panels.
  * A common use case is image previewing in the CSS rule view, but more complex
  * use cases may include color pickers, object inspection, etc...
@@ -131,48 +132,62 @@ let PanelFactory = {
  *       return true;
  *     }
  *   });
  *   t.destroy();
  *
  * @param {XULDocument} doc
  *        The XUL document hosting this tooltip
  * @param {Object} options
- *        Optional options that give options to consumers
+ *        Optional options that give options to consumers:
  *        - consumeOutsideClick {Boolean} Wether the first click outside of the
  *        tooltip should close the tooltip and be consumed or not.
- *        Defaults to false
+ *        Defaults to false.
  *        - closeOnKeys {Array} An array of key codes that should close the
- *        tooltip. Defaults to [27] (escape key)
+ *        tooltip. Defaults to [27] (escape key).
+ *        - closeOnEvents [{emitter: {Object}, event: {String}, useCapture: {Boolean}}]
+ *        Provide an optional list of emitter objects and event names here to
+ *        trigger the closing of the tooltip when these events are fired by the
+ *        emitters. The emitter objects should either implement on/off(event, cb)
+ *        or addEventListener/removeEventListener(event, cb). Defaults to [].
+ *        For instance, the following would close the tooltip whenever the
+ *        toolbox selects a new tool and when a DOM node gets scrolled:
+ *        new Tooltip(doc, {
+ *          closeOnEvents: [
+ *            {emitter: toolbox, event: "select"},
+ *            {emitter: myContainer, event: "scroll", useCapture: true}
+ *          ]
+ *        });
  *        - noAutoFocus {Boolean} Should the focus automatically go to the panel
- *        when it opens. Defaults to true
+ *        when it opens. Defaults to true.
  *
  * Fires these events:
  * - showing : just before the tooltip shows
  * - shown : when the tooltip is shown
  * - hiding : just before the tooltip closes
  * - hidden : when the tooltip gets hidden
  * - keypress : when any key gets pressed, with keyCode
  */
 function Tooltip(doc, options) {
   EventEmitter.decorate(this);
 
   this.doc = doc;
   this.options = new OptionsStore({
     consumeOutsideClick: false,
     closeOnKeys: [ESCAPE_KEYCODE],
-    noAutoFocus: true
+    noAutoFocus: true,
+    closeOnEvents: []
   }, options);
   this.panel = PanelFactory.get(doc, this.options);
 
   // Used for namedTimeouts in the mouseover handling
   this.uid = "tooltip-" + Date.now();
 
   // Emit show/hide events
-  for (let event of ["shown", "hidden", "showing", "hiding"]) {
+  for (let event of POPUP_EVENTS) {
     this["_onPopup" + event] = ((e) => {
       return () => this.emit(e);
     })(event);
     this.panel.addEventListener("popup" + event,
       this["_onPopup" + event], false);
   }
 
   // Listen to keypress events to close the tooltip if configured to do so
@@ -182,16 +197,28 @@ function Tooltip(doc, options) {
     if (this.options.get("closeOnKeys").indexOf(event.keyCode) !== -1) {
       if (!this.panel.hidden) {
         event.stopPropagation();
       }
       this.hide();
     }
   };
   win.addEventListener("keypress", this._onKeyPress, false);
+
+  // Listen to custom emitters' events to close the tooltip
+  this.hide = this.hide.bind(this);
+  let closeOnEvents = this.options.get("closeOnEvents");
+  for (let {emitter, event, useCapture} of closeOnEvents) {
+    for (let add of ["addEventListener", "on"]) {
+      if (add in emitter) {
+        emitter[add](event, this.hide, useCapture);
+        break;
+      }
+    }
+  }
 }
 
 module.exports.Tooltip = Tooltip;
 
 Tooltip.prototype = {
   defaultPosition: "before_start",
   defaultOffsetX: 0, // px
   defaultOffsetY: 0, // px
@@ -259,24 +286,34 @@ Tooltip.prototype = {
   },
 
   /**
    * Get rid of references and event listeners
    */
   destroy: function () {
     this.hide();
 
-    for (let event of ["shown", "hidden", "showing", "hiding"]) {
+    for (let event of POPUP_EVENTS) {
       this.panel.removeEventListener("popup" + event,
         this["_onPopup" + event], false);
     }
 
     let win = this.doc.querySelector("window");
     win.removeEventListener("keypress", this._onKeyPress, false);
 
+    let closeOnEvents = this.options.get("closeOnEvents");
+    for (let {emitter, event, useCapture} of closeOnEvents) {
+      for (let remove of ["removeEventListener", "off"]) {
+        if (remove in emitter) {
+          emitter[remove](event, this.hide, useCapture);
+          break;
+        }
+      }
+    }
+
     this.content = null;
 
     if (this._basedNode) {
       this.stopTogglingOnHover();
     }
 
     this.doc = null;
 
@@ -471,23 +508,27 @@ Tooltip.prototype = {
    *        Options for the variables view controller.
    * @param {object} relayEvents [optional]
    *        A collection of events to listen on the variables view widget.
    *        For example, { fetched: () => ... }
    * @param {boolean} reuseCachedWidget [optional]
    *        Pass false to instantiate a brand new widget for this variable.
    *        Otherwise, if a variable was previously inspected, its widget
    *        will be reused.
+   * @param {Toolbox} toolbox [optional]
+   *        Pass the instance of the current toolbox if you want the variables
+   *        view widget to allow highlighting and selection of DOM nodes
    */
   setVariableContent: function(
     objectActor,
     viewOptions = {},
     controllerOptions = {},
     relayEvents = {},
-    extraButtons = []) {
+    extraButtons = [],
+    toolbox = null) {
 
     let vbox = this.doc.createElement("vbox");
     vbox.className = "devtools-tooltip-variables-view-box";
     vbox.setAttribute("flex", "1");
 
     let innerbox = this.doc.createElement("vbox");
     innerbox.className = "devtools-tooltip-variables-view-innerbox";
     innerbox.setAttribute("flex", "1");
@@ -498,16 +539,21 @@ Tooltip.prototype = {
       button.className = className;
       button.setAttribute("label", label);
       button.addEventListener("command", command);
       vbox.appendChild(button);
     }
 
     let widget = new VariablesView(innerbox, viewOptions);
 
+    // If a toolbox was provided, link it to the vview
+    if (toolbox) {
+      widget.toolbox = toolbox;
+    }
+
     // Analyzing state history isn't useful with transient object inspectors.
     widget.commitHierarchy = () => {};
 
     for (let e in relayEvents) widget.on(e, relayEvents[e]);
     VariablesViewController.attach(widget, controllerOptions);
 
     // Some of the view options are allowed to change between uses.
     widget.searchPlaceholder = viewOptions.searchPlaceholder;
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -18,16 +18,18 @@ const PAGE_SIZE_MAX_JUMPS = 30;
 const SEARCH_ACTION_MAX_DELAY = 300; // ms
 const ITEM_FLASH_DURATION = 300 // ms
 
 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");
 Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
 
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
   "resource://gre/modules/devtools/Loader.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
   "resource://gre/modules/PluralForm.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
@@ -206,16 +208,22 @@ VariablesView.prototype = {
       if (!this._store.length) {
         this._appendEmptyNotice();
         this._toggleSearchVisibility(false);
       }
     }, aTimeout);
   },
 
   /**
+   * Optional DevTools toolbox containing this VariablesView. Used to
+   * communicate with the inspector and highlighter.
+   */
+  toolbox: null,
+
+  /**
    * The controller for this VariablesView, if it has one.
    */
   controller: null,
 
   /**
    * The amount of time (in milliseconds) it takes to empty this view lazily.
    */
   lazyEmptyDelay: LAZY_EMPTY_DELAY,
@@ -320,16 +328,25 @@ VariablesView.prototype = {
    * in order to change the variable or property to a plain value.
    *
    * This flag is applied recursively onto each scope in this view and
    * affects only the child nodes when they're created.
    */
   editButtonTooltip: STR.GetStringFromName("variablesEditButtonTooltip"),
 
   /**
+   * The tooltip text shown on a variable or property's value if that value is
+   * a DOMNode that can be highlighted and selected in the inspector.
+   *
+   * This flag is applied recursively onto each scope in this view and
+   * affects only the child nodes when they're created.
+   */
+  domNodeValueTooltip: STR.GetStringFromName("variablesDomNodeValueTooltip"),
+
+  /**
    * The tooltip text shown on a variable or property's delete button if a
    * |delete| function is provided, in order to delete the variable or property.
    *
    * This flag is applied recursively onto each scope in this view and
    * affects only the child nodes when they're created.
    */
   deleteButtonTooltip: STR.GetStringFromName("variablesCloseButtonTooltip"),
 
@@ -1204,16 +1221,17 @@ function Scope(aView, aName, aFlags = {}
   this.delete = aView.delete;
   this.new = aView.new;
   this.preventDisableOnChange = aView.preventDisableOnChange;
   this.preventDescriptorModifiers = aView.preventDescriptorModifiers;
   this.editableNameTooltip = aView.editableNameTooltip;
   this.editableValueTooltip = aView.editableValueTooltip;
   this.editButtonTooltip = aView.editButtonTooltip;
   this.deleteButtonTooltip = aView.deleteButtonTooltip;
+  this.domNodeValueTooltip = aView.domNodeValueTooltip;
   this.contextMenuId = aView.contextMenuId;
   this.separatorStr = aView.separatorStr;
 
   this._init(aName.trim(), aFlags);
 }
 
 Scope.prototype = {
   /**
@@ -2064,16 +2082,17 @@ Scope.prototype = {
   delete: null,
   new: null,
   preventDisableOnChange: false,
   preventDescriptorModifiers: false,
   editableNameTooltip: "",
   editableValueTooltip: "",
   editButtonTooltip: "",
   deleteButtonTooltip: "",
+  domNodeValueTooltip: "",
   contextMenuId: "",
   separatorStr: "",
 
   _store: null,
   _enumItems: null,
   _nonEnumItems: null,
   _fetched: false,
   _committed: false,
@@ -2114,16 +2133,19 @@ XPCOMUtils.defineLazyGetter(Scope, "elli
  *        The variable's name.
  * @param object aDescriptor
  *        The variable's descriptor.
  */
 function Variable(aScope, aName, aDescriptor) {
   this._setTooltips = this._setTooltips.bind(this);
   this._activateNameInput = this._activateNameInput.bind(this);
   this._activateValueInput = this._activateValueInput.bind(this);
+  this.openNodeInInspector = this.openNodeInInspector.bind(this);
+  this.highlightDomNode = this.highlightDomNode.bind(this);
+  this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
 
   // Treat safe getter descriptors as descriptors with a value.
   if ("getterValue" in aDescriptor) {
     aDescriptor.value = aDescriptor.getterValue;
     delete aDescriptor.get;
     delete aDescriptor.set;
   }
 
@@ -2166,16 +2188,23 @@ Variable.prototype = Heritage.extend(Sco
   _createChild: function(aName, aDescriptor) {
     return new Property(this, aName, aDescriptor);
   },
 
   /**
    * Remove this Variable from its parent and remove all children recursively.
    */
   remove: function() {
+    if (this._linkedToInspector) {
+      this.unhighlightDomNode();
+      this._valueLabel.removeEventListener("mouseover", this.highlightDomNode, false);
+      this._valueLabel.removeEventListener("mouseout", this.unhighlightDomNode, false);
+      this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, false);
+    }
+
     this.ownerView._store.delete(this._nameString);
     this._variablesView._itemsByElement.delete(this._target);
     this._variablesView._currHierarchy.delete(this._absoluteName);
 
     this._target.remove();
 
     for (let property of this._store.values()) {
       property.remove();
@@ -2380,16 +2409,21 @@ Variable.prototype = Heritage.extend(Sco
       concise: true,
       noEllipsis: true,
     });
     this._valueClassName = VariablesView.getClass(aGrip);
 
     this._valueLabel.classList.add(this._valueClassName);
     this._valueLabel.setAttribute("value", this._valueString);
     this._separatorLabel.hidden = false;
+
+    // DOMNodes get special treatment since they can be linked to the inspector
+    if (this._valueGrip.preview && this._valueGrip.preview.kind === "DOMNode") {
+      this._linkToInspector();
+    }
   },
 
   /**
    * Marks this variable as overridden.
    *
    * @param boolean aFlag
    *        Whether this variable is overridden or not.
    */
@@ -2550,17 +2584,17 @@ Variable.prototype = Heritage.extend(Sco
     }
 
     if (ownerView.preventDescriptorModifiers) {
       return;
     }
 
     if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
       let nonWritableIcon = this.document.createElement("hbox");
-      nonWritableIcon.className = "variable-or-property-non-writable-icon";
+      nonWritableIcon.className = "plain variable-or-property-non-writable-icon";
       nonWritableIcon.setAttribute("optional-visibility", "");
       this._title.appendChild(nonWritableIcon);
     }
     if (descriptor.value && typeof descriptor.value == "object") {
       if (descriptor.value.frozen) {
         let frozenLabel = this.document.createElement("label");
         frozenLabel.className = "plain variable-or-property-frozen-label";
         frozenLabel.setAttribute("optional-visibility", "");
@@ -2618,28 +2652,135 @@ Variable.prototype = Heritage.extend(Sco
     }
 
     this._target.appendChild(tooltip);
     this._target.setAttribute("tooltip", tooltip.id);
 
     if (this._editNode && ownerView.eval) {
       this._editNode.setAttribute("tooltiptext", ownerView.editButtonTooltip);
     }
+    if (this._openInspectorNode && this._linkedToInspector) {
+      this._openInspectorNode.setAttribute("tooltiptext", this.ownerView.domNodeValueTooltip);
+    }
     if (this._valueLabel && ownerView.eval) {
       this._valueLabel.setAttribute("tooltiptext", ownerView.editableValueTooltip);
     }
     if (this._name && ownerView.switch) {
       this._name.setAttribute("tooltiptext", ownerView.editableNameTooltip);
     }
     if (this._deleteNode && ownerView.delete) {
       this._deleteNode.setAttribute("tooltiptext", ownerView.deleteButtonTooltip);
     }
   },
 
   /**
+   * Get the parent variablesview toolbox, if any.
+   */
+  get toolbox() {
+    return this._variablesView.toolbox;
+  },
+
+  /**
+   * Checks if this variable is a DOMNode and is part of a variablesview that
+   * has been linked to the toolbox, so that highlighting and jumping to the
+   * inspector can be done.
+   */
+  _isLinkableToInspector: function() {
+    let isDomNode = this._valueGrip && this._valueGrip.preview.kind === "DOMNode";
+    let hasBeenLinked = this._linkedToInspector;
+    let hasToolbox = !!this.toolbox;
+
+    return isDomNode && !hasBeenLinked && hasToolbox;
+  },
+
+  /**
+   * If the variable is a DOMNode, and if a toolbox is set, then link it to the
+   * inspector (highlight on hover, and jump to markup-view on click)
+   */
+  _linkToInspector: function() {
+    if (!this._isLinkableToInspector()) {
+      return;
+    }
+
+    // Listen to value mouseover/click events to highlight and jump
+    this._valueLabel.addEventListener("mouseover", this.highlightDomNode, false);
+    this._valueLabel.addEventListener("mouseout", this.unhighlightDomNode, false);
+
+    // Add a button to open the node in the inspector
+    this._openInspectorNode = this.document.createElement("toolbarbutton");
+    this._openInspectorNode.className = "plain variables-view-open-inspector";
+    this._openInspectorNode.addEventListener("mousedown", this.openNodeInInspector, false);
+    this._title.insertBefore(this._openInspectorNode, this._title.querySelector("toolbarbutton"));
+
+    this._linkedToInspector = true;
+  },
+
+  /**
+   * In case this variable is a DOMNode and part of a variablesview that has been
+   * linked to the toolbox's inspector, then select the corresponding node in
+   * the inspector, and switch the inspector tool in the toolbox
+   * @return a promise that resolves when the node is selected and the inspector
+   * has been switched to and is ready
+   */
+  openNodeInInspector: function(event) {
+    if (!this.toolbox) {
+      return promise.reject(new Error("Toolbox not available"));
+    }
+
+    event && event.stopPropagation();
+
+    return Task.spawn(function*() {
+      yield this.toolbox.initInspector();
+
+      let nodeFront = this._nodeFront;
+      if (!nodeFront) {
+        nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this._valueGrip.actor);
+      }
+
+      if (nodeFront) {
+        yield this.toolbox.selectTool("inspector");
+
+        let inspectorReady = promise.defer();
+        this.toolbox.getPanel("inspector").once("inspector-updated", inspectorReady.resolve);
+        yield this.toolbox.selection.setNodeFront(nodeFront, "variables-view");
+        yield inspectorReady.promise;
+      }
+    }.bind(this));
+  },
+
+  /**
+   * In case this variable is a DOMNode and part of a variablesview that has been
+   * linked to the toolbox's inspector, then highlight the corresponding node
+   */
+  highlightDomNode: function() {
+    if (this.toolbox) {
+      if (this._nodeFront) {
+        // If the nodeFront has been retrieved before, no need to ask the server
+        // again for it
+        this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront);
+        return;
+      }
+
+      this.toolbox.highlighterUtils.highlightDomValueGrip(this._valueGrip).then(front => {
+        this._nodeFront = front;
+      });
+    }
+  },
+
+  /**
+   * Unhighlight a previously highlit node
+   * @see highlightDomNode
+   */
+  unhighlightDomNode: function() {
+    if (this.toolbox) {
+      this.toolbox.highlighterUtils.unhighlight();
+    }
+  },
+
+  /**
    * Sets a variable's configurable, enumerable and writable attributes,
    * and specifies if it's a 'this', '<exception>', '<return>' or '__proto__'
    * reference.
    */
   _setAttributes: function() {
     let ownerView = this.ownerView;
     if (ownerView.preventDescriptorModifiers) {
       return;
@@ -2735,16 +2876,19 @@ Variable.prototype = Heritage.extend(Sco
   },
 
   /**
    * Makes this variable's value editable.
    */
   _activateValueInput: function(e) {
     EditableValue.create(this, {
       onSave: aString => {
+        if (this._linkedToInspector) {
+          this.unhighlightDomNode();
+        }
         if (!this._variablesView.preventDisableOnChange) {
           this._disable();
         }
         this.ownerView.eval(this, aString);
       }
     }, e);
   },
 
@@ -3568,16 +3712,23 @@ VariablesView.stringifiers._getNMoreStri
  *
  * @param any aGrip
  *        @see Variable.setGrip
  * @return string
  *         The custom class style.
  */
 VariablesView.getClass = function(aGrip) {
   if (aGrip && typeof aGrip == "object") {
+    if (aGrip.preview) {
+      switch (aGrip.preview.kind) {
+        case "DOMNode":
+          return "token-domnode";
+      }
+    }
+
     switch (aGrip.type) {
       case "undefined":
         return "token-undefined";
       case "null":
         return "token-null";
       case "Infinity":
       case "-Infinity":
       case "NaN":
--- a/browser/devtools/shared/widgets/widgets.css
+++ b/browser/devtools/shared/widgets/widgets.css
@@ -74,30 +74,33 @@
 .variable-or-property:not([non-extensible]) > tooltip > label.extensible,
 .variable-or-property:not([frozen]) > tooltip > label.frozen,
 .variable-or-property:not([sealed]) > tooltip > label.sealed {
   display: none;
 }
 
 .variable-or-property[pseudo-item] > tooltip,
 .variable-or-property[pseudo-item] > .title > .variables-view-edit,
+.variable-or-property[pseudo-item] > .title > .variables-view-open-inspector,
 .variable-or-property[pseudo-item] > .title > .variables-view-delete,
 .variable-or-property[pseudo-item] > .title > .variables-view-add-property,
 .variable-or-property[pseudo-item] > .title > .variable-or-property-frozen-label,
 .variable-or-property[pseudo-item] > .title > .variable-or-property-sealed-label,
 .variable-or-property[pseudo-item] > .title > .variable-or-property-non-extensible-label,
 .variable-or-property[pseudo-item] > .title > .variable-or-property-non-writable-icon {
   display: none;
 }
 
 *:not(:hover) .variables-view-delete,
+*:not(:hover) .variables-view-open-inspector,
 *:not(:hover) .variables-view-add-property {
   visibility: hidden;
 }
 
 .variables-view-delete > .toolbarbutton-text,
+.variables-view-open-inspector > .toolbarbutton-text,
 .variables-view-add-property > .toolbarbutton-text {
   display: none;
 }
 
 .variables-view-container[aligned-values] [optional-visibility] {
   display: none;
 }
--- a/browser/devtools/styleeditor/StyleEditorUI.jsm
+++ b/browser/devtools/styleeditor/StyleEditorUI.jsm
@@ -9,16 +9,17 @@ this.EXPORTED_SYMBOLS = ["StyleEditorUI"
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PluralForm.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
 let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
 Cu.import("resource:///modules/devtools/SplitView.jsm");
 Cu.import("resource:///modules/devtools/StyleSheetEditor.jsm");
 
 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
@@ -222,16 +223,17 @@ StyleEditorUI.prototype = {
     }
 
     styleSheet.getOriginalSources().then((sources) => {
       if (sources && sources.length) {
         this._removeStyleSheetEditor(editor);
         sources.forEach((source) => {
           // set so the first sheet will be selected, even if it's a source
           source.styleSheetIndex = styleSheet.styleSheetIndex;
+          source.relatedStyleSheet = styleSheet;
 
           this._addStyleSheetEditor(source);
         });
       }
     });
   },
 
   /**
@@ -245,16 +247,18 @@ StyleEditorUI.prototype = {
    *         Optional if stylesheet is a new sheet created by user
    */
   _addStyleSheetEditor: function(styleSheet, file, isNew) {
     let editor =
       new StyleSheetEditor(styleSheet, this._window, file, isNew, this._walker);
 
     editor.on("property-change", this._summaryChange.bind(this, editor));
     editor.on("style-applied", this._summaryChange.bind(this, editor));
+    editor.on("linked-css-file", this._summaryChange.bind(this, editor));
+    editor.on("linked-css-file-error", this._summaryChange.bind(this, editor));
     editor.on("error", this._onError);
 
     this.editors.push(editor);
 
     editor.fetchSource(this._sourceLoaded.bind(this, editor));
     return editor;
   },
 
@@ -552,37 +556,48 @@ StyleEditorUI.prototype = {
    *        Optional item's summary element to update. If none, item corresponding
    *        to passed editor is used.
    */
   _updateSummaryForEditor: function(editor, summary) {
     summary = summary || editor.summary;
     if (!summary) {
       return;
     }
-    let ruleCount = "-";
-    if (editor.styleSheet.ruleCount !== undefined) {
-      ruleCount = editor.styleSheet.ruleCount;
+
+    let ruleCount = editor.styleSheet.ruleCount;
+    if (editor.styleSheet.relatedStyleSheet) {
+      ruleCount = editor.styleSheet.relatedStyleSheet.ruleCount;
+    }
+    if (ruleCount === undefined) {
+      ruleCount = "-";
     }
 
     var flags = [];
     if (editor.styleSheet.disabled) {
       flags.push("disabled");
     }
     if (editor.unsaved) {
       flags.push("unsaved");
     }
+    if (editor.linkedCSSFileError) {
+      flags.push("linked-file-error");
+    }
     this._view.setItemClassName(summary, flags.join(" "));
 
     let label = summary.querySelector(".stylesheet-name > label");
     label.setAttribute("value", editor.friendlyName);
 
+    let linkedCSSFile = "";
+    if (editor.linkedCSSFile) {
+      linkedCSSFile = OS.Path.basename(editor.linkedCSSFile);
+    }
+    text(summary, ".stylesheet-linked-file", linkedCSSFile);
     text(summary, ".stylesheet-title", editor.styleSheet.title || "");
     text(summary, ".stylesheet-rule-count",
       PluralForm.get(ruleCount, _("ruleCount.label")).replace("#1", ruleCount));
-    text(summary, ".stylesheet-error-message", editor.errorMessage);
   },
 
   destroy: function() {
     this._clearStyleSheetEditors();
 
     this._prefObserver.off(PREF_ORIG_SOURCES, this._onNewDocument);
     this._prefObserver.destroy();
   }
--- a/browser/devtools/styleeditor/StyleSheetEditor.jsm
+++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm
@@ -29,16 +29,23 @@ const SAVE_ERROR = "error-save";
 
 // max update frequency in ms (avoid potential typing lag and/or flicker)
 // @see StyleEditor.updateStylesheet
 const UPDATE_STYLESHEET_THROTTLE_DELAY = 500;
 
 // Pref which decides if CSS autocompletion is enabled in Style Editor or not.
 const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled";
 
+// How long to wait to update linked CSS file after original source was saved
+// to disk. Time in ms.
+const CHECK_LINKED_SHEET_DELAY=500;
+
+// How many times to check for linked file changes
+const MAX_CHECK_COUNT=10;
+
 /**
  * StyleSheetEditor controls the editor linked to a particular StyleSheet
  * object.
  *
  * Emits events:
  *   'property-change': A property on the underlying stylesheet has changed
  *   'source-editor-load': The source editor for this editor has been loaded
  *   'error': An error has occured
@@ -54,53 +61,52 @@ const AUTOCOMPLETION_PREF = "devtools.st
  * @param {Walker} walker
  *        Optional walker used for selectors autocompletion
  */
 function StyleSheetEditor(styleSheet, win, file, isNew, walker) {
   EventEmitter.decorate(this);
 
   this.styleSheet = styleSheet;
   this._inputElement = null;
-  this._sourceEditor = null;
+  this.sourceEditor = null;
   this._window = win;
   this._isNew = isNew;
-  this.savedFile = file;
   this.walker = walker;
 
-  this.errorMessage = null;
-
-  let readOnly = false;
-  if (styleSheet.isOriginalSource) {
-    // live-preview won't work with sources that need compilation
-    readOnly = true;
-  }
-
   this._state = {   // state to use when inputElement attaches
     text: "",
     selection: {
       start: {line: 0, ch: 0},
       end: {line: 0, ch: 0}
     },
-    readOnly: readOnly,
-    topIndex: 0,              // the first visible line
+    topIndex: 0              // the first visible line
   };
 
   this._styleSheetFilePath = null;
   if (styleSheet.href &&
       Services.io.extractScheme(this.styleSheet.href) == "file") {
     this._styleSheetFilePath = this.styleSheet.href;
   }
 
   this._onPropertyChange = this._onPropertyChange.bind(this);
   this._onError = this._onError.bind(this);
+  this.checkLinkedFileForChanges = this.checkLinkedFileForChanges.bind(this);
+  this.markLinkedFileBroken = this.markLinkedFileBroken.bind(this);
 
   this._focusOnSourceEditorReady = false;
 
+  let relatedSheet = this.styleSheet.relatedStyleSheet;
+  if (relatedSheet) {
+    relatedSheet.on("property-change", this._onPropertyChange);
+  }
   this.styleSheet.on("property-change", this._onPropertyChange);
   this.styleSheet.on("error", this._onError);
+
+  this.savedFile = file;
+  this.linkCSSFile();
 }
 
 StyleSheetEditor.prototype = {
   /**
    * Whether there are unsaved changes in the editor
    */
   get unsaved() {
     return this.sourceEditor && !this.sourceEditor.isClean();
@@ -109,16 +115,26 @@ StyleSheetEditor.prototype = {
   /**
    * Whether the editor is for a stylesheet created by the user
    * through the style editor UI.
    */
   get isNew() {
     return this._isNew;
   },
 
+  get savedFile() {
+    return this._savedFile;
+  },
+
+  set savedFile(name) {
+    this._savedFile = name;
+
+    this.linkCSSFile();
+  },
+
   /**
    * Get a user-friendly name for the style sheet.
    *
    * @return string
    */
   get friendlyName() {
     if (this.savedFile) {
       return this.savedFile.leafName;
@@ -141,16 +157,58 @@ StyleSheetEditor.prototype = {
         this._friendlyName = decodeURI(this._friendlyName);
       } catch (ex) {
       }
     }
     return this._friendlyName;
   },
 
   /**
+   * If this is an original source, get the path of the CSS file it generated.
+   */
+  linkCSSFile: function() {
+    if (!this.styleSheet.isOriginalSource) {
+      return;
+    }
+
+    let relatedSheet = this.styleSheet.relatedStyleSheet;
+
+    let path;
+    var uri = NetUtil.newURI(relatedSheet.href);
+
+    if (uri.scheme == "file") {
+      var file = uri.QueryInterface(Ci.nsIFileURL).file;
+      path = file.path;
+    }
+    else if (this.savedFile) {
+      let origUri = NetUtil.newURI(this.styleSheet.href);
+      path = findLinkedFilePath(uri, origUri, this.savedFile);
+    }
+    else {
+      // we can't determine path to generated file on disk
+      return;
+    }
+
+    if (this.linkedCSSFile == path) {
+      return;
+    }
+
+    this.linkedCSSFile = path;
+
+    this.linkedCSSFileError = null;
+
+    // save last file change time so we can compare when we check for changes.
+    OS.File.stat(path).then((info) => {
+      this._fileModDate = info.lastModificationDate.getTime();
+    }, this.markLinkedFileBroken);
+
+    this.emit("linked-css-file");
+  },
+
+  /**
    * Start fetching the full text source for this editor's sheet.
    */
   fetchSource: function(callback) {
     this.styleSheet.getText().then((longStr) => {
       longStr.string().then((source) => {
         this._state.text = prettifyCSS(source);
         this.sourceLoaded = true;
 
@@ -191,35 +249,37 @@ StyleSheetEditor.prototype = {
    */
   load: function(inputElement) {
     this._inputElement = inputElement;
 
     let config = {
       value: this._state.text,
       lineNumbers: true,
       mode: Editor.modes.css,
-      readOnly: this._state.readOnly,
+      readOnly: false,
       autoCloseBrackets: "{}()[]",
       extraKeys: this._getKeyBindings(),
       contextMenu: "sourceEditorContextMenu"
     };
     let sourceEditor = new Editor(config);
 
     sourceEditor.appendTo(inputElement).then(() => {
       if (Services.prefs.getBoolPref(AUTOCOMPLETION_PREF)) {
         sourceEditor.extend(AutoCompleter);
         sourceEditor.setupAutoCompletion(this.walker);
       }
       sourceEditor.on("save", () => {
         this.saveToFile();
       });
 
-      sourceEditor.on("change", () => {
-        this.updateStyleSheet();
-      });
+      if (this.styleSheet.update) {
+        sourceEditor.on("change", () => {
+          this.updateStyleSheet();
+        });
+      }
 
       this.sourceEditor = sourceEditor;
 
       if (this._focusOnSourceEditorReady) {
         this._focusOnSourceEditorReady = false;
         sourceEditor.focus();
       }
 
@@ -250,29 +310,29 @@ StyleSheetEditor.prototype = {
     });
     return deferred.promise;
   },
 
   /**
    * Focus the Style Editor input.
    */
   focus: function() {
-    if (this._sourceEditor) {
-      this._sourceEditor.focus();
+    if (this.sourceEditor) {
+      this.sourceEditor.focus();
     } else {
       this._focusOnSourceEditorReady = true;
     }
   },
 
   /**
    * Event handler for when the editor is shown.
    */
   onShow: function() {
-    if (this._sourceEditor) {
-      this._sourceEditor.setFirstVisibleLine(this._state.topIndex);
+    if (this.sourceEditor) {
+      this.sourceEditor.setFirstVisibleLine(this._state.topIndex);
     }
     this.focus();
   },
 
   /**
    * Toggled the disabled state of the underlying stylesheet.
    */
   toggleDisabled: function() {
@@ -338,18 +398,18 @@ StyleSheetEditor.prototype = {
     let onFile = (returnFile) => {
       if (!returnFile) {
         if (callback) {
           callback(null);
         }
         return;
       }
 
-      if (this._sourceEditor) {
-        this._state.text = this._sourceEditor.getText();
+      if (this.sourceEditor) {
+        this._state.text = this.sourceEditor.getText();
       }
 
       let ostream = FileUtils.openSafeFileOutputStream(returnFile);
       let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                         .createInstance(Ci.nsIScriptableUnicodeConverter);
       converter.charset = "UTF-8";
       let istream = converter.convertToInputStream(this._state.text);
 
@@ -357,36 +417,106 @@ StyleSheetEditor.prototype = {
         if (!Components.isSuccessCode(status)) {
           if (callback) {
             callback(null);
           }
           this.emit("error", SAVE_ERROR);
           return;
         }
         FileUtils.closeSafeFileOutputStream(ostream);
-        // remember filename for next save if any
-        this._friendlyName = null;
-        this.savedFile = returnFile;
+
+        this.onFileSaved(returnFile);
 
         if (callback) {
           callback(returnFile);
         }
-        this.sourceEditor.setClean();
-
-        this.emit("property-change");
       }.bind(this));
     };
 
     let defaultName;
     if (this._friendlyName) {
       defaultName = OS.Path.basename(this._friendlyName);
     }
     showFilePicker(file || this._styleSheetFilePath, true, this._window,
                    onFile, defaultName);
- },
+  },
+
+  /**
+   * Called when this source has been successfully saved to disk.
+   */
+  onFileSaved: function(returnFile) {
+    this._friendlyName = null;
+    this.savedFile = returnFile;
+
+    this.sourceEditor.setClean();
+
+    this.emit("property-change");
+
+    // TODO: replace with file watching
+    this._modCheckCount = 0;
+    this._window.clearTimeout(this._timeout);
+
+    if (this.linkedCSSFile && !this.linkedCSSFileError) {
+      this._timeout = this._window.setTimeout(this.checkLinkedFileForChanges,
+                                              CHECK_LINKED_SHEET_DELAY);
+    }
+  },
+
+  /**
+   * Check to see if our linked CSS file has changed on disk, and
+   * if so, update the live style sheet.
+   */
+  checkLinkedFileForChanges: function() {
+    OS.File.stat(this.linkedCSSFile).then((info) => {
+      let lastChange = info.lastModificationDate.getTime();
+
+      if (this._fileModDate && lastChange != this._fileModDate) {
+        this._fileModDate = lastChange;
+        this._modCheckCount = 0;
+
+        this.updateLinkedStyleSheet();
+        return;
+      }
+
+      if (++this._modCheckCount > MAX_CHECK_COUNT) {
+        this.updateLinkedStyleSheet();
+        return;
+      }
+
+      // try again in a bit
+      this._timeout = this._window.setTimeout(this.checkLinkedFileForChanges,
+                                              CHECK_LINKED_SHEET_DELAY);
+    }, this.markLinkedFileBroken);
+  },
+
+  /**
+   * Notify that the linked CSS file (if this is an original source)
+   * doesn't exist on disk in the place we think it does.
+   *
+   * @param string error
+   *        The error we got when trying to access the file.
+   */
+  markLinkedFileBroken: function(error) {
+    this.linkedCSSFileError = error || true;
+    this.emit("linked-css-file-error");
+  },
+
+  /**
+   * For original sources (e.g. Sass files). Fetch contents of linked CSS
+   * file from disk and live update the stylesheet object with the contents.
+   */
+  updateLinkedStyleSheet: function() {
+    OS.File.read(this.linkedCSSFile).then((array) => {
+      let decoder = new TextDecoder();
+      let text = decoder.decode(array);
+
+      let relatedSheet = this.styleSheet.relatedStyleSheet;
+      relatedSheet.update(text, true);
+    }, this.markLinkedFileBroken);
+  },
 
   /**
     * Retrieve custom key bindings objects as expected by Editor.
     * Editor action names are not displayed to the user.
     *
     * @return {array} key binding objects for the source editor
     */
   _getKeyBindings: function() {
@@ -477,8 +607,78 @@ function prettifyCSS(text)
 
     if (c == "{") {
       indent = TAB_CHARS.repeat(++indentLevel);
     }
   }
   return parts.join(LINE_SEPARATOR);
 }
 
+/**
+ * Find a path on disk for a file given it's hosted uri, the uri of the
+ * original resource that generated it (e.g. Sass file), and the location of the
+ * local file for that source.
+ */
+function findLinkedFilePath(uri, origUri, file) {
+  let project = findProjectPath(origUri, file);
+  let branch = findUnsharedBranch(origUri, uri);
+
+  let parts = project.concat(branch);
+  let path = OS.Path.join.apply(this, parts);
+
+  return path;
+}
+
+/**
+ * Find the path of a project given a file in the project and the uri
+ * of that resource. e.g.:
+ * "http://localhost/src/a.css" and "/Users/moz/proj/src/a.css"
+ * would yeild ["Users", "moz", "proj"]
+ *
+ * @param {nsIURI} uri
+ *        uri of hosted resource
+ * @param {nsIFile} file
+ *        file for that resource on disk
+ * @return {array}
+ *        array of path parts
+ */
+function findProjectPath(uri, file) {
+  let uri = OS.Path.split(uri.path).components;
+  let path = OS.Path.split(file.path).components;
+
+  // don't care about differing leaf names
+  uri.pop();
+  path.pop();
+
+  let dir = path.pop();
+  while(dir) {
+    let serverDir = uri.pop();
+    if (serverDir != dir) {
+      return path.concat([dir]);
+    }
+    dir = path.pop();
+  }
+  return [];
+}
+
+/**
+ * Find the part of a uri past the root it shares with another uri. e.g:
+ * "http://localhost/built/a.scss" and "http://localhost/src/a.css"
+ * would yeild ["built", "a.scss"];
+ *
+ * @param {nsIURI} origUri
+ *        uri to find unshared branch of
+ * @param {nsIURI} origUri
+ *        uri to compare against to get a shared root
+ * @return {array}
+ *         array of path parts for branch
+ */
+function findUnsharedBranch(origUri, uri) {
+  origUri = OS.Path.split(origUri.path).components;
+  uri = OS.Path.split(uri.path).components;
+
+  for (var i = 0; i < uri.length - 1; i++) {
+    if (uri[i] != origUri[i]) {
+      return uri.slice(i);
+    }
+  }
+  return uri;
+}
--- a/browser/devtools/styleeditor/styleeditor-panel.js
+++ b/browser/devtools/styleeditor/styleeditor-panel.js
@@ -131,17 +131,16 @@ StyleEditorPanel.prototype = {
     if (!this._destroyed) {
       this._destroyed = true;
 
       this._target.off("close", this.destroy);
       this._target = null;
       this._toolbox = null;
       this._panelDoc = null;
 
-      this._debuggee.destroy();
       this.UI.destroy();
     }
 
     return promise.resolve(null);
   },
 }
 
 XPCOMUtils.defineLazyGetter(StyleEditorPanel.prototype, "strings",
--- a/browser/devtools/styleeditor/styleeditor.css
+++ b/browser/devtools/styleeditor/styleeditor.css
@@ -43,16 +43,32 @@ li.error > .stylesheet-info > .styleshee
 .stylesheet-name {
   white-space: nowrap;
 }
 
 li.unsaved > hgroup > h1 > .stylesheet-name:before {
   content: "*";
 }
 
+li.linked-file-error .stylesheet-linked-file {
+  text-decoration: line-through;
+}
+
+li.linked-file-error .stylesheet-linked-file:after {
+  content: " ✘";
+}
+
+li.linked-file-error .stylesheet-rule-count {
+  visibility: hidden;
+}
+
+.stylesheet-linked-file:not(:empty):before {
+  content: " ↳ ";
+}
+
 .stylesheet-enabled {
   display: -moz-box;
   cursor: pointer;
 }
 
 .stylesheet-saveButton {
   display: none;
   margin-top: 0px;
--- a/browser/devtools/styleeditor/styleeditor.xul
+++ b/browser/devtools/styleeditor/styleeditor.xul
@@ -106,18 +106,18 @@
       <li id="splitview-tpl-summary-stylesheet" tabindex="0">
         <xul:label class="stylesheet-enabled" tabindex="0"
           tooltiptext="&visibilityToggle.tooltip;"
           accesskey="&saveButton.accesskey;"></xul:label>
         <hgroup class="stylesheet-info">
           <h1><a class="stylesheet-name" tabindex="0"><xul:label crop="start"/></a></h1>
           <div class="stylesheet-more">
             <h3 class="stylesheet-title"></h3>
+            <h3 class="stylesheet-linked-file"></h3>
             <h3 class="stylesheet-rule-count"></h3>
-            <h3 class="stylesheet-error-message"></h3>
             <xul:spacer/>
             <h3><xul:label class="stylesheet-saveButton"
                   tooltiptext="&saveButton.tooltip;"
                   accesskey="&saveButton.accesskey;">&saveButton.label;</xul:label></h3>
           </div>
         </hgroup>
       </li>
 
--- a/browser/devtools/styleeditor/test/browser.ini
+++ b/browser/devtools/styleeditor/test/browser.ini
@@ -44,8 +44,9 @@ support-files =
 # Disabled because of intermittent failures - See Bug 942473
 skip-if = true
 [browser_styleeditor_private_perwindowpb.js]
 [browser_styleeditor_reload.js]
 [browser_styleeditor_sv_keynav.js]
 [browser_styleeditor_sv_resize.js]
 [browser_styleeditor_selectstylesheet.js]
 [browser_styleeditor_sourcemaps.js]
+[browser_styleeditor_sourcemap_watching.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_sourcemap_watching.js
@@ -0,0 +1,182 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/Task.jsm");
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let promise = devtools.require("sdk/core/promise");
+
+const TESTCASE_URI_HTML = TEST_BASE + "sourcemaps.html";
+const TESTCASE_URI_CSS = TEST_BASE + "sourcemaps.css";
+const TESTCASE_URI_REG_CSS = TEST_BASE + "simple.css";
+const TESTCASE_URI_SCSS = TEST_BASE + "sourcemaps.scss";
+const TESTCASE_URI_MAP = TEST_BASE + "sourcemaps.css.map";
+
+const PREF = "devtools.styleeditor.source-maps-enabled";
+
+const CSS_TEXT = "* { color: blue }";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+let tempScope = {};
+Components.utils.import("resource://gre/modules/FileUtils.jsm", tempScope);
+Components.utils.import("resource://gre/modules/NetUtil.jsm", tempScope);
+let FileUtils = tempScope.FileUtils;
+let NetUtil = tempScope.NetUtil;
+
+function test()
+{
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref(PREF, true);
+
+  Task.spawn(function() {
+    // copy all our files over so we don't screw them up for other tests
+    let HTMLFile = yield copy(TESTCASE_URI_HTML, "sourcemaps.html");
+    let CSSFile = yield copy(TESTCASE_URI_CSS, "sourcemaps.css");
+    yield copy(TESTCASE_URI_SCSS, "sourcemaps.scss");
+    yield copy(TESTCASE_URI_MAP, "sourcemaps.css.map");
+    yield copy(TESTCASE_URI_REG_CSS, "simple.css");
+
+    let uri = Services.io.newFileURI(HTMLFile);
+    let testcaseURI = uri.resolve("");
+
+    let editor = yield openEditor(testcaseURI);
+
+    let element = content.document.querySelector("div");
+    let style = content.getComputedStyle(element, null);
+
+    is(style.color, "rgb(255, 0, 102)", "div is red before saving file");
+
+    editor.styleSheet.relatedStyleSheet.once("style-applied", function() {
+      is(style.color, "rgb(0, 0, 255)", "div is blue after saving file");
+      finishUp();
+    });
+
+    yield pauseForTimeChange();
+
+    // Edit and save Sass in the editor. This will start off a file-watching
+    // process waiting for the CSS file to change.
+    yield editSCSS(editor);
+
+    // We can't run Sass or another compiler, so we fake it by just
+    // directly changing the CSS file.
+    yield editCSSFile(CSSFile);
+
+    info("wrote to CSS file");
+  })
+}
+
+function openEditor(testcaseURI) {
+  let deferred = promise.defer();
+
+  addTabAndOpenStyleEditor((panel) => {
+    info("style editor panel opened");
+
+    let UI = panel.UI;
+    let count = 0;
+
+    UI.on("editor-added", (event, editor) => {
+      if (++count == 3) {
+        // wait for 3 editors - 1 for first style sheet, 1 for the
+        // generated style sheet, and 1 for original source after it
+        // loads and replaces the generated style sheet.
+        let editor = UI.editors[1];
+
+        let link = getStylesheetNameLinkFor(editor);
+        link.click();
+
+        editor.getSourceEditor().then(deferred.resolve);
+      }
+    });
+  })
+  content.location = testcaseURI;
+
+  return deferred.promise;
+}
+
+function editSCSS(editor) {
+  let deferred = promise.defer();
+
+  let pos = {line: 0, ch: 0};
+  editor.sourceEditor.replaceText(CSS_TEXT, pos, pos);
+
+  editor.saveToFile(null, function (file) {
+    ok(file, "Scss file should be saved");
+    deferred.resolve();
+  });
+
+  return deferred.promise;
+}
+
+function editCSSFile(CSSFile) {
+  return write(CSS_TEXT, CSSFile);
+}
+
+function pauseForTimeChange() {
+  let deferred = promise.defer();
+
+  // We have to wait for the system time to turn over > 1000 ms so that
+  // our file's last change time will show a change. This reflects what
+  // would happen in real life with a user manually saving the file.
+  setTimeout(deferred.resolve, 2000);
+
+  return deferred.promise;
+}
+
+function finishUp() {
+  Services.prefs.clearUserPref(PREF);
+  finish();
+}
+
+/* Helpers */
+
+function getStylesheetNameLinkFor(editor) {
+  return editor.summary.querySelector(".stylesheet-name");
+}
+
+function copy(aSrcChromeURL, aDestFileName)
+{
+  let destFile = FileUtils.getFile("ProfD", [aDestFileName]);
+  return write(read(aSrcChromeURL), destFile);
+}
+
+function read(aSrcChromeURL)
+{
+  let scriptableStream = Cc["@mozilla.org/scriptableinputstream;1"]
+    .getService(Ci.nsIScriptableInputStream);
+
+  let channel = Services.io.newChannel(aSrcChromeURL, null, null);
+  let input = channel.open();
+  scriptableStream.init(input);
+
+  let data = scriptableStream.read(input.available());
+  scriptableStream.close();
+  input.close();
+
+  return data;
+}
+
+function write(aData, aFile)
+{
+  let deferred = promise.defer();
+
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+    .createInstance(Ci.nsIScriptableUnicodeConverter);
+
+  converter.charset = "UTF-8";
+
+  let istream = converter.convertToInputStream(aData);
+  let ostream = FileUtils.openSafeFileOutputStream(aFile);
+
+  NetUtil.asyncCopy(istream, ostream, function(status) {
+    if (!Components.isSuccessCode(status)) {
+      info("Coudln't write to " + aFile.path);
+      return;
+    }
+    deferred.resolve(aFile);
+  });
+
+  return deferred.promise;
+}
--- a/browser/devtools/webconsole/test/browser.ini
+++ b/browser/devtools/webconsole/test/browser.ini
@@ -99,16 +99,17 @@ support-files =
   test_bug_770099_violation.html
   test_bug_770099_violation.html^headers^
   test-autocomplete-in-stackframe.html
   testscript.js
   test-bug_923281_console_log_filter.html
   test-bug_923281_test1.js
   test-bug_923281_test2.js
   test-bug_939783_console_trace_duplicates.html
+  test-bug-952277-highlight-nodes-in-vview.html
 
 [browser_bug664688_sandbox_update_after_navigation.js]
 [browser_bug_638949_copy_link_location.js]
 [browser_bug_862916_console_dir_and_filter_off.js]
 [browser_bug_865288_repeat_different_objects.js]
 [browser_bug_865871_variables_view_close_on_esc_key.js]
 [browser_bug_869003_inspect_cross_domain_object.js]
 [browser_bug_871156_ctrlw_close_tab.js]
@@ -253,9 +254,10 @@ run-if = os == "mac"
 [browser_webconsole_expandable_timestamps.js]
 [browser_webconsole_autocomplete_in_debugger_stackframe.js]
 [browser_webconsole_autocomplete_popup_close_on_tab_switch.js]
 [browser_webconsole_output_01.js]
 [browser_webconsole_output_02.js]
 [browser_webconsole_output_03.js]
 [browser_webconsole_output_04.js]
 [browser_webconsole_output_events.js]
+[browser_console_variables_view_highlighter.js]
 [browser_webconsole_console_trace_duplicates.js]
--- a/browser/devtools/webconsole/test/browser_console_click_focus.js
+++ b/browser/devtools/webconsole/test/browser_console_click_focus.js
@@ -11,29 +11,50 @@ function test() {
   addTab(TEST_URI);
   browser.addEventListener("DOMContentLoaded", testInputFocus, false);
 }
 
 function testInputFocus() {
   browser.removeEventListener("DOMContentLoaded", testInputFocus, false);
 
   openConsole().then((hud) => {
-    let inputNode = hud.jsterm.inputNode;
-    ok(inputNode.getAttribute("focused"), "input node is focused");
+    waitForMessages({
+      webconsole: hud,
+      messages: [{
+        text: "Dolske Digs Bacon",
+        category: CATEGORY_WEBDEV,
+        severity: SEVERITY_LOG,
+      }],
+    }).then(([result]) => {
+      let msg = [...result.matched][0];
+      let outputItem = msg.querySelector(".body");
+      ok(outputItem, "found a logged message");
+      let inputNode = hud.jsterm.inputNode;
+      ok(inputNode.getAttribute("focused"), "input node is focused, first");
 
-    let lostFocus = () => {
-      inputNode.removeEventListener("blur", lostFocus);
-      info("input node lost focus");
-    }
-
-   	inputNode.addEventListener("blur", lostFocus);
+      let lostFocus = () => {
+        inputNode.removeEventListener("blur", lostFocus);
+        info("input node lost focus");
+      }
 
-   	browser.ownerDocument.getElementById("urlbar").click();
+      inputNode.addEventListener("blur", lostFocus);
+
+      browser.ownerDocument.getElementById("urlbar").click();
 
-  	ok(!inputNode.getAttribute("focused"), "input node is not focused");
+      ok(!inputNode.getAttribute("focused"), "input node is not focused");
+
+      EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
+
+      ok(inputNode.getAttribute("focused"), "input node is focused, second time")
 
-   	hud.outputNode.click();
+      // test click-drags are not focusing the input element.
+      EventUtils.sendMouseEvent({type: "mousedown", clientX: 3, clientY: 4},
+        outputItem);
+      EventUtils.sendMouseEvent({type: "click", clientX: 15, clientY: 5},
+        outputItem);
 
-   	ok(inputNode.getAttribute("focused"), "input node is focused");
-
-      finishTest();
+      executeSoon(() => {
+        todo(!inputNode.getAttribute("focused"), "input node is not focused after drag");
+        finishTest();
+      });
+    });
   });
 }
--- a/browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js
+++ b/browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js
@@ -17,29 +17,39 @@ function test()
     openConsole(null, consoleOpened);
   }, true);
 
   function consoleOpened(aHud)
   {
     hud = aHud;
     ok(hud, "Web Console opened");
 
-    content.console.log("foobarz1");
+    info("dump some spew into the console for scrolling");
+    for (let i = 0; i < 100; i++)
+      content.console.log("foobarz" + i);
     waitForMessages({
       webconsole: hud,
       messages: [{
-        text: "foobarz1",
+        text: "foobarz99",
         category: CATEGORY_WEBDEV,
         severity: SEVERITY_LOG,
       }],
     }).then(onConsoleMessage);
   }
 
   function onConsoleMessage()
   {
+    let currentPosition = hud.outputNode.parentNode.scrollTop;
+    EventUtils.synthesizeKey("VK_PAGE_UP", {});
+    isnot(hud.outputNode.parentNode.scrollTop, currentPosition, "scroll position changed after page up");
+
+    currentPosition = hud.outputNode.parentNode.scrollTop;
+    EventUtils.synthesizeKey("VK_PAGE_DOWN", {});
+    ok(hud.outputNode.parentNode.scrollTop > currentPosition, "scroll position now at bottom");
+
     hud.jsterm.once("messages-cleared", onClear);
     info("try ctrl-l to clear output");
     EventUtils.synthesizeKey("l", { ctrlKey: true });
   }
 
   function onClear()
   {
     is(hud.outputNode.textContent.indexOf("foobarz1"), -1, "output cleared");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_variables_view_highlighter.js
@@ -0,0 +1,91 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Check that variables view is linked to the inspector for highlighting and
+// selecting DOM nodes
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html";
+
+let gWebConsole, gJSTerm, gVariablesView, gToolbox;
+
+function test()
+{
+  addTab(TEST_URI);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
+}
+
+function consoleOpened(hud)
+{
+  gWebConsole = hud;
+  gJSTerm = hud.jsterm;
+  gToolbox = gDevTools.getToolbox(hud.target);
+  gJSTerm.execute("document.querySelectorAll('p')", onQSAexecuted);
+}
+
+function onQSAexecuted(msg)
+{
+  ok(msg, "output message found");
+  let anchor = msg.querySelector("a");
+  ok(anchor, "object link found");
+
+  gJSTerm.once("variablesview-fetched", onNodeListVviewFetched);
+
+  executeSoon(() =>
+    EventUtils.synthesizeMouse(anchor, 2, 2, {}, gWebConsole.iframeWindow)
+  );
+}
+
+function onNodeListVviewFetched(aEvent, aVar)
+{
+  gVariablesView = aVar._variablesView;
+  ok(gVariablesView, "variables view object");
+
+  // Transform the vview into an array we can filter properties from
+  let props = [[id, prop] for([id, prop] of aVar)];
+  // These properties are the DOM nodes ones
+  props = props.filter(v => v[0].match(/[0-9]+/));
+
+  function hoverOverDomNodeVariableAndAssertHighlighter(index) {
+    if (props[index]) {
+      let prop = props[index][1];
+      let valueEl = prop._valueLabel;
+
+      gToolbox.once("node-highlight", () => {
+        ok(true, "The highlighter was shown on hover of the DOMNode");
+        gToolbox.highlighterUtils.unhighlight().then(() => {
+          clickOnDomNodeVariableAndAssertInspectorSelected(index);
+        });
+      });
+
+      // Rather than trying to emulate a mouseenter event, let's call the
+      // variable's highlightDomNode and see if it has the desired effect
+      prop.highlightDomNode();
+    } else {
+      finishTest();
+    }
+  }
+
+  function clickOnDomNodeVariableAndAssertInspectorSelected(index) {
+    let prop = props[index][1];
+
+    // Make sure the inspector is initialized so we can listen to its events
+    gToolbox.initInspector().then(() => {
+      // Rather than trying to click on the value here, let's just call the
+      // variable's openNodeInInspector function and see if it has the
+      // desired effect
+      prop.openNodeInInspector().then(() => {
+        is(gToolbox.currentToolId, "inspector", "The toolbox switched over the inspector on DOMNode click");
+        gToolbox.selectTool("webconsole").then(() => {
+          hoverOverDomNodeVariableAndAssertHighlighter(index + 1);
+        });
+      });
+    });
+  }
+
+  hoverOverDomNodeVariableAndAssertHighlighter(0);
+}
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
@@ -86,16 +86,28 @@ function consoleOpened(aHud) {
 
     EventUtils.synthesizeKey("VK_UP", {});
 
     is(popup.selectedIndex, 0, "index 0 is selected");
     is(popup.selectedItem.label, "watch", "watch is selected");
     is(completeNode.value, prefix + "watch",
         "completeNode.value holds watch");
 
+    let currentSelectionIndex = popup.selectedIndex;
+
+    EventUtils.synthesizeKey("VK_PAGE_DOWN", {});
+
+    ok(popup.selectedIndex > currentSelectionIndex,
+      "Index is greater after PGDN");
+
+    currentSelectionIndex = popup.selectedIndex;
+    EventUtils.synthesizeKey("VK_PAGE_UP", {});
+
+    ok(popup.selectedIndex < currentSelectionIndex, "Index is less after Page UP");
+
     info("press Tab and wait for popup to hide");
     popup._panel.addEventListener("popuphidden", popupHideAfterTab, false);
     EventUtils.synthesizeKey("VK_TAB", {});
   }, false);
 
   info("wait for completion: window.foobarBug585991.");
   jsterm.setInputValue("window.foobarBug585991");
   EventUtils.synthesizeKey(".", {});
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
@@ -44,17 +44,17 @@ function createDocument() {
 function setupHighlighterTests() {
   ok(h1, "we have the header node");
   openInspector(runSelectionTests);
 }
 
 function runSelectionTests(aInspector) {
   inspector = aInspector;
 
-  inspector.toolbox.startPicker();
+  inspector.toolbox.highlighterUtils.startPicker();
   inspector.toolbox.once("picker-started", () => {
     info("Picker mode started, now clicking on H1 to select that node");
     executeSoon(() => {
       EventUtils.synthesizeMouseAtCenter(h1, {}, content);
       inspector.toolbox.once("picker-stopped", () => {
         info("Picker mode stopped, H1 selected, now switching to the console");
         openConsole(gBrowser.selectedTab).then(performWebConsoleTests);
       });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</title>
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+  </head>
+  <body>
+    <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p>
+    <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p>
+    <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p>
+  </body>
+</html>
+
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -566,25 +566,23 @@ WebConsoleFrame.prototype = {
 
     let toolbox = gDevTools.getToolbox(this.owner.target);
     if (toolbox) {
       toolbox.on("webconsole-selected", this._onPanelSelected);
     }
 
     /*
      * Focus input line whenever the output area is clicked.
-     * Only focus when the target node (or parent, as in source links) is
-     * not an anchor.
+     * Reusing _addMEssageLinkCallback since it correctly filters
+     * drag and select events.
      */
-    this.outputNode.addEventListener("click", (e) => {
-      if ((e.button == 0) &&
-          (e.target.nodeName.toLowerCase() != "a") &&
-          (e.target.parentNode.nodeName.toLowerCase() != "a")) {
+    this._addFocusCallback(this.outputNode, (evt) => {
+      if ((evt.target.nodeName.toLowerCase() != "a") &&
+          (evt.target.parentNode.nodeName.toLowerCase() != "a"))
         this.jsterm.inputNode.focus();
-      }
     });
 
     // Toggle the timestamp on preference change
     gDevTools.on("pref-changed", this._onToolboxPrefChanged);
     this._onToolboxPrefChanged("pref-changed", {
       pref: PREF_MESSAGE_TIMESTAMP,
       newValue: Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP),
     });
@@ -2639,40 +2637,80 @@ WebConsoleFrame.prototype = {
    * @private
    * @param nsIDOMNode aNode
    *        The node for which you want to add the event handlers.
    * @param function aCallback
    *        The function you want to invoke on click.
    */
   _addMessageLinkCallback: function WCF__addMessageLinkCallback(aNode, aCallback)
   {
-    aNode.addEventListener("mousedown", function(aEvent) {
+    aNode.addEventListener("mousedown", (aEvent) => {
       this._mousedown = true;
       this._startX = aEvent.clientX;
       this._startY = aEvent.clientY;
     }, false);
 
-    aNode.addEventListener("click", function(aEvent) {
+    aNode.addEventListener("click", (aEvent) => {
       let mousedown = this._mousedown;
       this._mousedown = false;
 
       // Do not allow middle/right-click or 2+ clicks.
       if (aEvent.detail != 1 || aEvent.button != 0) {
         return;
       }
 
       aEvent.preventDefault();
 
       // If this event started with a mousedown event and it ends at a different
       // location, we consider this text selection.
-      if (mousedown && this._startX != aEvent.clientX &&
-          this._startY != aEvent.clientY) {
+      if (mousedown &&
+          (this._startX != aEvent.clientX) &&
+          (this._startY != aEvent.clientY))
+      {
+        this._startX = this._startY = undefined;
         return;
       }
 
+      this._startX = this._startY = undefined;
+
+      aCallback.call(this, aEvent);
+    }, false);
+  },
+
+  _addFocusCallback: function WCF__addFocusCallback(aNode, aCallback)
+  {
+    aNode.addEventListener("mousedown", (aEvent) => {
+      this._mousedown = true;
+      this._startX = aEvent.clientX;
+      this._startY = aEvent.clientY;
+    }, false);
+
+    aNode.addEventListener("click", (aEvent) => {
+      let mousedown = this._mousedown;
+      this._mousedown = false;
+
+      // Do not allow middle/right-click or 2+ clicks.
+      if (aEvent.detail != 1 || aEvent.button != 0) {
+        return;
+      }
+
+      // If this event started with a mousedown event and it ends at a different
+      // location, we consider this text selection.
+      // Add a fuzz modifier of two pixels in any direction to account for sloppy
+      // clicking.
+      if (mousedown &&
+          (Math.abs(aEvent.clientX - this._startX) >= 2) &&
+          (Math.abs(aEvent.clientY - this._startY) >= 1))
+      {
+        this._startX = this._startY = undefined;
+        return;
+      }
+
+      this._startX = this._startY = undefined;
+
       aCallback.call(this, aEvent);
     }, false);
   },
 
   /**
    * Handler for the pref-changed event coming from the toolbox.
    * Currently this function only handles the timestamps preferences.
    *
@@ -3018,16 +3056,18 @@ JSTerm.prototype = {
    * Getter for the debugger WebConsoleClient.
    * @type object
    */
   get webConsoleClient() this.hud.webConsoleClient,
 
   COMPLETE_FORWARD: 0,
   COMPLETE_BACKWARD: 1,
   COMPLETE_HINT_ONLY: 2,
+  COMPLETE_PAGEUP: 3,
+  COMPLETE_PAGEDOWN: 4,
 
   /**
    * Initialize the JSTerminal UI.
    */
   init: function JST_init()
   {
     let autocompleteOptions = {
       onSelect: this.onAutocompleteSelect.bind(this),
@@ -3415,16 +3455,17 @@ JSTerm.prototype = {
    *        - hideFilterInput: boolean, if true the variables filter input is
    *        hidden.
    * @return object
    *         The new Variables View instance.
    */
   _createVariablesView: function JST__createVariablesView(aOptions)
   {
     let view = new VariablesView(aOptions.container);
+    view.toolbox = gDevTools.getToolbox(this.hud.owner.target);
     view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder");
     view.emptyText = l10n.getStr("emptyPropertiesList");
     view.searchEnabled = !aOptions.hideFilterInput;
     view.lazyEmpty = this._lazyVariablesView;
 
     VariablesViewController.attach(view, {
       getEnvironmentClient: aGrip => {
         return new EnvironmentClient(this.hud.proxy.client, aGrip);
@@ -3878,16 +3919,50 @@ JSTerm.prototype = {
         else if (this.canCaretGoNext()) {
           inputUpdated = this.historyPeruse(HISTORY_FORWARD);
         }
         if (inputUpdated) {
           aEvent.preventDefault();
         }
         break;
 
+      case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP:
+        if (this.autocompletePopup.isOpen) {
+          inputUpdated = this.complete(this.COMPLETE_PAGEUP);
+          if (inputUpdated) {
+            this._autocompletePopupNavigated = true;
+          }
+        }
+        else {
+          this.hud.outputNode.parentNode.scrollTop =
+            Math.max(0,
+              this.hud.outputNode.parentNode.scrollTop -
+              this.hud.outputNode.parentNode.clientHeight
+            );
+        }
+        aEvent.preventDefault();
+        break;
+
+      case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN:
+        if (this.autocompletePopup.isOpen) {
+          inputUpdated = this.complete(this.COMPLETE_PAGEDOWN);
+          if (inputUpdated) {
+            this._autocompletePopupNavigated = true;
+          }
+        }
+        else {
+          this.hud.outputNode.parentNode.scrollTop =
+            Math.min(this.hud.outputNode.parentNode.scrollHeight,
+              this.hud.outputNode.parentNode.scrollTop +
+              this.hud.outputNode.parentNode.clientHeight
+            );
+        }
+        aEvent.preventDefault();
+        break;
+
       case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
       case Ci.nsIDOMKeyEvent.DOM_VK_END:
       case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
         if (this.autocompletePopup.isOpen || this.lastCompletion.value) {
           this.clearCompletion();
         }
         break;
 
@@ -4049,16 +4124,20 @@ JSTerm.prototype = {
    *          completions is used. If the value changed, then the first possible
    *          completion is used and the selection is set from the current
    *          cursor position to the end of the completed text.
    *          If there is only one possible completion, then this completion
    *          value is used and the cursor is put at the end of the completion.
    *    - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the
    *          value stayed the same as the last time the function was called,
    *          then the previous completion of all possible completions is used.
+   *    - this.COMPLETE_PAGEUP: Scroll up one page if available or select the first
+   *          item.
+   *    - this.COMPLETE_PAGEDOWN: Scroll down one page if available or select the
+   *          last item.
    *    - this.COMPLETE_HINT_ONLY: If there is more than one possible
    *          completion and the input value stayed the same compared to the
    *          last time this function was called, then the same completion is
    *          used again. If there is only one possible completion, then
    *          the inputNode.value is set to this value and the selection is set
    *          from the current cursor position to the end of the completed text.
    * @param function aCallback
    *        Optional function invoked when the autocomplete properties are
@@ -4102,16 +4181,22 @@ JSTerm.prototype = {
       accepted = true;
     }
     else if (aType == this.COMPLETE_BACKWARD) {
       popup.selectPreviousItem();
     }
     else if (aType == this.COMPLETE_FORWARD) {
       popup.selectNextItem();
     }
+    else if (aType == this.COMPLETE_PAGEUP) {
+      popup.selectPreviousPageItem();
+    }
+    else if (aType == this.COMPLETE_PAGEDOWN) {
+      popup.selectNextPageItem();
+    }
 
     aCallback && aCallback(this);
     this.emit("autocomplete-updated");
     return accepted || popup.itemCount > 0;
   },
 
   /**
    * Update the completion result. This operation is performed asynchronously by
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -170,17 +170,16 @@ These should match what Safari and other
 
 <!ENTITY locationItem.title           "Location">
 <!ENTITY searchItem.title             "Search">
 
 <!-- Toolbar items --> 
 <!ENTITY homeButton.label             "Home">
 
 <!ENTITY tabGroupsButton.label        "Tab Groups">
-<!ENTITY tabGroupsButton.tooltip      "Group your tabs">
 
 <!ENTITY bookmarksButton.label          "Bookmarks">
 <!ENTITY bookmarksCmd.commandkey "b">
 
 <!ENTITY bookmarksMenuButton.label          "Bookmarks">
 <!ENTITY bookmarksMenuButton.unsorted.label "Unsorted Bookmarks">
 <!ENTITY viewBookmarksSidebar2.label        "View Bookmarks Sidebar">
 <!ENTITY viewBookmarksToolbar.label         "View Bookmarks Toolbar">
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.dtd
@@ -59,130 +59,130 @@
 <!ENTITY debuggerUI.clearButton "Clear">
 
 <!-- LOCALIZATION NOTE (debuggerUI.clearButton.tooltip): This is the tooltip for
   -  the button that clears the collected tracing data in the tracing tab. -->
 <!ENTITY debuggerUI.clearButton.tooltip "Clear the collected traces">
 
 <!-- LOCALIZATION NOTE (debuggerUI.pauseExceptions): This is the label for the
   -  checkbox that toggles pausing on exceptions. -->
-<!ENTITY debuggerUI.pauseExceptions           "Pause on exceptions">
+<!ENTITY debuggerUI.pauseExceptions           "Pause on Exceptions">
 <!ENTITY debuggerUI.pauseExceptions.accesskey "E">
 
 <!-- LOCALIZATION NOTE (debuggerUI.ignoreCaughtExceptions): This is the label for the
   -  checkbox that toggles ignoring caught exceptions. -->
-<!ENTITY debuggerUI.ignoreCaughtExceptions           "Ignore caught exceptions">
+<!ENTITY debuggerUI.ignoreCaughtExceptions           "Ignore Caught Exceptions">
 <!ENTITY debuggerUI.ignoreCaughtExceptions.accesskey "C">
 
 <!-- LOCALIZATION NOTE (debuggerUI.showPanesOnInit): This is the label for the
   -  checkbox that toggles visibility of panes when opening the debugger. -->
-<!ENTITY debuggerUI.showPanesOnInit           "Show panes on startup">
+<!ENTITY debuggerUI.showPanesOnInit           "Show Panes on Startup">
 <!ENTITY debuggerUI.showPanesOnInit.accesskey "S">
 
 <!-- LOCALIZATION NOTE (debuggerUI.showVarsFilter): This is the label for the
   -  checkbox that toggles visibility of a designated variables filter box. -->
-<!ENTITY debuggerUI.showVarsFilter           "Show variables filter box">
+<!ENTITY debuggerUI.showVarsFilter           "Show Variables Filter Box">
 <!ENTITY debuggerUI.showVarsFilter.accesskey "V">
 
 <!-- LOCALIZATION NOTE (debuggerUI.showOnlyEnum): This is the label for the
   -  checkbox that toggles visibility of hidden (non-enumerable) variables and
   -  properties in stack views. The "enumerable" flag is a state of a property
   -  defined in JavaScript. When in doubt, leave untranslated. -->
-<!ENTITY debuggerUI.showOnlyEnum           "Show only enumerable properties">
+<!ENTITY debuggerUI.showOnlyEnum           "Show Only Enumerable Properties">
 <!ENTITY debuggerUI.showOnlyEnum.accesskey "P">
 
 <!-- LOCALIZATION NOTE (debuggerUI.showOriginalSource): This is the label for
   -  the checkbox that toggles the display of original or sourcemap-derived
   -  sources. -->
-<!ENTITY debuggerUI.showOriginalSource           "Show original sources">
+<!ENTITY debuggerUI.showOriginalSource           "Show Original Sources">
 <!ENTITY debuggerUI.showOriginalSource.accesskey "O">
 
 <!-- LOCALIZATION NOTE (debuggerUI.searchPanelOperators): This is the text that
   -  appears in the filter panel popup as a header for the operators part. -->
 <!ENTITY debuggerUI.searchPanelOperators    "Operators:">
 
 <!-- LOCALIZATION NOTE (debuggerUI.searchFile): This is the text that appears
   -  in the source editor's context menu for the scripts search operation. -->
-<!ENTITY debuggerUI.searchFile           "Filter scripts">
+<!ENTITY debuggerUI.searchFile           "Filter Scripts">
 <!ENTITY debuggerUI.searchFile.key       "P">
 <!ENTITY debuggerUI.searchFile.altkey    "O">
 <!ENTITY debuggerUI.searchFile.accesskey "P">
 
 <!-- LOCALIZATION NOTE (debuggerUI.searchGlobal): This is the text that appears
   -  in the source editor's context menu for the global search operation. -->
-<!ENTITY debuggerUI.searchGlobal           "Search in all files">
+<!ENTITY debuggerUI.searchGlobal           "Search in All Files">
 <!ENTITY debuggerUI.searchGlobal.key       "F">
 <!ENTITY debuggerUI.searchGlobal.accesskey "F">
 
 <!-- LOCALIZATION NOTE (debuggerUI.searchFunction): This is the text that appears
   -  in the source editor's context menu for the function search operation. -->
-<!ENTITY debuggerUI.searchFunction           "Search for function definition">
+<!ENTITY debuggerUI.searchFunction           "Search for Function Definition">
 <!ENTITY debuggerUI.searchFunction.key       "D">
 <!ENTITY debuggerUI.searchFunction.accesskey "D">
 
 <!-- LOCALIZATION NOTE (debuggerUI.searchToken): This is the text that appears
   -  in the source editor's context menu for the token search operation. -->
 <!ENTITY debuggerUI.searchToken           "Find">
 <!ENTITY debuggerUI.searchToken.key       "F">
 <!ENTITY debuggerUI.searchToken.accesskey "F">
 
 <!-- LOCALIZATION NOTE (debuggerUI.searchLine): This is the text that appears
   -  in the source editor's context menu for the line search operation. -->
-<!ENTITY debuggerUI.searchGoToLine           "Go to line…">
+<!ENTITY debuggerUI.searchGoToLine           "Go to Line…">
 <!ENTITY debuggerUI.searchGoToLine.key       "L">
 <!ENTITY debuggerUI.searchGoToLine.accesskey "L">
 
 <!-- LOCALIZATION NOTE (debuggerUI.searchVariable): This is the text that appears
   -  in the source editor's context menu for the variables search operation. -->
-<!ENTITY debuggerUI.searchVariable           "Filter variables">
+<!ENTITY debuggerUI.searchVariable           "Filter Variables">
 <!ENTITY debuggerUI.searchVariable.key       "V">
 <!ENTITY debuggerUI.searchVariable.accesskey "V">
 
 <!-- LOCALIZATION NOTE (debuggerUI.focusVariables): This is the text that appears
   -  in the source editor's context menu for the variables focus operation. -->
-<!ENTITY debuggerUI.focusVariables           "Focus variables tree">
+<!ENTITY debuggerUI.focusVariables           "Focus Variables Tree">
 <!ENTITY debuggerUI.focusVariables.key       "V">
 <!ENTITY debuggerUI.focusVariables.accesskey "V">
 
 <!-- LOCALIZATION NOTE (debuggerUI.condBreakPanelTitle): This is the text that
   -  appears in the conditional breakpoint panel popup as a description. -->
 <!ENTITY debuggerUI.condBreakPanelTitle "This breakpoint will stop execution only if the following expression is true">
 
 <!-- LOCALIZATION NOTE (debuggerUI.seMenuBreak): This is the text that
   -  appears in the source editor context menu for adding a breakpoint. -->
-<!ENTITY debuggerUI.seMenuBreak     "Add breakpoint">
+<!ENTITY debuggerUI.seMenuBreak     "Add Breakpoint">
 <!ENTITY debuggerUI.seMenuBreak.key "B">
 
 <!-- LOCALIZATION NOTE (debuggerUI.seMenuCondBreak): This is the text that
   -  appears in the source editor context menu for adding a conditional
   -  breakpoint. -->
-<!ENTITY debuggerUI.seMenuCondBreak     "Add conditional breakpoint">
+<!ENTITY debuggerUI.seMenuCondBreak     "Add Conditional Breakpoint">
 <!ENTITY debuggerUI.seMenuCondBreak.key "B">
 
 <!-- LOCALIZATION NOTE (debuggerUI.tabs.*): This is the text that
   -  appears in the debugger's side pane tabs. -->
 <!ENTITY debuggerUI.tabs.sources        "Sources">
 <!ENTITY debuggerUI.tabs.traces         "Traces">
 <!ENTITY debuggerUI.tabs.callstack      "Call Stack">
 <!ENTITY debuggerUI.tabs.variables      "Variables">
 <!ENTITY debuggerUI.tabs.events         "Events">
 
 <!-- LOCALIZATION NOTE (debuggerUI.seMenuAddWatch): This is the text that
   -  appears in the source editor context menu for adding an expression. -->
-<!ENTITY debuggerUI.seMenuAddWatch      "Selection to watch expression">
+<!ENTITY debuggerUI.seMenuAddWatch      "Selection to Watch Expression">
 <!ENTITY debuggerUI.seMenuAddWatch.key  "E">
 
 <!-- LOCALIZATION NOTE (debuggerUI.addWatch): This is the text that
   -  appears in the watch expressions context menu for adding an expression. -->
-<!ENTITY debuggerUI.addWatch            "Add watch expression">
+<!ENTITY debuggerUI.addWatch            "Add Watch Expression">
 <!ENTITY debuggerUI.addWatch.accesskey  "E">
 
 <!-- LOCALIZATION NOTE (debuggerUI.removeWatch): This is the text that
   -  appears in the watch expressions context menu for removing all expressions. -->
-<!ENTITY debuggerUI.removeAllWatch           "Remove all watch expressions">
+<!ENTITY debuggerUI.removeAllWatch           "Remove All Watch Expressions">
 <!ENTITY debuggerUI.removeAllWatch.key       "E">
 <!ENTITY debuggerUI.removeAllWatch.accesskey "E">
 
 <!-- LOCALIZATION NOTE (debuggerUI.stepping): These are the keycodes that
   -  control the stepping commands in the debugger (continue, step over,
   -  step in and step out). -->
 <!ENTITY debuggerUI.stepping.resume1    "VK_F8">
 <!ENTITY debuggerUI.stepping.resume2    "VK_SLASH">
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.properties
@@ -261,16 +261,21 @@ variablesEditableValueTooltip=Click to c
 # LOCALIZATION NOTE (variablesCloseButtonTooltip): The text that is displayed
 # in the variables list on an item which can be removed.
 variablesCloseButtonTooltip=Click to remove
 
 # LOCALIZATION NOTE (variablesEditButtonTooltip): The text that is displayed
 # in the variables list on a getter or setter which can be edited.
 variablesEditButtonTooltip=Click to set value
 
+# LOCALIZATION NOTE (variablesEditableValueTooltip): The text that is displayed
+# in a tooltip on the "open in inspector" button in the the variables list for a
+# DOMNode item.
+variablesDomNodeValueTooltip=Click to select the node in the inspector
+
 # LOCALIZATION NOTE (configurable|...|Tooltip): The text that is displayed
 # in the variables list on certain variables or properties as tooltips.
 # Expanations of what these represent can be found at the following links:
 # https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
 # https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/isExtensible
 # https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/isFrozen
 # https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/isSealed
 # It's probably best to keep these in English.
--- 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,58 @@
   -  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 (netmonitorUI.context.copyImageAsDataUri): This is the label displayed
+  -  on the context menu that copies the selected image as data uri -->
+<!ENTITY netmonitorUI.context.copyImageAsDataUri "Copy Image as Data URI">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.context.copyImageAsDataUri.accesskey): This is the access key
+  -  for the Copy Image As Data URI menu item displayed in the context menu for a request -->
+<!ENTITY netmonitorUI.context.copyImageAsDataUri.accesskey  "I">
 
 <!-- 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 +247,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/metro/base/content/browser-ui.js
+++ b/browser/metro/base/content/browser-ui.js
@@ -1120,18 +1120,19 @@ var BrowserUI = {
       case "cmd_savePage":
         this.savePage();
         break;
     }
   },
 
   confirmSanitizeDialog: function () {
     let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
-    let title = bundle.GetStringFromName("clearPrivateData.title");
-    let message = bundle.GetStringFromName("clearPrivateData.message");
+    let title = bundle.GetStringFromName("clearPrivateData.title2");
+    let options = bundle.GetStringFromName("optionsCharm");
+    let message = bundle.GetStringFromName("clearPrivateData.message2").replace("#1", options);
     let clearbutton = bundle.GetStringFromName("clearPrivateData.clearButton");
 
     let prefsClearButton = document.getElementById("prefs-clear-data");
     prefsClearButton.disabled = true; 
 
     let buttonPressed = Services.prompt.confirmEx(
                           null,
                           title,
--- a/browser/metro/locales/en-US/chrome/browser.properties
+++ b/browser/metro/locales/en-US/chrome/browser.properties
@@ -33,18 +33,19 @@ contextAppbar2.delete=Delete
 # Button with this label only appears immediately after a deletion.
 contextAppbar2.restore=Undo delete
 
 # LOCALIZATION NOTE (contextAppbar2.clear): Unselects pages without modification.
 contextAppbar2.clear=Clear selection
 
 # Clear private data
 clearPrivateData.clearButton=Clear
-clearPrivateData.title=Clear Private Data
-clearPrivateData.message=Clear your private data?
+clearPrivateData.title2=Clear private data
+# LOCALIZATION NOTE (clearPrivateData.message2): #1 is optionsCharm
+clearPrivateData.message2=This will permanently delete the private data you have selected in #1
 
 # Settings Charms
 aboutCharm1=About
 optionsCharm=Options
 searchCharm=Search
 helpOnlineCharm=Help (online)
 
 # General
deleted file mode 100644
index 0945af275e8860ed810900b6d1f36d1707622eae..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index af42a28df9c4a32ecdc6cbe670abbe5c7f4fe316..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -219,38 +219,40 @@ browser.jar:
   skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
   skin/classic/browser/devtools/responsive-horizontal-resizer.png (devtools/responsive-horizontal-resizer.png)
   skin/classic/browser/devtools/responsive-background.png (devtools/responsive-background.png)
   skin/classic/browser/devtools/toggle-tools.png          (devtools/toggle-tools.png)
   skin/classic/browser/devtools/dock-bottom@2x.png        (../shared/devtools/images/dock-bottom@2x.png)
   skin/classic/browser/devtools/dock-side@2x.png          (../shared/devtools/images/dock-side@2x.png)
   skin/classic/browser/devtools/floating-scrollbars.css   (devtools/floating-scrollbars.css)
   skin/classic/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css)
-  skin/classic/browser/devtools/inspector.css             (devtools/inspector.css)
-  skin/classic/browser/devtools/profiler-stopwatch.png    (../shared/devtools/images/profiler-stopwatch.png)
+  skin/classic/browser/devtools/inspector.css               (devtools/inspector.css)
+  skin/classic/browser/devtools/profiler-stopwatch.png      (../shared/devtools/images/profiler-stopwatch.png)
   skin/classic/browser/devtools/tool-options.svg            (../shared/devtools/images/tool-options.svg)
   skin/classic/browser/devtools/tool-webconsole.svg         (../shared/devtools/images/tool-webconsole.svg)
   skin/classic/browser/devtools/tool-debugger.svg           (../shared/devtools/images/tool-debugger.svg)
   skin/classic/browser/devtools/tool-debugger-paused.svg    (../shared/devtools/images/tool-debugger-paused.svg)
   skin/classic/browser/devtools/tool-inspector.svg          (../shared/devtools/images/tool-inspector.svg)
   skin/classic/browser/devtools/tool-inspector.svg          (../shared/devtools/images/tool-inspector.svg)
   skin/classic/browser/devtools/tool-styleeditor.svg        (../shared/devtools/images/tool-styleeditor.svg)
   skin/classic/browser/devtools/tool-profiler.svg           (../shared/devtools/images/tool-profiler.svg)
   skin/classic/browser/devtools/tool-network.svg            (../shared/devtools/images/tool-network.svg)
   skin/classic/browser/devtools/tool-scratchpad.svg         (../shared/devtools/images/tool-scratchpad.svg)
-  skin/classic/browser/devtools/close.png                     (../shared/devtools/images/close.png)
-  skin/classic/browser/devtools/close@2x.png                  (../shared/devtools/images/close@2x.png)
-  skin/classic/browser/devtools/vview-delete.png          (devtools/vview-delete.png)
-  skin/classic/browser/devtools/vview-edit.png            (devtools/vview-edit.png)
-  skin/classic/browser/devtools/undock@2x.png             (../shared/devtools/images/undock@2x.png)
-  skin/classic/browser/devtools/font-inspector.css        (devtools/font-inspector.css)
-  skin/classic/browser/devtools/computedview.css          (devtools/computedview.css)
-  skin/classic/browser/devtools/arrow-e.png               (devtools/arrow-e.png)
-  skin/classic/browser/devtools/responsiveui-rotate.png   (../shared/devtools/responsiveui-rotate.png)
-  skin/classic/browser/devtools/responsiveui-touch.png    (../shared/devtools/responsiveui-touch.png)
+  skin/classic/browser/devtools/close.png                   (../shared/devtools/images/close.png)
+  skin/classic/browser/devtools/close@2x.png                (../shared/devtools/images/close@2x.png)
+  skin/classic/browser/devtools/vview-delete.png            (../shared/devtools/images/vview-delete.png)
+  skin/classic/browser/devtools/vview-lock.png              (../shared/devtools/images/vview-lock.png)
+  skin/classic/browser/devtools/vview-edit.png              (../shared/devtools/images/vview-edit.png)
+  skin/classic/browser/devtools/vview-open-inspector.png    (../shared/devtools/images/vview-open-inspector.png)
+  skin/classic/browser/devtools/undock@2x.png               (../shared/devtools/images/undock@2x.png)
+  skin/classic/browser/devtools/font-inspector.css          (devtools/font-inspector.css)
+  skin/classic/browser/devtools/computedview.css            (devtools/computedview.css)
+  skin/classic/browser/devtools/arrow-e.png                 (devtools/arrow-e.png)
+  skin/classic/browser/devtools/responsiveui-rotate.png     (../shared/devtools/responsiveui-rotate.png)
+  skin/classic/browser/devtools/responsiveui-touch.png      (../shared/devtools/responsiveui-touch.png)
   skin/classic/browser/devtools/responsiveui-screenshot.png (../shared/devtools/responsiveui-screenshot.png)
   skin/classic/browser/devtools/app-manager/connection-footer.css     (../shared/devtools/app-manager/connection-footer.css)
   skin/classic/browser/devtools/app-manager/index.css                 (../shared/devtools/app-manager/index.css)
   skin/classic/browser/devtools/app-manager/device.css                (../shared/devtools/app-manager/device.css)
   skin/classic/browser/devtools/app-manager/projects.css              (../shared/devtools/app-manager/projects.css)
   skin/classic/browser/devtools/app-manager/help.css                  (../shared/devtools/app-manager/help.css)
   skin/classic/browser/devtools/app-manager/warning.svg               (../shared/devtools/app-manager/images/warning.svg)
   skin/classic/browser/devtools/app-manager/error.svg                 (../shared/devtools/app-manager/images/error.svg)
deleted file mode 100644
index 0945af275e8860ed810900b6d1f36d1707622eae..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index af42a28df9c4a32ecdc6cbe670abbe5c7f4fe316..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -340,18 +340,20 @@ browser.jar:
   skin/classic/browser/devtools/tool-inspector.svg          (../shared/devtools/images/tool-inspector.svg)
   skin/classic/browser/devtools/tool-inspector.svg          (../shared/devtools/images/tool-inspector.svg)
   skin/classic/browser/devtools/tool-styleeditor.svg        (../shared/devtools/images/tool-styleeditor.svg)
   skin/classic/browser/devtools/tool-profiler.svg           (../shared/devtools/images/tool-profiler.svg)
   skin/classic/browser/devtools/tool-network.svg            (../shared/devtools/images/tool-network.svg)
   skin/classic/browser/devtools/tool-scratchpad.svg         (../shared/devtools/images/tool-scratchpad.svg)
   skin/classic/browser/devtools/close.png                   (../shared/devtools/images/close.png)
   skin/classic/browser/devtools/close@2x.png                (../shared/devtools/images/close@2x.png)
-  skin/classic/browser/devtools/vview-delete.png            (devtools/vview-delete.png)
-  skin/classic/browser/devtools/vview-edit.png              (devtools/vview-edit.png)
+  skin/classic/browser/devtools/vview-delete.png            (../shared/devtools/images/vview-delete.png)
+  skin/classic/browser/devtools/vview-lock.png              (../shared/devtools/images/vview-lock.png)
+  skin/classic/browser/devtools/vview-edit.png              (../shared/devtools/images/vview-edit.png)
+  skin/classic/browser/devtools/vview-open-inspector.png    (../shared/devtools/images/vview-open-inspector.png)
   skin/classic/browser/devtools/undock@2x.png               (../shared/devtools/images/undock@2x.png)
   skin/classic/browser/devtools/font-inspector.css          (devtools/font-inspector.css)
   skin/classic/browser/devtools/computedview.css            (devtools/computedview.css)
   skin/classic/browser/devtools/arrow-e.png                 (devtools/arrow-e.png)
   skin/classic/browser/devtools/responsiveui-rotate.png     (../shared/devtools/responsiveui-rotate.png)
   skin/classic/browser/devtools/responsiveui-touch.png      (../shared/devtools/responsiveui-touch.png)
   skin/classic/browser/devtools/responsiveui-screenshot.png (../shared/devtools/responsiveui-screenshot.png)
   skin/classic/browser/devtools/app-manager/connection-footer.css     (../shared/devtools/app-manager/connection-footer.css)
--- a/browser/themes/shared/devtools/dark-theme.css
+++ b/browser/themes/shared/devtools/dark-theme.css
@@ -143,20 +143,25 @@
   color: #b26b47;
 }
 
 .theme-fg-color7,
 .cm-s-mozilla .cm-atom,
 .cm-s-mozilla .cm-quote,
 .cm-s-mozilla .cm-error,
 .variable-or-property .token-boolean,
+.variable-or-property .token-domnode,
 .variable-or-property[exception] > .title > .name { /* Red */
   color: #bf5656;
 }
 
+.variable-or-property .token-domnode {
+  font-weight: bold;
+}
+
 .theme-toolbar,
 .devtools-toolbar,
 .devtools-sidebar-tabs > tabs { /* General toolbar styling */
   color: #b6babf;
   background-color: #343c45;
   border-color: hsla(210,8%,5%,.6);
 }
 
--- a/browser/themes/shared/devtools/debugger.inc.css
+++ b/browser/themes/shared/devtools/debugger.inc.css
@@ -473,16 +473,24 @@
   transition: transform 0.3s ease-in-out;
 }
 
 .dbg-results-line-contents-string[match=true][focused] {
   transition-duration: 0.1s;
   transform: scale(1.75, 1.75);
 }
 
+.theme-dark .dbg-source-results:not(.selected):hover {
+  background-color: #181d20; /* Sidebar background */
+}
+
+.theme-light .dbg-source-results:not(.selected):hover {
+  background-color: #f7f7f7; /* Sidebar background */
+}
+
 .theme-dark .dbg-results-header {
   background-color: #252c33; /* Tab toolbar */
   color: #b8c8d9; /* Light content text */
 }
 
 .theme-light .dbg-results-header {
   background-color: #ebeced; /* Tab toolbar */
   color: #667380; /* Dark grey content text */
new file mode 100644
index 0000000000000000000000000000000000000000..db4b0620c4118ace6695af18f663d594170f3b20
GIT binary patch
literal 3229
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
P00000NkvXXu0mjfDrh$J
new file mode 100644
index 0000000000000000000000000000000000000000..ae6fbb21c31c46e94fa7739d3be5a5b64f1ae6b7
GIT binary patch
literal 3542
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
QGXMYp07*qoM6N<$f~r{2#sB~S
new file mode 100644
index 0000000000000000000000000000000000000000..f575032008eb5a312879954586423c41ecc7a412
GIT binary patch
literal 3329
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
LNkvXXu0mjfST$*#
new file mode 100644
index 0000000000000000000000000000000000000000..a34c8ede6f8bb66511270fb55caa379d7d5e7944
GIT binary patch
literal 4062
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
QX#fBK07*qoM6N<$g0O<+g8%>k
new file mode 100644
index 0000000000000000000000000000000000000000..b68cc5e7dc7b54060db5f48dc879bc16ff3928c4
GIT binary patch
literal 3269
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
D8JSYg
new file mode 100644
index 0000000000000000000000000000000000000000..5d7640bbe2f04c932eec767ab8d08255af88a9d2
GIT binary patch
literal 3839
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
Bf4Kkv
new file mode 100644
index 0000000000000000000000000000000000000000..ee1d7a59e4b2abe40bda3a5ac25071c0d860f9b1
GIT binary patch
literal 2942
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
new file mode 100644
index 0000000000000000000000000000000000000000..2b09f01a566a624abcf0aa6b904c61fe0d5f9ce8
GIT binary patch
literal 3095
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
--- a/browser/themes/shared/devtools/light-theme.css
+++ b/browser/themes/shared/devtools/light-theme.css
@@ -142,20 +142,25 @@
   color: hsl(24,85%,39%);
 }
 
 .theme-fg-color7,
 .cm-s-mozilla .cm-atom,
 .cm-s-mozilla .cm-quote,
 .cm-s-mozilla .cm-error,
 .variable-or-property .token-boolean,
+.variable-or-property .token-domnode,
 .variable-or-property[exception] > .title > .name { /* Red */
   color: #bf5656;
 }
 
+.variable-or-property .token-domnode {
+  font-weight: bold;
+}
+
 .theme-fg-contrast { /* To be used for text on theme-bg-contrast */
   color: black;
 }
 
 .theme-toolbar,
 .devtools-toolbar,
 .devtools-sidebar-tabs > tabs { /* General toolbar styling */
   color: #585959;
--- 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;
@@ -510,21 +521,28 @@ box.requests-menu-status {
 #headers-summary-resend {
   margin-top: -10px;
   -moz-margin-end: 6px;
 }
 
 /* Response tabpanel */
 
 #response-content-info-header {
-  background: linear-gradient(hsl(0,60%,40%), hsl(0,60%,30%)) repeat-x top left;
-  box-shadow: inset 0 1px 0 hsla(210,40%,80%,.15),
-              inset 0 -1px 0 hsla(210,40%,80%,.05);
   margin: 0;
-  padding: 5px 8px;
+  padding: 3px 8px;
+}
+
+.theme-dark  #response-content-info-header {
+  background: url(background-noise-toolbar.png), #eb5368; /* Red highlight */
+  color: #f5f7fa; /* Light foreground text */
+}
+
+.theme-light  #response-content-info-header {
+  background: url(background-noise-toolbar.png), #ed2655; /* Red highlight */
+  color: #f5f7fa; /* Light foreground text */
 }
 
 #response-content-image-box {
   padding-top: 10px;
   padding-bottom: 10px;
 }
 
 #response-content-image {
@@ -573,41 +591,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 +641,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/shadereditor.inc.css
+++ b/browser/themes/shared/devtools/shadereditor.inc.css
@@ -88,17 +88,17 @@
 
 .editor-label {
   padding: 1px 12px;
   border-top: 1px solid;
 }
 
 .theme-dark .editor-label {
   background: #343c45; /* Dark toolbars */
-  border-color: #222426; /* Match the splitter color. */
+  border-color: #000; /* Match the splitter color. */
   color: #f5f7fa; /* Light foreground text */
 }
 
 .theme-light .editor-label {
   background: #f0f1f2; /* Light toolbars */
   border-color: #aaa; /* Match the splitter color. */
   color: #585959; /* Grey foreground text */
 }
--- a/browser/themes/shared/devtools/styleeditor.css
+++ b/browser/themes/shared/devtools/styleeditor.css
@@ -13,38 +13,41 @@
 }
 
 .theme-dark .stylesheet-title,
 .theme-dark .stylesheet-name {
   color: #f5f7fa;
 }
 
 .theme-dark .stylesheet-rule-count,
+.theme-dark .stylesheet-linked-file,
 .theme-dark .stylesheet-saveButton {
   color: #b6babf;
 }
 
 .theme-light .stylesheet-title,
 .theme-light .stylesheet-name {
   color: #585959;
 }
 
 .theme-light .stylesheet-rule-count,
+.theme-light .stylesheet-linked-file,
 .theme-light .stylesheet-saveButton {
   color: #18191a;
 }
 
 .stylesheet-saveButton {
   text-decoration: underline;
   cursor: pointer;
 }
 
 .splitview-active .stylesheet-title,
 .splitview-active .stylesheet-name,
 .theme-light .splitview-active .stylesheet-rule-count,
+.theme-light .splitview-active .stylesheet-linked-file,
 .theme-light .splitview-active .stylesheet-saveButton {
   color: #f5f7fa;
 }
 
 .splitview-nav:focus {
   outline: 0; /* focus ring is on the stylesheet name */
 }
 
@@ -80,18 +83,26 @@
   background-position: -24px 8px;
 }
 
 .splitview-nav > li > .stylesheet-enabled:focus,
 .splitview-nav > li:hover > .stylesheet-enabled {
   outline: 0;
 }
 
-.stylesheet-error-message {
-  color: red;
+.stylesheet-linked-file:not(:empty){
+  -moz-margin-end: 0.4em;
+}
+
+.stylesheet-linked-file:not(:empty):before {
+  -moz-margin-start: 0.4em;
+}
+
+li.linked-file-error .stylesheet-linked-file:after {
+  font-size: 110%;
 }
 
 .stylesheet-more > h3 {
   font-size: 11px;
   -moz-margin-end: 2px;
 }
 
 .devtools-searchinput {
--- 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
@@ -216,34 +216,40 @@
 
 .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-id,
 .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-tag,
 .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-pseudo-classes,
 .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-classes {
   color: #f5f7fa; /* Foreground (Text) - Light */
 }
 
-.theme-dark .breadcrumbs-widget-item,
-.theme-dark .breadcrumbs-widget-item-classes {
+.theme-dark .breadcrumbs-widget-item {
   color: #f5f7fa; /* Foreground (Text) - Light */
 }
 
-.theme-light .breadcrumbs-widget-item,
-.theme-light .breadcrumbs-widget-item-classes {
+.theme-light .breadcrumbs-widget-item {
   color: #18191a; /* Foreground (Text) - Dark */
 }
 
 .theme-dark .breadcrumbs-widget-item-id {
   color: #b6babf; /* Foreground (Text) - Grey */
 }
 
 .theme-light .breadcrumbs-widget-item-id {
   color: #585959; /* Foreground (Text) - Grey */
 }
 
+.theme-dark .breadcrumbs-widget-item-classes {
+  color: #b8c8d9; /* Content (Text) - Light */
+}
+
+.theme-light .breadcrumbs-widget-item-classes {
+  color: #667380; /* Content (Text) - Dark Grey */
+}
+
 .theme-dark .breadcrumbs-widget-item-pseudo-classes {
   color: #d99b28; /* Light Orange */
 }
 
 .theme-light .breadcrumbs-widget-item-pseudo-classes {
   color: #d97e00; /* Light Orange */
 }
 
@@ -252,36 +258,37 @@
 }
 
 .theme-light .breadcrumbs-widget-item:not([checked]):hover label {
   color: black;
 }
 
 /* SimpleListWidget */
 
-%filter substitution
-%define slw_selectionGradient linear-gradient(hsl(206,59%,39%), hsl(206,59%,29%))
-%define slw_selectionTextColor #fff
-
 .simple-list-widget-container {
   /* Hack: force hardware acceleration */
   transform: translateZ(1px);
 }
 
-.simple-list-widget-item.selected {
-  background: @slw_selectionGradient@;
-  color: @slw_selectionTextColor@;
+.theme-dark .simple-list-widget-item.selected {
+  background-color: #1d4f73; /* Select Highlight Blue */
+  color: #f5f7fa; /* Light foreground text */
+}
+
+.theme-light .simple-list-widget-item.selected {
+  background-color: #4c9ed9; /* Select Highlight Blue */
+  color: #f5f7fa; /* Light foreground text */
 }
 
 .theme-dark .simple-list-widget-item:not(.selected):hover {
-  background-color: #181d20; /* Sidebar background */
+  background-color: rgba(255,255,255,.05);
 }
 
 .theme-light .simple-list-widget-item:not(.selected):hover {
-  background-color: #f7f7f7; /* Sidebar background */
+  background-color: rgba(0,0,0,.05);
 }
 
 .simple-list-widget-empty-text,
 .simple-list-widget-perma-text {
   padding: 4px 8px;
 }
 
 .theme-dark .simple-list-widget-empty-text,
@@ -311,18 +318,17 @@
 
 .theme-light .fast-list-widget-empty-text {
   color: #585959; /* Grey foreground text */
 }
 
 /* SideMenuWidget */
 
 %filter substitution
-%define smw_selectionTextColor #f5f7fa
-%define smw_marginDark #222426
+%define smw_marginDark #000
 %define smw_marginLight #aaa
 %define smw_itemDarkTopBorder rgba(0,0,0,0.2)
 %define smw_itemDarkBottomBorder rgba(128,128,128,0.15)
 %define smw_itemLightTopBorder rgba(128,128,128,0.15)
 %define smw_itemLightBottomBorder transparent
 
 .side-menu-widget-container {
   /* Hack: force hardware acceleration */
@@ -408,22 +414,22 @@
 }
 
 .theme-light .side-menu-widget-item:last-of-type {
   box-shadow: inset 0 -1px 0 @smw_itemLightTopBorder@;
 }
 
 .theme-dark .side-menu-widget-item.selected {
   background-color: #1d4f73; /* Select Highlight Blue */
-  color: @smw_selectionTextColor@;
+  color: #f5f7fa; /* Light foreground text */
 }
 
 .theme-light .side-menu-widget-item.selected {
   background-color: #4c9ed9; /* Select Highlight Blue */
-  color: @smw_selectionTextColor@;
+  color: #f5f7fa; /* Light foreground text */
 }
 
 .side-menu-widget-item-arrow {
   -moz-margin-start: -7px;
   width: 7px; /* The image's width is 7 pixels */
   /* Cover the border of the side-menu-widget-item */
   margin-top: -1px;
   margin-bottom: -1px;
@@ -497,22 +503,22 @@
 
 .theme-light .side-menu-widget-item.selected .side-menu-widget-item-other {
   background-color: rgba(255,255,255,.8); /* Lighten the selection by 20% */
   color: #18191a; /* Dark foreground text */
 }
 
 .theme-dark .side-menu-widget-item.selected .side-menu-widget-item-other.selected {
   background-color: transparent;
-  color: @smw_selectionTextColor@;
+  color: #f5f7fa; /* Light foreground text */
 }
 
 .theme-light .side-menu-widget-item.selected .side-menu-widget-item-other.selected {
   background-color: transparent;
-  color: @smw_selectionTextColor@;
+  color: #f5f7fa; /* Light foreground text */
 }
 
 /* SideMenuWidget checkboxes */
 
 .side-menu-widget-group-checkbox {
   margin: 0;
   -moz-margin-end: 4px;
 }
@@ -638,29 +644,28 @@
   border-bottom: 1px dashed #f99;
 }
 
 .variable-or-property[safe-getter]:not([pseudo-item]) > .title > .name {
   border-bottom: 1px dashed #8b0;
 }
 
 .variable-or-property-non-writable-icon {
-  background: url("chrome://browser/skin/identity-icons-https.png") no-repeat;
+  background: url("chrome://browser/skin/devtools/vview-lock.png") no-repeat;
   width: 16px;
   height: 16px;
-  opacity: 0.5;
 }
 
-@media (min-resolution: 2dppx) {
+/*@media (min-resolution: 2dppx) {
   .variable-or-property-non-writable-icon {
     background-image: url("chrome://browser/skin/identity-icons-https@2x.png");
     background-size: 32px;
   }
 }
-
+*/
 .variable-or-property-frozen-label,
 .variable-or-property-sealed-label,
 .variable-or-property-non-extensible-label {
   -moz-padding-end: 4px;
 }
 
 .variable-or-property:not(:focus) > .title > .variable-or-property-frozen-label,
 .variable-or-property:not(:focus) > .title > .variable-or-property-sealed-label,
@@ -681,16 +686,17 @@
 
 .variables-view-container[aligned-values] .title > .element-value-input {
   width: calc(70vw - 10px);
 }
 
 /* Actions first */
 
 .variables-view-container[actions-first] .variables-view-delete,
+.variables-view-container[actions-first] .variables-view-open-inspector,
 .variables-view-container[actions-first] .variables-view-add-property {
   -moz-box-ordinal-group: 0;
 }
 
 .variables-view-container[actions-first] [invisible] {
   visibility: hidden;
 }
 
@@ -722,28 +728,62 @@
 /* Variables and properties editing */
 
 .variables-view-delete {
   list-style-image: url("chrome://browser/skin/devtools/vview-delete.png");
   -moz-image-region: rect(0,16px,16px,0);
 }
 
 .variables-view-delete:hover {
-  -moz-image-region: rect(0,32px,16px,16px);
+  -moz-image-region: rect(0,48px,16px,32px);
 }
 
 .variables-view-delete:active {
-  -moz-image-region: rect(0,48px,16px,32px);
+  -moz-image-region: rect(0,32px,16px,16px);
+}
+
+.variable-or-property:focus .variables-view-delete {
+  -moz-image-region: rect(0,16px,16px,0);
 }
 
 .variables-view-edit {
-  background: url("chrome://browser/skin/devtools/vview-edit.png") center no-repeat;
-  width: 20px;
-  height: 16px;
+  list-style-image: url("chrome://browser/skin/devtools/vview-edit.png");
+  -moz-image-region: rect(0,16px,16px,0);
   cursor: pointer;
+  padding-left: 2px;
+}
+
+.variables-view-edit:hover {
+  -moz-image-region: rect(0,48px,16px,32px);
+}
+
+.variables-view-edit:active {
+  -moz-image-region: rect(0,32px,16px,16px);
+}
+
+.variable-or-property:focus .variables-view-edit {
+  -moz-image-region: rect(0,16px,16px,0);
+}
+
+.variables-view-open-inspector {
+  list-style-image: url("chrome://browser/skin/devtools/vview-open-inspector.png");
+  -moz-image-region: rect(0,16px,16px,0);
+  cursor: pointer;
+}
+
+.variables-view-open-inspector:hover {
+  -moz-image-region: rect(0,48px,16px,32px);
+}
+
+.variables-view-open-inspector:active {
+  -moz-image-region: rect(0,32px,16px,16px);
+}
+
+.variable-or-property:focus .variables-view-open-inspector {
+  -moz-image-region: rect(0,16px,16px,0);
 }
 
 .variables-view-throbber {
   background: url("chrome://global/skin/icons/loading_16.png") center no-repeat;
   width: 16px;
   height: 16px;
 }
 
@@ -787,9 +827,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
deleted file mode 100644
index 9604653c0d4280d4bc4901b85786181f4f3d7929..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index af42a28df9c4a32ecdc6cbe670abbe5c7f4fe316..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -261,18 +261,20 @@ browser.jar:
         skin/classic/browser/devtools/tool-debugger-paused.svg      (../shared/devtools/images/tool-debugger-paused.svg)
         skin/classic/browser/devtools/tool-inspector.svg            (../shared/devtools/images/tool-inspector.svg)
         skin/classic/browser/devtools/tool-styleeditor.svg          (../shared/devtools/images/tool-styleeditor.svg)
         skin/classic/browser/devtools/tool-profiler.svg             (../shared/devtools/images/tool-profiler.svg)
         skin/classic/browser/devtools/tool-network.svg              (../shared/devtools/images/tool-network.svg)
         skin/classic/browser/devtools/tool-scratchpad.svg           (../shared/devtools/images/tool-scratchpad.svg)
         skin/classic/browser/devtools/close.png                     (../shared/devtools/images/close.png)
         skin/classic/browser/devtools/close@2x.png                  (../shared/devtools/images/close@2x.png)
-        skin/classic/browser/devtools/vview-delete.png              (devtools/vview-delete.png)
-        skin/classic/browser/devtools/vview-edit.png                (devtools/vview-edit.png)
+        skin/classic/browser/devtools/vview-delete.png              (../shared/devtools/images/vview-delete.png)
+        skin/classic/browser/devtools/vview-lock.png                (../shared/devtools/images/vview-lock.png)
+        skin/classic/browser/devtools/vview-edit.png                (../shared/devtools/images/vview-edit.png)
+        skin/classic/browser/devtools/vview-open-inspector.png      (../shared/devtools/images/vview-open-inspector.png)
         skin/classic/browser/devtools/undock@2x.png                 (../shared/devtools/images/undock@2x.png)
         skin/classic/browser/devtools/font-inspector.css            (devtools/font-inspector.css)
         skin/classic/browser/devtools/computedview.css              (devtools/computedview.css)
         skin/classic/browser/devtools/arrow-e.png                   (devtools/arrow-e.png)
         skin/classic/browser/devtools/responsiveui-rotate.png       (../shared/devtools/responsiveui-rotate.png)
         skin/classic/browser/devtools/responsiveui-touch.png        (../shared/devtools/responsiveui-touch.png)
         skin/classic/browser/devtools/responsiveui-screenshot.png   (../shared/devtools/responsiveui-screenshot.png)
         skin/classic/browser/devtools/app-manager/connection-footer.css     (../shared/devtools/app-manager/connection-footer.css)
@@ -567,18 +569,20 @@ browser.jar:
         skin/classic/aero/browser/devtools/tool-debugger-paused.svg  (../shared/devtools/images/tool-debugger-paused.svg)
         skin/classic/aero/browser/devtools/tool-inspector.svg        (../shared/devtools/images/tool-inspector.svg)
         skin/classic/aero/browser/devtools/tool-styleeditor.svg      (../shared/devtools/images/tool-styleeditor.svg)
         skin/classic/aero/browser/devtools/tool-profiler.svg         (../shared/devtools/images/tool-profiler.svg)
         skin/classic/aero/browser/devtools/tool-network.svg          (../shared/devtools/images/tool-network.svg)
         skin/classic/aero/browser/devtools/tool-scratchpad.svg       (../shared/devtools/images/tool-scratchpad.svg)
         skin/classic/aero/browser/devtools/close.png                 (../shared/devtools/images/close.png)
         skin/classic/aero/browser/devtools/close@2x.png              (../shared/devtools/images/close@2x.png)
-        skin/classic/aero/browser/devtools/vview-delete.png          (devtools/vview-delete.png)
-        skin/classic/aero/browser/devtools/vview-edit.png            (devtools/vview-edit.png)
+        skin/classic/aero/browser/devtools/vview-delete.png          (../shared/devtools/images/vview-delete.png)
+        skin/classic/aero/browser/devtools/vview-lock.png            (../shared/devtools/images/vview-lock.png)
+        skin/classic/aero/browser/devtools/vview-edit.png            (../shared/devtools/images/vview-edit.png)
+        skin/classic/aero/browser/devtools/vview-open-inspector.png  (../shared/devtools/images/vview-open-inspector.png)
         skin/classic/aero/browser/devtools/undock@2x.png             (../shared/devtools/images/undock@2x.png)
         skin/classic/aero/browser/devtools/font-inspector.css        (devtools/font-inspector.css)
         skin/classic/aero/browser/devtools/computedview.css          (devtools/computedview.css)
         skin/classic/aero/browser/devtools/arrow-e.png               (devtools/arrow-e.png)
         skin/classic/aero/browser/devtools/responsiveui-rotate.png   (../shared/devtools/responsiveui-rotate.png)
         skin/classic/aero/browser/devtools/responsiveui-touch.png    (../shared/devtools/responsiveui-touch.png)
         skin/classic/aero/browser/devtools/responsiveui-screenshot.png (../shared/devtools/responsiveui-screenshot.png)
         skin/classic/aero/browser/devtools/app-manager/connection-footer.css     (../shared/devtools/app-manager/connection-footer.css)
--- a/mobile/android/base/tests/BaseTest.java
+++ b/mobile/android/base/tests/BaseTest.java
@@ -85,16 +85,26 @@ abstract class BaseTest extends Activity
                 geckoReadyExpector.blockForEvent();
             }
             geckoReadyExpector.unregisterListener();
         } catch (Exception e) {
             mAsserter.dumpLog("Exception in blockForGeckoReady", e);
         }
     }
 
+    protected void blockForGeckoDelayedStartup() {
+        try {
+            Actions.EventExpecter geckoReadyExpector = mActions.expectGeckoEvent("Gecko:DelayedStartup");
+            geckoReadyExpector.blockForEvent();
+            geckoReadyExpector.unregisterListener();
+        } catch (Exception e) {
+            mAsserter.dumpLog("Exception in blockForGeckoDelayedStartup", e);
+        }
+    }
+
     static {
         try {
             mLauncherActivityClass = (Class<Activity>)Class.forName(LAUNCH_ACTIVITY_FULL_CLASSNAME);
         } catch (ClassNotFoundException e) {
             throw new RuntimeException(e);
         }
     }
 
--- a/mobile/android/base/tests/testFormHistory.java
+++ b/mobile/android/base/tests/testFormHistory.java
@@ -24,17 +24,17 @@ public class testFormHistory extends Bas
     }
 
     public void testFormHistory() {
         Context context = (Context)getActivity();
         ContentResolver cr = context.getContentResolver();
         ContentValues[] cvs = new ContentValues[1];
         cvs[0] = new ContentValues();
  
-        blockForGeckoReady();
+        blockForGeckoDelayedStartup();
 
         Uri formHistoryUri;
         Uri insertUri;
         Uri expectedUri;
         int numUpdated;
         int numDeleted;
 
         cvs[0].put("fieldname", "fieldname");
--- a/mobile/android/base/tests/testPasswordProvider.java
+++ b/mobile/android/base/tests/testPasswordProvider.java
@@ -25,17 +25,17 @@ public class testPasswordProvider extend
     }
 
     public void testPasswordProvider() {
         Context context = (Context)getActivity();
         ContentResolver cr = context.getContentResolver();
         ContentValues[] cvs = new ContentValues[1];
         cvs[0] = new ContentValues();
   
-        blockForGeckoReady();
+        blockForGeckoDelayedStartup();
   
         cvs[0].put("hostname", "http://www.example.com");
         cvs[0].put("httpRealm", "http://www.example.com");
         cvs[0].put("formSubmitURL", "http://www.example.com");
         cvs[0].put("usernameField", "usernameField");
         cvs[0].put("passwordField", "passwordField");
         cvs[0].put("encryptedUsername", "username");
         cvs[0].put("encryptedPassword", "password");
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -282,16 +282,24 @@ var BrowserApp = {
 
   deck: null,
 
   startup: function startup() {
     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
     dump("zerdatime " + Date.now() + " - browser chrome startup finished.");
 
     this.deck = document.getElementById("browsers");
+    this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
+      try {
+        BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
+        Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
+        sendMessageToJava({ type: "Gecko:DelayedStartup" });
+      } catch(ex) { console.log(ex); }
+    }, false);
+
     BrowserEventHandler.init();
     ViewportHandler.init();
 
     Services.androidBridge.browserApp = this;
 
     Services.obs.addObserver(this, "Locale:Changed", false);
     Services.obs.addObserver(this, "Tab:Load", false);
     Services.obs.addObserver(this, "Tab:Selected", false);
@@ -410,16 +418,18 @@ var BrowserApp = {
     // XXX maybe we don't do this if the launch was kicked off from external
     Services.io.offline = false;
 
     // Broadcast a UIReady message so add-ons know we are finished with startup
     let event = document.createEvent("Events");
     event.initEvent("UIReady", true, false);
     window.dispatchEvent(event);
 
+    Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
+
     if (this._startupStatus)
       this.onAppUpdated();
 
     // Store the low-precision buffer pref
     this.gUseLowPrecision = Services.prefs.getBoolPref("layers.low-precision-buffer");
 
     // notify java that gecko has loaded
     sendMessageToJava({ type: "Gecko:Ready" });
@@ -681,20 +691,16 @@ var BrowserApp = {
         // Skipped trying to pull MIME type out of cache for now
         ContentAreaUtils.internalSave(url, null, null, null, null, false,
                                       filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject,
                                       aTarget.ownerDocument, true, null);
       });
   },
 
   onAppUpdated: function() {
-    // initialize the form history and passwords databases on upgrades
-    Services.obs.notifyObservers(null, "FormHistory:Init", "");
-    Services.obs.notifyObservers(null, "Passwords:Init", "");
-
     // Migrate user-set "plugins.click_to_play" pref. See bug 884694.
     // Because the default value is true, a user-set pref means that the pref was set to false.
     if (Services.prefs.prefHasUserValue("plugins.click_to_play")) {
       Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED);
       Services.prefs.clearUserPref("plugins.click_to_play");
     }
   },
 
@@ -1601,23 +1607,35 @@ var BrowserApp = {
         console.log("Locale:Changed: " + aData);
 
         // TODO: do we need to be more nuanced here -- e.g., checking for the
         // OS locale -- or should it always be false on Fennec?
         Services.prefs.setBoolPref("intl.locale.matchOS", false);
         Services.prefs.setCharPref("general.useragent.locale", aData);
         break;
 
+      case "browser-delayed-startup-finished":
+        this._delayedStartup();
+        break;
+
       default:
         dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n');
         break;
 
     }
   },
 
+  _delayedStartup: function() {
+    // initialize the form history and passwords databases on upgrades
+    if (this._startupStatus) {
+      Services.obs.notifyObservers(null, "FormHistory:Init", "");
+      Services.obs.notifyObservers(null, "Passwords:Init", "");
+    }
+  },
+
   get defaultBrowserWidth() {
     delete this.defaultBrowserWidth;
     let width = Services.prefs.getIntPref("browser.viewport.desktopWidth");
     return this.defaultBrowserWidth = width;
   },
 
   // nsIAndroidBrowserApp
   getBrowserTab: function(tabId) {
--- a/mobile/android/components/Snippets.js
+++ b/mobile/android/components/Snippets.js
@@ -317,16 +317,20 @@ function Snippets() {}
 
 Snippets.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsITimerCallback]),
   classID: Components.ID("{a78d7e59-b558-4321-a3d6-dffe2f1e76dd}"),
 
   observe: function(subject, topic, data) {
     switch(topic) {
       case "profile-after-change":
+        Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
+        break;
+      case "browser-delayed-startup-finished":
+        Services.obs.removeObserver(this, "browser-delayed-startup-finished", false);
         if (Services.prefs.getBoolPref("browser.snippets.syncPromo.enabled")) {
           loadSyncPromoBanner();
         }
 
         if (Services.prefs.getBoolPref("browser.snippets.enabled")) {
           loadSnippetsFromCache();
         }
         break;
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -2081,17 +2081,41 @@ var WalkerActor = protocol.ActorClass({
         added: [],
         removed: []
       });
     }
 
     // Need to force a release of this node, because those nodes can't
     // be accessed anymore.
     this.releaseNode(documentActor, { force: true });
-  }
+  },
+
+  /**
+   * Given an ObjectActor (identified by its ID), commonly used in the debugger,
+   * webconsole and variablesView, return the corresponding inspector's NodeActor
+   */
+  getNodeActorFromObjectActor: method(function(objectActorID) {
+    let debuggerObject = this.conn.poolFor(objectActorID).get(objectActorID).obj;
+    let rawNode = debuggerObject.unsafeDereference();
+
+    // This is a special case for the document object whereby it is considered
+    // as document.documentElement (the <html> node)
+    if (rawNode.defaultView && rawNode === rawNode.defaultView.document) {
+      rawNode = rawNode.documentElement;
+    }
+
+    return this.attachElement(rawNode);
+  }, {
+    request: {
+      objectActorID: Arg(0, "string")
+    },
+    response: {
+      nodeFront: RetVal("disconnectedNode")
+    }
+  }),
 });
 
 /**
  * Client side of the DOM walker.
  */
 var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
   // Set to true if cleanup should be requested after every mutation list.
   autoCleanup: true,
@@ -2215,16 +2239,24 @@ var WalkerFront = exports.WalkerFront = 
   querySelector: protocol.custom(function(queryNode, selector) {
     return this._querySelector(queryNode, selector).then(response => {
       return response.node;
     });
   }, {
     impl: "_querySelector"
   }),
 
+  getNodeActorFromObjectActor: protocol.custom(function(objectActorID) {
+    return this._getNodeActorFromObjectActor(objectActorID).then(response => {
+      return response.node;
+    });
+  }, {
+    impl: "_getNodeActorFromObjectActor"
+  }),
+
   _releaseFront: function(node, force) {
     if (node.retained && !force) {
       node.reparent(null);
       this._retainedOrphans.add(node);
       return;
     }
 
     if (node.retained) {
--- a/toolkit/devtools/server/actors/stylesheets.js
+++ b/toolkit/devtools/server/actors/stylesheets.js
@@ -542,17 +542,17 @@ let StyleSheetActor = protocol.ActorClas
     this._originalSources = null;
   },
 
   /**
    * Sets the source map's sourceRoot to be relative to the source map url.
    */
   _setSourceMapRoot: function(aSourceMap, aAbsSourceMapURL, aScriptURL) {
     const base = dirname(
-      aAbsSourceMapURL.indexOf("data:") === 0
+      aAbsSourceMapURL.startsWith("data:")
         ? aScriptURL
         : aAbsSourceMapURL);
     aSourceMap.sourceRoot = aSourceMap.sourceRoot
       ? normalize(aSourceMap.sourceRoot, base)
       : base;
   },
 
   /**
@@ -695,36 +695,36 @@ let StyleSheetActor = protocol.ActorClas
 
     // Set up clean up and commit after transition duration (+10% buffer)
     // @see _onTransitionEnd
     this.window.setTimeout(this._onTransitionEnd.bind(this),
                            Math.floor(TRANSITION_DURATION_MS * 1.1));
   },
 
   /**
-    * This cleans up class and rule added for transition effect and then
-    * notifies that the style has been applied.
-    */
+   * This cleans up class and rule added for transition effect and then
+   * notifies that the style has been applied.
+   */
   _onTransitionEnd: function()
   {
     if (--this._transitionRefCount == 0) {
       this.document.documentElement.classList.remove(TRANSITION_CLASS);
       this.rawSheet.deleteRule(this.rawSheet.cssRules.length - 1);
     }
 
     events.emit(this, "style-applied");
   }
 })
 
 /**
  * StyleSheetFront is the client-side counterpart to a StyleSheetActor.
  */
 var StyleSheetFront = protocol.FrontClass(StyleSheetActor, {
-  initialize: function(conn, form, ctx, detail) {
-    protocol.Front.prototype.initialize.call(this, conn, form, ctx, detail);
+  initialize: function(conn, form) {
+    protocol.Front.prototype.initialize.call(this, conn, form);
 
     this._onPropertyChange = this._onPropertyChange.bind(this);
     events.on(this, "property-change", this._onPropertyChange);
   },
 
   destroy: function() {
     events.off(this, "property-change", this._onPropertyChange);
 
@@ -770,17 +770,17 @@ let OriginalSourceActor = protocol.Actor
 
     this.text = null;
   },
 
   form: function() {
     return {
       actor: this.actorID, // actorID is set when it's added to a pool
       url: this.url,
-      parentSource: this.parentActor.actorID
+      relatedStyleSheet: this.parentActor.form()
     };
   },
 
   _getText: function() {
     if (this.text) {
       return promise.resolve(this.text);
     }
     return fetch(this.url, { window: this.window }).then(({content}) => {
--- 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) {