Bug 855544 - Implement a network monitor, r=dcamp,msucan
authorVictor Porof <vporof@mozilla.com>
Mon, 11 Mar 2013 23:50:42 -0700
changeset 127915 3fda72d2c6c31f5555f3ce1f900d80dac45869e1
parent 127914 f6d26cee5c6cfd874795a299dda060b649b4e7df
child 127916 bba5637808ef0e6a1fb0836ea8f9ed0b11aebfa2
push id24515
push uservporof@mozilla.com
push dateSun, 07 Apr 2013 09:12:16 +0000
treeherdermozilla-central@5e01908df6d7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdcamp, msucan
bugs855544
milestone23.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
Bug 855544 - Implement a network monitor, r=dcamp,msucan
browser/app/profile/firefox.js
browser/devtools/debugger/debugger-controller.js
browser/devtools/debugger/debugger-panes.js
browser/devtools/debugger/debugger-toolbar.js
browser/devtools/debugger/debugger-view.js
browser/devtools/debugger/debugger.xul
browser/devtools/debugger/test/browser_dbg_pane-collapse.js
browser/devtools/debugger/test/browser_dbg_pause-resume.js
browser/devtools/debugger/test/browser_dbg_sources-cache.js
browser/devtools/framework/ToolDefinitions.jsm
browser/devtools/jar.mn
browser/devtools/moz.build
browser/devtools/netmonitor/Makefile.in
browser/devtools/netmonitor/NetMonitorPanel.jsm
browser/devtools/netmonitor/moz.build
browser/devtools/netmonitor/netmonitor-controller.js
browser/devtools/netmonitor/netmonitor-view.js
browser/devtools/netmonitor/netmonitor.css
browser/devtools/netmonitor/netmonitor.xul
browser/devtools/netmonitor/test/Makefile.in
browser/devtools/netmonitor/test/browser_net_aaa_leaktest.js
browser/devtools/netmonitor/test/browser_net_content-type.js
browser/devtools/netmonitor/test/browser_net_page-nav.js
browser/devtools/netmonitor/test/browser_net_pane-collapse.js
browser/devtools/netmonitor/test/browser_net_post-data.js
browser/devtools/netmonitor/test/browser_net_prefs-and-l10n.js
browser/devtools/netmonitor/test/browser_net_prefs-reload.js
browser/devtools/netmonitor/test/browser_net_simple-init.js
browser/devtools/netmonitor/test/browser_net_simple-request-data.js
browser/devtools/netmonitor/test/browser_net_simple-request-details.js
browser/devtools/netmonitor/test/browser_net_simple-request.js
browser/devtools/netmonitor/test/browser_net_status-codes.js
browser/devtools/netmonitor/test/head.js
browser/devtools/netmonitor/test/html_content-type-test-page.html
browser/devtools/netmonitor/test/html_navigate-test-page.html
browser/devtools/netmonitor/test/html_post-data-test-page.html
browser/devtools/netmonitor/test/html_simple-test-page.html
browser/devtools/netmonitor/test/html_status-codes-test-page.html
browser/devtools/netmonitor/test/moz.build
browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs
browser/devtools/netmonitor/test/sjs_simple-test-server.sjs
browser/devtools/netmonitor/test/sjs_status-codes-test-server.sjs
browser/devtools/netmonitor/test/test-image.png
browser/devtools/shared/Parser.jsm
browser/devtools/shared/widgets/SideMenuWidget.jsm
browser/devtools/shared/widgets/VariablesView.jsm
browser/devtools/shared/widgets/ViewHelpers.jsm
browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
browser/locales/en-US/chrome/browser/devtools/netmonitor.properties
browser/locales/jar.mn
browser/themes/linux/devtools/common.css
browser/themes/linux/devtools/debugger.css
browser/themes/linux/devtools/netmonitor.css
browser/themes/linux/devtools/widgets.css
browser/themes/linux/jar.mn
browser/themes/osx/devtools/common.css
browser/themes/osx/devtools/debugger.css
browser/themes/osx/devtools/netmonitor.css
browser/themes/osx/devtools/widgets.css
browser/themes/osx/jar.mn
browser/themes/windows/devtools/common.css
browser/themes/windows/devtools/debugger.css
browser/themes/windows/devtools/netmonitor.css
browser/themes/windows/devtools/widgets.css
browser/themes/windows/jar.mn
toolkit/devtools/webconsole/NetworkHelper.jsm
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1072,16 +1072,22 @@ pref("devtools.debugger.ui.pause-on-exce
 pref("devtools.debugger.ui.panes-visible-on-startup", false);
 pref("devtools.debugger.ui.variables-sorting-enabled", true);
 pref("devtools.debugger.ui.variables-only-enum-visible", false);
 pref("devtools.debugger.ui.variables-searchbox-visible", false);
 
 // Enable the Profiler
 pref("devtools.profiler.enabled", true);
 
+// Enable the Network Monitor
+pref("devtools.netmonitor.enabled", true);
+
+// The default Network Monitor UI settings
+pref("devtools.netmonitor.panes-network-details-width", 450);
+
 // Enable the Tilt inspector
 pref("devtools.tilt.enabled", true);
 pref("devtools.tilt.intro_transition", true);
 pref("devtools.tilt.outro_transition", true);
 
 // Enable the Scratchpad tool.
 pref("devtools.scratchpad.enabled", true);
 
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -26,29 +26,30 @@ const VARIABLES_VIEW_NON_SORTABLE = [
   "Float32Array",
   "Float64Array"
 ];
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 Cu.import("resource:///modules/source-editor.jsm");
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
 Cu.import("resource:///modules/devtools/VariablesView.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
-  "resource://gre/modules/commonjs/sdk/core/promise.js");
-
 XPCOMUtils.defineLazyModuleGetter(this, "Parser",
   "resource:///modules/devtools/Parser.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
+  "resource://gre/modules/devtools/NetworkHelper.jsm");
+
 /**
  * Object defining the debugger controller components.
  */
 let DebuggerController = {
   /**
    * Initializes the debugger controller.
    */
   initialize: function DC_initialize() {
@@ -70,22 +71,22 @@ let DebuggerController = {
   /**
    * Initializes the view.
    *
    * @return object
    *         A promise that is resolved when the debugger finishes startup.
    */
   startupDebugger: function DC_startupDebugger() {
     if (this._isInitialized) {
-      return;
+      return this._startup.promise;
     }
     this._isInitialized = true;
     window.removeEventListener("DOMContentLoaded", this.startupDebugger, true);
 
-    let deferred = Promise.defer();
+    let deferred = this._startup = Promise.defer();
 
     DebuggerView.initialize(() => {
       DebuggerView._isInitialized = true;
 
       // Chrome debugging needs to initiate the connection by itself.
       if (window._isChromeDebugger) {
         this.connect().then(deferred.resolve);
       } else {
@@ -98,23 +99,24 @@ let DebuggerController = {
 
   /**
    * Destroys the view and disconnects the debugger client from the server.
    *
    * @return object
    *         A promise that is resolved when the debugger finishes shutdown.
    */
   shutdownDebugger: function DC__shutdownDebugger() {
-    if (this._isDestroyed || !DebuggerView._isInitialized) {
-      return;
+    if (this._isDestroyed) {
+      return this._shutdown.promise;
     }
     this._isDestroyed = true;
+    this._startup = null;
     window.removeEventListener("unload", this.shutdownDebugger, true);
 
-    let deferred = Promise.defer();
+    let deferred = this._shutdown = Promise.defer();
 
     DebuggerView.destroy(() => {
       DebuggerView._isDestroyed = true;
       this.SourceScripts.disconnect();
       this.StackFrames.disconnect();
       this.ThreadState.disconnect();
 
       this.disconnect();
@@ -123,24 +125,31 @@ let DebuggerController = {
       // Chrome debugging needs to close its parent process on shutdown.
       window._isChromeDebugger && this._quitApp();
     });
 
     return deferred.promise;
   },
 
   /**
-   * Initializes a debugger client and connects it to the debugger server,
+   * Initiates remote or chrome debugging based on the current target,
    * wiring event handlers as necessary.
    *
+   * In case of a chrome debugger living in a different process, a socket
+   * connection pipe is opened as well.
+   *
    * @return object
    *         A promise that is resolved when the debugger finishes connecting.
    */
   connect: function DC_connect() {
-    let deferred = Promise.defer();
+    if (this._connection) {
+      return this._connection.promise;
+    }
+
+    let deferred = this._connection = Promise.defer();
 
     if (!window._isChromeDebugger) {
       let target = this._target;
       let { client, form } = target;
       target.on("close", this._onTabDetached);
       target.on("navigate", this._onTabNavigated);
       target.on("will-navigate", this._onTabNavigated);
 
@@ -176,22 +185,23 @@ let DebuggerController = {
   disconnect: function DC_disconnect() {
     // Return early if the client didn't even have a chance to instantiate.
     if (!this.client) {
       return;
     }
     this.client.removeListener("tabNavigated", this._onTabNavigated);
     this.client.removeListener("tabDetached", this._onTabDetached);
 
-    // When debugging content or a remote instance, the connection is closed by
+    // When debugging local or a remote instance, the connection is closed by
     // the RemoteTarget.
     if (window._isChromeDebugger) {
       this.client.close();
     }
 
+    this._connection = null;
     this.client = null;
     this.tabClient = null;
     this.activeThread = null;
   },
 
   /**
    * Called for each location change in the debugged tab.
    *
@@ -310,17 +320,26 @@ let DebuggerController = {
 
     Services.obs.notifyObservers(canceled, "quit-application-requested", null);
 
     // Somebody canceled our quit request.
     if (canceled.data) {
       return;
     }
     Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit);
-  }
+  },
+
+  _isInitialized: false,
+  _isDestroyed: false,
+  _startup: null,
+  _shutdown: null,
+  _connection: null,
+  client: null,
+  tabClient: null,
+  activeThread: null
 };
 
 /**
  * ThreadState keeps the UI up to date with the state of the
  * thread (paused/attached/etc.).
  */
 function ThreadState() {
   this._update = this._update.bind(this);
@@ -906,17 +925,17 @@ StackFrames.prototype = {
    * Adds the specified stack frame to the list.
    *
    * @param object aFrame
    *        The new frame to add.
    */
   _addFrame: function SF__addFrame(aFrame) {
     let depth = aFrame.depth;
     let { url, line } = aFrame.where;
-    let frameLocation = SourceUtils.convertToUnicode(window.unescape(url));
+    let frameLocation = NetworkHelper.convertToUnicode(unescape(url));
     let frameTitle = StackFrameUtils.getFrameTitle(aFrame);
 
     DebuggerView.StackFrames.addFrame(frameTitle, frameLocation, line, depth);
   },
 
   /**
    * Loads more stack frames from the debugger server cache.
    */
@@ -1186,17 +1205,17 @@ SourceScripts.prototype = {
     }
     return sources.sort(([first], [second]) => first > second);
   },
 
   /**
    * Clears all the fetched sources from cache.
    */
   clearCache: function SS_clearCache() {
-    this._cache = new Map();
+    this._cache.clear();
   },
 
   /**
    * Starts fetching all the sources, silently.
    *
    * @param array aUrls
    *        The urls for the sources to fetch.
    * @param object aCallbacks [optional]
@@ -1626,110 +1645,41 @@ Breakpoints.prototype = {
     }
     return null;
   }
 };
 
 /**
  * Localization convenience methods.
  */
-let L10N = {
-  /**
-   * L10N shortcut function.
-   *
-   * @param string aName
-   * @return string
-   */
-  getStr: function L10N_getStr(aName) {
-    return this.stringBundle.GetStringFromName(aName);
-  },
-
-  /**
-   * L10N shortcut function.
-   *
-   * @param string aName
-   * @param array aArray
-   * @return string
-   */
-  getFormatStr: function L10N_getFormatStr(aName, aArray) {
-    return this.stringBundle.formatStringFromName(aName, aArray, aArray.length);
-  }
-};
-
-XPCOMUtils.defineLazyGetter(L10N, "stringBundle", function() {
-  return Services.strings.createBundle(DBG_STRINGS_URI);
-});
-
-XPCOMUtils.defineLazyGetter(L10N, "ellipsis", function() {
-  return Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
-});
+let L10N = new ViewHelpers.L10N(DBG_STRINGS_URI);
 
 /**
  * Shortcuts for accessing various debugger preferences.
  */
-let Prefs = {
-  /**
-   * Helper method for getting a pref value.
-   *
-   * @param string aType
-   * @param string aPrefName
-   * @return any
-   */
-  _get: function P__get(aType, aPrefName) {
-    if (this[aPrefName] === undefined) {
-      this[aPrefName] = Services.prefs["get" + aType + "Pref"](aPrefName);
-    }
-    return this[aPrefName];
-  },
-
-  /**
-   * Helper method for setting a pref value.
-   *
-   * @param string aType
-   * @param string aPrefName
-   * @param any aValue
-   */
-  _set: function P__set(aType, aPrefName, aValue) {
-    Services.prefs["set" + aType + "Pref"](aPrefName, aValue);
-    this[aPrefName] = aValue;
-  },
-
-  /**
-   * Maps a property name to a pref, defining lazy getters and setters.
-   *
-   * @param string aType
-   * @param string aPropertyName
-   * @param string aPrefName
-   */
-  map: function P_map(aType, aPropertyName, aPrefName) {
-    Object.defineProperty(this, aPropertyName, {
-      get: function() this._get(aType, aPrefName),
-      set: function(aValue) this._set(aType, aPrefName, aValue)
-    });
-  }
-};
-
-Prefs.map("Char", "chromeDebuggingHost", "devtools.debugger.chrome-debugging-host");
-Prefs.map("Int", "chromeDebuggingPort", "devtools.debugger.chrome-debugging-port");
-Prefs.map("Int", "windowX", "devtools.debugger.ui.win-x");
-Prefs.map("Int", "windowY", "devtools.debugger.ui.win-y");
-Prefs.map("Int", "windowWidth", "devtools.debugger.ui.win-width");
-Prefs.map("Int", "windowHeight", "devtools.debugger.ui.win-height");
-Prefs.map("Int", "sourcesWidth", "devtools.debugger.ui.panes-sources-width");
-Prefs.map("Int", "instrumentsWidth", "devtools.debugger.ui.panes-instruments-width");
-Prefs.map("Bool", "pauseOnExceptions", "devtools.debugger.ui.pause-on-exceptions");
-Prefs.map("Bool", "panesVisibleOnStartup", "devtools.debugger.ui.panes-visible-on-startup");
-Prefs.map("Bool", "variablesSortingEnabled", "devtools.debugger.ui.variables-sorting-enabled");
-Prefs.map("Bool", "variablesOnlyEnumVisible", "devtools.debugger.ui.variables-only-enum-visible");
-Prefs.map("Bool", "variablesSearchboxVisible", "devtools.debugger.ui.variables-searchbox-visible");
-Prefs.map("Char", "remoteHost", "devtools.debugger.remote-host");
-Prefs.map("Int", "remotePort", "devtools.debugger.remote-port");
-Prefs.map("Bool", "remoteAutoConnect", "devtools.debugger.remote-autoconnect");
-Prefs.map("Int", "remoteConnectionRetries", "devtools.debugger.remote-connection-retries");
-Prefs.map("Int", "remoteTimeout", "devtools.debugger.remote-timeout");
+let Prefs = new ViewHelpers.Prefs("devtools.debugger", {
+  chromeDebuggingHost: ["Char", "chrome-debugging-host"],
+  chromeDebuggingPort: ["Int", "chrome-debugging-port"],
+  windowX: ["Int", "ui.win-x"],
+  windowY: ["Int", "ui.win-y"],
+  windowWidth: ["Int", "ui.win-width"],
+  windowHeight: ["Int", "ui.win-height"],
+  sourcesWidth: ["Int", "ui.panes-sources-width"],
+  instrumentsWidth: ["Int", "ui.panes-instruments-width"],
+  pauseOnExceptions: ["Bool", "ui.pause-on-exceptions"],
+  panesVisibleOnStartup: ["Bool", "ui.panes-visible-on-startup"],
+  variablesSortingEnabled: ["Bool", "ui.variables-sorting-enabled"],
+  variablesOnlyEnumVisible: ["Bool", "ui.variables-only-enum-visible"],
+  variablesSearchboxVisible: ["Bool", "ui.variables-searchbox-visible"],
+  remoteHost: ["Char", "remote-host"],
+  remotePort: ["Int", "remote-port"],
+  remoteAutoConnect: ["Bool", "remote-autoconnect"],
+  remoteConnectionRetries: ["Int", "remote-connection-retries"],
+  remoteTimeout: ["Int", "remote-timeout"]
+});
 
 /**
  * Returns true if this is a remote debugger instance.
  * @return boolean
  */
 XPCOMUtils.defineLazyGetter(window, "_isRemoteDebugger", function() {
   // We're inside a single top level XUL window, not an iframe container.
   return !(window.frameElement instanceof XULElement) &&
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -954,27 +954,27 @@ create({ constructor: SourcesView, proto
   _editorContextMenuLineNumber: -1,
   _conditionalPopupVisible: false
 });
 
 /**
  * Utility functions for handling sources.
  */
 let SourceUtils = {
-  _labelsCache: new Map(),
+  _labelsCache: new Map(), // Can't use WeakMaps because keys are strings.
   _groupsCache: new Map(),
 
   /**
    * Clears the labels cache, populated by methods like
    * SourceUtils.getSourceLabel or Source Utils.getSourceGroup.
    * This should be done every time the content location changes.
    */
   clearCache: function SU_clearCache() {
-    this._labelsCache = new Map();
-    this._groupsCache = new Map();
+    this._labelsCache.clear();
+    this._groupsCache.clear();
   },
 
   /**
    * Gets a unique, simplified label from a source url.
    *
    * @param string aUrl
    *        The source url.
    * @return string
@@ -982,17 +982,17 @@ let SourceUtils = {
    */
   getSourceLabel: function SU_getSourceLabel(aUrl) {
     let cachedLabel = this._labelsCache.get(aUrl);
     if (cachedLabel) {
       return cachedLabel;
     }
 
     let sourceLabel = this.trimUrl(aUrl);
-    let unicodeLabel = this.convertToUnicode(window.unescape(sourceLabel));
+    let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel));
     this._labelsCache.set(aUrl, unicodeLabel);
     return unicodeLabel;
   },
 
   /**
    * Gets as much information as possible about the hostname and directory paths
    * of an url to create a short url group identifier.
    *
@@ -1031,17 +1031,17 @@ let SourceUtils = {
     }
     // Append the last directory if the path leads to an actual file.
     // e.g. http://foo.org/bar/ should only show "foo.org", not "foo.org bar"
     if (fileName) {
       group.push(lastDir);
     }
 
     let groupLabel = group.join(" ");
-    let unicodeLabel = this.convertToUnicode(window.unescape(groupLabel));
+    let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel));
     this._groupsCache.set(aUrl, unicodeLabel)
     return unicodeLabel;
   },
 
   /**
    * Trims the url by shortening it if it exceeds a certain length, adding an
    * ellipsis at the end.
    *
@@ -1176,41 +1176,16 @@ let SourceUtils = {
       aSeq++;
     }
     // Use the whole url spec but ignoring the reference.
     if (aSeq == 5) {
       return this.trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1);
     }
     // Give up.
     return aUrl.spec;
-  },
-
-  /**
-   * Convert a given string, encoded in a given character set, to unicode.
-   *
-   * @param string aString
-   *        A string.
-   * @param string aCharset [optional]
-   *        A character set.
-   * @return string
-   *         A unicode string.
-   */
-  convertToUnicode: function SU_convertToUnicode(aString, aCharset) {
-    // Decoding primitives.
-    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
-        .createInstance(Ci.nsIScriptableUnicodeConverter);
-
-    try {
-      if (aCharset) {
-        converter.charset = aCharset;
-      }
-      return converter.ConvertToUnicode(aString);
-    } catch(e) {
-      return aString;
-    }
   }
 };
 
 /**
  * Functions handling the watch expressions UI.
  */
 function WatchExpressionsView() {
   dumpn("WatchExpressionsView was instantiated");
--- a/browser/devtools/debugger/debugger-toolbar.js
+++ b/browser/devtools/debugger/debugger-toolbar.js
@@ -32,21 +32,21 @@ ToolbarView.prototype = {
     this._stepInButton = document.getElementById("step-in");
     this._stepOutButton = document.getElementById("step-out");
     this._chromeGlobals = document.getElementById("chrome-globals");
 
     let resumeKey = LayoutHelpers.prettyKey(document.getElementById("resumeKey"), true);
     let stepOverKey = LayoutHelpers.prettyKey(document.getElementById("stepOverKey"), true);
     let stepInKey = LayoutHelpers.prettyKey(document.getElementById("stepInKey"), true);
     let stepOutKey = LayoutHelpers.prettyKey(document.getElementById("stepOutKey"), true);
-    this._resumeTooltip = L10N.getFormatStr("resumeButtonTooltip", [resumeKey]);
-    this._pauseTooltip = L10N.getFormatStr("pauseButtonTooltip", [resumeKey]);
-    this._stepOverTooltip = L10N.getFormatStr("stepOverTooltip", [stepOverKey]);
-    this._stepInTooltip = L10N.getFormatStr("stepInTooltip", [stepInKey]);
-    this._stepOutTooltip = L10N.getFormatStr("stepOutTooltip", [stepOutKey]);
+    this._resumeTooltip = L10N.getFormatStr("resumeButtonTooltip", resumeKey);
+    this._pauseTooltip = L10N.getFormatStr("pauseButtonTooltip", resumeKey);
+    this._stepOverTooltip = L10N.getFormatStr("stepOverTooltip", stepOverKey);
+    this._stepInTooltip = L10N.getFormatStr("stepInTooltip", stepInKey);
+    this._stepOutTooltip = L10N.getFormatStr("stepOutTooltip", stepOutKey);
 
     this._instrumentsPaneToggleButton.addEventListener("mousedown", this._onTogglePanesPressed, false);
     this._resumeButton.addEventListener("mousedown", this._onResumePressed, false);
     this._stepOverButton.addEventListener("mousedown", this._onStepOverPressed, false);
     this._stepInButton.addEventListener("mousedown", this._onStepInPressed, false);
     this._stepOutButton.addEventListener("mousedown", this._onStepOutPressed, false);
 
     this._stepOverButton.setAttribute("tooltiptext", this._stepOverTooltip);
@@ -640,17 +640,17 @@ let StackFrameUtils = {
     if (!aEnv.parent) {
       name = L10N.getStr("globalScopeLabel");
     }
     // Otherwise construct the scope name.
     else {
       name = aEnv.type.charAt(0).toUpperCase() + aEnv.type.slice(1);
     }
 
-    let label = L10N.getFormatStr("scopeLabel", [name]);
+    let label = L10N.getFormatStr("scopeLabel", name);
     switch (aEnv.type) {
       case "with":
       case "object":
         label += " [" + aEnv.object.class + "]";
         break;
       case "function":
         let f = aEnv.function;
         label += " [" +
@@ -709,25 +709,25 @@ FilterView.prototype = {
 
     this._globalOperatorButton.setAttribute("label", SEARCH_GLOBAL_FLAG);
     this._functionOperatorButton.setAttribute("label", SEARCH_FUNCTION_FLAG);
     this._tokenOperatorButton.setAttribute("label", SEARCH_TOKEN_FLAG);
     this._lineOperatorButton.setAttribute("label", SEARCH_LINE_FLAG);
     this._variableOperatorButton.setAttribute("label", SEARCH_VARIABLE_FLAG);
 
     this._globalOperatorLabel.setAttribute("value",
-      L10N.getFormatStr("searchPanelGlobal", [this._globalSearchKey]));
+      L10N.getFormatStr("searchPanelGlobal", this._globalSearchKey));
     this._functionOperatorLabel.setAttribute("value",
-      L10N.getFormatStr("searchPanelFunction", [this._filteredFunctionsKey]));
+      L10N.getFormatStr("searchPanelFunction", this._filteredFunctionsKey));
     this._tokenOperatorLabel.setAttribute("value",
-      L10N.getFormatStr("searchPanelToken", [this._tokenSearchKey]));
+      L10N.getFormatStr("searchPanelToken", this._tokenSearchKey));
     this._lineOperatorLabel.setAttribute("value",
-      L10N.getFormatStr("searchPanelLine", [this._lineSearchKey]));
+      L10N.getFormatStr("searchPanelLine", this._lineSearchKey));
     this._variableOperatorLabel.setAttribute("value",
-      L10N.getFormatStr("searchPanelVariable", [this._variableSearchKey]));
+      L10N.getFormatStr("searchPanelVariable", this._variableSearchKey));
 
     // TODO: bug 806775
     // if (window._isChromeDebugger) {
     //   this.target = DebuggerView.ChromeGlobals;
     // } else {
     this.target = DebuggerView.Sources;
     // }
   },
@@ -748,20 +748,20 @@ FilterView.prototype = {
   /**
    * Sets the target container to be currently filtered.
    * @param object aView
    */
   set target(aView) {
     let placeholder = "";
     switch (aView) {
       case DebuggerView.ChromeGlobals:
-        placeholder = L10N.getFormatStr("emptyChromeGlobalsFilterText", [this._fileSearchKey]);
+        placeholder = L10N.getFormatStr("emptyChromeGlobalsFilterText", this._fileSearchKey);
         break;
       case DebuggerView.Sources:
-        placeholder = L10N.getFormatStr("emptyFilterText", [this._fileSearchKey]);
+        placeholder = L10N.getFormatStr("emptyFilterText", this._fileSearchKey);
         break;
     }
     this._searchbox.setAttribute("placeholder", placeholder);
     this._target = aView;
   },
 
   /**
    * Gets the target container to be currently filtered.
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -7,17 +7,16 @@
 
 const SOURCE_URL_DEFAULT_MAX_LENGTH = 64; // chars
 const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 1048576; // 1 MB in bytes
 const STACK_FRAMES_SOURCE_URL_MAX_LENGTH = 15; // chars
 const STACK_FRAMES_SOURCE_URL_TRIM_SECTION = "center";
 const STACK_FRAMES_POPUP_SOURCE_URL_MAX_LENGTH = 32; // chars
 const STACK_FRAMES_POPUP_SOURCE_URL_TRIM_SECTION = "center";
 const STACK_FRAMES_SCROLL_DELAY = 100; // ms
-const PANES_APPEARANCE_DELAY = 50; // ms
 const BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH = 1000; // chars
 const BREAKPOINT_CONDITIONAL_POPUP_POSITION = "before_start";
 const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X = 7; // px
 const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y = -3; // px
 const RESULTS_PANEL_POPUP_POSITION = "before_end";
 const RESULTS_PANEL_MAX_RESULTS = 10;
 const GLOBAL_SEARCH_EXPAND_MAX_RESULTS = 50;
 const GLOBAL_SEARCH_LINE_MAX_LENGTH = 300; // chars
@@ -411,77 +410,40 @@ let DebuggerView = {
     return this.editor.getText(selection.start, selection.end);
   },
 
   /**
    * Gets the visibility state of the instruments pane.
    * @return boolean
    */
   get instrumentsPaneHidden()
-    this._instrumentsPaneToggleButton.hasAttribute("toggled"),
+    this._instrumentsPane.hasAttribute("pane-collapsed"),
 
   /**
    * Sets the instruments pane hidden or visible.
    *
-   * @param object aFlags [optional]
-   *        An object containing some of the following boolean properties:
-   *        - visible: true if the pane should be shown, false for hidden
+   * @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
    *        - delayed: true to wait a few cycles before toggle
    *        - callback: a function to invoke when the toggle finishes
    */
-  toggleInstrumentsPane: function DV__toggleInstrumentsPane(aFlags = {}) {
-    // Avoid useless toggles.
-    if (aFlags.visible == !this.instrumentsPaneHidden) {
-      if (aFlags.callback) aFlags.callback();
-      return;
-    }
+  toggleInstrumentsPane: function DV__toggleInstrumentsPane(aFlags) {
+    let pane = this._instrumentsPane;
+    let button = this._instrumentsPaneToggleButton;
 
-    // Computes and sets the pane margins in order to hide or show it.
-    function set() {
-      if (aFlags.visible) {
-        this._instrumentsPane.style.marginLeft = "0";
-        this._instrumentsPane.style.marginRight = "0";
-        this._instrumentsPaneToggleButton.removeAttribute("toggled");
-        this._instrumentsPaneToggleButton.setAttribute("tooltiptext", this._collapsePaneString);
-      } else {
-        let margin = ~~(this._instrumentsPane.getAttribute("width")) + 1;
-        this._instrumentsPane.style.marginLeft = -margin + "px";
-        this._instrumentsPane.style.marginRight = -margin + "px";
-        this._instrumentsPaneToggleButton.setAttribute("toggled", "true");
-        this._instrumentsPaneToggleButton.setAttribute("tooltiptext", this._expandPaneString);
-      }
+    ViewHelpers.togglePane(aFlags, pane);
 
-      if (aFlags.animated) {
-        // Displaying the panes may have the effect of triggering scrollbars to
-        // appear in the source editor, which would render the currently
-        // highlighted line to appear behind them in some cases.
-        window.addEventListener("transitionend", function onEvent() {
-          window.removeEventListener("transitionend", onEvent, false);
-          DebuggerView.updateEditor();
-
-          // Invoke the callback when the transition ended.
-          if (aFlags.callback) aFlags.callback();
-        }, false);
-      } else {
-        // Invoke the callback immediately since there's no transition.
-        if (aFlags.callback) aFlags.callback();
-      }
-    }
-
-    if (aFlags.animated) {
-      this._instrumentsPane.setAttribute("animated", "");
+    if (aFlags.visible) {
+      button.removeAttribute("pane-collapsed");
+      button.setAttribute("tooltiptext", this._collapsePaneString);
     } else {
-      this._instrumentsPane.removeAttribute("animated");
-    }
-
-    if (aFlags.delayed) {
-      window.setTimeout(set.bind(this), PANES_APPEARANCE_DELAY);
-    } else {
-      set.call(this);
+      button.setAttribute("pane-collapsed", "");
+      button.setAttribute("tooltiptext", this._expandPaneString);
     }
   },
 
   /**
    * Sets the instruments pane visible after a short period of time.
    *
    * @param function aCallback
    *        A function to invoke when the toggle finishes.
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -268,17 +268,17 @@
       <splitter class="devtools-horizontal-splitter" hidden="true"/>
       <hbox flex="1">
         <vbox id="sources-pane">
           <vbox id="sources" flex="1"/>
         </vbox>
         <splitter class="devtools-side-splitter"/>
         <vbox id="editor" flex="1"/>
         <splitter class="devtools-side-splitter"/>
-        <vbox id="instruments-pane">
+        <vbox id="instruments-pane" hidden="true">
           <vbox id="expressions"/>
           <splitter class="devtools-horizontal-splitter"/>
           <vbox id="variables" flex="1"/>
         </vbox>
       </hbox>
     </vbox>
   </vbox>
 
--- a/browser/devtools/debugger/test/browser_dbg_pane-collapse.js
+++ b/browser/devtools/debugger/test/browser_dbg_pane-collapse.js
@@ -23,20 +23,23 @@ function test() {
 
     gView.toggleInstrumentsPane({ visible: true, animated: false });
     testInstrumentsPaneCollapse();
     testPanesStartupPref();
   });
 }
 
 function testPanesState() {
+  let instrumentsPane =
+    gDebugger.document.getElementById("instruments-pane");
   let instrumentsPaneToggleButton =
     gDebugger.document.getElementById("instruments-pane-toggle");
 
-  ok(instrumentsPaneToggleButton.getAttribute("toggled"),
+  ok(instrumentsPane.hasAttribute("pane-collapsed") &&
+     instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
     "The debugger view instruments pane should initially be hidden.");
   is(gDebugger.Prefs.panesVisibleOnStartup, false,
     "The debugger view instruments pane should initially be preffed as hidden.");
   isnot(gDebugger.DebuggerView.Options._showPanesOnStartupItem.getAttribute("checked"), "true",
     "The options menu item should not be checked.");
 }
 
 function testInstrumentsPaneCollapse() {
@@ -49,17 +52,18 @@ function testInstrumentsPaneCollapse() {
   is(width, gDebugger.Prefs.instrumentsWidth,
     "The instruments pane has an incorrect width.");
   is(instrumentsPane.style.marginLeft, "0px",
     "The instruments pane has an incorrect left margin.");
   is(instrumentsPane.style.marginRight, "0px",
     "The instruments pane has an incorrect right margin.");
   ok(!instrumentsPane.hasAttribute("animated"),
     "The instruments pane has an incorrect animated attribute.");
-  ok(!instrumentsPaneToggleButton.getAttribute("toggled"),
+  ok(!instrumentsPane.hasAttribute("pane-collapsed") &&
+     !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
     "The instruments pane should at this point be visible.");
 
   gView.toggleInstrumentsPane({ visible: false, animated: true });
 
   is(gDebugger.Prefs.panesVisibleOnStartup, false,
     "The debugger view panes should still initially be preffed as hidden.");
   isnot(gDebugger.DebuggerView.Options._showPanesOnStartupItem.getAttribute("checked"), "true",
     "The options menu item should still not be checked.");
@@ -68,17 +72,18 @@ function testInstrumentsPaneCollapse() {
   is(width, gDebugger.Prefs.instrumentsWidth,
     "The instruments pane has an incorrect width after collapsing.");
   is(instrumentsPane.style.marginLeft, margin,
     "The instruments pane has an incorrect left margin after collapsing.");
   is(instrumentsPane.style.marginRight, margin,
     "The instruments pane has an incorrect right margin after collapsing.");
   ok(instrumentsPane.hasAttribute("animated"),
     "The instruments pane has an incorrect attribute after an animated collapsing.");
-  ok(instrumentsPaneToggleButton.hasAttribute("toggled"),
+  ok(instrumentsPane.hasAttribute("pane-collapsed") &&
+     instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
     "The instruments pane should not be visible after collapsing.");
 
   gView.toggleInstrumentsPane({ visible: true, animated: false });
 
   is(gDebugger.Prefs.panesVisibleOnStartup, false,
     "The debugger view panes should still initially be preffed as hidden.");
   isnot(gDebugger.DebuggerView.Options._showPanesOnStartupItem.getAttribute("checked"), "true",
     "The options menu item should still not be checked.");
@@ -86,52 +91,56 @@ function testInstrumentsPaneCollapse() {
   is(width, gDebugger.Prefs.instrumentsWidth,
     "The instruments pane has an incorrect width after uncollapsing.");
   is(instrumentsPane.style.marginLeft, "0px",
     "The instruments pane has an incorrect left margin after uncollapsing.");
   is(instrumentsPane.style.marginRight, "0px",
     "The instruments pane has an incorrect right margin after uncollapsing.");
   ok(!instrumentsPane.hasAttribute("animated"),
     "The instruments pane has an incorrect attribute after an unanimated uncollapsing.");
-  ok(!instrumentsPaneToggleButton.getAttribute("toggled"),
+  ok(!instrumentsPane.hasAttribute("pane-collapsed") &&
+     !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
     "The instruments pane should be visible again after uncollapsing.");
 }
 
 function testPanesStartupPref() {
   let instrumentsPane =
     gDebugger.document.getElementById("instruments-pane");
   let instrumentsPaneToggleButton =
     gDebugger.document.getElementById("instruments-pane-toggle");
 
   is(gDebugger.Prefs.panesVisibleOnStartup, false,
     "The debugger view panes should still initially be preffed as hidden.");
 
-  ok(!instrumentsPaneToggleButton.getAttribute("toggled"),
+  ok(!instrumentsPane.hasAttribute("pane-collapsed") &&
+     !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
     "The debugger instruments pane should at this point be visible.");
   is(gDebugger.Prefs.panesVisibleOnStartup, false,
     "The debugger view panes should initially be preffed as hidden.");
   isnot(gDebugger.DebuggerView.Options._showPanesOnStartupItem.getAttribute("checked"), "true",
     "The options menu item should still not be checked.");
 
   gDebugger.DebuggerView.Options._showPanesOnStartupItem.setAttribute("checked", "true");
   gDebugger.DebuggerView.Options._toggleShowPanesOnStartup();
 
   executeSoon(function() {
-    ok(!instrumentsPaneToggleButton.getAttribute("toggled"),
+    ok(!instrumentsPane.hasAttribute("pane-collapsed") &&
+       !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
       "The debugger instruments pane should at this point be visible.");
     is(gDebugger.Prefs.panesVisibleOnStartup, true,
       "The debugger view panes should now be preffed as visible.");
     is(gDebugger.DebuggerView.Options._showPanesOnStartupItem.getAttribute("checked"), "true",
       "The options menu item should now be checked.");
 
     gDebugger.DebuggerView.Options._showPanesOnStartupItem.setAttribute("checked", "false");
     gDebugger.DebuggerView.Options._toggleShowPanesOnStartup();
 
     executeSoon(function() {
-      ok(!instrumentsPaneToggleButton.getAttribute("toggled"),
+      ok(!instrumentsPane.hasAttribute("pane-collapsed") &&
+         !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
         "The debugger instruments pane should at this point be visible.");
       is(gDebugger.Prefs.panesVisibleOnStartup, false,
         "The debugger view panes should now be preffed as hidden.");
       isnot(gDebugger.DebuggerView.Options._showPanesOnStartupItem.getAttribute("checked"), "true",
         "The options menu item should now be unchecked.");
 
       executeSoon(function() {
         closeDebuggerAndFinish();
--- a/browser/devtools/debugger/test/browser_dbg_pause-resume.js
+++ b/browser/devtools/debugger/test/browser_dbg_pause-resume.js
@@ -25,32 +25,32 @@ function test() {
 }
 
 function testPause() {
   is(gDebugger.DebuggerController.activeThread.paused, false,
     "Should be running after debug_tab_pane.");
 
   let button = gDebugger.document.getElementById("resume");
   is(button.getAttribute("tooltiptext"),
-     gL10N.getFormatStr("pauseButtonTooltip", [
-      gLH.prettyKey(gDebugger.document.getElementById("resumeKey"))]),
+     gL10N.getFormatStr("pauseButtonTooltip",
+      gLH.prettyKey(gDebugger.document.getElementById("resumeKey"))),
     "Button tooltip should be pause when running.");
 
   gDebugger.DebuggerController.activeThread.addOneTimeListener("paused", function() {
     Services.tm.currentThread.dispatch({ run: function() {
 
       let frames = gDebugger.DebuggerView.StackFrames._container._list;
       let childNodes = frames.childNodes;
 
       is(gDebugger.DebuggerController.activeThread.paused, true,
         "Should be paused after an interrupt request.");
 
       is(button.getAttribute("tooltiptext"),
-         gL10N.getFormatStr("resumeButtonTooltip", [
-          gLH.prettyKey(gDebugger.document.getElementById("resumeKey"))]),
+         gL10N.getFormatStr("resumeButtonTooltip",
+          gLH.prettyKey(gDebugger.document.getElementById("resumeKey"))),
         "Button tooltip should be resume when paused.");
 
       is(frames.querySelectorAll(".dbg-stackframe").length, 0,
         "Should have no frames when paused in the main loop.");
 
       testResume();
     }}, 0);
   });
@@ -64,18 +64,18 @@ function testResume() {
   gDebugger.DebuggerController.activeThread.addOneTimeListener("resumed", function() {
     Services.tm.currentThread.dispatch({ run: function() {
 
       is(gDebugger.DebuggerController.activeThread.paused, false,
         "Should be paused after an interrupt request.");
 
       let button = gDebugger.document.getElementById("resume");
       is(button.getAttribute("tooltiptext"),
-         gL10N.getFormatStr("pauseButtonTooltip", [
-          gLH.prettyKey(gDebugger.document.getElementById("resumeKey"))]),
+         gL10N.getFormatStr("pauseButtonTooltip",
+          gLH.prettyKey(gDebugger.document.getElementById("resumeKey"))),
         "Button tooltip should be pause when running.");
 
       closeDebuggerAndFinish();
     }}, 0);
   });
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
     gDebugger.document.getElementById("resume"),
--- a/browser/devtools/debugger/test/browser_dbg_sources-cache.js
+++ b/browser/devtools/debugger/test/browser_dbg_sources-cache.js
@@ -79,17 +79,17 @@ function testSourcesCache()
   is(gControllerSources.getCache().length, 0,
     "The sources cache should be empty when debugger starts.");
   is(gDebugger.SourceUtils._labelsCache.size, TOTAL_SOURCES,
     "There should be " + TOTAL_SOURCES + " labels cached");
   is(gDebugger.SourceUtils._groupsCache.size, TOTAL_SOURCES,
     "There should be " + TOTAL_SOURCES + " groups cached");
 
   gPrevLabelsCache = gDebugger.SourceUtils._labelsCache;
-  gPrevLabelsCache = gDebugger.SourceUtils._groupsCache;
+  gPrevGroupsCache = gDebugger.SourceUtils._groupsCache;
 
   fetchSources(function() {
     performReload(function() {
       closeDebuggerAndFinish();
     });
   });
 }
 
@@ -137,20 +137,20 @@ function performReload(callback) {
   gDebuggee.location.reload();
 }
 
 function testStateBeforeReload() {
   is(gSources.itemCount, 0,
     "There should be no sources present in the sources list during reload.");
   is(gControllerSources.getCache().length, 0,
     "The sources cache should be empty during reload.");
-  isnot(gDebugger.SourceUtils._labelsCache, gPrevLabelsCache,
-    "The labels cache has been refreshed during reload.")
-  isnot(gDebugger.SourceUtils._groupsCache, gPrevGroupsCache,
-    "The groups cache has been refreshed during reload.")
+  is(gDebugger.SourceUtils._labelsCache, gPrevLabelsCache,
+    "The labels cache has been refreshed during reload and no new objects were created.");
+  is(gDebugger.SourceUtils._groupsCache, gPrevGroupsCache,
+    "The groups cache has been refreshed during reload and no new objects were created.");
   is(gDebugger.SourceUtils._labelsCache.size, 0,
     "There should be no labels cached during reload");
   is(gDebugger.SourceUtils._groupsCache.size, 0,
     "There should be no groups cached during reload");
 }
 
 function testStateAfterReload() {
   is(gSources.itemCount, TOTAL_SOURCES,
--- a/browser/devtools/framework/ToolDefinitions.jsm
+++ b/browser/devtools/framework/ToolDefinitions.jsm
@@ -4,26 +4,28 @@
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
                           "defaultTools",
                           "webConsoleDefinition",
                           "debuggerDefinition",
                           "inspectorDefinition",
-                          "styleEditorDefinition"
+                          "styleEditorDefinition",
+                          "netMonitorDefinition"
                         ];
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 const inspectorProps = "chrome://browser/locale/devtools/inspector.properties";
 const debuggerProps = "chrome://browser/locale/devtools/debugger.properties";
 const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties";
 const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
 const profilerProps = "chrome://browser/locale/devtools/profiler.properties";
+const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/EventEmitter.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "osString",
   function() Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS);
 
@@ -38,32 +40,38 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource:///modules/devtools/StyleEditorPanel.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "InspectorPanel",
   "resource:///modules/devtools/InspectorPanel.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ProfilerPanel",
   "resource:///modules/devtools/ProfilerPanel.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "NetMonitorPanel",
+  "resource:///modules/devtools/NetMonitorPanel.jsm");
+
 // Strings
 XPCOMUtils.defineLazyGetter(this, "webConsoleStrings",
   function() Services.strings.createBundle(webConsoleProps));
 
 XPCOMUtils.defineLazyGetter(this, "debuggerStrings",
   function() Services.strings.createBundle(debuggerProps));
 
 XPCOMUtils.defineLazyGetter(this, "styleEditorStrings",
   function() Services.strings.createBundle(styleEditorProps));
 
 XPCOMUtils.defineLazyGetter(this, "inspectorStrings",
   function() Services.strings.createBundle(inspectorProps));
 
 XPCOMUtils.defineLazyGetter(this, "profilerStrings",
   function() Services.strings.createBundle(profilerProps));
 
+XPCOMUtils.defineLazyGetter(this, "netMonitorStrings",
+  function() Services.strings.createBundle(netMonitorProps));
+
 // Definitions
 let webConsoleDefinition = {
   id: "webconsole",
   key: l10n("cmd.commandkey", webConsoleStrings),
   accesskey: l10n("webConsoleCmd.accesskey", webConsoleStrings),
   modifiers: Services.appinfo.OS == "Darwin" ? "accel,alt" : "accel,shift",
   ordinal: 0,
   icon: "chrome://browser/skin/devtools/tool-webconsole.png",
@@ -124,19 +132,19 @@ let inspectorDefinition = {
 };
 
 let styleEditorDefinition = {
   id: "styleeditor",
   key: l10n("open.commandkey", styleEditorStrings),
   ordinal: 3,
   accesskey: l10n("open.accesskey", styleEditorStrings),
   modifiers: "shift",
-  label: l10n("ToolboxStyleEditor.label", styleEditorStrings),
   icon: "chrome://browser/skin/devtools/tool-styleeditor.png",
   url: "chrome://browser/content/styleeditor.xul",
+  label: l10n("ToolboxStyleEditor.label", styleEditorStrings),
   tooltip: l10n("ToolboxStyleEditor.tooltip", styleEditorStrings),
 
   isTargetSupported: function(target) {
     return !target.isRemote;
   },
 
   build: function(iframeWindow, toolbox) {
     let panel = new StyleEditorPanel(iframeWindow, toolbox);
@@ -146,37 +154,59 @@ let styleEditorDefinition = {
 
 let profilerDefinition = {
   id: "jsprofiler",
   accesskey: l10n("profiler.accesskey", profilerStrings),
   key: l10n("profiler2.commandkey", profilerStrings),
   ordinal: 4,
   modifiers: "shift",
   killswitch: "devtools.profiler.enabled",
+  icon: "chrome://browser/skin/devtools/tool-profiler.png",
   url: "chrome://browser/content/profiler.xul",
   label: l10n("profiler.label", profilerStrings),
-  icon: "chrome://browser/skin/devtools/tool-profiler.png",
   tooltip: l10n("profiler.tooltip", profilerStrings),
 
   isTargetSupported: function (target) {
     return true;
   },
 
   build: function (frame, target) {
     let panel = new ProfilerPanel(frame, target);
     return panel.open();
   }
 };
 
+let netMonitorDefinition = {
+  id: "netmonitor",
+  accesskey: l10n("netmonitor.accesskey", netMonitorStrings),
+  key: l10n("netmonitor.commandkey", netMonitorStrings),
+  ordinal: 5,
+  modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
+  killswitch: "devtools.netmonitor.enabled",
+  icon: "chrome://browser/skin/devtools/tool-profiler.png",
+  url: "chrome://browser/content/devtools/netmonitor.xul",
+  label: l10n("netmonitor.label", netMonitorStrings),
+  tooltip: l10n("netmonitor.tooltip", netMonitorStrings),
+
+  isTargetSupported: function(target) {
+    return true;
+  },
+
+  build: function(iframeWindow, toolbox) {
+    let panel = new NetMonitorPanel(iframeWindow, toolbox);
+    return panel.open();
+  }
+};
 
 this.defaultTools = [
   styleEditorDefinition,
   webConsoleDefinition,
   debuggerDefinition,
   inspectorDefinition,
+  netMonitorDefinition
 ];
 
 if (Services.prefs.getBoolPref("devtools.profiler.enabled")) {
   defaultTools.push(profilerDefinition);
 }
 
 /**
  * Lookup l10n string from a string bundle.
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -1,16 +1,20 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 browser.jar:
     content/browser/devtools/widgets.css          (shared/widgets/widgets.css)
     content/browser/devtools/markup-view.xhtml    (markupview/markup-view.xhtml)
     content/browser/devtools/markup-view.css      (markupview/markup-view.css)
+    content/browser/devtools/netmonitor.xul           (netmonitor/netmonitor.xul)
+    content/browser/devtools/netmonitor.css           (netmonitor/netmonitor.css)
+    content/browser/devtools/netmonitor-controller.js (netmonitor/netmonitor-controller.js)
+    content/browser/devtools/netmonitor-view.js       (netmonitor/netmonitor-view.js)
     content/browser/NetworkPanel.xhtml            (webconsole/NetworkPanel.xhtml)
     content/browser/devtools/webconsole.js        (webconsole/webconsole.js)
     content/browser/devtools/webconsole.xul       (webconsole/webconsole.xul)
 *   content/browser/scratchpad.xul                (scratchpad/scratchpad.xul)
     content/browser/scratchpad.js                 (scratchpad/scratchpad.js)
     content/browser/splitview.css                 (shared/splitview.css)
     content/browser/devtools/theme-switching.js   (shared/theme-switching.js)
     content/browser/styleeditor.xul               (styleeditor/styleeditor.xul)
--- a/browser/devtools/moz.build
+++ b/browser/devtools/moz.build
@@ -10,15 +10,16 @@ DIRS += [
     'webconsole',
     'commandline',
     'sourceeditor',
     'styleeditor',
     'styleinspector',
     'tilt',
     'scratchpad',
     'debugger',
+    'netmonitor',
     'layoutview',
     'shared',
     'responsivedesign',
     'framework',
     'profiler',
     'fontinspector',
 ]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/Makefile.in
@@ -0,0 +1,15 @@
+# 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/.
+
+DEPTH     = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir    = @srcdir@
+VPATH     = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+	$(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/NetMonitorPanel.jsm
@@ -0,0 +1,66 @@
+/* -*- 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+this.EXPORTED_SYMBOLS = ["NetMonitorPanel"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/EventEmitter.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+  "resource://gre/modules/commonjs/sdk/core/promise.js");
+
+function NetMonitorPanel(iframeWindow, toolbox) {
+  this.panelWin = iframeWindow;
+  this._toolbox = toolbox;
+
+  this._view = this.panelWin.NetMonitorView;
+  this._controller = this.panelWin.NetMonitorController;
+  this._controller._target = this.target;
+
+  EventEmitter.decorate(this);
+}
+
+NetMonitorPanel.prototype = {
+  /**
+   * Open is effectively an asynchronous constructor.
+   *
+   * @return object
+   *         A Promise that is resolved when the NetMonitor completes opening.
+   */
+  open: function NetMonitorPanel_open() {
+    let promise;
+
+    // Local monitoring needs to make the target remote.
+    if (!this.target.isRemote) {
+      promise = this.target.makeRemote();
+    } else {
+      promise = Promise.resolve(this.target);
+    }
+
+    return promise
+      .then(() => this._controller.startupNetMonitor())
+      .then(() => this._controller.connect())
+      .then(() => {
+        this.isReady = true;
+        this.emit("ready");
+        return this;
+      })
+      .then(null, function onError(aReason) {
+        Cu.reportError("NetMonitorPanel open failed. " +
+                       reason.error + ": " + reason.message);
+      });
+  },
+
+  // DevToolPanel API
+  get target() this._toolbox.target,
+
+  destroy: function() {
+    this._controller.shutdownNetMonitor().then(() => this.emit("destroyed"));
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+TEST_DIRS += ['test']
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -0,0 +1,514 @@
+/* -*- 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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource:///modules/source-editor.jsm");
+Cu.import("resource:///modules/devtools/EventEmitter.jsm");
+Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
+Cu.import("resource:///modules/devtools/VariablesView.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
+  "resource://gre/modules/devtools/NetworkHelper.jsm");
+
+const NET_STRINGS_URI = "chrome://browser/locale/devtools/netmonitor.properties";
+const LISTENERS = [ "NetworkActivity" ];
+const NET_PREFS = { "NetworkMonitor.saveRequestAndResponseBodies": true };
+
+/**
+ * Object defining the network monitor controller components.
+ */
+let NetMonitorController = {
+  /**
+   * Initializes the view.
+   *
+   * @return object
+   *         A promise that is resolved when the monitor finishes startup.
+   */
+  startupNetMonitor: function() {
+    if (this._isInitialized) {
+      return this._startup.promise;
+    }
+    this._isInitialized = true;
+
+    let deferred = this._startup = Promise.defer();
+
+    NetMonitorView.initialize(() => {
+      NetMonitorView._isInitialized = true;
+      deferred.resolve();
+    });
+
+    return deferred.promise;
+  },
+
+  /**
+   * Destroys the view and disconnects the monitor client from the server.
+   *
+   * @return object
+   *         A promise that is resolved when the monitor finishes shutdown.
+   */
+  shutdownNetMonitor: function() {
+    if (this._isDestroyed) {
+      return this._shutdown.promise;
+    }
+    this._isDestroyed = true;
+    this._startup = null;
+
+    let deferred = this._shutdown = Promise.defer();
+
+    NetMonitorView.destroy(() => {
+      NetMonitorView._isDestroyed = true;
+      this.TargetEventsHandler.disconnect();
+      this.NetworkEventsHandler.disconnect();
+
+      this.disconnect();
+      deferred.resolve();
+    });
+
+    return deferred.promise;
+  },
+
+  /**
+   * Initiates remote or chrome network monitoring based on the current target,
+   * wiring event handlers as necessary.
+   *
+   * @return object
+   *         A promise that is resolved when the monitor finishes connecting.
+   */
+  connect: function() {
+    if (this._connection) {
+      return this._connection.promise;
+    }
+
+    let deferred = this._connection = Promise.defer();
+
+    let target = this._target;
+    let { client, form } = target;
+    if (target.chrome) {
+      this._startChromeMonitoring(client, form.consoleActor, deferred.resolve);
+    } else {
+      this._startMonitoringTab(client, form, deferred.resolve);
+    }
+
+    return deferred.promise;
+  },
+
+  /**
+   * Disconnects the debugger client and removes event handlers as necessary.
+   */
+  disconnect: function() {
+    // When debugging local or a remote instance, the connection is closed by
+    // the RemoteTarget.
+    this._connection = null;
+    this.client = null;
+    this.tabClient = null;
+    this.webConsoleClient = null;
+  },
+
+  /**
+   * Sets up a monitoring session.
+   *
+   * @param DebuggerClient aClient
+   *        The debugger client.
+   * @param object aTabGrip
+   *        The remote protocol grip of the tab.
+   * @param function aCallback
+   *        A function to invoke once the client attached to the console client.
+   */
+  _startMonitoringTab: function(aClient, aTabGrip, aCallback) {
+    if (!aClient) {
+      Cu.reportError("No client found!");
+      return;
+    }
+    this.client = aClient;
+
+    aClient.attachTab(aTabGrip.actor, (aResponse, aTabClient) => {
+      if (!aTabClient) {
+        Cu.reportError("No tab client found!");
+        return;
+      }
+      this.tabClient = aTabClient;
+
+      aClient.attachConsole(aTabGrip.consoleActor, LISTENERS, (aResponse, aWebConsoleClient) => {
+        if (!aWebConsoleClient) {
+          Cu.reportError("Couldn't attach to console: " + aResponse.error);
+          return;
+        }
+        this.webConsoleClient = aWebConsoleClient;
+        this.webConsoleClient.setPreferences(NET_PREFS, () => {
+          this.TargetEventsHandler.connect();
+          this.NetworkEventsHandler.connect();
+
+          if (aCallback) {
+            aCallback();
+          }
+        });
+      });
+    });
+  },
+
+  /**
+   * Sets up a chrome monitoring session.
+   *
+   * @param DebuggerClient aClient
+   *        The debugger client.
+   * @param object aConsoleActor
+   *        The remote protocol grip of the chrome debugger.
+   * @param function aCallback
+   *        A function to invoke once the client attached to the console client.
+   */
+  _startChromeMonitoring: function(aClient, aConsoleActor, aCallback) {
+    if (!aClient) {
+      Cu.reportError("No client found!");
+      return;
+    }
+    this.client = aClient;
+
+    aClient.attachConsole(aConsoleActor, LISTENERS, (aResponse, aWebConsoleClient) => {
+      if (!aWebConsoleClient) {
+        Cu.reportError("Couldn't attach to console: " + aResponse.error);
+        return;
+      }
+      this.webConsoleClient = aWebConsoleClient;
+      this.webConsoleClient.setPreferences(NET_PREFS, () => {
+        this.TargetEventsHandler.connect();
+        this.NetworkEventsHandler.connect();
+
+        if (aCallback) {
+          aCallback();
+        }
+      });
+    });
+  },
+
+  _isInitialized: false,
+  _isDestroyed: false,
+  _startup: null,
+  _shutdown: null,
+  _connection: null,
+  client: null,
+  tabClient: null,
+  webConsoleClient: null
+};
+
+/**
+ * Functions handling target-related lifetime events.
+ */
+function TargetEventsHandler() {
+  this._onTabNavigated = this._onTabNavigated.bind(this);
+  this._onTabDetached = this._onTabDetached.bind(this);
+}
+
+TargetEventsHandler.prototype = {
+  get target() NetMonitorController._target,
+  get webConsoleClient() NetMonitorController.webConsoleClient,
+
+  /**
+   * Listen for events emitted by the current tab target.
+   */
+  connect: function() {
+    dumpn("TargetEventsHandler is connecting...");
+    this.target.on("close", this._onTabDetached);
+    this.target.on("navigate", this._onTabNavigated);
+    this.target.on("will-navigate", this._onTabNavigated);
+  },
+
+  /**
+   * Remove events emitted by the current tab target.
+   */
+  disconnect: function() {
+    if (!this.target) {
+      return;
+    }
+    dumpn("TargetEventsHandler is disconnecting...");
+    this.target.off("close", this._onTabDetached);
+    this.target.off("navigate", this._onTabNavigated);
+    this.target.off("will-navigate", this._onTabNavigated);
+  },
+
+  /**
+   * Called for each location change in the monitored tab.
+   *
+   * @param string aType
+   *        Packet type.
+   * @param object aPacket
+   *        Packet received from the server.
+   */
+  _onTabNavigated: function(aType, aPacket) {
+    if (aType == "will-navigate") {
+      NetMonitorView.RequestsMenu.reset();
+      NetMonitorView.NetworkDetails.toggle(false);
+      window.emit("NetMonitor:TargetWillNavigate");
+    }
+    if (aType == "navigate") {
+      window.emit("NetMonitor:TargetNavigate");
+    }
+  },
+
+  /**
+   * Called when the monitored tab is closed.
+   */
+  _onTabDetached: function() {
+    NetMonitorController.shutdownNetMonitor();
+  }
+};
+
+/**
+ * Functions handling target network events.
+ */
+function NetworkEventsHandler() {
+  this._onNetworkEvent = this._onNetworkEvent.bind(this);
+  this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
+  this._onRequestHeaders = this._onRequestHeaders.bind(this);
+  this._onRequestCookies = this._onRequestCookies.bind(this);
+  this._onRequestPostData = this._onRequestPostData.bind(this);
+  this._onResponseHeaders = this._onResponseHeaders.bind(this);
+  this._onResponseCookies = this._onResponseCookies.bind(this);
+  this._onResponseContent = this._onResponseContent.bind(this);
+  this._onEventTimings = this._onEventTimings.bind(this);
+}
+
+NetworkEventsHandler.prototype = {
+  get client() NetMonitorController._target.client,
+  get webConsoleClient() NetMonitorController.webConsoleClient,
+
+  /**
+   * Connect to the current target client.
+   */
+  connect: function() {
+    dumpn("NetworkEventsHandler is connecting...");
+    this.client.addListener("networkEvent", this._onNetworkEvent);
+    this.client.addListener("networkEventUpdate", this._onNetworkEventUpdate);
+  },
+
+  /**
+   * Disconnect from the client.
+   */
+  disconnect: function() {
+    if (!this.client) {
+      return;
+    }
+    dumpn("NetworkEventsHandler is disconnecting...");
+    this.client.removeListener("networkEvent", this._onNetworkEvent);
+    this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate);
+  },
+
+  /**
+   * The "networkEvent" message type handler.
+   *
+   * @param string aType
+   *        Message type.
+   * @param object aPacket
+   *        The message received from the server.
+   */
+  _onNetworkEvent: function(aType, aPacket) {
+    let { actor, startedDateTime, method, url } = aPacket.eventActor;
+    NetMonitorView.RequestsMenu.addRequest(actor, startedDateTime, method, url);
+
+    window.emit("NetMonitor:NetworkEvent");
+  },
+
+  /**
+   * The "networkEventUpdate" message type handler.
+   *
+   * @param string aType
+   *        Message type.
+   * @param object aPacket
+   *        The message received from the server.
+   */
+  _onNetworkEventUpdate: function(aType, aPacket) {
+    let actor = aPacket.from;
+
+    switch (aPacket.updateType) {
+      case "requestHeaders":
+        this.webConsoleClient.getRequestHeaders(actor, this._onRequestHeaders);
+        window.emit("NetMonitor:NetworkEventUpdating:RequestHeaders");
+        break;
+      case "requestCookies":
+        this.webConsoleClient.getRequestCookies(actor, this._onRequestCookies);
+        window.emit("NetMonitor:NetworkEventUpdating:RequestCookies");
+        break;
+      case "requestPostData":
+        this.webConsoleClient.getRequestPostData(actor, this._onRequestPostData);
+        window.emit("NetMonitor:NetworkEventUpdating:RequestPostData");
+        break;
+      case "responseHeaders":
+        this.webConsoleClient.getResponseHeaders(actor, this._onResponseHeaders);
+        window.emit("NetMonitor:NetworkEventUpdating:ResponseHeaders");
+        break;
+      case "responseCookies":
+        this.webConsoleClient.getResponseCookies(actor, this._onResponseCookies);
+        window.emit("NetMonitor:NetworkEventUpdating:ResponseCookies");
+        break;
+      case "responseStart":
+        NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
+          httpVersion: aPacket.response.httpVersion,
+          status: aPacket.response.status,
+          statusText: aPacket.response.statusText,
+          headersSize: aPacket.response.headersSize
+        });
+        window.emit("NetMonitor:NetworkEventUpdating:ResponseStart");
+        break;
+      case "responseContent":
+        NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
+          contentSize: aPacket.contentSize,
+          mimeType: aPacket.mimeType
+        });
+        this.webConsoleClient.getResponseContent(actor, this._onResponseContent);
+        window.emit("NetMonitor:NetworkEventUpdating:ResponseContent");
+        break;
+      case "eventTimings":
+        NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
+          totalTime: aPacket.totalTime
+        });
+        this.webConsoleClient.getEventTimings(actor, this._onEventTimings);
+        window.emit("NetMonitor:NetworkEventUpdating:EventTimings");
+        break;
+    }
+  },
+
+  /**
+   * Handles additional information received for a "requestHeaders" packet.
+   *
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _onRequestHeaders: function(aResponse) {
+    NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+      requestHeaders: aResponse
+    });
+    window.emit("NetMonitor:NetworkEventUpdated:RequestHeaders");
+  },
+
+  /**
+   * Handles additional information received for a "requestCookies" packet.
+   *
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _onRequestCookies: function(aResponse) {
+    NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+      requestCookies: aResponse
+    });
+    window.emit("NetMonitor:NetworkEventUpdated:RequestCookies");
+  },
+
+  /**
+   * Handles additional information received for a "requestPostData" packet.
+   *
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _onRequestPostData: function(aResponse) {
+    NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+      requestPostData: aResponse
+    });
+    window.emit("NetMonitor:NetworkEventUpdated:RequestPostData");
+  },
+
+  /**
+   * Handles additional information received for a "responseHeaders" packet.
+   *
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _onResponseHeaders: function(aResponse) {
+    NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+      responseHeaders: aResponse
+    });
+    window.emit("NetMonitor:NetworkEventUpdated:ResponseHeaders");
+  },
+
+  /**
+   * Handles additional information received for a "responseCookies" packet.
+   *
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _onResponseCookies: function(aResponse) {
+    NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+      responseCookies: aResponse
+    });
+    window.emit("NetMonitor:NetworkEventUpdated:ResponseCookies");
+  },
+
+  /**
+   * Handles additional information received for a "responseContent" packet.
+   *
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _onResponseContent: function(aResponse) {
+    NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+      responseContent: aResponse
+    });
+    window.emit("NetMonitor:NetworkEventUpdated:ResponseContent");
+  },
+
+  /**
+   * Handles additional information received for a "eventTimings" packet.
+   *
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _onEventTimings: function NEH__onEventTimings(aResponse) {
+    NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
+      eventTimings: aResponse
+    });
+    window.emit("NetMonitor:NetworkEventUpdated:EventTimings");
+  }
+};
+
+/**
+ * Localization convenience methods.
+ */
+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"]
+});
+
+/**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
+ * Preliminary setup for the NetMonitorController object.
+ */
+NetMonitorController.TargetEventsHandler = new TargetEventsHandler();
+NetMonitorController.NetworkEventsHandler = new NetworkEventsHandler();
+
+/**
+ * Export some properties to the global scope for easier access.
+ */
+Object.defineProperties(window, {
+  "create": {
+    get: function() ViewHelpers.create,
+  }
+});
+
+/**
+ * Helper method for debugging.
+ * @param string
+ */
+function dumpn(str) {
+  if (wantLogging) {
+    dump("NET-FRONTEND: " + str + "\n");
+  }
+}
+
+let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -0,0 +1,1154 @@
+/* -*- 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 EPSILON = 0.001;
+const REQUESTS_REFRESH_RATE = 50; // ms
+const REQUESTS_WATERFALL_SAFE_BOUNDS = 100; // px
+const REQUESTS_WATERFALL_BACKGROUND_PATTERN = [5, 250, 1000, 2000]; // ms
+const DEFAULT_HTTP_VERSION = "HTTP/1.1";
+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 = {
+  "/ecmascript": SourceEditor.MODES.JAVASCRIPT,
+  "/javascript": SourceEditor.MODES.JAVASCRIPT,
+  "/x-javascript": SourceEditor.MODES.JAVASCRIPT,
+  "/html": SourceEditor.MODES.HTML,
+  "/xhtml": SourceEditor.MODES.HTML,
+  "/xml": SourceEditor.MODES.HTML,
+  "/atom": SourceEditor.MODES.HTML,
+  "/soap": SourceEditor.MODES.HTML,
+  "/rdf": SourceEditor.MODES.HTML,
+  "/rss": SourceEditor.MODES.HTML,
+  "/css": SourceEditor.MODES.CSS
+};
+const DEFAULT_EDITOR_CONFIG = {
+  mode: SourceEditor.MODES.TEXT,
+  readOnly: true,
+  showLineNumbers: true
+};
+const GENERIC_VARIABLES_VIEW_SETTINGS = {
+  lazyEmpty: false,
+  lazyEmptyDelay: 10, // ms
+  searchEnabled: true,
+  descriptorTooltip: false,
+  editableValueTooltip: "",
+  editableNameTooltip: "",
+  preventDisableOnChage: true,
+  eval: () => {},
+  switch: () => {}
+};
+
+function $(aSelector, aTarget = document) aTarget.querySelector(aSelector);
+
+/**
+ * Object defining the network monitor view components.
+ */
+let NetMonitorView = {
+  /**
+   * Initializes the network monitor view.
+   *
+   * @param function aCallback
+   *        Called after the view finishes initializing.
+   */
+  initialize: function NV_initialize(aCallback) {
+    dumpn("Initializing the NetMonitorView");
+
+    this._initializePanes();
+
+    this.Toolbar.initialize();
+    this.RequestsMenu.initialize();
+    this.NetworkDetails.initialize();
+
+    aCallback();
+  },
+
+  /**
+   * Destroys the network monitor view.
+   *
+   * @param function aCallback
+   *        Called after the view finishes destroying.
+   */
+  destroy: function NV_destroy(aCallback) {
+    dumpn("Destroying the NetMonitorView");
+
+    this.Toolbar.destroy();
+    this.RequestsMenu.destroy();
+    this.NetworkDetails.destroy();
+
+    this._destroyPanes();
+
+    aCallback();
+  },
+
+  /**
+   * Initializes the UI for all the displayed panes.
+   */
+  _initializePanes: function DV__initializePanes() {
+    dumpn("Initializing the NetMonitorView panes");
+
+    this._detailsPane = $("#details-pane");
+    this._detailsPaneToggleButton = $("#details-pane-toggle");
+
+    this._collapsePaneString = L10N.getStr("collapseDetailsPane");
+    this._expandPaneString = L10N.getStr("expandDetailsPane");
+
+    this._detailsPane.setAttribute("width", Prefs.networkDetailsWidth);
+    this.toggleDetailsPane({ visible: false });
+  },
+
+  /**
+   * Destroys the UI for all the displayed panes.
+   */
+  _destroyPanes: function DV__destroyPanes() {
+    dumpn("Destroying the NetMonitorView panes");
+
+    Prefs.networkDetailsWidth = this._detailsPane.getAttribute("width");
+
+    this._detailsPane = null;
+    this._detailsPaneToggleButton = null;
+  },
+
+  /**
+   * Gets the visibility state of the network details pane.
+   * @return boolean
+   */
+  get detailsPaneHidden()
+    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
+   *        - delayed: true to wait a few cycles before toggle
+   *        - callback: a function to invoke when the toggle finishes
+   * @param number aTabIndex [optional]
+   *        The index of the intended selected tab in the details pane.
+   */
+  toggleDetailsPane: function DV__toggleDetailsPane(aFlags, aTabIndex) {
+    let pane = this._detailsPane;
+    let button = this._detailsPaneToggleButton;
+
+    ViewHelpers.togglePane(aFlags, pane);
+
+    if (aFlags.visible) {
+      button.removeAttribute("pane-collapsed");
+      button.setAttribute("tooltiptext", this._collapsePaneString);
+    } else {
+      button.setAttribute("pane-collapsed", "");
+      button.setAttribute("tooltiptext", this._expandPaneString);
+    }
+
+    if (aTabIndex !== undefined) {
+      $("#details-pane").selectedIndex = aTabIndex;
+    }
+  },
+  /**
+   * Lazily initializes and returns a promise for a SourceEditor 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 NV_editor(aId) {
+    dumpn("Getting a NetMonitorView editor: " + aId);
+
+    if (this._editorPromises.has(aId)) {
+      return this._editorPromises.get(aId);
+    }
+
+    let deferred = Promise.defer();
+    this._editorPromises.set(aId, deferred.promise);
+
+    // Initialize the source editor and store the newly created instance
+    // in the ether of a resolved promise's value.
+    new SourceEditor().init($(aId), DEFAULT_EDITOR_CONFIG, deferred.resolve);
+
+    return deferred.promise;
+  },
+
+  _editorPromises: new Map(),
+  _collapsePaneString: "",
+  _expandPaneString: "",
+  _isInitialized: false,
+  _isDestroyed: false
+};
+
+/**
+ * Functions handling the toolbar view: expand/collapse button etc.
+ */
+function ToolbarView() {
+  dumpn("ToolbarView was instantiated");
+
+  this._onTogglePanesPressed = this._onTogglePanesPressed.bind(this);
+}
+
+ToolbarView.prototype = {
+  /**
+   * Initialization function, called when the debugger is started.
+   */
+  initialize: function NVT_initialize() {
+    dumpn("Initializing the ToolbarView");
+
+    this._detailsPaneToggleButton = $("#details-pane-toggle");
+    this._detailsPaneToggleButton.addEventListener("mousedown", this._onTogglePanesPressed, false);
+  },
+
+  /**
+   * Destruction function, called when the debugger is closed.
+   */
+  destroy: function NVT_destroy() {
+    dumpn("Destroying the ToolbarView");
+
+    this._detailsPaneToggleButton.removeEventListener("mousedown", this._onTogglePanesPressed, false);
+  },
+
+  /**
+   * Listener handling the toggle button click event.
+   */
+  _onTogglePanesPressed: function NVT__onTogglePanesPressed() {
+    let requestsMenu = NetMonitorView.RequestsMenu;
+    let networkDetails = NetMonitorView.NetworkDetails;
+
+    // Make sure there's a selection if the button is pressed, to avoid
+    // showing an empty network details pane.
+    if (!requestsMenu.selectedItem && requestsMenu.itemCount) {
+      requestsMenu.selectedIndex = 0;
+    }
+    // Proceed with toggling the network details pane normally.
+    else {
+      networkDetails.toggle(NetMonitorView.detailsPaneHidden);
+    }
+  },
+
+  _detailsPaneToggleButton: null
+};
+
+/**
+ * Functions handling the requests menu (containing details about each request,
+ * like status, method, file, domain, as well as a waterfall representing
+ * timing imformation).
+ */
+function RequestsMenuView() {
+  dumpn("RequestsMenuView was instantiated");
+
+  this._cache = new Map(); // Can't use a WeakMap because keys are strings.
+  this._flushRequests = this._flushRequests.bind(this);
+  this._onRequestItemRemoved = this._onRequestItemRemoved.bind(this);
+  this._onMouseDown = this._onMouseDown.bind(this);
+  this._onSelect = this._onSelect.bind(this);
+  this._onResize = this._onResize.bind(this);
+}
+
+create({ constructor: RequestsMenuView, proto: MenuContainer.prototype }, {
+  /**
+   * Initialization function, called when the network monitor is started.
+   */
+  initialize: function NVRM_initialize() {
+    dumpn("Initializing the RequestsMenuView");
+
+    this.node = new SideMenuWidget($("#requests-menu-contents"));
+
+    this.node.addEventListener("mousedown", this._onMouseDown, false);
+    this.node.addEventListener("select", this._onSelect, false);
+    window.addEventListener("resize", this._onResize, false);
+  },
+
+  /**
+   * Destruction function, called when the network monitor is closed.
+   */
+  destroy: function NVRM_destroy() {
+    dumpn("Destroying the SourcesView");
+
+    this.node.removeEventListener("mousedown", this._onMouseDown, false);
+    this.node.removeEventListener("select", this._onSelect, false);
+    window.removeEventListener("resize", this._onResize, false);
+  },
+
+  /**
+   * Resets this container (removes all the networking information).
+   */
+  reset: function NVRM_reset() {
+    this.empty();
+    this._firstRequestStartedMillis = -1;
+    this._lastRequestEndedMillis = -1;
+  },
+
+  /**
+   * Specifies if this view may be updated lazily.
+   */
+  lazyUpdate: true,
+
+  /**
+   * Adds a network request to this container.
+   *
+   * @param string aId
+   *        An identifier coming from the network monitor controller.
+   * @param string aStartedDateTime
+   *        A string representation of when the request was started, which
+   *        can be parsed by Date (for example "2012-09-17T19:50:03.699Z").
+   * @param string aMethod
+   *        Specifies the request method (e.g. "GET", "POST", etc.)
+   * @param string aUrl
+   *        Specifies the request's url.
+   */
+  addRequest: function NVRM_addRequest(aId, aStartedDateTime, aMethod, aUrl) {
+    // Convert the received date/time string to a unix timestamp.
+    let unixTime = Date.parse(aStartedDateTime);
+
+    // Create the element node for the network request item.
+    let menuView = this._createMenuView(aMethod, aUrl);
+
+    // Remember the first and last event boundaries.
+    this._registerFirstRequestStart(unixTime);
+    this._registerLastRequestEnd(unixTime);
+
+    // Append a network request item to this container.
+    let requestItem = this.push(menuView, {
+      relaxed: true, /* this container should allow dupes & degenerates */
+      attachment: {
+        id: aId,
+        startedDeltaMillis: unixTime - this._firstRequestStartedMillis,
+        startedMillis: unixTime,
+        method: aMethod,
+        url: aUrl
+      },
+      finalize: this._onRequestItemRemoved
+    });
+
+    $(".requests-menu-empty-notice").hidden = true;
+    this._cache.set(aId, requestItem);
+  },
+
+  /**
+   * Schedules adding additional information to a network request.
+   *
+   * @param string aId
+   *        An identifier coming from the network monitor controller.
+   * @param object aData
+   *        An object containing several { key: value } tuples of network info.
+   *        Supported keys are "httpVersion", "status", "statusText" etc.
+   */
+  updateRequest: function NVRM_updateRequest(aId, aData) {
+    // Prevent interference from zombie updates received after target closed.
+    if (NetMonitorView._isDestroyed) {
+      return;
+    }
+    this._updateQueue.push([aId, aData]);
+
+    // Lazy updating is disabled in some tests.
+    if (!this.lazyUpdate) {
+      return void this._flushRequests();
+    }
+    window.clearTimeout(this._updateTimeout);
+    this._updateTimeout = window.setTimeout(this._flushRequests, REQUESTS_REFRESH_RATE);
+  },
+
+  /**
+   * Starts adding all queued additional information about network requests.
+   */
+  _flushRequests: function NVRM__flushRequests() {
+    // For each queued additional information packet, get the corresponding
+    // request item in the view and update it based on the specified data.
+    for (let [id, data] of this._updateQueue) {
+      let requestItem = this._cache.get(id);
+      if (!requestItem) {
+        // Packet corresponds to a dead request item, target navigated.
+        continue;
+      }
+
+      // Each information packet may contain several { key: value } tuples of
+      // network info, so update the view based on each one.
+      for (let key in data) {
+        let value = data[key];
+        if (value === undefined) {
+          // The information in the packet is empty, it can be safely ignored.
+          continue;
+        }
+
+        switch (key) {
+          case "requestHeaders":
+            requestItem.attachment.requestHeaders = value;
+            break;
+          case "requestCookies":
+            requestItem.attachment.requestCookies = value;
+            break;
+          case "requestPostData":
+            requestItem.attachment.requestPostData = value;
+            break;
+          case "responseHeaders":
+            requestItem.attachment.responseHeaders = value;
+            break;
+          case "responseCookies":
+            requestItem.attachment.responseCookies = value;
+            break;
+          case "httpVersion":
+            requestItem.attachment.httpVersion = value;
+            break;
+          case "status":
+            requestItem.attachment.status = value;
+            this._updateMenuView(requestItem, key, value);
+            break;
+          case "statusText":
+            requestItem.attachment.statusText = value;
+            break;
+          case "headersSize":
+            requestItem.attachment.headersSize = value;
+            break;
+          case "contentSize":
+            requestItem.attachment.contentSize = value;
+            this._updateMenuView(requestItem, key, value);
+            break;
+          case "mimeType":
+            requestItem.attachment.mimeType = value;
+            this._updateMenuView(requestItem, key, value);
+            break;
+          case "responseContent":
+            requestItem.attachment.responseContent = value;
+            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":
+            requestItem.attachment.eventTimings = value;
+            this._createWaterfallView(requestItem, value.timings);
+            break;
+        }
+      }
+      // This update may have additional information about a request which
+      // isn't shown yet in the network details pane.
+      let selectedItem = this.selectedItem;
+      if (selectedItem && selectedItem.attachment.id == id) {
+        NetMonitorView.NetworkDetails.populate(selectedItem.attachment);
+      }
+    }
+    // We're done flushing all the requests, clear the update queue.
+    this._updateQueue = [];
+  },
+
+  /**
+   * Customization function for creating an item's UI.
+   *
+   * @param string aMethod
+   *        Specifies the request method (e.g. "GET", "POST", etc.)
+   * @param string aUrl
+   *        Specifies the request's url.
+   * @return nsIDOMNode
+   *         The network request view.
+   */
+  _createMenuView: function NVRM__createMenuView(aMethod, aUrl) {
+    let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
+    let name = NetworkHelper.convertToUnicode(unescape(uri.fileName)) || "/";
+    let query = NetworkHelper.convertToUnicode(unescape(uri.query));
+    let hostPort = NetworkHelper.convertToUnicode(unescape(uri.hostPort));
+
+    let template = $("#requests-menu-item-template");
+    let requestsMenuItem = template.firstChild.cloneNode(true);
+
+    $(".requests-menu-method", requestsMenuItem)
+      .setAttribute("value", aMethod);
+
+    $(".requests-menu-file", requestsMenuItem)
+      .setAttribute("value", name + (query ? "?" + query : ""));
+
+    $(".requests-menu-domain", requestsMenuItem)
+      .setAttribute("value", hostPort);
+
+    return requestsMenuItem;
+  },
+
+  /**
+   * Updates the information displayed in a network request item view.
+   *
+   * @param MenuItem aItem
+   *        The network request item in this container.
+   * @param string aKey
+   *        The type of information that is to be updated.
+   * @param any aValue
+   *        The new value to be shown.
+   */
+  _updateMenuView: function NVRM__updateMenuView(aItem, aKey, aValue) {
+    switch (aKey) {
+      case "status": {
+        let node = $(".requests-menu-status", aItem.target);
+        node.setAttribute("code", aValue);
+        break;
+      }
+      case "contentSize": {
+        let size = (aValue / 1024).toFixed(CONTENT_SIZE_DECIMALS);
+        let node = $(".requests-menu-size", aItem.target);
+        node.setAttribute("value", L10N.getFormatStr("networkMenu.size", size));
+        break;
+      }
+      case "mimeType": {
+        let type = aValue.split(";")[0].split("/")[1] || "?";
+        let node = $(".requests-menu-type", aItem.target);
+        node.setAttribute("value", CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type);
+        break;
+      }
+      case "totalTime": {
+        let node = $(".requests-menu-timings-total", aItem.target);
+        node.setAttribute("value", L10N.getFormatStr("networkMenu.total", aValue));
+        break;
+      }
+    }
+  },
+
+  /**
+   * Creates a waterfall representing timing information in a network request item view.
+   *
+   * @param MenuItem aItem
+   *        The network request item in this container.
+   * @param object aTimings
+   *        An object containing timing information.
+   */
+  _createWaterfallView: function NVRM__createWaterfallView(aItem, aTimings) {
+    let { target, attachment } = aItem;
+    let sections = ["blocked", "dns", "connect", "send", "wait", "receive"];
+
+    let timingsNode = $(".requests-menu-timings", target);
+    let startCapNode = $(".requests-menu-timings-cap.start", timingsNode);
+    let endCapNode = $(".requests-menu-timings-cap.end", timingsNode);
+    let firstBox;
+
+    // Add a set of boxes representing timing information.
+    for (let key of sections) {
+      let width = aTimings[key];
+
+      // Don't render anything if it surely won't be visible.
+      // One millisecond == one unscaled pixel.
+      if (width > 0) {
+        let timingBox = document.createElement("hbox");
+        timingBox.className = "requests-menu-timings-box " + key;
+        timingBox.setAttribute("width", width);
+        timingsNode.insertBefore(timingBox, endCapNode);
+
+        // Make the start cap inherit the aspect of the first timing box.
+        if (!firstBox) {
+          firstBox = timingBox;
+          startCapNode.classList.add(key);
+        }
+        // Same goes for the end cap, inherit the aspect of the last timing box.
+        endCapNode.classList.add(key);
+      }
+    }
+
+    // Since at least one timing box should've been rendered, unhide the
+    // start and end timing cap nodes.
+    startCapNode.hidden = false;
+    endCapNode.hidden = false;
+
+    // Rescale all the waterfalls so that everything is visible at once.
+    this._flushWaterfallViews();
+  },
+
+  /**
+   * Rescales and redraws all the waterfalls in this container.
+   *
+   * @param boolean aReset
+   *        True if this container's width was changed.
+   */
+  _flushWaterfallViews: function NVRM__flushWaterfallViews(aReset) {
+    // To avoid expensive operations like getBoundingClientRect(), the
+    // waterfalls width is cached. However, in certain scenarios like when
+    // the window is resized, this needs to be invalidated.
+    if (aReset) {
+      this._cachedWaterfallWidth = 0;
+    }
+
+    // Determine the scaling to be applied to all the waterfalls so that
+    // everything is visible at once. One millisecond == one unscaled pixel.
+    let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
+    let longestWidth = this._lastRequestEndedMillis - this._firstRequestStartedMillis;
+    let scale = Math.min(Math.max(availableWidth / longestWidth, EPSILON), 1);
+
+    // Apply CSS transforms to each waterfall in this container totalTime
+    // accurately translate and resize as needed.
+    for (let [, { target, attachment }] of this._cache) {
+      let timingsNode = $(".requests-menu-timings", target);
+      let startCapNode = $(".requests-menu-timings-cap.start", target);
+      let endCapNode = $(".requests-menu-timings-cap.end", target);
+      let totalNode = $(".requests-menu-timings-total", target);
+
+      // Render the timing information at a specific horizontal translation
+      // based on the delta to the first monitored event network.
+      let translateX = "translateX(" + attachment.startedDeltaMillis + "px)";
+
+      // Based on the total time passed until the last request, rescale
+      // all the waterfalls to a reasonable size.
+      let scaleX = "scaleX(" + scale + ")";
+
+      // Certain nodes should not be scaled, even if they're children of
+      // another scaled node. In this case, apply a reversed transformation.
+      let revScaleX = "scaleX(" + (1 / scale) + ")";
+
+      timingsNode.style.transform = scaleX + " " + translateX;
+      startCapNode.style.transform = revScaleX + " translateX(0.5px)";
+      endCapNode.style.transform = revScaleX + " translateX(-0.5px)";
+      totalNode.style.transform = revScaleX;
+    }
+  },
+
+  /**
+   * Function called each time a network request item is removed.
+   *
+   * @param MenuItem aItem
+   *        The corresponding menu item.
+   */
+  _onRequestItemRemoved: function NVRM__onRequestItemRemoved(aItem) {
+    dumpn("Finalizing network request item: " + aItem);
+    this._cache.delete(aItem.attachment.id);
+  },
+
+  /**
+   * The mouse down listener for this container.
+   */
+  _onMouseDown: function NVRM__onMouseDown(e) {
+    let item = this.getItemForElement(e.target);
+    if (item) {
+      // The container is not empty and we clicked on an actual item.
+      this.selectedItem = item;
+    }
+  },
+
+  /**
+   * The selection listener for this container.
+   */
+  _onSelect: function NVRM__onSelect(e) {
+    NetMonitorView.NetworkDetails.populate(this.selectedItem.attachment);
+    NetMonitorView.NetworkDetails.toggle(true);
+  },
+
+  /**
+   * The resize listener for this container's window.
+   */
+  _onResize: function NVRM__onResize(e) {
+    this._flushWaterfallViews(true);
+  },
+
+  /**
+   * 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.
+   */
+  _registerFirstRequestStart: function NVRM__registerFirstRequestStart(aUnixTime) {
+    if (this._firstRequestStartedMillis == -1) {
+      this._firstRequestStartedMillis = aUnixTime;
+    }
+  },
+
+  /**
+   * Checks if the specified unix time is the last one to be known of,
+   * and saves it if so.
+   *
+   * @param number aUnixTime
+   *        The milliseconds to check and save.
+   */
+  _registerLastRequestEnd: function NVRM__registerLastRequestEnd(aUnixTime) {
+    if (this._lastRequestEndedMillis < aUnixTime) {
+      this._lastRequestEndedMillis = aUnixTime;
+    }
+  },
+
+  /**
+   * Gets the available waterfall width in this container.
+   * @return number
+   */
+  get _waterfallWidth() {
+    if (this._cachedWaterfallWidth == 0) {
+      let container = $("#requests-menu-toolbar");
+      let waterfall = $("#requests-menu-waterfall-label");
+      let containerBounds = container.getBoundingClientRect();
+      let waterfallBounds = waterfall.getBoundingClientRect();
+      this._cachedWaterfallWidth = containerBounds.width - waterfallBounds.left;
+    }
+    return this._cachedWaterfallWidth;
+  },
+
+  _cache: null,
+  _cachedWaterfallWidth: 0,
+  _firstRequestStartedMillis: -1,
+  _lastRequestEndedMillis: -1,
+  _updateQueue: [],
+  _updateTimeout: null
+});
+
+/**
+ * Functions handling the requests details view.
+ */
+function NetworkDetailsView() {
+  dumpn("NetworkDetailsView was instantiated");
+};
+
+create({ constructor: NetworkDetailsView, proto: MenuContainer.prototype }, {
+  /**
+   * Initialization function, called when the network monitor is started.
+   */
+  initialize: function NVND_initialize() {
+    dumpn("Initializing the RequestsMenuView");
+
+    this.node = $("#details-pane");
+
+    this._headers = new VariablesView($("#all-headers"),
+      Object.create(GENERIC_VARIABLES_VIEW_SETTINGS, {
+        emptyText: { value: L10N.getStr("headersEmptyText"), enumerable: true },
+        searchPlaceholder: { value: L10N.getStr("headersFilterText"), enumerable: true }
+      }));
+    this._cookies = new VariablesView($("#all-cookies"),
+      Object.create(GENERIC_VARIABLES_VIEW_SETTINGS, {
+        emptyText: { value: L10N.getStr("cookiesEmptyText"), enumerable: true },
+        searchPlaceholder: { value: L10N.getStr("cookiesFilterText"), enumerable: true }
+      }));
+    this._params = new VariablesView($("#request-params"),
+      Object.create(GENERIC_VARIABLES_VIEW_SETTINGS, {
+        emptyText: { value: L10N.getStr("paramsEmptyText"), enumerable: true },
+        searchPlaceholder: { value: L10N.getStr("paramsFilterText"), enumerable: true }
+      }));
+    this._json = new VariablesView($("#response-content-json"),
+      Object.create(GENERIC_VARIABLES_VIEW_SETTINGS, {
+        searchPlaceholder: { value: L10N.getStr("jsonFilterText"), enumerable: true }
+      }));
+
+    this._paramsQueryString = L10N.getStr("paramsQueryString");
+    this._paramsFormData = L10N.getStr("paramsFormData");
+    this._paramsPostPayload = L10N.getStr("paramsPostPayload");
+    this._requestHeaders = L10N.getStr("requestHeaders");
+    this._responseHeaders = L10N.getStr("responseHeaders");
+    this._requestCookies = L10N.getStr("requestCookies");
+    this._responseCookies = L10N.getStr("responseCookies");
+  },
+
+  /**
+   * Destruction function, called when the network monitor is closed.
+   */
+  destroy: function NVND_destroy() {
+    dumpn("Destroying the SourcesView");
+  },
+
+  /**
+   * Sets this view hidden or visible. It's visible by default.
+   *
+   * @param boolean aVisibleFlag
+   *        Specifies the intended visibility.
+   */
+  toggle: function NVND_toggle(aVisibleFlag) {
+    NetMonitorView.toggleDetailsPane({ visible: aVisibleFlag });
+    NetMonitorView.RequestsMenu._flushWaterfallViews(true);
+  },
+
+  /**
+   * Populates this view with the specified data.
+   *
+   * @param object aData
+   *        The data source (this should be the attachment of a request item).
+   */
+  populate: function NVND_populate(aData) {
+    $("#request-params-box").setAttribute("flex", "1");
+    $("#request-params-box").hidden = false;
+    $("#request-post-data-textarea-box").hidden = true;
+    $("#response-content-json-box").hidden = true;
+    $("#response-content-textarea-box").hidden = true;
+    $("#response-content-image-box").hidden = true;
+
+    this._headers.empty();
+    this._cookies.empty();
+    this._params.empty();
+    this._json.empty();
+
+    if (aData) {
+      this._setSummary(aData);
+      this._setResponseHeaders(aData.responseHeaders);
+      this._setRequestHeaders(aData.requestHeaders);
+      this._setResponseCookies(aData.responseCookies);
+      this._setRequestCookies(aData.requestCookies);
+      this._setRequestGetParams(aData.url);
+      this._setRequestPostParams(aData.requestHeaders, aData.requestPostData);
+      this._setResponseBody(aData.url, aData.responseContent);
+      this._setTimingsInformation(aData.eventTimings);
+    }
+  },
+
+  /**
+   * Sets the network request summary shown in this view.
+   *
+   * @param object aData
+   *        The data source (this should be the attachment of a request item).
+   */
+  _setSummary: function NVND__setSummary(aData) {
+    if (aData.url) {
+      let unicodeUrl = NetworkHelper.convertToUnicode(unescape(aData.url));
+      $("#headers-summary-url-value").setAttribute("value", unicodeUrl);
+      $("#headers-summary-url").removeAttribute("hidden");
+    } else {
+      $("#headers-summary-url").setAttribute("hidden", "true");
+    }
+
+    if (aData.method) {
+      $("#headers-summary-method-value").setAttribute("value", aData.method);
+      $("#headers-summary-method").removeAttribute("hidden");
+    } else {
+      $("#headers-summary-method").setAttribute("hidden", "true");
+    }
+
+    if (aData.status) {
+      $("#headers-summary-status-circle").setAttribute("code", aData.status);
+      $("#headers-summary-status-value").setAttribute("value", aData.status + " " + aData.statusText);
+      $("#headers-summary-status").removeAttribute("hidden");
+    } else {
+      $("#headers-summary-status").setAttribute("hidden", "true");
+    }
+
+    if (aData.httpVersion && aData.httpVersion != DEFAULT_HTTP_VERSION) {
+      $("#headers-summary-version-value").setAttribute("value", aData.httpVersion);
+      $("#headers-summary-version").removeAttribute("hidden");
+    } else {
+      $("#headers-summary-version").setAttribute("hidden", "true");
+    }
+  },
+
+  /**
+   * Sets the network request headers shown in this view.
+   *
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _setRequestHeaders: function NVND__setRequestHeaders(aResponse) {
+    if (aResponse && aResponse.headers.length) {
+      this._addHeaders(this._requestHeaders, aResponse);
+    }
+  },
+
+  /**
+   * Sets the network response headers shown in this view.
+   *
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _setResponseHeaders: function NVND__setResponseHeaders(aResponse) {
+    if (aResponse && aResponse.headers.length) {
+      aResponse.headers.sort((a, b) => a.name > b.name);
+      this._addHeaders(this._responseHeaders, aResponse);
+    }
+  },
+
+  /**
+   * Populates the headers container in this view with the specified data.
+   *
+   * @param string aName
+   *        The type of headers to populate (request or response).
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _addHeaders: function NVND__addHeaders(aName, aResponse) {
+    let kb = (aResponse.headersSize / 1024).toFixed(HEADERS_SIZE_DECIMALS);
+    let size = L10N.getFormatStr("networkMenu.size", kb);
+    let headersScope = this._headers.addScope(aName + " (" + size + ")");
+    headersScope.expanded = true;
+
+    for (let header of aResponse.headers) {
+      let headerVar = headersScope.addVar(header.name, { null: true }, true);
+      headerVar.setGrip(header.value);
+    }
+  },
+
+  /**
+   * Sets the network request cookies shown in this view.
+   *
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _setRequestCookies: function NVND__setRequestCookies(aResponse) {
+    if (aResponse && aResponse.cookies.length) {
+      aResponse.cookies.sort((a, b) => a.name > b.name);
+      this._addCookies(this._requestCookies, aResponse);
+    }
+  },
+
+  /**
+   * Sets the network response cookies shown in this view.
+   *
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _setResponseCookies: function NVND__setResponseCookies(aResponse) {
+    if (aResponse && aResponse.cookies.length) {
+      this._addCookies(this._responseCookies, aResponse);
+    }
+  },
+
+  /**
+   * Populates the cookies container in this view with the specified data.
+   *
+   * @param string aName
+   *        The type of cookies to populate (request or response).
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _addCookies: function NVND__addCookies(aName, aResponse) {
+    let cookiesScope = this._cookies.addScope(aName);
+    cookiesScope.expanded = true;
+
+    for (let cookie of aResponse.cookies) {
+      let cookieVar = cookiesScope.addVar(cookie.name, { null: true }, true);
+      cookieVar.setGrip(cookie.value);
+
+      // By default the cookie name and value are shown. If this is the only
+      // information available, then nothing else is to be displayed.
+      let cookieProps = Object.keys(cookie);
+      if (cookieProps.length == 2) {
+        continue;
+      }
+
+      // Display any other information other than the cookie name and value
+      // which may be available.
+      let rawObject = Object.create(null);
+      let otherProps = cookieProps.filter((e) => e != "name" && e != "value");
+      for (let prop of otherProps) {
+        rawObject[prop] = cookie[prop];
+      }
+      cookieVar.populate(rawObject);
+      cookieVar.twisty = true;
+      cookieVar.expanded = true;
+    }
+  },
+
+  /**
+   * Sets the network request get params shown in this view.
+   *
+   * @param string aUrl
+   *        The request's url.
+   */
+  _setRequestGetParams: function NVND__setRequestGetParams(aUrl) {
+    let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
+    let query = uri.query;
+    if (query) {
+      this._addParams(this._paramsQueryString, query.split("&").map((e) =>
+        let (param = e.split("=")) {
+          name: param[0],
+          value: NetworkHelper.convertToUnicode(unescape(param[1]))
+        }
+      ));
+    }
+  },
+
+  /**
+   * Sets the network request post params shown in this view.
+   *
+   * @param object aHeadersResponse
+   *        The "requestHeaders" message received from the server.
+   * @param object aPostResponse
+   *        The "requestPostData" message received from the server.
+   */
+  _setRequestPostParams: function NVND__setRequestPostParams(aHeadersResponse, aPostResponse) {
+    if (!aHeadersResponse || !aPostResponse) {
+      return;
+    }
+    let contentType = aHeadersResponse.headers.filter(({ name }) => name == "Content-Type")[0];
+    let text = aPostResponse.postData.text;
+
+    if (contentType.value.contains("x-www-form-urlencoded")) {
+      this._addParams(this._paramsFormData, text.replace(/^[?&]/, "").split("&").map((e) =>
+        let (param = e.split("=")) {
+          name: param[0],
+          value: NetworkHelper.convertToUnicode(unescape(param[1]))
+        }
+      ));
+    } else {
+      // This is really awkward, but hey, it works. Let's show an empty
+      // scope in the params view and place the source editor containing
+      // the raw post data directly underneath.
+      $("#request-params-box").removeAttribute("flex");
+      let paramsScope = this._params.addScope(this._paramsPostPayload);
+      paramsScope.expanded = true;
+      paramsScope.locked = true;
+
+      $("#request-post-data-textarea-box").hidden = false;
+      NetMonitorView.editor("#request-post-data-textarea").then((aEditor) => {
+        aEditor.setText(text);
+      });
+    }
+  },
+
+  /**
+   * Populates the params container in this view with the specified data.
+   *
+   * @param string aName
+   *        The type of params to populate (get or post).
+   * @param object aParams
+   *        An array containing { name: value } param information tuples.
+   */
+  _addParams: function NVND__addParams(aName, aParams) {
+    let paramsScope = this._params.addScope(aName);
+    paramsScope.expanded = true;
+
+    for (let param of aParams) {
+      let headerVar = paramsScope.addVar(param.name, { null: true }, true);
+      headerVar.setGrip(param.value);
+    }
+  },
+
+  /**
+   * Sets the network response body shown in this view.
+   *
+   * @param string aUrl
+   *        The request's url.
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _setResponseBody: function NVND__setresponseBody(aUrl, aResponse) {
+    if (!aResponse) {
+      return;
+    }
+    let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
+    let { mimeType: mime, text, encoding } = aResponse.content;
+
+    if (mime.contains("/json")) {
+      $("#response-content-json-box").hidden = false;
+      let jsonScope = this._json.addScope("JSON");
+      jsonScope.addVar().populate(JSON.parse(text), { expanded: true });
+      jsonScope.expanded = true;
+    }
+    else if (mime.contains("image/")) {
+      $("#response-content-image-box").setAttribute("align", "center");
+      $("#response-content-image-box").setAttribute("pack", "center");
+      $("#response-content-image-box").hidden = false;
+      $("#response-content-image").src = "data:" + mime + ";" + encoding + "," + text;
+
+      // Immediately display additional information about the image:
+      // file name, mime type and encoding.
+      $("#response-content-image-name-value").setAttribute("value", uri.fileName);
+      $("#response-content-image-mime-value").setAttribute("value", mime);
+      $("#response-content-image-encoding-value").setAttribute("value", encoding);
+
+      // Wait for the image to load in order to display the width and height.
+      $("#response-content-image").onload = (e) => {
+        // XUL images are majestic so they don't bother storing their dimensions
+        // in width and height attributes like the rest of the folk. Hack around
+        // this by getting the bounding client rect and subtracting the margins.
+        let { width, height } = e.target.getBoundingClientRect();
+        let dimensions = (width - 2) + " x " + (height - 2);
+        $("#response-content-image-dimensions-value").setAttribute("value", dimensions);
+      };
+    }
+    else {
+      $("#response-content-textarea-box").hidden = false;
+      NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+        aEditor.setMode(SourceEditor.MODES.TEXT);
+        aEditor.setText(typeof text == "string" ? text : text.initial);
+
+        // Maybe set a more appropriate mode in the Source Editor if possible.
+        for (let key in CONTENT_MIME_TYPE_MAPPINGS) {
+          if (mime.contains(key)) {
+            aEditor.setMode(CONTENT_MIME_TYPE_MAPPINGS[key]);
+            break;
+          }
+        }
+      });
+    }
+  },
+
+  /**
+   * Sets the timings information shown in this view.
+   *
+   * @param object aResponse
+   *        The message received from the server.
+   */
+  _setTimingsInformation: function NVND__setTimingsInformation(aResponse) {
+    if (!aResponse) {
+      return;
+    }
+    let { blocked, dns, connect, send, wait, receive } = aResponse.timings;
+
+    let tabboxWidth = $("#details-pane").getAttribute("width");
+    let availableWidth = tabboxWidth / 2; // Other nodes also take some space.
+    let scale = Math.max(availableWidth / aResponse.totalTime, 0);
+
+    $("#timings-summary-blocked .requests-menu-timings-box")
+      .setAttribute("width", blocked * scale);
+    $("#timings-summary-blocked .requests-menu-timings-total")
+      .setAttribute("value", L10N.getFormatStr("networkMenu.total", blocked));
+
+    $("#timings-summary-dns .requests-menu-timings-box")
+      .setAttribute("width", dns * scale);
+    $("#timings-summary-dns .requests-menu-timings-total")
+      .setAttribute("value", L10N.getFormatStr("networkMenu.total", dns));
+
+    $("#timings-summary-connect .requests-menu-timings-box")
+      .setAttribute("width", connect * scale);
+    $("#timings-summary-connect .requests-menu-timings-total")
+      .setAttribute("value", L10N.getFormatStr("networkMenu.total", connect));
+
+    $("#timings-summary-send .requests-menu-timings-box")
+      .setAttribute("width", send * scale);
+    $("#timings-summary-send .requests-menu-timings-total")
+      .setAttribute("value", L10N.getFormatStr("networkMenu.total", send));
+
+    $("#timings-summary-wait .requests-menu-timings-box")
+      .setAttribute("width", wait * scale);
+    $("#timings-summary-wait .requests-menu-timings-total")
+      .setAttribute("value", L10N.getFormatStr("networkMenu.total", wait));
+
+    $("#timings-summary-receive .requests-menu-timings-box")
+      .setAttribute("width", receive * scale);
+    $("#timings-summary-receive .requests-menu-timings-total")
+      .setAttribute("value", L10N.getFormatStr("networkMenu.total", receive));
+
+    $("#timings-summary-dns .requests-menu-timings-box")
+      .style.transform = "translateX(" + (scale * blocked) + "px)";
+    $("#timings-summary-connect .requests-menu-timings-box")
+      .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)";
+    $("#timings-summary-send .requests-menu-timings-box")
+      .style.transform = "translateX(" + (scale * (blocked + dns + connect)) + "px)";
+    $("#timings-summary-wait .requests-menu-timings-box")
+      .style.transform = "translateX(" + (scale * (blocked + dns + connect + send)) + "px)";
+    $("#timings-summary-receive .requests-menu-timings-box")
+      .style.transform = "translateX(" + (scale * (blocked + dns + connect + send + wait)) + "px)";
+
+    $("#timings-summary-dns .requests-menu-timings-total")
+      .style.transform = "translateX(" + (scale * blocked) + "px)";
+    $("#timings-summary-connect .requests-menu-timings-total")
+      .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)";
+    $("#timings-summary-send .requests-menu-timings-total")
+      .style.transform = "translateX(" + (scale * (blocked + dns + connect)) + "px)";
+    $("#timings-summary-wait .requests-menu-timings-total")
+      .style.transform = "translateX(" + (scale * (blocked + dns + connect + send)) + "px)";
+    $("#timings-summary-receive .requests-menu-timings-total")
+      .style.transform = "translateX(" + (scale * (blocked + dns + connect + send + wait)) + "px)";
+  },
+
+  _headers: null,
+  _cookies: null,
+  _params: null,
+  _json: null,
+  _paramsQueryString: "",
+  _paramsFormData: "",
+  _paramsPostPayload: "",
+  _requestHeaders: "",
+  _responseHeaders: "",
+  _requestCookies: "",
+  _responseCookies: ""
+});
+
+/**
+ * Preliminary setup for the NetMonitorView object.
+ */
+NetMonitorView.Toolbar = new ToolbarView();
+NetMonitorView.RequestsMenu = new RequestsMenuView();
+NetMonitorView.NetworkDetails = new NetworkDetailsView();
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/netmonitor.css
@@ -0,0 +1,8 @@
+/* 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/. */
+
+#response-content-image-box {
+  overflow: auto;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/netmonitor.xul
@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/netmonitor.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/netmonitor.css" type="text/css"?>
+<!DOCTYPE window [
+  <!ENTITY % netmonitorDTD SYSTEM "chrome://browser/locale/devtools/netmonitor.dtd">
+  %netmonitorDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="text/javascript" src="netmonitor-controller.js"/>
+  <script type="text/javascript" src="netmonitor-view.js"/>
+
+  <hbox id="body" flex="1">
+    <vbox id="network-table" flex="1">
+      <toolbar id="requests-menu-toolbar"
+               class="devtools-toolbar"
+               align="center">
+        <label id="requests-menu-status-and-method-label"
+               class="plain requests-menu-header requests-menu-status-and-method"
+               value="&netmonitorUI.toolbar.method;"
+               crop="end"/>
+        <label id="requests-menu-file-label"
+               class="plain requests-menu-header requests-menu-file"
+               value="&netmonitorUI.toolbar.file;"
+               crop="end"/>
+        <label id="requests-menu-domain-label"
+               class="plain requests-menu-header requests-menu-domain"
+               value="&netmonitorUI.toolbar.domain;"
+               crop="end"/>
+        <label id="requests-menu-type-label"
+               class="plain requests-menu-header requests-menu-type"
+               value="&netmonitorUI.toolbar.type;"
+               crop="end"/>
+        <label id="requests-menu-size-label"
+               class="plain requests-menu-header requests-menu-size"
+               value="&netmonitorUI.toolbar.size;"
+               crop="end"/>
+        <label id="requests-menu-waterfall-label"
+               class="plain requests-menu-header requests-menu-waterfall"
+               value="&netmonitorUI.toolbar.waterfall;"
+               crop="end"/>
+        <spacer flex="1"/>
+        <toolbarbutton id="details-pane-toggle"
+                       class="devtools-toolbarbutton"
+                       tooltiptext="&netmonitorUI.panesButton.tooltip;"
+                       tabindex="0"/>
+      </toolbar>
+      <label class="plain requests-menu-empty-notice"
+             value="&netmonitorUI.emptyNotice;"/>
+      <vbox id="requests-menu-contents" flex="1">
+        <template id="requests-menu-item-template">
+          <hbox class="requests-menu-item">
+            <hbox class="requests-menu-subitem requests-menu-status-and-method"
+                  align="center">
+              <hbox class="requests-menu-status"/>
+              <label class="plain requests-menu-method"
+                     crop="end"
+                     flex="1"/>
+            </hbox>
+            <label class="plain requests-menu-subitem requests-menu-file"
+                   crop="end"/>
+            <label class="plain requests-menu-subitem requests-menu-domain"
+                   crop="end"/>
+            <label class="plain requests-menu-subitem requests-menu-type"
+                   crop="end"/>
+            <label class="plain requests-menu-subitem requests-menu-size"
+                   crop="end"/>
+            <hbox class="requests-menu-subitem requests-menu-waterfall"
+                  align="center"
+                  flex="1">
+              <hbox class="requests-menu-timings"
+                    align="center">
+                <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>
+        </template>
+      </vbox>
+    </vbox>
+
+    <splitter class="devtools-side-splitter"/>
+
+    <tabbox id="details-pane" class="devtools-sidebar-tabs" hidden="true">
+      <tabs>
+        <tab label="&netmonitorUI.tab.headers;"/>
+        <tab label="&netmonitorUI.tab.cookies;"/>
+        <tab label="&netmonitorUI.tab.params;"/>
+        <tab label="&netmonitorUI.tab.response;"/>
+        <tab label="&netmonitorUI.tab.timings;"/>
+      </tabs>
+      <tabpanels flex="1">
+        <tabpanel id="headers-tabppanel"
+                  class="tabpanel-content">
+          <vbox flex="1">
+            <hbox id="headers-summary-url"
+                  class="tabpanel-summary-container"
+                  align="center">
+              <label class="plain tabpanel-summary-label"
+                     value="&netmonitorUI.summary.url;"/>
+              <label id="headers-summary-url-value"
+                     class="plain tabpanel-summary-value"
+                     crop="end"
+                     flex="1"/>
+            </hbox>
+            <hbox id="headers-summary-method"
+                  class="tabpanel-summary-container"
+                  align="center">
+              <label class="plain tabpanel-summary-label"
+                     value="&netmonitorUI.summary.method;"/>
+              <label id="headers-summary-method-value"
+                     class="plain tabpanel-summary-value"
+                     crop="end"
+                     flex="1"/>
+            </hbox>
+            <hbox id="headers-summary-status"
+                  class="tabpanel-summary-container"
+                  align="center">
+              <label class="plain tabpanel-summary-label"
+                     value="&netmonitorUI.summary.status;"/>
+              <hbox id="headers-summary-status-circle"
+                    class="requests-menu-status"/>
+              <label id="headers-summary-status-value"
+                     class="plain tabpanel-summary-value"
+                     crop="end"
+                     flex="1"/>
+            </hbox>
+            <hbox id="headers-summary-version"
+                  class="tabpanel-summary-container"
+                  align="center">
+              <label class="plain tabpanel-summary-label"
+                     value="&netmonitorUI.summary.version;"/>
+              <label id="headers-summary-version-value"
+                     class="plain tabpanel-summary-value"
+                     crop="end"
+                     flex="1"/>
+            </hbox>
+            <vbox id="all-headers" flex="1"/>
+          </vbox>
+        </tabpanel>
+        <tabpanel id="cookies-tabpanel"
+                  class="tabpanel-content">
+          <vbox flex="1">
+            <vbox id="all-cookies" flex="1"/>
+          </vbox>
+        </tabpanel>
+        <tabpanel id="params-tabpanel"
+                  class="tabpanel-content">
+          <vbox flex="1">
+            <vbox id="request-params-box" flex="1" hidden="true">
+              <vbox id="request-params" flex="1"/>
+            </vbox>
+            <vbox id="request-post-data-textarea-box" flex="1" hidden="true">
+              <vbox id="request-post-data-textarea" flex="1"/>
+            </vbox>
+          </vbox>
+        </tabpanel>
+        <tabpanel id="response-tabpanel"
+                  class="tabpanel-content">
+          <vbox flex="1">
+            <vbox id="response-content-json-box" flex="1" hidden="true">
+              <vbox id="response-content-json" flex="1"/>
+            </vbox>
+            <vbox id="response-content-textarea-box" flex="1" hidden="true">
+              <vbox id="response-content-textarea" flex="1"/>
+            </vbox>
+            <vbox id="response-content-image-box" flex="1" hidden="true">
+              <image id="response-content-image"/>
+              <hbox>
+                <label class="plain tabpanel-summary-label"
+                       value="&netmonitorUI.response.name;"/>
+                <label id="response-content-image-name-value"
+                       class="plain tabpanel-summary-value"
+                       crop="end"
+                       flex="1"/>
+              </hbox>
+              <hbox>
+                <label class="plain tabpanel-summary-label"
+                       value="&netmonitorUI.response.dimensions;"/>
+                <label id="response-content-image-dimensions-value"
+                       class="plain tabpanel-summary-value"
+                       crop="end"
+                       flex="1"/>
+              </hbox>
+              <hbox>
+                <label class="plain tabpanel-summary-label"
+                       value="&netmonitorUI.response.mime;"/>
+                <label id="response-content-image-mime-value"
+                       class="plain tabpanel-summary-value"
+                       crop="end"
+                       flex="1"/>
+              </hbox>
+              <hbox>
+                <label class="plain tabpanel-summary-label"
+                       value="&netmonitorUI.response.encoding;"/>
+                <label id="response-content-image-encoding-value"
+                       class="plain tabpanel-summary-value"
+                       crop="end"
+                       flex="1"/>
+              </hbox>
+            </vbox>
+          </vbox>
+        </tabpanel>
+        <tabpanel id="timings-tabpanel"
+                  class="tabpanel-content">
+          <vbox flex="1">
+            <hbox id="timings-summary-blocked"
+                  class="tabpanel-summary-container"
+                  align="center">
+              <label class="plain tabpanel-summary-label"
+                     value="&netmonitorUI.timings.blocked;"/>
+              <hbox class="requests-menu-timings-box blocked"/>
+              <label class="plain requests-menu-timings-total"/>
+            </hbox>
+            <hbox id="timings-summary-dns"
+                  class="tabpanel-summary-container"
+                  align="center">
+              <label class="plain tabpanel-summary-label"
+                     value="&netmonitorUI.timings.dns;"/>
+              <hbox class="requests-menu-timings-box dns"/>
+              <label class="plain requests-menu-timings-total"/>
+            </hbox>
+            <hbox id="timings-summary-connect"
+                  class="tabpanel-summary-container"
+                  align="center">
+              <label class="plain tabpanel-summary-label"
+                     value="&netmonitorUI.timings.connect;"/>
+              <hbox class="requests-menu-timings-box connect"/>
+              <label class="plain requests-menu-timings-total"/>
+            </hbox>
+            <hbox id="timings-summary-send"
+                  class="tabpanel-summary-container"
+                  align="center">
+              <label class="plain tabpanel-summary-label"
+                     value="&netmonitorUI.timings.send;"/>
+              <hbox class="requests-menu-timings-box send"/>
+              <label class="plain requests-menu-timings-total"/>
+            </hbox>
+            <hbox id="timings-summary-wait"
+                  class="tabpanel-summary-container"
+                  align="center">
+              <label class="plain tabpanel-summary-label"
+                     value="&netmonitorUI.timings.wait;"/>
+              <hbox class="requests-menu-timings-box wait"/>
+              <label class="plain requests-menu-timings-total"/>
+            </hbox>
+            <hbox id="timings-summary-receive"
+                  class="tabpanel-summary-container"
+                  align="center">
+              <label class="plain tabpanel-summary-label"
+                     value="&netmonitorUI.timings.receive;"/>
+              <hbox class="requests-menu-timings-box receive"/>
+              <label class="plain requests-menu-timings-total"/>
+            </hbox>
+          </vbox>
+        </tabpanel>
+      </tabpanels>
+    </tabbox>
+  </hbox>
+
+</window>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/Makefile.in
@@ -0,0 +1,43 @@
+# 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/.
+
+DEPTH           = @DEPTH@
+topsrcdir       = @top_srcdir@
+srcdir          = @srcdir@
+VPATH           = @srcdir@
+relativesrcdir  = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_TESTS = \
+	browser_net_aaa_leaktest.js \
+	browser_net_simple-init.js \
+	browser_net_page-nav.js \
+	browser_net_prefs-and-l10n.js \
+	browser_net_prefs-reload.js \
+	browser_net_pane-collapse.js \
+	browser_net_simple-request.js \
+	browser_net_simple-request-data.js \
+	browser_net_simple-request-details.js \
+	browser_net_content-type.js \
+	browser_net_status-codes.js \
+	browser_net_post-data.js \
+	head.js \
+	$(NULL)
+
+MOCHITEST_BROWSER_PAGES = \
+	test-image.png \
+	html_simple-test-page.html \
+	html_navigate-test-page.html \
+	html_content-type-test-page.html \
+	html_status-codes-test-page.html \
+	html_post-data-test-page.html \
+	sjs_simple-test-server.sjs \
+	sjs_content-type-test-server.sjs \
+	sjs_status-codes-test-server.sjs \
+	$(NULL)
+
+MOCHITEST_BROWSER_FILES_PARTS = MOCHITEST_BROWSER_TESTS MOCHITEST_BROWSER_PAGES
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_aaa_leaktest.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the network monitor leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, NetMonitorView, NetMonitorController } = aMonitor.panelWin;
+    let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+    ok(aTab, "Should have a tab available.");
+    ok(aDebuggee, "Should have a debuggee available.");
+    ok(aMonitor, "Should have a network monitor pane available.");
+
+    ok(document, "Should have a document available.");
+    ok(NetMonitorView, "Should have a NetMonitorView object available.");
+    ok(NetMonitorController, "Should have a NetMonitorController object available.");
+    ok(RequestsMenu, "Should have a RequestsMenu object available.");
+    ok(NetworkDetails, "Should have a NetworkDetails object available.");
+
+    teardown(aMonitor).then(finish);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_content-type.js
@@ -0,0 +1,214 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if different response content types are handled correctly.
+ */
+
+function test() {
+  initNetMonitor(CONTENT_TYPE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu } = NetMonitorView;
+
+    RequestsMenu.lazyUpdate = false;
+
+    waitForNetworkEvents(aMonitor, 6).then(() => {
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+        "GET", CONTENT_TYPE_SJS + "?fmt=xml", {
+          status: 200,
+          type: "xml",
+          size: "0.04kb",
+          time: true
+        });
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1),
+        "GET", CONTENT_TYPE_SJS + "?fmt=css", {
+          status: 200,
+          type: "css",
+          size: "0.03kb",
+          time: true
+        });
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(2),
+        "GET", CONTENT_TYPE_SJS + "?fmt=js", {
+          status: 200,
+          type: "js",
+          size: "0.03kb",
+          time: true
+        });
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(3),
+        "GET", CONTENT_TYPE_SJS + "?fmt=json", {
+          status: 200,
+          type: "json",
+          size: "0.03kb",
+          time: true
+        });
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(4),
+        "GET", CONTENT_TYPE_SJS + "?fmt=bogus", {
+          status: 404,
+          type: "html",
+          size: "0.02kb",
+          time: true
+        });
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(5),
+        "GET", TEST_IMAGE, {
+          status: 200,
+          type: "png",
+          size: "0.76kb",
+          time: true
+        });
+
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.getElementById("details-pane-toggle"));
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll("#details-pane tab")[3]);
+
+      testResponseTab("xml")
+        .then(() => {
+          RequestsMenu.selectedIndex = 1;
+          return testResponseTab("css");
+        })
+        .then(() => {
+          RequestsMenu.selectedIndex = 2;
+          return testResponseTab("js");
+        })
+        .then(() => {
+          RequestsMenu.selectedIndex = 3;
+          return testResponseTab("json");
+        })
+        .then(() => {
+          RequestsMenu.selectedIndex = 4;
+          return testResponseTab("html");
+        })
+        .then(() => {
+          RequestsMenu.selectedIndex = 5;
+          return testResponseTab("png");
+        })
+        .then(() => {
+          return teardown(aMonitor);
+        })
+        .then(finish);
+
+      function testResponseTab(aType) {
+        let tab = document.querySelectorAll("#details-pane tab")[3];
+        let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+        is(tab.getAttribute("selected"), "true",
+          "The response tab in the network details pane should be selected.");
+
+        function checkVisibility(aBox) {
+          is(tabpanel.querySelector("#response-content-json-box")
+            .hasAttribute("hidden"), aBox != "json",
+            "The response content json box doesn't have the intended visibility.");
+
+          is(tabpanel.querySelector("#response-content-textarea-box")
+            .hasAttribute("hidden"), aBox != "textarea",
+            "The response content textarea box doesn't have the intended visibility.");
+
+          is(tabpanel.querySelector("#response-content-image-box")
+            .hasAttribute("hidden"), aBox != "image",
+            "The response content image box doesn't have the intended visibility.");
+        }
+
+        switch (aType) {
+          case "xml": {
+            checkVisibility("textarea");
+
+            return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+              is(aEditor.getText(), "<label value='greeting'>Hello XML!</label>",
+                "The text shown in the source editor is incorrect for the xml request.");
+              is(aEditor.getMode(), SourceEditor.MODES.HTML,
+                "The mode active in the source editor is incorrect for the xml request.");
+            });
+          }
+          case "css": {
+            checkVisibility("textarea");
+
+            return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+              is(aEditor.getText(), "body:pre { content: 'Hello CSS!' }",
+                "The text shown in the source editor is incorrect for the xml request.");
+              is(aEditor.getMode(), SourceEditor.MODES.CSS,
+                "The mode active in the source editor is incorrect for the xml request.");
+            });
+          }
+          case "js": {
+            checkVisibility("textarea");
+
+            return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+              is(aEditor.getText(), "function() { return 'Hello JS!'; }",
+                "The text shown in the source editor is incorrect for the xml request.");
+              is(aEditor.getMode(), SourceEditor.MODES.JAVASCRIPT,
+                "The mode active in the source editor is incorrect for the xml request.");
+            });
+          }
+          case "json": {
+            checkVisibility("json");
+
+            is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+              "There should be 1 json scope displayed in this tabpanel.");
+            is(tabpanel.querySelectorAll(".variables-view-property").length, 2,
+              "There should be 2 json properties displayed in this tabpanel.");
+            is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+              "The empty notice should not be displayed in this tabpanel.");
+
+            let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+
+            is(jsonScope.querySelector(".name").getAttribute("value"),
+              "JSON", "The json scope doesn't have the correct title.");
+
+            is(jsonScope.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
+              "greeting", "The first json property name was incorrect.");
+            is(jsonScope.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
+              "\"Hello JSON!\"", "The first json property value was incorrect.");
+
+            is(jsonScope.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
+              "__proto__", "The second json property name was incorrect.");
+            is(jsonScope.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
+              "[object Object]", "The second json property value was incorrect.");
+
+            return Promise.resolve();
+          }
+          case "html": {
+            checkVisibility("textarea");
+
+            return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+              is(aEditor.getText(), "<blink>Not Found</blink>",
+                "The text shown in the source editor is incorrect for the xml request.");
+              is(aEditor.getMode(), SourceEditor.MODES.HTML,
+                "The mode active in the source editor is incorrect for the xml request.");
+            });
+          }
+          case "png": {
+            checkVisibility("image");
+
+            let imageNode = tabpanel.querySelector("#response-content-image");
+            let deferred = Promise.defer();
+
+            imageNode.addEventListener("load", function onLoad() {
+              imageNode.removeEventListener("load", onLoad);
+
+              is(tabpanel.querySelector("#response-content-image-name-value")
+                .getAttribute("value"), "test-image.png",
+                "The image name info isn't correct.");
+              is(tabpanel.querySelector("#response-content-image-mime-value")
+                .getAttribute("value"), "image/png",
+                "The image mime info isn't correct.");
+              is(tabpanel.querySelector("#response-content-image-encoding-value")
+                .getAttribute("value"), "base64",
+                "The image encoding info isn't correct.");
+              is(tabpanel.querySelector("#response-content-image-dimensions-value")
+                .getAttribute("value"), "16 x 16",
+                "The image dimensions info isn't correct.");
+
+              deferred.resolve();
+            });
+
+            return deferred.promise;
+          }
+        }
+      }
+    });
+
+    aDebuggee.performRequests();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_page-nav.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if page navigation ("close", "navigate", etc.) triggers an appropriate
+ * action in the network monitor.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    testNavigate(() => testNavigateBack(() => testClose(() => finish())));
+
+    function testNavigate(aCallback) {
+      info("Navigating forward...");
+
+      aMonitor.panelWin.once("NetMonitor:TargetWillNavigate", () => {
+        is(aDebuggee.location, SIMPLE_URL,
+          "Target started navigating to the correct location.");
+
+        aMonitor.panelWin.once("NetMonitor:TargetNavigate", () => {
+          is(aDebuggee.location, NAVIGATE_URL,
+            "Target finished navigating to the correct location.");
+
+          aCallback();
+        });
+      });
+
+      aDebuggee.location = NAVIGATE_URL;
+    }
+
+    function testNavigateBack(aCallback) {
+      info("Navigating backward...");
+
+      aMonitor.panelWin.once("NetMonitor:TargetWillNavigate", () => {
+        is(aDebuggee.location, NAVIGATE_URL,
+          "Target started navigating back to the previous location.");
+
+        aMonitor.panelWin.once("NetMonitor:TargetNavigate", () => {
+          is(aDebuggee.location, SIMPLE_URL,
+            "Target finished navigating back to the previous location.");
+
+          aCallback();
+        });
+      });
+
+      aDebuggee.location = SIMPLE_URL;
+    }
+
+    function testClose(aCallback) {
+      info("Closing...");
+
+      aMonitor.once("destroyed", () => {
+        ok(!aMonitor._controller.client,
+          "There shouldn't be a client available after destruction.");
+        ok(!aMonitor._controller.tabClient,
+          "There shouldn't be a tabClient available after destruction.");
+        ok(!aMonitor._controller.webConsoleClient,
+          "There shouldn't be a webConsoleClient available after destruction.");
+
+        aCallback();
+      });
+
+      removeTab(aTab);
+    }
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_pane-collapse.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the network monitor panes collapse properly.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, Prefs, NetMonitorView } = aMonitor.panelWin;
+    let detailsPane = document.getElementById("details-pane");
+    let detailsPaneToggleButton = document.getElementById("details-pane-toggle");
+
+    ok(detailsPane.hasAttribute("pane-collapsed") &&
+       detailsPaneToggleButton.hasAttribute("pane-collapsed"),
+      "The details pane should initially be hidden.");
+
+    NetMonitorView.toggleDetailsPane({ visible: true, animated: false });
+
+    let width = ~~(detailsPane.getAttribute("width"));
+    is(width, Prefs.networkDetailsWidth,
+      "The details pane has an incorrect width.");
+    is(detailsPane.style.marginLeft, "0px",
+      "The details pane has an incorrect left margin.");
+    is(detailsPane.style.marginRight, "0px",
+      "The details pane has an incorrect right margin.");
+    ok(!detailsPane.hasAttribute("animated"),
+      "The details pane has an incorrect animated attribute.");
+    ok(!detailsPane.hasAttribute("pane-collapsed") &&
+       !detailsPaneToggleButton.hasAttribute("pane-collapsed"),
+      "The details pane should at this point be visible.");
+
+    NetMonitorView.toggleDetailsPane({ visible: false, animated: true });
+
+    let margin = -(width + 1) + "px";
+    is(width, Prefs.networkDetailsWidth,
+      "The details pane has an incorrect width after collapsing.");
+    is(detailsPane.style.marginLeft, margin,
+      "The details pane has an incorrect left margin after collapsing.");
+    is(detailsPane.style.marginRight, margin,
+      "The details pane has an incorrect right margin after collapsing.");
+    ok(detailsPane.hasAttribute("animated"),
+      "The details pane has an incorrect attribute after an animated collapsing.");
+    ok(detailsPane.hasAttribute("pane-collapsed") &&
+       detailsPaneToggleButton.hasAttribute("pane-collapsed"),
+      "The details pane should not be visible after collapsing.");
+
+    NetMonitorView.toggleDetailsPane({ visible: true, animated: false });
+
+    is(width, Prefs.networkDetailsWidth,
+      "The details pane has an incorrect width after uncollapsing.");
+    is(detailsPane.style.marginLeft, "0px",
+      "The details pane has an incorrect left margin after uncollapsing.");
+    is(detailsPane.style.marginRight, "0px",
+      "The details pane has an incorrect right margin after uncollapsing.");
+    ok(!detailsPane.hasAttribute("animated"),
+      "The details pane has an incorrect attribute after an unanimated uncollapsing.");
+    ok(!detailsPane.hasAttribute("pane-collapsed") &&
+       !detailsPaneToggleButton.hasAttribute("pane-collapsed"),
+      "The details pane should be visible again after uncollapsing.");
+
+    teardown(aMonitor).then(finish);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_post-data.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the POST requests display the correct information in the UI.
+ */
+
+function test() {
+  initNetMonitor(POST_DATA_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+    RequestsMenu.lazyUpdate = false;
+    NetworkDetails._params.lazyEmpty = false;
+
+    waitForNetworkEvents(aMonitor, 0, 2).then(() => {
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+        "POST", SIMPLE_SJS + "?foo=bar&baz=42&type=urlencoded", {
+          status: 200,
+          type: "plain",
+          size: "0.01kb",
+          time: true
+        });
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1),
+        "POST", SIMPLE_SJS + "?foo=bar&baz=42&type=multipart", {
+          status: 200,
+          type: "plain",
+          size: "0.01kb",
+          time: true
+        });
+
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.getElementById("details-pane-toggle"));
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll("#details-pane tab")[2]);
+
+      testParamsTab("urlencoded")
+        .then(() => {
+          RequestsMenu.selectedIndex = 1;
+          return testParamsTab("multipart");
+        })
+        .then(() => {
+          return teardown(aMonitor);
+        })
+        .then(finish);
+
+      function testParamsTab(aType) {
+        let tab = document.querySelectorAll("#details-pane tab")[2];
+        let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+        is(tab.getAttribute("selected"), "true",
+          "The params tab in the network details pane should be selected.");
+
+        function checkVisibility(aBox) {
+          is(tabpanel.querySelector("#request-params-box")
+            .hasAttribute("hidden"), !aBox.contains("params"),
+            "The request params box doesn't have the indended visibility.");
+
+          is(tabpanel.querySelector("#request-post-data-textarea-box")
+            .hasAttribute("hidden"), !aBox.contains("textarea"),
+            "The request post data textarea box doesn't have the indended visibility.");
+        }
+
+        is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+          "There should be 2 param scopes displayed in this tabpanel.");
+        is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+          "The empty notice should not be displayed in this tabpanel.");
+
+        let queryScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+        let postScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+
+        is(queryScope.querySelector(".name").getAttribute("value"),
+          L10N.getStr("paramsQueryString"),
+          "The query scope doesn't have the correct title.");
+
+        is(postScope.querySelector(".name").getAttribute("value"),
+          L10N.getStr(aType == "urlencoded" ? "paramsFormData" : "paramsPostPayload"),
+          "The post scope doesn't have the correct title.");
+
+        is(queryScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
+          "foo", "The first query param name was incorrect.");
+        is(queryScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
+          "\"bar\"", "The first query param value was incorrect.");
+        is(queryScope.querySelectorAll(".variables-view-variable .name")[1].getAttribute("value"),
+          "baz", "The second query param name was incorrect.");
+        is(queryScope.querySelectorAll(".variables-view-variable .value")[1].getAttribute("value"),
+          "\"42\"", "The second query param value was incorrect.");
+        is(queryScope.querySelectorAll(".variables-view-variable .name")[2].getAttribute("value"),
+          "type", "The third query param name was incorrect.");
+        is(queryScope.querySelectorAll(".variables-view-variable .value")[2].getAttribute("value"),
+          "\"" + aType + "\"", "The third query param value was incorrect.");
+
+        if (aType == "urlencoded") {
+          checkVisibility("params");
+
+          is(tabpanel.querySelectorAll(".variables-view-variable").length, 5,
+            "There should be 6 param values displayed in this tabpanel.");
+          is(queryScope.querySelectorAll(".variables-view-variable").length, 3,
+            "There should be 3 param values displayed in the query scope.");
+          is(postScope.querySelectorAll(".variables-view-variable").length, 2,
+            "There should be 3 param values displayed in the post scope.");
+
+          is(postScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
+            "foo", "The first post param name was incorrect.");
+          is(postScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
+            "\"bar\"", "The first post param value was incorrect.");
+          is(postScope.querySelectorAll(".variables-view-variable .name")[1].getAttribute("value"),
+            "baz", "The second post param name was incorrect.");
+          is(postScope.querySelectorAll(".variables-view-variable .value")[1].getAttribute("value"),
+            "\"123\"", "The second post param value was incorrect.");
+
+          return Promise.resolve();
+        }
+        else {
+          checkVisibility("params textarea");
+
+          is(tabpanel.querySelectorAll(".variables-view-variable").length, 3,
+            "There should be 3 param values displayed in this tabpanel.");
+          is(queryScope.querySelectorAll(".variables-view-variable").length, 3,
+            "There should be 3 param values displayed in the query scope.");
+          is(postScope.querySelectorAll(".variables-view-variable").length, 0,
+            "There should be 0 param values displayed in the post scope.");
+
+          return NetMonitorView.editor("#request-post-data-textarea").then((aEditor) => {
+            ok(aEditor.getText().contains("Content-Disposition: form-data; name=\"text\""),
+              "The text shown in the source editor is incorrect (1.1).");
+            ok(aEditor.getText().contains("Content-Disposition: form-data; name=\"email\""),
+              "The text shown in the source editor is incorrect (2.1).");
+            ok(aEditor.getText().contains("Content-Disposition: form-data; name=\"range\""),
+              "The text shown in the source editor is incorrect (3.1).");
+            ok(aEditor.getText().contains("Content-Disposition: form-data; name=\"Custom field\""),
+              "The text shown in the source editor is incorrect (4.1).");
+            ok(aEditor.getText().contains("Some text..."),
+              "The text shown in the source editor is incorrect (2.2).");
+            ok(aEditor.getText().contains("42"),
+              "The text shown in the source editor is incorrect (3.2).");
+            ok(aEditor.getText().contains("Extra data"),
+              "The text shown in the source editor is incorrect (4.2).");
+            is(aEditor.getMode(), SourceEditor.MODES.TEXT,
+              "The mode active in the source editor is incorrect.");
+          });
+        }
+      }
+    });
+
+    aDebuggee.performRequests();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_prefs-and-l10n.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the preferences and localization objects work correctly.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    ok(aMonitor.panelWin.L10N,
+      "Should have a localization object available on the panel window.");
+    ok(aMonitor.panelWin.Prefs,
+      "Should have a preferences object available on the panel window.");
+
+    function testL10N() {
+      let { L10N } = aMonitor.panelWin;
+
+      ok(L10N.stringBundle,
+        "The localization object should have a string bundle available.");
+
+      let bundleName = "chrome://browser/locale/devtools/netmonitor.properties";
+      let stringBundle = Services.strings.createBundle(bundleName);
+
+      is(L10N.getStr("netmonitor.label"),
+        stringBundle.GetStringFromName("netmonitor.label"),
+        "The getStr() method didn't return the expected string.");
+
+      is(L10N.getFormatStr("networkMenu.total", "foo"),
+        stringBundle.formatStringFromName("networkMenu.total", ["foo"], 1),
+        "The getFormatStr() method didn't return the expected string.");
+    }
+
+    function testPrefs() {
+      let { Prefs } = aMonitor.panelWin;
+
+      is(Prefs.root, "devtools.netmonitor",
+        "The preferences object should have a correct root path.");
+
+      is(Prefs.networkDetailsWidth,
+        Services.prefs.getIntPref("devtools.netmonitor.panes-network-details-width"),
+        "Getting a pref should work correctly.");
+
+      let previousValue = Prefs.networkDetailsWidth;
+      let bogusValue = ~~(Math.random() * 100);
+      Prefs.networkDetailsWidth = bogusValue;
+      is(Prefs.networkDetailsWidth,
+        Services.prefs.getIntPref("devtools.netmonitor.panes-network-details-width"),
+        "Getting a pref after it has been modified should work correctly.");
+      is(Prefs.networkDetailsWidth, bogusValue,
+        "The pref wasn't updated correctly in the preferences object.");
+
+      Prefs.networkDetailsWidth = previousValue;
+      is(Prefs.networkDetailsWidth,
+        Services.prefs.getIntPref("devtools.netmonitor.panes-network-details-width"),
+        "Getting a pref after it has been modified again should work correctly.");
+      is(Prefs.networkDetailsWidth, previousValue,
+        "The pref wasn't updated correctly again in the preferences object.");
+    }
+
+    testL10N();
+    testPrefs();
+
+    teardown(aMonitor).then(finish);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_prefs-reload.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the prefs that should survive across tool reloads work.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let prefsToCheck = {
+      networkDetailsWidth: {
+        newValue: ~~(Math.random() * 200 + 100),
+        validate: () =>
+          ~~aMonitor._view._detailsPane.getAttribute("width"),
+        modifyFrontend: (aValue) =>
+          aMonitor._view._detailsPane.setAttribute("width", aValue)
+      },
+      /* add more prefs here... */
+    };
+
+    function storeFirstPrefValues() {
+      info("Caching initial pref values.");
+
+      for (let name in prefsToCheck) {
+        let currentValue = aMonitor.panelWin.Prefs[name];
+        prefsToCheck[name].firstValue = currentValue;
+      }
+    }
+
+    function validateFirstPrefValues() {
+      info("Validating current pref values to the UI elements.");
+
+      for (let name in prefsToCheck) {
+        let currentValue = aMonitor.panelWin.Prefs[name];
+        let firstValue = prefsToCheck[name].firstValue;
+        let validate = prefsToCheck[name].validate;
+
+        is(currentValue, firstValue,
+          "Pref " + name + " should be equal to first value: " + firstValue);
+        is(currentValue, validate(),
+          "Pref " + name + " should validate: " + currentValue);
+      }
+    }
+
+    function modifyFrontend() {
+      info("Modifying UI elements to the specified new values.");
+
+      for (let name in prefsToCheck) {
+        let currentValue = aMonitor.panelWin.Prefs[name];
+        let firstValue = prefsToCheck[name].firstValue;
+        let newValue = prefsToCheck[name].newValue;
+        let validate = prefsToCheck[name].validate;
+        let modifyFrontend = prefsToCheck[name].modifyFrontend;
+
+        modifyFrontend(newValue);
+        info("Modified UI element affecting " + name + " to: " + newValue);
+
+        is(currentValue, firstValue,
+          "Pref " + name + " should still be equal to first value: " + firstValue);
+        isnot(currentValue, newValue,
+          "Pref " + name + " should't yet be equal to second value: " + newValue);
+        is(newValue, validate(),
+          "The UI element affecting " + name + " should validate: " + newValue);
+      }
+    }
+
+    function validateNewPrefValues() {
+      info("Invalidating old pref values to the modified UI elements.");
+
+      for (let name in prefsToCheck) {
+        let currentValue = aMonitor.panelWin.Prefs[name];
+        let firstValue = prefsToCheck[name].firstValue;
+        let newValue = prefsToCheck[name].newValue;
+        let validate = prefsToCheck[name].validate;
+        let modifyFrontend = prefsToCheck[name].modifyFrontend;
+
+        isnot(currentValue, firstValue,
+          "Pref " + name + " should't be equal to first value: " + firstValue);
+        is(currentValue, newValue,
+          "Pref " + name + " should now be equal to second value: " + newValue);
+        is(newValue, validate(),
+          "The UI element affecting " + name + " should validate: " + newValue);
+      }
+    }
+
+    function resetFrontend() {
+      info("Resetting UI elements to the cached initial pref values.");
+
+      for (let name in prefsToCheck) {
+        let currentValue = aMonitor.panelWin.Prefs[name];
+        let firstValue = prefsToCheck[name].firstValue;
+        let newValue = prefsToCheck[name].newValue;
+        let validate = prefsToCheck[name].validate;
+        let modifyFrontend = prefsToCheck[name].modifyFrontend;
+
+        modifyFrontend(firstValue);
+        info("Modified UI element affecting " + name + " to: " + firstValue);
+
+        isnot(currentValue, firstValue,
+          "Pref " + name + " should't yet be equal to first value: " + firstValue);
+        is(currentValue, newValue,
+          "Pref " + name + " should still be equal to second value: " + newValue);
+        is(firstValue, validate(),
+          "The UI element affecting " + name + " should validate: " + firstValue);
+      }
+    }
+
+    storeFirstPrefValues();
+
+    // Validate and modify.
+    validateFirstPrefValues();
+    modifyFrontend();
+    restartNetMonitor(aMonitor).then(([,, aNewMonitor]) => {
+      aMonitor = aNewMonitor;
+
+      // Revalidate and reset.
+      validateNewPrefValues();
+      resetFrontend();
+      restartNetMonitor(aMonitor).then(([,, aNewMonitor]) => {
+        aMonitor = aNewMonitor;
+
+        // Revalidate and finish.
+        validateFirstPrefValues();
+        teardown(aMonitor).then(finish);
+      });
+    });
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_simple-init.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Simple check if the network monitor starts up and shuts down properly.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    is(aTab.linkedBrowser.contentWindow.wrappedJSObject.location, SIMPLE_URL,
+      "The current tab's location is the correct one.");
+    is(aDebuggee.location, SIMPLE_URL,
+      "The current debuggee's location is the correct one.");
+
+    function checkIfInitialized(aTag) {
+      info("Checking if initialization is ok (" + aTag + ").");
+
+      ok(aMonitor._view,
+        "The network monitor view object exists (" + aTag + ").");
+      ok(aMonitor._view._isInitialized,
+        "The network monitor view object exists and is initialized (" + aTag + ").");
+
+      ok(aMonitor._controller,
+        "The network monitor controller object exists (" + aTag + ").");
+      ok(aMonitor._controller._isInitialized,
+        "The network monitor controller object exists and is initialized (" + aTag + ").");
+
+      ok(aMonitor.isReady,
+        "The network monitor panel appears to be ready (" + aTag + ").");
+
+      ok(aMonitor._controller.client,
+        "There should be a client available at this point (" + aTag + ").");
+      ok(aMonitor._controller.tabClient,
+        "There should be a tabClient available at this point (" + aTag + ").");
+      ok(aMonitor._controller.webConsoleClient,
+        "There should be a webConsoleClient available at this point (" + aTag + ").");
+    }
+
+    function checkIfDestroyed(aTag) {
+      info("Checking if destruction is ok.");
+
+      ok(!aMonitor._controller.client,
+        "There shouldn't be a client available after destruction (" + aTag + ").");
+      ok(!aMonitor._controller.tabClient,
+        "There shouldn't be a tabClient available after destruction (" + aTag + ").");
+      ok(!aMonitor._controller.webConsoleClient,
+        "There shouldn't be a webConsoleClient available after destruction (" + aTag + ").");
+    }
+
+    executeSoon(() => {
+      checkIfInitialized(1);
+
+      aMonitor._controller.startupNetMonitor()
+        .then(() => {
+          info("Starting up again shouldn't do anything special.");
+          checkIfInitialized(2);
+          return aMonitor._controller.connect();
+        })
+        .then(() => {
+          info("Connecting again shouldn't do anything special.");
+          checkIfInitialized(3);
+          return teardown(aMonitor);
+        })
+        .then(finish);
+    });
+
+    registerCleanupFunction(() => {
+      checkIfDestroyed(1);
+
+      aMonitor._controller.shutdownNetMonitor()
+        .then(() => {
+          info("Shutting down again shouldn't do anything special.");
+          checkIfDestroyed(2);
+          return aMonitor._controller.disconnect();
+        })
+        .then(() => {
+          info("Disconnecting again shouldn't do anything special.");
+          checkIfDestroyed(3);
+        });
+    });
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_simple-request-data.js
@@ -0,0 +1,232 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if requests render correct information in the menu UI.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_SJS).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu } = NetMonitorView;
+
+    RequestsMenu.lazyUpdate = false;
+
+    waitForNetworkEvents(aMonitor, 1)
+      .then(() => teardown(aMonitor))
+      .then(finish);
+
+    aMonitor.panelWin.once("NetMonitor:NetworkEvent", () => {
+      is(RequestsMenu.selectedItem, null,
+        "There shouldn't be any selected item in the requests menu.");
+      is(RequestsMenu.itemCount, 1,
+        "The requests menu should not be empty after the first request.");
+      is(NetMonitorView.detailsPaneHidden, true,
+        "The details pane should still be hidden after the first request.");
+
+      let requestItem = RequestsMenu.getItemAtIndex(0);
+      let target = requestItem.target;
+
+      is(typeof requestItem.attachment.id, "string",
+        "The attached id is incorrect.");
+      isnot(requestItem.attachment.id, "",
+        "The attached id should not be empty.");
+
+      is(typeof requestItem.attachment.startedDeltaMillis, "number",
+        "The attached startedDeltaMillis is incorrect.");
+      is(requestItem.attachment.startedDeltaMillis, 0,
+        "The attached startedDeltaMillis should be zero.");
+
+      is(typeof requestItem.attachment.startedMillis, "number",
+        "The attached startedMillis is incorrect.");
+      isnot(requestItem.attachment.startedMillis, 0,
+        "The attached startedMillis should not be zero.");
+
+      is(requestItem.attachment.requestHeaders, undefined,
+        "The requestHeaders should not yet be set.");
+      is(requestItem.attachment.requestCookies, undefined,
+        "The requestCookies should not yet be set.");
+      is(requestItem.attachment.requestPostData, undefined,
+        "The requestPostData should not yet be set.");
+
+      is(requestItem.attachment.responseHeaders, undefined,
+        "The responseHeaders should not yet be set.");
+      is(requestItem.attachment.responseCookies, undefined,
+        "The responseCookies should not yet be set.");
+
+      is(requestItem.attachment.httpVersion, undefined,
+        "The httpVersion should not yet be set.");
+      is(requestItem.attachment.status, undefined,
+        "The status should not yet be set.");
+      is(requestItem.attachment.statusText, undefined,
+        "The statusText should not yet be set.");
+
+      is(requestItem.attachment.headersSize, undefined,
+        "The headersSize should not yet be set.");
+      is(requestItem.attachment.contentSize, undefined,
+        "The contentSize should not yet be set.");
+
+      is(requestItem.attachment.mimeType, undefined,
+        "The mimeType should not yet be set.");
+      is(requestItem.attachment.responseContent, undefined,
+        "The responseContent should not yet be set.");
+
+      is(requestItem.attachment.totalTime, undefined,
+        "The totalTime should not yet be set.");
+      is(requestItem.attachment.eventTimings, undefined,
+        "The eventTimings should not yet be set.");
+
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+    });
+
+    aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:RequestHeaders", () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
+
+      ok(requestItem.attachment.requestHeaders,
+        "There should be a requestHeaders attachment available.");
+      is(requestItem.attachment.requestHeaders.headers.length, 7,
+        "The requestHeaders attachment has an incorrect |headers| property.");
+      isnot(requestItem.attachment.requestHeaders.headersSize, 0,
+        "The requestHeaders attachment has an incorrect |headersSize| property.");
+      // Can't test for the exact request headers size because the value may
+      // vary across platforms ("User-Agent" header differs).
+
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+    });
+
+    aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:RequestCookies", () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
+
+      ok(requestItem.attachment.requestCookies,
+        "There should be a requestCookies attachment available.");
+      is(requestItem.attachment.requestCookies.cookies.length, 0,
+        "The requestCookies attachment has an incorrect |cookies| property.");
+
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+    });
+
+    aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:RequestPostData", () => {
+      ok(false, "Trap listener: this request doesn't have any post data.")
+    });
+
+    aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:ResponseHeaders", () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
+
+      ok(requestItem.attachment.responseHeaders,
+        "There should be a responseHeaders attachment available.");
+      is(requestItem.attachment.responseHeaders.headers.length, 6,
+        "The responseHeaders attachment has an incorrect |headers| property.");
+      is(requestItem.attachment.responseHeaders.headersSize, 173,
+        "The responseHeaders attachment has an incorrect |headersSize| property.");
+
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+    });
+
+    aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:ResponseCookies", () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
+
+      ok(requestItem.attachment.responseCookies,
+        "There should be a responseCookies attachment available.");
+      is(requestItem.attachment.responseCookies.cookies.length, 0,
+        "The responseCookies attachment has an incorrect |cookies| property.");
+
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+    });
+
+    aMonitor.panelWin.once("NetMonitor:NetworkEventUpdating:ResponseStart", () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
+
+      is(requestItem.attachment.httpVersion, "HTTP/1.1",
+        "The httpVersion attachment has an incorrect value.");
+      is(requestItem.attachment.status, "200",
+        "The status attachment has an incorrect value.");
+      is(requestItem.attachment.statusText, "Och Aye",
+        "The statusText attachment has an incorrect value.");
+      is(requestItem.attachment.headersSize, 173,
+        "The headersSize attachment has an incorrect value.");
+
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+        status: "200"
+      });
+    });
+
+    aMonitor.panelWin.once("NetMonitor:NetworkEventUpdating:ResponseContent", () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
+
+      is(requestItem.attachment.contentSize, "12",
+        "The contentSize attachment has an incorrect value.");
+      is(requestItem.attachment.mimeType, "text/plain; charset=utf-8",
+        "The mimeType attachment has an incorrect value.");
+
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+        type: "plain",
+        size: "0.01kb"
+      });
+    });
+
+    aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:ResponseContent", () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
+
+      ok(requestItem.attachment.responseContent,
+        "There should be a responseContent attachment available.");
+      is(requestItem.attachment.responseContent.content.mimeType, "text/plain; charset=utf-8",
+        "The responseContent attachment has an incorrect |content.mimeType| property.");
+      is(requestItem.attachment.responseContent.content.text, "Hello world!",
+        "The responseContent attachment has an incorrect |content.text| property.");
+      is(requestItem.attachment.responseContent.content.size, 12,
+        "The responseContent attachment has an incorrect |content.size| property.");
+
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+        type: "plain",
+        size: "0.01kb"
+      });
+    });
+
+    aMonitor.panelWin.once("NetMonitor:NetworkEventUpdating:EventTimings", () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
+
+      is(typeof requestItem.attachment.totalTime, "number",
+        "The attached totalTime is incorrect.");
+      ok(requestItem.attachment.totalTime >= 0,
+        "The attached totalTime should be positive.");
+
+      is(typeof requestItem.attachment.endedMillis, "number",
+        "The attached endedMillis is incorrect.");
+      ok(requestItem.attachment.endedMillis >= 0,
+        "The attached endedMillis should be positive.");
+
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+        time: true
+      });
+    });
+
+    aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:EventTimings", () => {
+      let requestItem = RequestsMenu.getItemAtIndex(0);
+
+      ok(requestItem.attachment.eventTimings,
+        "There should be a eventTimings attachment available.");
+      is(typeof requestItem.attachment.eventTimings.timings.blocked, "number",
+        "The eventTimings attachment has an incorrect |timings.blocked| property.");
+      is(typeof requestItem.attachment.eventTimings.timings.dns, "number",
+        "The eventTimings attachment has an incorrect |timings.dns| property.");
+      is(typeof requestItem.attachment.eventTimings.timings.connect, "number",
+        "The eventTimings attachment has an incorrect |timings.connect| property.");
+      is(typeof requestItem.attachment.eventTimings.timings.send, "number",
+        "The eventTimings attachment has an incorrect |timings.send| property.");
+      is(typeof requestItem.attachment.eventTimings.timings.wait, "number",
+        "The eventTimings attachment has an incorrect |timings.wait| property.");
+      is(typeof requestItem.attachment.eventTimings.timings.receive, "number",
+        "The eventTimings attachment has an incorrect |timings.receive| property.");
+      is(typeof requestItem.attachment.eventTimings.totalTime, "number",
+        "The eventTimings attachment has an incorrect |totalTime| property.");
+
+      verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+        time: true
+      });
+    });
+
+    aDebuggee.location.reload();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_simple-request-details.js
@@ -0,0 +1,225 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if requests render correct information in the details UI.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_SJS).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+    RequestsMenu.lazyUpdate = false;
+
+    waitForNetworkEvents(aMonitor, 1).then(() => {
+      is(RequestsMenu.selectedItem, null,
+        "There shouldn't be any selected item in the requests menu.");
+      is(RequestsMenu.itemCount, 1,
+        "The requests menu should not be empty after the first request.");
+      is(NetMonitorView.detailsPaneHidden, true,
+        "The details pane should still be hidden after the first request.");
+
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.getElementById("details-pane-toggle"));
+
+      isnot(RequestsMenu.selectedItem, null,
+        "There should be a selected item in the requests menu.");
+      is(RequestsMenu.selectedIndex, 0,
+        "The first item should be selected in the requests menu.");
+      is(NetMonitorView.detailsPaneHidden, false,
+        "The details pane should not be hidden after toggle button was pressed.");
+
+      testHeadersTab();
+      testCookiesTab();
+      testParamsTab();
+      testResponseTab()
+        .then(() => {
+          testTimingsTab();
+          return teardown(aMonitor);
+        })
+        .then(finish);
+    });
+
+    function testHeadersTab() {
+      let tab = document.querySelectorAll("#details-pane tab")[0];
+      let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
+
+      is(tab.getAttribute("selected"), "true",
+        "The headers tab in the network details pane should be selected.");
+
+      is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
+        SIMPLE_SJS, "The url summary value is incorrect.");
+      is(tabpanel.querySelector("#headers-summary-method-value").getAttribute("value"),
+        "GET", "The method summary value is incorrect.");
+      is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("code"),
+        "200", "The status summary code is incorrect.");
+      is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
+        "200 Och Aye", "The status summary value is incorrect.");
+
+      is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+        "There should be 2 header scopes displayed in this tabpanel.");
+      is(tabpanel.querySelectorAll(".variable-or-property").length, 13,
+        "There should be 13 header values displayed in this tabpanel.");
+      is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+        "The empty notice should not be displayed in this tabpanel.");
+
+      let responseScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+      let requestScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+
+      is(responseScope.querySelector(".name").getAttribute("value"),
+        L10N.getStr("responseHeaders") + " (" +
+        L10N.getFormatStr("networkMenu.size", "0.169") + ")",
+        "The response headers scope doesn't have the correct title.");
+
+      ok(requestScope.querySelector(".name").getAttribute("value").contains(
+        L10N.getStr("requestHeaders") + " (0."),
+        // Can't test for full request headers title because the size may
+        // vary across platforms ("User-Agent" header differs).
+        "The request headers scope doesn't have the correct title.");
+
+      is(responseScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
+        "Connection", "The first response header name was incorrect.");
+      is(responseScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
+        "\"close\"", "The first response header value was incorrect.");
+      is(responseScope.querySelectorAll(".variables-view-variable .name")[1].getAttribute("value"),
+        "Content-Length", "The second response header name was incorrect.");
+      is(responseScope.querySelectorAll(".variables-view-variable .value")[1].getAttribute("value"),
+        "\"12\"", "The second response header value was incorrect.");
+      is(responseScope.querySelectorAll(".variables-view-variable .name")[2].getAttribute("value"),
+        "Content-Type", "The third response header name was incorrect.");
+      is(responseScope.querySelectorAll(".variables-view-variable .value")[2].getAttribute("value"),
+        "\"text/plain; charset=utf-8\"", "The third response header value was incorrect.");
+      is(responseScope.querySelectorAll(".variables-view-variable .name")[5].getAttribute("value"),
+        "foo-bar", "The last response header name was incorrect.");
+      is(responseScope.querySelectorAll(".variables-view-variable .value")[5].getAttribute("value"),
+        "\"baz\"", "The last response header value was incorrect.");
+
+      is(requestScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
+        "Host", "The first request header name was incorrect.");
+      is(requestScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
+        "\"example.com\"", "The first request header value was incorrect.");
+      is(requestScope.querySelectorAll(".variables-view-variable .name")[5].getAttribute("value"),
+        "Connection", "The penultimate request header name was incorrect.");
+      is(requestScope.querySelectorAll(".variables-view-variable .value")[5].getAttribute("value"),
+        "\"keep-alive\"", "The penultimate request header value was incorrect.");
+      is(requestScope.querySelectorAll(".variables-view-variable .name")[6].getAttribute("value"),
+        "Cache-Control", "The last request header name was incorrect.");
+      is(requestScope.querySelectorAll(".variables-view-variable .value")[6].getAttribute("value"),
+        "\"max-age=0\"", "The last request header value was incorrect.");
+    }
+
+    function testCookiesTab() {
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll("#details-pane tab")[1]);
+
+      let tab = document.querySelectorAll("#details-pane tab")[1];
+      let tabpanel = document.querySelectorAll("#details-pane tabpanel")[1];
+
+      is(tab.getAttribute("selected"), "true",
+        "The cookies tab in the network details pane should be selected.");
+
+      is(tabpanel.querySelectorAll(".variables-view-scope").length, 0,
+        "There should be no cookie scopes displayed in this tabpanel.");
+      is(tabpanel.querySelectorAll(".variable-or-property").length, 0,
+        "There should be no cookie values displayed in this tabpanel.");
+      is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 1,
+        "The empty notice should be displayed in this tabpanel.");
+    }
+
+    function testParamsTab() {
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll("#details-pane tab")[2]);
+
+      let tab = document.querySelectorAll("#details-pane tab")[2];
+      let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+      is(tab.getAttribute("selected"), "true",
+        "The params tab in the network details pane should be selected.");
+
+      is(tabpanel.querySelectorAll(".variables-view-scope").length, 0,
+        "There should be no param scopes displayed in this tabpanel.");
+      is(tabpanel.querySelectorAll(".variable-or-property").length, 0,
+        "There should be no param values displayed in this tabpanel.");
+      is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 1,
+        "The empty notice should be displayed in this tabpanel.");
+
+      is(tabpanel.querySelector("#request-params-box")
+        .hasAttribute("hidden"), false,
+        "The request params box should not be hidden.");
+
+      is(tabpanel.querySelector("#request-post-data-textarea-box")
+        .hasAttribute("hidden"), true,
+        "The request post data textarea box should be hidden.");
+    }
+
+    function testResponseTab() {
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll("#details-pane tab")[3]);
+
+      let tab = document.querySelectorAll("#details-pane tab")[3];
+      let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+      is(tab.getAttribute("selected"), "true",
+        "The response tab in the network details pane should be selected.");
+
+      is(tabpanel.querySelector("#response-content-json-box")
+        .hasAttribute("hidden"), true,
+        "The response content json box should be hidden.");
+
+      is(tabpanel.querySelector("#response-content-textarea-box")
+        .hasAttribute("hidden"), false,
+        "The response content textarea box should not be hidden.");
+
+      is(tabpanel.querySelector("#response-content-image-box")
+        .hasAttribute("hidden"), true,
+        "The response content image box should be hidden.");
+
+      return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+        is(aEditor.getText(), "Hello world!",
+          "The text shown in the source editor is incorrect.");
+        is(aEditor.getMode(), SourceEditor.MODES.TEXT,
+          "The mode active in the source editor is incorrect.");
+      });
+    }
+
+    function testTimingsTab() {
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll("#details-pane tab")[4]);
+
+      let tab = document.querySelectorAll("#details-pane tab")[4];
+      let tabpanel = document.querySelectorAll("#details-pane tabpanel")[4];
+
+      is(tab.getAttribute("selected"), "true",
+        "The timings tab in the network details pane should be selected.");
+
+      ok(tabpanel.querySelector("#timings-summary-blocked .requests-menu-timings-total")
+        .getAttribute("value").match(/[0-9]+ms$/),
+        "The blocked timing info does not appear to be correct.");
+
+      ok(tabpanel.querySelector("#timings-summary-dns .requests-menu-timings-total")
+        .getAttribute("value").match(/[0-9]+ms$/),
+        "The dns timing info does not appear to be correct.");
+
+      ok(tabpanel.querySelector("#timings-summary-connect .requests-menu-timings-total")
+        .getAttribute("value").match(/[0-9]+ms$/),
+        "The connect timing info does not appear to be correct.");
+
+      ok(tabpanel.querySelector("#timings-summary-send .requests-menu-timings-total")
+        .getAttribute("value").match(/[0-9]+ms$/),
+        "The send timing info does not appear to be correct.");
+
+      ok(tabpanel.querySelector("#timings-summary-wait .requests-menu-timings-total")
+        .getAttribute("value").match(/[0-9]+ms$/),
+        "The wait timing info does not appear to be correct.");
+
+      ok(tabpanel.querySelector("#timings-summary-receive .requests-menu-timings-total")
+        .getAttribute("value").match(/[0-9]+ms$/),
+        "The receive timing info does not appear to be correct.");
+    }
+
+    aDebuggee.location.reload();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_simple-request.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if requests are handled correctly.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu } = NetMonitorView;
+
+    RequestsMenu.lazyUpdate = false;
+
+    is(document.querySelector(".requests-menu-empty-notice")
+      .hasAttribute("hidden"), false,
+      "An empty notice should be displayed when the frontend is opened.");
+    is(RequestsMenu.itemCount, 0,
+      "The requests menu should be empty when the frontend is opened.");
+    is(NetMonitorView.detailsPaneHidden, true,
+      "The details pane should be hidden when the frontend is opened.");
+
+    aMonitor.panelWin.once("NetMonitor:NetworkEvent", () => {
+      is(document.querySelector(".requests-menu-empty-notice")
+        .hasAttribute("hidden"), true,
+        "The empty notice should be hidden after the first request.");
+      is(RequestsMenu.itemCount, 1,
+        "The requests menu should not be empty after the first request.");
+      is(NetMonitorView.detailsPaneHidden, true,
+        "The details pane should still be hidden after the first request.");
+
+      aMonitor.panelWin.once("NetMonitor:NetworkEvent", () => {
+        is(document.querySelector(".requests-menu-empty-notice")
+          .hasAttribute("hidden"), true,
+          "The empty notice should be still hidden after a reload.");
+        is(RequestsMenu.itemCount, 1,
+          "The requests menu should not be empty after a reload.");
+        is(NetMonitorView.detailsPaneHidden, true,
+          "The details pane should still be hidden after a reload.");
+
+        teardown(aMonitor).then(finish);
+      });
+
+      aDebuggee.location.reload();
+    });
+
+    aDebuggee.location.reload();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_status-codes.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if requests display the correct status code and text in the UI.
+ */
+
+function test() {
+  initNetMonitor(STATUS_CODES_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, L10N, NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+    RequestsMenu.lazyUpdate = false;
+    NetworkDetails._params.lazyEmpty = false;
+
+    waitForNetworkEvents(aMonitor, 5).then(() => {
+      let requestItems = [];
+
+      verifyRequestItemTarget(requestItems[0] = RequestsMenu.getItemAtIndex(0),
+        "GET", STATUS_CODES_SJS + "?sts=100", {
+          status: 101,
+          type: "plain",
+          size: "0.00kb",
+          time: true
+        });
+      verifyRequestItemTarget(requestItems[1] = RequestsMenu.getItemAtIndex(1),
+        "GET", STATUS_CODES_SJS + "?sts=200", {
+          status: 202,
+          type: "plain",
+          size: "0.02kb",
+          time: true
+        });
+      verifyRequestItemTarget(requestItems[2] = RequestsMenu.getItemAtIndex(2),
+        "GET", STATUS_CODES_SJS + "?sts=300", {
+          status: 303,
+          type: "plain",
+          size: "0.00kb",
+          time: true
+        });
+      verifyRequestItemTarget(requestItems[3] = RequestsMenu.getItemAtIndex(3),
+        "GET", STATUS_CODES_SJS + "?sts=400", {
+          status: 404,
+          type: "plain",
+          size: "0.02kb",
+          time: true
+        });
+      verifyRequestItemTarget(requestItems[4] = RequestsMenu.getItemAtIndex(4),
+        "GET", STATUS_CODES_SJS + "?sts=500", {
+          status: 501,
+          type: "plain",
+          size: "0.02kb",
+          time: true
+        });
+
+      // Test summaries...
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll("#details-pane tab")[0]);
+
+      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[0].target);
+      testSummary("GET", STATUS_CODES_SJS + "?sts=100", "101", "Switching Protocols");
+
+      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[1].target);
+      testSummary("GET", STATUS_CODES_SJS + "?sts=200", "202", "Created");
+
+      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[2].target);
+      testSummary("GET", STATUS_CODES_SJS + "?sts=300", "303", "See Other");
+
+      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[3].target);
+      testSummary("GET", STATUS_CODES_SJS + "?sts=400", "404", "Not Found");
+
+      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[4].target);
+      testSummary("GET", STATUS_CODES_SJS + "?sts=500", "501", "Not Implemented");
+
+      // Test params...
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll("#details-pane tab")[2]);
+
+      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[0].target);
+      testParamsTab("\"100\"");
+
+      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[1].target);
+      testParamsTab("\"200\"");
+
+      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[2].target);
+      testParamsTab("\"300\"");
+
+      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[3].target);
+      testParamsTab("\"400\"");
+
+      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[4].target);
+      testParamsTab("\"500\"");
+
+      // We're done here.
+      teardown(aMonitor).then(finish);
+
+      function testSummary(aMethod, aUrl, aStatus, aStatusText) {
+        let tab = document.querySelectorAll("#details-pane tab")[0];
+        let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
+
+        is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
+          aUrl, "The url summary value is incorrect.");
+        is(tabpanel.querySelector("#headers-summary-method-value").getAttribute("value"),
+          aMethod, "The method summary value is incorrect.");
+        is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("code"),
+          aStatus, "The status summary code is incorrect.");
+        is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
+          aStatus + " " + aStatusText, "The status summary value is incorrect.");
+      }
+
+      function testParamsTab(aStatusParamValue) {
+        let tab = document.querySelectorAll("#details-pane tab")[2];
+        let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+        is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+          "There should be 1 param scope displayed in this tabpanel.");
+        is(tabpanel.querySelectorAll(".variable-or-property").length, 1,
+          "There should be 1 param value displayed in this tabpanel.");
+        is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+          "The empty notice should not be displayed in this tabpanel.");
+
+        let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+
+        is(paramsScope.querySelector(".name").getAttribute("value"),
+          L10N.getStr("paramsQueryString"),
+          "The params scope doesn't have the correct title.");
+
+        is(paramsScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
+          "sts", "The param name was incorrect.");
+        is(paramsScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
+          aStatusParamValue, "The param value was incorrect.");
+
+        is(tabpanel.querySelector("#request-params-box")
+          .hasAttribute("hidden"), false,
+          "The request params box should not be hidden.");
+
+        is(tabpanel.querySelector("#request-post-data-textarea-box")
+          .hasAttribute("hidden"), true,
+          "The request post data textarea box should be hidden.");
+      }
+    });
+
+    aDebuggee.performRequests();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/head.js
@@ -0,0 +1,224 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+let { Promise } = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let { TargetFactory } = Cu.import("resource:///modules/devtools/Target.jsm", {});
+let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+
+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 STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html";
+const POST_DATA_URL = EXAMPLE_URL + "html_post-data-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 TEST_IMAGE = EXAMPLE_URL + "test-image.png";
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+// Enable logging for all the relevant tests.
+let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+
+registerCleanupFunction(() => {
+  info("finish() was called, cleaning up...");
+  Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+});
+
+function addTab(aUrl, aWindow) {
+  info("Adding tab: " + aUrl);
+
+  let deferred = Promise.defer();
+  let targetWindow = aWindow || window;
+  let targetBrowser = targetWindow.gBrowser;
+
+  targetWindow.focus();
+  let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
+
+  tab.addEventListener("load", function onLoad() {
+    tab.removeEventListener("load", onLoad, true);
+    deferred.resolve(tab);
+  }, true);
+
+  return deferred.promise;
+}
+
+function removeTab(aTab, aWindow) {
+  info("Removing tab.");
+
+  let targetWindow = aWindow || window;
+  let targetBrowser = targetWindow.gBrowser;
+
+  targetBrowser.removeTab(aTab);
+}
+
+function initNetMonitor(aUrl, aWindow) {
+  info("Initializing a network monitor pane.");
+
+  return addTab(aUrl).then((aTab) => {
+    info("Net tab added successfully: " + aUrl);
+
+    let deferred = Promise.defer();
+    let debuggee = aTab.linkedBrowser.contentWindow.wrappedJSObject;
+    let target = TargetFactory.forTab(aTab);
+
+    gDevTools.showToolbox(target, "netmonitor").then((aToolbox) => {
+      info("Netork monitor pane shown successfully.");
+
+      let monitor = aToolbox.getCurrentPanel();
+      deferred.resolve([aTab, debuggee, monitor]);
+    });
+
+    return deferred.promise;
+  });
+}
+
+function restartNetMonitor(aMonitor, aNewUrl) {
+  info("Restarting the specified network monitor.");
+
+  let deferred = Promise.defer();
+  let tab = aMonitor.target.tab;
+  let url = aNewUrl || tab.linkedBrowser.contentWindow.wrappedJSObject.location.href;
+
+  aMonitor.once("destroyed", () => initNetMonitor(url).then(deferred.resolve));
+  removeTab(tab);
+
+  return deferred.promise;
+}
+
+function teardown(aMonitor) {
+  info("Destroying the specified network monitor.");
+
+  let deferred = Promise.defer();
+  let tab = aMonitor.target.tab;
+
+  aMonitor.once("destroyed", deferred.resolve);
+  removeTab(tab);
+
+  return deferred.promise;
+}
+
+function waitForNetworkEvents(aMonitor, aGetRequests, aPostRequests = 0) {
+  let deferred = Promise.defer();
+
+  let panel = aMonitor.panelWin;
+  let genericEvents = 0;
+  let postEvents = 0;
+
+  function onGenericEvent() {
+    genericEvents++;
+    maybeResolve();
+  }
+
+  function onPostEvent() {
+    postEvents++;
+    maybeResolve();
+  }
+
+  function maybeResolve() {
+    info("> Network events progress: " +
+      genericEvents + "/" + ((aGetRequests + aPostRequests) * 13) + ", " +
+      postEvents + "/" + (aPostRequests * 2));
+
+    // There are 15 updates which need to be fired for a request to be
+    // considered finished. RequestPostData isn't fired for non-POST requests.
+    if (genericEvents == (aGetRequests + aPostRequests) * 13 &&
+        postEvents == aPostRequests * 2) {
+
+      panel.off("NetMonitor:NetworkEventUpdating:RequestHeaders", onGenericEvent);
+      panel.off("NetMonitor:NetworkEventUpdated:RequestHeaders", onGenericEvent);
+      panel.off("NetMonitor:NetworkEventUpdating:RequestCookies", onGenericEvent);
+      panel.off("NetMonitor:NetworkEventUpdating:RequestPostData", onPostEvent);
+      panel.off("NetMonitor:NetworkEventUpdated:RequestPostData", onPostEvent);
+      panel.off("NetMonitor:NetworkEventUpdated:RequestCookies", onGenericEvent);
+      panel.off("NetMonitor:NetworkEventUpdating:ResponseHeaders", onGenericEvent);
+      panel.off("NetMonitor:NetworkEventUpdated:ResponseHeaders", onGenericEvent);
+      panel.off("NetMonitor:NetworkEventUpdating:ResponseCookies", onGenericEvent);
+      panel.off("NetMonitor:NetworkEventUpdated:ResponseCookies", onGenericEvent);
+      panel.off("NetMonitor:NetworkEventUpdating:ResponseStart", onGenericEvent);
+      panel.off("NetMonitor:NetworkEventUpdating:ResponseContent", onGenericEvent);
+      panel.off("NetMonitor:NetworkEventUpdated:ResponseContent", onGenericEvent);
+      panel.off("NetMonitor:NetworkEventUpdating:EventTimings", onGenericEvent);
+      panel.off("NetMonitor:NetworkEventUpdated:EventTimings", onGenericEvent);
+
+      executeSoon(deferred.resolve);
+    }
+  }
+
+  panel.on("NetMonitor:NetworkEventUpdating:RequestHeaders", onGenericEvent);
+  panel.on("NetMonitor:NetworkEventUpdated:RequestHeaders", onGenericEvent);
+  panel.on("NetMonitor:NetworkEventUpdating:RequestCookies", onGenericEvent);
+  panel.on("NetMonitor:NetworkEventUpdating:RequestPostData", onPostEvent);
+  panel.on("NetMonitor:NetworkEventUpdated:RequestPostData", onPostEvent);
+  panel.on("NetMonitor:NetworkEventUpdated:RequestCookies", onGenericEvent);
+  panel.on("NetMonitor:NetworkEventUpdating:ResponseHeaders", onGenericEvent);
+  panel.on("NetMonitor:NetworkEventUpdated:ResponseHeaders", onGenericEvent);
+  panel.on("NetMonitor:NetworkEventUpdating:ResponseCookies", onGenericEvent);
+  panel.on("NetMonitor:NetworkEventUpdated:ResponseCookies", onGenericEvent);
+  panel.on("NetMonitor:NetworkEventUpdating:ResponseStart", onGenericEvent);
+  panel.on("NetMonitor:NetworkEventUpdating:ResponseContent", onGenericEvent);
+  panel.on("NetMonitor:NetworkEventUpdated:ResponseContent", onGenericEvent);
+  panel.on("NetMonitor:NetworkEventUpdating:EventTimings", onGenericEvent);
+  panel.on("NetMonitor:NetworkEventUpdated:EventTimings", onGenericEvent);
+
+  return deferred.promise;
+}
+
+function verifyRequestItemTarget(aRequestItem, aMethod, aUrl, aData = {}) {
+  info("> Verifying: " + aMethod + " " + aUrl + " " + aData.toSource());
+  info("> Request: " + aRequestItem.attachment.toSource());
+
+  let { status, type, size, time } = aData;
+  let { attachment, target } = aRequestItem
+
+  let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
+  let name = uri.fileName || "/";
+  let query = uri.query;
+  let hostPort = uri.hostPort;
+
+  is(attachment.method, aMethod,
+    "The attached method is incorrect.");
+
+  is(attachment.url, aUrl,
+    "The attached url is incorrect.");
+
+  is(target.querySelector(".requests-menu-method").getAttribute("value"),
+    aMethod, "The displayed method is incorrect.");
+
+  is(target.querySelector(".requests-menu-file").getAttribute("value"),
+    name + (query ? "?" + query : ""), "The displayed file is incorrect.");
+
+  is(target.querySelector(".requests-menu-domain").getAttribute("value"),
+    hostPort, "The displayed domain is incorrect.");
+
+  if (status !== undefined) {
+    let value = target.querySelector(".requests-menu-status").getAttribute("code");
+    info("Displayed status: " + value);
+    is(value, status, "The displayed status is incorrect.");
+  }
+  if (type !== undefined) {
+    let value = target.querySelector(".requests-menu-type").getAttribute("value");
+    info("Displayed type: " + value);
+    is(value, type, "The displayed type is incorrect.");
+  }
+  if (size !== undefined) {
+    let value = target.querySelector(".requests-menu-size").getAttribute("value");
+    info("Displayed size: " + value);
+    is(value, size, "The displayed size is incorrect.");
+  }
+  if (time !== undefined) {
+    let value = target.querySelector(".requests-menu-timings-total").getAttribute("value");
+    info("Displayed time: " + value);
+    ok(~~(value.match(/[0-9]+/)) >= 0, "The displayed time is incorrect.");
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_content-type-test-page.html
@@ -0,0 +1,43 @@
+<!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", function() {
+                    // Done.
+                  });
+                });
+              });
+            });
+          });
+        });
+      }
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_navigate-test-page.html
@@ -0,0 +1,13 @@
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Network Monitor test page</title>
+  </head>
+
+  <body>
+    <p>Navigation test</p>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_post-data-test-page.html
@@ -0,0 +1,72 @@
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Network Monitor test page</title>
+    <style>
+      input {
+        display: block;
+        margin: 12px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <p>POST data test</p>
+    <form enctype="multipart/form-data" method="post" name="form-name">
+      <input type="text" name="text" placeholder="text" value="Some text..."/>
+      <input type="email" name="email" placeholder="email"/>
+      <input type="range" name="range" value="42"/>
+      <input type="button" value="Post me!" onclick="window.form()">
+    </form>
+
+    <script type="text/javascript">
+      function post(aAddress, aMessage, aCallback) {
+        var xhr = new XMLHttpRequest();
+        xhr.open("POST", aAddress, true);
+        xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+
+        var data = "";
+        for (var i in aMessage) {
+          data += "&" + i + "=" + aMessage[i];
+        }
+
+        xhr.onreadystatechange = function() {
+          if (this.readyState == this.DONE) {
+            aCallback();
+          }
+        };
+        xhr.send(data);
+      }
+
+      function form(aAddress, aForm, aCallback) {
+        var formData = new FormData(document.forms.namedItem(aForm));
+        formData.append("Custom field", "Extra data");
+
+        var xhr = new XMLHttpRequest();
+        xhr.open("POST", aAddress, true);
+
+        xhr.onreadystatechange = function() {
+          if (this.readyState == this.DONE) {
+            aCallback();
+          }
+        };
+        xhr.send(formData);
+      }
+
+      function performRequests() {
+        var url = "sjs_simple-test-server.sjs";
+        var url1 = url + "?foo=bar&baz=42&type=urlencoded";
+        var url2 = url + "?foo=bar&baz=42&type=multipart";
+
+        post(url1, { foo: "bar", baz: 123 }, function() {
+          form(url2, "form-name", function() {
+            // Done.
+          });
+        });
+      }
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_simple-test-page.html
@@ -0,0 +1,13 @@
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Network Monitor test page</title>
+  </head>
+
+  <body>
+    <p>Simple test</p>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_status-codes-test-page.html
@@ -0,0 +1,41 @@
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Network Monitor test page</title>
+  </head>
+
+  <body>
+    <p>Status codes 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_status-codes-test-server.sjs?sts=100", function() {
+          get("sjs_status-codes-test-server.sjs?sts=200", function() {
+            get("sjs_status-codes-test-server.sjs?sts=300", function() {
+              get("sjs_status-codes-test-server.sjs?sts=400", function() {
+                get("sjs_status-codes-test-server.sjs?sts=500", function() {
+                  // Done.
+                });
+              });
+            });
+          });
+        });
+      }
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/moz.build
@@ -0,0 +1,5 @@
+# vim: set filetype=python:
+# 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/.
+
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+   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];
+
+  Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer).initWithCallback(() => {
+    switch (format) {
+      case "xml":
+        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setHeader("Content-Type", "text/xml; charset=utf-8", false);
+        response.write("<label value='greeting'>Hello XML!</label>");
+        response.finish();
+        break;
+      case "css":
+        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setHeader("Content-Type", "text/css; charset=utf-8", false);
+        response.write("body:pre { content: 'Hello CSS!' }");
+        response.finish();
+        break;
+      case "js":
+        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setHeader("Content-Type", "application/javascript; charset=utf-8", false);
+        response.write("function() { return 'Hello JS!'; }");
+        response.finish();
+        break;
+      case "json":
+        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.setHeader("Content-Type", "application/json; charset=utf-8", false);
+        response.write("{ \"greeting\": \"Hello JSON!\" }");
+        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();
+        break;
+    }
+  }, 10, Ci.nsITimer.TYPE_ONE_SHOT); // Make sure this request takes a few ms.
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/sjs_simple-test-server.sjs
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "Och Aye");
+  response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+  response.setHeader("Foo-Bar", "baz", false);
+  response.write("Hello world!");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/sjs_status-codes-test-server.sjs
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+  let params = request.queryString.split("&");
+  let status = params.filter((s) => s.contains("sts="))[0].split("=")[1];
+
+  switch (status) {
+    case "100":
+      response.setStatusLine(request.httpVersion, 101, "Switching Protocols");
+      break;
+    case "200":
+      response.setStatusLine(request.httpVersion, 202, "Created");
+      break;
+    case "300":
+      response.setStatusLine(request.httpVersion, 303, "See Other");
+      break;
+    case "400":
+      response.setStatusLine(request.httpVersion, 404, "Not Found");
+      break;
+    case "500":
+      response.setStatusLine(request.httpVersion, 501, "Not Implemented");
+      break;
+  }
+  response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+  response.write("Hello status code " + status + "!");
+}
new file mode 100644
index 0000000000000000000000000000000000000000..769c636340e11f9d2a0b7eb6a84d574dd9563f0c
GIT binary patch
literal 580
zc$@)50=xZ*P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004XF*Lt006JZ
zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUz)=5M`RCwBA
z{Qv(y10?_;fS8au@87>?@9gaSt*os4-<6T^zln*-e<1(Y>eZ{a-@A8D7MlS80mJ}u
z0SKQtbH>fs*!aH-1H=C_K)ecw?*efe5W7I}s#U9Y!PLVrKmdV>nKNfT1{(H16si$K
zmjm&CV+al6D=8`c7X*o+82}JK4A3z64>JJB_`e(E3S)?2<zN>X{|^lf1sei(+1<Me
zFarPr2&}oIqvIC?)OL^o?-&p^!wh-{#k-;2f_VoZfS{%bLi}zFF<>UtAY{W<LBj?n
zz6$CsfB=HI;P*^44eyZn#$YcBg1rF~2L~`P&;bI75#$0;v@zVf$It;Z4d`qJS0Dzu
zh@l)BQx!mb7Roj*FK2kaXAgtR*|Q9LfP8=e0=od{pY0$UI-sVXf!f>w#jt?wfcn3@
zy!=0d5|Evi_8%aC7~Z{m#}1AKK|~<_M{=g1px}QcP=ErR57Iaj7$e5OumVLr$n^jL
z1djz!sJcLHd57c@W2lWFi_p^m2m=HVoB>kgf@C|$pqa*ykjAAMgaHBwo)>`0c=vmt
z+g3yQpuk*x7A(#H^u|wInF%0(FiZq_r2`t3pnwGh6fWCA7$ATcDb3CR0R{jJCzQv)
SYsoAC0000<MNUMnLSTYrIq9PS
--- a/browser/devtools/shared/Parser.jsm
+++ b/browser/devtools/shared/Parser.jsm
@@ -85,17 +85,17 @@ Parser.prototype = {
     this._cache.set(aUrl, pool);
     return pool;
   },
 
   /**
    * Clears all the parsed sources from cache.
    */
   clearCache: function P_clearCache() {
-    this._cache = new Map();
+    this._cache.clear();
   },
 
   _cache: null
 };
 
 /**
  * A pool handling a collection of AST nodes generated by the reflection API.
  *
--- a/browser/devtools/shared/widgets/SideMenuWidget.jsm
+++ b/browser/devtools/shared/widgets/SideMenuWidget.jsm
@@ -76,16 +76,17 @@ SideMenuWidget.prototype = {
    *        A tooltip attribute for the displayed item.
    * @param string aGroup [optional]
    *        The group to place the displayed item into.
    * @return nsIDOMNode
    *         The element associated with the displayed item.
    */
   insertItemAt: function SMW_insertItemAt(aIndex, aContents, aTooltip = "", aGroup = "") {
     this.ensureSelectionIsVisible(true, true); // Don't worry, it's delayed.
+
     let group = this._getGroupForName(aGroup);
     return group.insertItemAt(aIndex, aContents, aTooltip);
   },
 
   /**
    * Returns the child node in this container situated at the specified index.
    *
    * @param number aIndex
@@ -121,17 +122,17 @@ SideMenuWidget.prototype = {
     let list = this._list;
     let firstChild;
 
     while (firstChild = list.firstChild) {
       list.removeChild(firstChild);
     }
     this._selectedItem = null;
 
-    this._groupsByName = new Map();
+    this._groupsByName.clear();
     this._orderedGroupElementsArray.length = 0;
     this._orderedMenuElementsArray.length = 0;
   },
 
   /**
    * Gets the currently selected child node in this container.
    * @return nsIDOMNode
    */
@@ -399,23 +400,27 @@ SideMenuGroup.prototype = {
    * @return nsIDOMNode
    *         The element associated with the displayed item.
    */
   insertItemAt: function SMG_insertItemAt(aIndex, aContents, aTooltip) {
     let list = this._list;
     let menuArray = this._menuElementsArray;
     let item = new SideMenuItem(this, aContents, aTooltip);
 
+    // Invalidate any notices set on the owner widget.
+    this.ownerView.removeAttribute("notice");
+
     if (aIndex >= 0) {
       list.insertBefore(item._container, list.childNodes[aIndex]);
       menuArray.splice(aIndex, 0, item._target);
     } else {
       list.appendChild(item._container);
       menuArray.push(item._target);
     }
+
     return item._target;
   },
 
   /**
    * Inserts this group in the parent container at the specified index.
    *
    * @param number aIndex
    *        The position in the container intended for this group.
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -4,29 +4,33 @@
  * 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 DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
+const LAZY_EMPTY_DELAY = 150; // ms
 const LAZY_EXPAND_DELAY = 50; // ms
 const LAZY_APPEND_DELAY = 100; // ms
 const LAZY_APPEND_BATCH = 100; // nodes
 const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
 const PAGE_SIZE_MAX_JUMPS = 30;
 const SEARCH_ACTION_MAX_DELAY = 300; // ms
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this,
-  "WebConsoleUtils", "resource://gre/modules/devtools/WebConsoleUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
+  "resource://gre/modules/devtools/NetworkHelper.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
+  "resource://gre/modules/devtools/WebConsoleUtils.jsm");
 
 this.EXPORTED_SYMBOLS = ["VariablesView"];
 
 /**
  * Debugger localization strings.
  */
 const STR = Services.strings.createBundle(DBG_STRINGS_URI);
 
@@ -37,18 +41,21 @@ const STR = Services.strings.createBundl
  *
  * To allow replacing variable or property values in this view, provide an
  * "eval" function property. To allow replacing variable or property names,
  * provide a "switch" function. To handle deleting variables or properties,
  * provide a "delete" function.
  *
  * @param nsIDOMNode aParentNode
  *        The parent node to hold this view.
+ * @param object aFlags [optional]
+ *        An object contaning initialization options for this view.
+ *        e.g. { lazyEmpty: true, searchEnabled: true ... }
  */
-this.VariablesView = function VariablesView(aParentNode) {
+this.VariablesView = function VariablesView(aParentNode, aFlags = {}) {
   this._store = new Map();
   this._itemsByElement = new WeakMap();
   this._prevHierarchy = new Map();
   this._currHierarchy = new Map();
 
   this._parent = aParentNode;
   this._parent.classList.add("variables-view-container");
   this._appendEmptyNotice();
@@ -58,16 +65,20 @@ this.VariablesView = function VariablesV
   this._onViewKeyPress = this._onViewKeyPress.bind(this);
 
   // Create an internal scrollbox container.
   this._list = this.document.createElement("scrollbox");
   this._list.setAttribute("orient", "vertical");
   this._list.addEventListener("keypress", this._onViewKeyPress, false);
   this._parent.appendChild(this._list);
   this._boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+
+  for (let name in aFlags) {
+    this[name] = aFlags[name];
+  }
 };
 
 VariablesView.prototype = {
   /**
    * Helper setter for populating this container with a raw object.
    *
    * @param object aData
    *        The raw object to display. You can only provide this object
@@ -118,18 +129,18 @@ VariablesView.prototype = {
 
     let list = this._list;
     let firstChild;
 
     while (firstChild = list.firstChild) {
       list.removeChild(firstChild);
     }
 
-    this._store = new Map();
-    this._itemsByElement = new WeakMap();
+    this._store.clear();
+    this._itemsByElement.clear();
 
     this._appendEmptyNotice();
     this._toggleSearchVisibility(false);
   },
 
   /**
    * Emptying this container and rebuilding it immediately afterwards would
    * result in a brief redraw flicker, because the previously expanded nodes
@@ -144,18 +155,18 @@ VariablesView.prototype = {
    *
    * @see VariablesView.empty
    * @see VariablesView.commitHierarchy
    */
   _emptySoon: function VV__emptySoon(aTimeout) {
     let prevList = this._list;
     let currList = this._list = this.document.createElement("scrollbox");
 
-    this._store = new Map();
-    this._itemsByElement = new WeakMap();
+    this._store.clear();
+    this._itemsByElement.clear();
 
     this._emptyTimeout = this.window.setTimeout(function() {
       this._emptyTimeout = null;
 
       prevList.removeEventListener("keypress", this._onViewKeyPress, false);
       currList.addEventListener("keypress", this._onViewKeyPress, false);
       currList.setAttribute("orient", "vertical");
 
@@ -168,17 +179,17 @@ VariablesView.prototype = {
         this._toggleSearchVisibility(false);
       }
     }.bind(this), aTimeout);
   },
 
   /**
    * The amount of time (in milliseconds) it takes to empty this view lazily.
    */
-  lazyEmptyDelay: 150,
+  lazyEmptyDelay: LAZY_EMPTY_DELAY,
 
   /**
    * Specifies if this view may be emptied lazily.
    * @see VariablesView.prototype.empty
    */
   lazyEmpty: false,
 
   /**
@@ -210,16 +221,22 @@ VariablesView.prototype = {
    * user interaction. If null, then deletions are disabled.
    *
    * This property is applied recursively onto each scope in this view and
    * affects only the child nodes when they're created.
    */
   delete: null,
 
   /**
+   * Specifies if after an eval or switch operation, the variable or property
+   * which has been edited should be disabled.
+   */
+  preventDisableOnChage: false,
+
+  /**
    * The tooltip text shown on a variable or property's value if an |eval|
    * function is provided, in order to change the variable or property's value.
    *
    * This flag is applied recursively onto each scope in this view and
    * affects only the child nodes when they're created.
    */
   editableValueTooltip: STR.GetStringFromName("variablesEditableValueTooltip"),
 
@@ -1078,21 +1095,23 @@ Scope.prototype = {
    *        e.g. - { value: 42 }
    *             - { value: true }
    *             - { value: "nasu" }
    *             - { value: { type: "undefined" } }
    *             - { value: { type: "null" } }
    *             - { value: { type: "object", class: "Object" } }
    *             - { get: { type: "object", class: "Function" },
    *                 set: { type: "undefined" } }
+   * @param boolean aRelaxed
+   *        True if name duplicates should be allowed.
    * @return Variable
    *         The newly created Variable instance, null if it already exists.
    */
-  addVar: function S_addVar(aName = "", aDescriptor = {}) {
-    if (this._store.has(aName)) {
+  addVar: function S_addVar(aName = "", aDescriptor = {}, aRelaxed = false) {
+    if (this._store.has(aName) && !aRelaxed) {
       return null;
     }
 
     let variable = new Variable(this, aName, aDescriptor);
     this._store.set(aName, variable);
     this._variablesView._currHierarchy.set(variable._absoluteName, variable);
     this._variablesView._itemsByElement.set(variable._target, variable);
     variable.header = !!aName;
@@ -1850,21 +1869,23 @@ ViewHelpers.create({ constructor: Variab
    *        e.g. - { value: 42 }
    *             - { value: true }
    *             - { value: "nasu" }
    *             - { value: { type: "undefined" } }
    *             - { value: { type: "null" } }
    *             - { value: { type: "object", class: "Object" } }
    *             - { get: { type: "object", class: "Function" },
    *                 set: { type: "undefined" } }
+   * @param boolean aRelaxed
+   *        True if name duplicates should be allowed.
    * @return Property
    *         The newly created Property instance, null if it already exists.
    */
-  addProperty: function V_addProperty(aName = "", aDescriptor = {}) {
-    if (this._store.has(aName)) {
+  addProperty: function V_addProperty(aName = "", aDescriptor = {}, aRelaxed = false) {
+    if (this._store.has(aName) && !aRelaxed) {
       return null;
     }
 
     let property = new Property(this, aName, aDescriptor);
     this._store.set(aName, property);
     this._variablesView._currHierarchy.set(property._absoluteName, property);
     this._variablesView._itemsByElement.set(property._target, property);
     property.header = !!aName;
@@ -1913,16 +1934,17 @@ ViewHelpers.create({ constructor: Variab
   /**
    * Populates this variable to contain all the properties of an object.
    *
    * @param object aObject
    *        The raw object you want to display.
    * @param object aOptions [optional]
    *        Additional options for adding the properties. Supported options:
    *        - sorted: true to sort all the properties before adding them
+   *        - expanded: true to expand all the properties after adding them
    */
   populate: function V_populate(aObject, aOptions = {}) {
     // Retrieve the properties only once.
     if (this._fetched) {
       return;
     }
     this._fetched = true;
 
@@ -1932,19 +1954,25 @@ ViewHelpers.create({ constructor: Variab
     // Sort all of the properties before adding them, if preferred.
     if (aOptions.sorted) {
       propertyNames.sort();
     }
     // Add all the variable properties.
     for (let name of propertyNames) {
       let descriptor = Object.getOwnPropertyDescriptor(aObject, name);
       if (descriptor.get || descriptor.set) {
-        this._addRawNonValueProperty(name, descriptor);
+        let prop = this._addRawNonValueProperty(name, descriptor);
+        if (aOptions.expanded) {
+          prop.expanded = true;
+        }
       } else {
-        this._addRawValueProperty(name, descriptor, aObject[name]);
+        let prop = this._addRawValueProperty(name, descriptor, aObject[name]);
+        if (aOptions.expanded) {
+          prop.expanded = true;
+        }
       }
     }
     // Add the variable's __proto__.
     if (prototype) {
       this._addRawValueProperty("__proto__", {}, prototype);
     }
   },
 
@@ -1967,46 +1995,51 @@ ViewHelpers.create({ constructor: Variab
    *
    * @param string aName
    *        The property's name.
    * @param object aDescriptor
    *        Specifies the exact property descriptor as returned by a call to
    *        Object.getOwnPropertyDescriptor.
    * @param object aValue
    *        The raw property value you want to display.
+   * @return Property
+   *         The newly added property instance.
    */
   _addRawValueProperty: function V__addRawValueProperty(aName, aDescriptor, aValue) {
     let descriptor = Object.create(aDescriptor);
     descriptor.value = VariablesView.getGrip(aValue);
 
     let propertyItem = this.addProperty(aName, descriptor);
     propertyItem._sourceValue = aValue;
 
     // Add an 'onexpand' callback for the property, lazily handling
     // the addition of new child properties.
     if (!VariablesView.isPrimitive(descriptor)) {
       propertyItem.onexpand = this._populateTarget;
     }
+    return propertyItem;
   },
 
   /**
    * Adds a property for this variable based on a getter/setter descriptor.
    *
    * @param string aName
    *        The property's name.
    * @param object aDescriptor
    *        Specifies the exact property descriptor as returned by a call to
    *        Object.getOwnPropertyDescriptor.
+   * @return Property
+   *         The newly added property instance.
    */
   _addRawNonValueProperty: function V__addRawNonValueProperty(aName, aDescriptor) {
     let descriptor = Object.create(aDescriptor);
     descriptor.get = VariablesView.getGrip(aDescriptor.get);
     descriptor.set = VariablesView.getGrip(aDescriptor.set);
 
-    this.addProperty(aName, descriptor);
+    return this.addProperty(aName, descriptor);
   },
 
   /**
    * Gets this variable's path to the topmost scope.
    * For example, a symbolic name may look like "arguments['0']['foo']['bar']".
    * @return string
    */
   get symbolicName() this._symbolicName,
@@ -2043,30 +2076,28 @@ ViewHelpers.create({ constructor: Variab
    *             - true
    *             - "nasu"
    *             - { type: "undefined" }
    *             - { type: "null" }
    *             - { type: "object", class: "Object" }
    */
   setGrip: function V_setGrip(aGrip) {
     // Don't allow displaying grip information if there's no name available.
-    if (!this._nameString) {
+    if (!this._nameString || aGrip === undefined || aGrip === null) {
       return;
     }
     // Getters and setters should display grip information in sub-properties.
     if (!this._isUndefined && (this.getter || this.setter)) {
       this._valueLabel.setAttribute("value", "");
       return;
     }
 
-    if (aGrip === undefined) {
-      aGrip = { type: "undefined" };
-    }
-    if (aGrip === null) {
-      aGrip = { type: "null" };
+    // Make sure the value is escaped unicode if it's a string.
+    if (typeof aGrip == "string") {
+      aGrip = NetworkHelper.convertToUnicode(unescape(aGrip));
     }
 
     let prevGrip = this._valueGrip;
     if (prevGrip) {
       this._valueLabel.classList.remove(VariablesView.getClass(prevGrip));
     }
     this._valueGrip = aGrip;
     this._valueString = VariablesView.getString(aGrip);
@@ -2251,23 +2282,23 @@ ViewHelpers.create({ constructor: Variab
    */
   _setAttributes: function V__setAttributes() {
     let descriptor = this._initialDescriptor;
     let name = this._nameString;
 
     if (this.ownerView.eval) {
       this._target.setAttribute("editable", "");
     }
-    if (!descriptor.configurable) {
+    if (!descriptor.null && !descriptor.configurable) {
       this._target.setAttribute("non-configurable", "");
     }
-    if (!descriptor.enumerable) {
+    if (!descriptor.null && !descriptor.enumerable) {
       this._target.setAttribute("non-enumerable", "");
     }
-    if (!descriptor.writable && !this.ownerView.getter && !this.ownerView.setter) {
+    if (!descriptor.null && !descriptor.writable && !this.ownerView.getter && !this.ownerView.setter) {
       this._target.setAttribute("non-writable", "");
     }
     if (name == "this") {
       this._target.setAttribute("self", "");
     }
     else if (name == "<exception>") {
       this._target.setAttribute("exception", "");
     }
@@ -2336,16 +2367,18 @@ ViewHelpers.create({ constructor: Variab
    *
    * @param nsIDOMNode aLabel
    *        The label which was replaced with a textbox.
    * @param object aCallbacks
    *        An object containing the onKeypress and onBlur callbacks.
    */
   _deactivateInput: function V__deactivateInput(aLabel, aInput, aCallbacks) {
     aInput.parentNode.replaceChild(aLabel, aInput);
+    this._variablesView._boxObject.scrollBy(-this._target.clientWidth, 0);
+
     aInput.removeEventListener("keypress", aCallbacks.onKeypress, false);
     aInput.removeEventListener("blur", aCallbacks.onBlur, false);
 
     this._locked = false;
     this.twisty = this._prevExpandable;
     this.expanded = this._prevExpanded;
 
     this._inputNode = null;
@@ -2449,33 +2482,37 @@ ViewHelpers.create({ constructor: Variab
    */
   _saveNameInput: function V__saveNameInput(e) {
     let input = e.target;
     let initialString = this._name.getAttribute("value");
     let currentString = input.value.trim();
     this._deactivateNameInput(e);
 
     if (initialString != currentString) {
-      this._disable();
-      this._name.value = currentString;
+      if (!this._variablesView.preventDisableOnChage) {
+        this._disable();
+        this._name.value = currentString;
+      }
       this.ownerView.switch(this, currentString);
     }
   },
 
   /**
    * Deactivates this variable's editable mode and evaluates the new value.
    */
   _saveValueInput: function V__saveValueInput(e) {
     let input = e.target;
     let initialString = this._valueLabel.getAttribute("value");
     let currentString = input.value.trim();
     this._deactivateValueInput(e);
 
     if (initialString != currentString) {
-      this._disable();
+      if (!this._variablesView.preventDisableOnChage) {
+        this._disable();
+      }
       this.ownerView.eval(this.evaluationMacro(this, currentString.trim()));
     }
   },
 
   /**
    * The current macro used to generate the string evaluated when performing
    * a variable or property value change.
    */
@@ -2630,27 +2667,27 @@ Property.prototype.__iterator__ = functi
   }
 };
 
 /**
  * Forget everything recorded about added scopes, variables or properties.
  * @see VariablesView.createHierarchy
  */
 VariablesView.prototype.clearHierarchy = function VV_clearHierarchy() {
-  this._prevHierarchy = new Map();
-  this._currHierarchy = new Map();
+  this._prevHierarchy.clear();
+  this._currHierarchy.clear();
 };
 
 /**
  * Start recording a hierarchy of any added scopes, variables or properties.
  * @see VariablesView.commitHierarchy
  */
 VariablesView.prototype.createHierarchy = function VV_createHierarchy() {
   this._prevHierarchy = this._currHierarchy;
-  this._currHierarchy = new Map();
+  this._currHierarchy = new Map(); // Don't clear, this is just simple swapping.
 };
 
 /**
  * Briefly flash the variables that changed between the previous and current
  * scope/variable/property hierarchies and reopen previously expanded nodes.
  */
 VariablesView.prototype.commitHierarchy = function VV_commitHierarchy() {
   let prevHierarchy = this._prevHierarchy;
--- a/browser/devtools/shared/widgets/ViewHelpers.jsm
+++ b/browser/devtools/shared/widgets/ViewHelpers.jsm
@@ -1,18 +1,24 @@
 /* -*- 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 Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
+const PANE_APPEARANCE_DELAY = 50;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
 this.EXPORTED_SYMBOLS = ["ViewHelpers", "MenuItem", "MenuContainer"];
 
 /**
  * Helpers for creating and messaging between UI components.
  */
 this.ViewHelpers = {
   /**
    * Sugar for prototypal inheritance using Object.create.
@@ -64,34 +70,210 @@ this.ViewHelpers = {
    * Helper delegating some of the DOM attribute methods of a node to a widget.
    * @see MenuContainer constructor
    *
    * @param object aWidget
    *        The widget to assign the methods to.
    * @param nsIDOMNode aNode
    *        A node to delegate the methods to.
    */
-  delegateWidgetAttributeMethods: function MC_delegateWidgetAttributeMethods(aWidget, aNode) {
+  delegateWidgetAttributeMethods: function VH_delegateWidgetAttributeMethods(aWidget, aNode) {
     aWidget.getAttribute = aNode.getAttribute.bind(aNode);
     aWidget.setAttribute = aNode.setAttribute.bind(aNode);
     aWidget.removeAttribute = aNode.removeAttribute.bind(aNode);
   },
 
   /**
    * Helper delegating some of the DOM event methods of a node to a widget.
    * @see MenuContainer constructor
    *
    * @param object aWidget
    *        The widget to assign the methods to.
    * @param nsIDOMNode aNode
    *        A node to delegate the methods to.
    */
-  delegateWidgetEventMethods: function MC_delegateWidgetEventMethods(aWidget, aNode) {
+  delegateWidgetEventMethods: function VH_delegateWidgetEventMethods(aWidget, aNode) {
     aWidget.addEventListener = aNode.addEventListener.bind(aNode);
     aWidget.removeEventListener = aNode.removeEventListener.bind(aNode);
+  },
+
+  /**
+   * Sets a side 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
+   *        - delayed: true to wait a few cycles before toggle
+   *        - callback: a function to invoke when the toggle finishes
+   * @param nsIDOMNode aPane
+   *        The element representing the pane to toggle.
+   */
+  togglePane: function VH_togglePane(aFlags, aPane) {
+    // Avoid useless toggles.
+    if (aFlags.visible == !aPane.hasAttribute("pane-collapsed")) {
+      if (aFlags.callback) aFlags.callback();
+      return;
+    }
+
+    // Computes and sets the pane margins in order to hide or show it.
+    function set() {
+      if (aFlags.visible) {
+        aPane.style.marginLeft = "0";
+        aPane.style.marginRight = "0";
+        aPane.removeAttribute("pane-collapsed");
+      } else {
+        let margin = ~~(aPane.getAttribute("width")) + 1;
+        aPane.style.marginLeft = -margin + "px";
+        aPane.style.marginRight = -margin + "px";
+        aPane.setAttribute("pane-collapsed", "");
+      }
+
+      // Invoke the callback when the transition ended.
+      if (aFlags.animated) {
+        aPane.addEventListener("transitionend", function onEvent() {
+          aPane.removeEventListener("transitionend", onEvent, false);
+          if (aFlags.callback) aFlags.callback();
+        }, false);
+      }
+      // Invoke the callback immediately since there's no transition.
+      else {
+        if (aFlags.callback) aFlags.callback();
+      }
+    }
+
+    // Add a class to the pane to handle min-widths, margins and animations.
+    if (!aPane.classList.contains("generic-toggled-side-pane")) {
+      aPane.classList.add("generic-toggled-side-pane");
+    }
+
+    // Hiding is always handled via margins, not the hidden attribute.
+    aPane.removeAttribute("hidden");
+
+    // The "animated" attributes enables animated toggles (slide in-out).
+    if (aFlags.animated) {
+      aPane.setAttribute("animated", "");
+    } else {
+      aPane.removeAttribute("animated");
+    }
+
+    // Sometimes it's useful delaying the toggle a few ticks to ensure
+    // a smoother slide in-out animation.
+    if (aFlags.delayed) {
+      aPane.ownerDocument.defaultView.setTimeout(set.bind(this), PANE_APPEARANCE_DELAY);
+    } else {
+      set.call(this);
+    }
+  }
+};
+
+/**
+ * Localization convenience methods.
+ *
+ * @param string aStringBundleName
+ *        The desired string bundle's name.
+ */
+ViewHelpers.L10N = function L10N(aStringBundleName) {
+  XPCOMUtils.defineLazyGetter(this, "stringBundle", () =>
+    Services.strings.createBundle(aStringBundleName));
+
+  XPCOMUtils.defineLazyGetter(this, "ellipsis", () =>
+    Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data);
+};
+
+ViewHelpers.L10N.prototype = {
+  stringBundle: null,
+
+  /**
+   * L10N shortcut function.
+   *
+   * @param string aName
+   * @return string
+   */
+  getStr: function L10N_getStr(aName) {
+    return this.stringBundle.GetStringFromName(aName);
+  },
+
+  /**
+   * L10N shortcut function.
+   *
+   * @param string aName
+   * @param array aArgs
+   * @return string
+   */
+  getFormatStr: function L10N_getFormatStr(aName, ...aArgs) {
+    return this.stringBundle.formatStringFromName(aName, aArgs, aArgs.length);
+  }
+};
+
+/**
+ * Shortcuts for lazily accessing and setting various preferences.
+ * Usage:
+ *   let prefs = new ViewHelpers.Prefs("root.path.to.branch", {
+ *     myIntPref: ["Int", "leaf.path.to.my-int-pref"],
+ *     myCharPref: ["Char", "leaf.path.to.my-char-pref"],
+ *     ...
+ *   });
+ *
+ *   prefs.myCharPref = "foo";
+ *   let aux = prefs.myCharPref;
+ *
+ * @param string aPrefsRoot
+ *        The root path to the required preferences branch.
+ * @param object aPrefsObject
+ *        An object containing { accessorName: [prefType, prefName] } keys.
+ */
+ViewHelpers.Prefs = function Prefs(aPrefsRoot = "", aPrefsObject = {}) {
+  this.root = aPrefsRoot;
+
+  for (let accessorName in aPrefsObject) {
+    let [prefType, prefName] = aPrefsObject[accessorName];
+    this.map(accessorName, prefType, prefName);
+  }
+};
+
+ViewHelpers.Prefs.prototype = {
+  /**
+   * Helper method for getting a pref value.
+   *
+   * @param string aType
+   * @param string aPrefName
+   * @return any
+   */
+  _get: function P__get(aType, aPrefName) {
+    if (this[aPrefName] === undefined) {
+      this[aPrefName] = Services.prefs["get" + aType + "Pref"](aPrefName);
+    }
+    return this[aPrefName];
+  },
+
+  /**
+   * Helper method for setting a pref value.
+   *
+   * @param string aType
+   * @param string aPrefName
+   * @param any aValue
+   */
+  _set: function P__set(aType, aPrefName, aValue) {
+    Services.prefs["set" + aType + "Pref"](aPrefName, aValue);
+    this[aPrefName] = aValue;
+  },
+
+  /**
+   * Maps a property name to a pref, defining lazy getters and setters.
+   *
+   * @param string aAccessorName
+   * @param string aType
+   * @param string aPrefName
+   */
+  map: function P_map(aAccessorName, aType, aPrefName) {
+    Object.defineProperty(this, aAccessorName, {
+      get: () => this._get(aType, [this.root, aPrefName].join(".")),
+      set: (aValue) => this._set(aType, [this.root, aPrefName].join("."), aValue)
+    });
   }
 };
 
 /**
  * A generic MenuItem is used to describe elements present in a MenuContainer.
  * The label, value and description properties are necessarily strings.
  * Iterable via "for (let childItem in parentItem) { }".
  *
@@ -214,17 +396,17 @@ MenuItem.prototype = {
    *
    * @param MenuItem aItem
    *        The item describing the element.
    * @param nsIDOMNode aElement
    *        The element displaying the item.
    */
   _entangleItem: function MI__entangleItem(aItem, aElement) {
     if (!this._itemsByElement) {
-      this._itemsByElement = new Map();
+      this._itemsByElement = new Map(); // This needs to be iterable.
     }
 
     this._itemsByElement.set(aElement, aItem);
     aItem.target = aElement;
   },
 
   /**
    * Untangles an item (model) from a displayed node element (view).
@@ -244,17 +426,23 @@ MenuItem.prototype = {
     aItem.target = null;
   },
 
   /**
    * Returns a string representing the object.
    * @return string
    */
   toString: function MI_toString() {
-    return this._label + " -> " + this._value;
+    if (this._label && this._value) {
+      return this._label + " -> " + this._value;
+    }
+    if (this.attachment) {
+      return this.attachment.toString();
+    }
+    return "(null)";
   },
 
   _label: "",
   _value: "",
   _description: "",
   target: null,
   finalize: null,
   attachment: null
@@ -340,17 +528,17 @@ MenuContainer.prototype = {
    *         The item associated with the displayed element if an unstaged push,
    *         undefined if the item was staged for a later commit.
    */
   push: function MC_push(aContents, aOptions = {}) {
     if (aContents instanceof Ci.nsIDOMNode ||
         aContents instanceof Ci.nsIDOMElement) {
       // Allow the insertion of prebuilt nodes.
       aOptions.node = aContents;
-      aContents = [];
+      aContents = ["", "", ""];
     }
 
     let [label, value, description] = aContents;
     let item = new MenuItem(aOptions.attachment, label, value, description);
 
     // Batch the item to be added later.
     if (aOptions.staged) {
       return void this._stagedItems.push({ item: item, options: aOptions });
@@ -430,19 +618,19 @@ MenuContainer.prototype = {
     this._container.setAttribute("notice", this.emptyText);
     this._container.setAttribute("label", this.emptyText);
     this._container.removeAttribute("tooltiptext");
 
     for (let [, item] of this._itemsByElement) {
       this._untangleItem(item);
     }
 
-    this._itemsByLabel = new Map();
-    this._itemsByValue = new Map();
-    this._itemsByElement = new Map();
+    this._itemsByLabel.clear();
+    this._itemsByValue.clear();
+    this._itemsByElement.clear();
     this._stagedItems.length = 0;
   },
 
   /**
    * Does not remove any item in this container. Instead, it overrides the
    * current label to signal that it is unavailable and removes the tooltip.
    */
   setUnavailable: function MC_setUnavailable() {
@@ -512,17 +700,17 @@ MenuContainer.prototype = {
    * Retrieves the item associated with the selected element.
    * @return MenuItem
    */
   get selectedItem() {
     let selectedElement = this._container.selectedItem;
     if (selectedElement) {
       return this._itemsByElement.get(selectedElement);
     }
-    return -1;
+    return null;
   },
 
   /**
    * Retrieves the selected element's index in this container.
    * @return number
    */
   get selectedIndex() {
     let selectedElement = this._container.selectedItem;
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
@@ -0,0 +1,130 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Network Monitor strings -->
+<!-- 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.emptyNotice): This is the label displayed
+  -  in the network table when empty. -->
+<!ENTITY netmonitorUI.emptyNotice         "Reload the page to see detailed information about network activity.">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.toolbar.method): This is the label displayed
+  -  in the network table toolbar, above the "method" column. -->
+<!ENTITY netmonitorUI.toolbar.method      "Method">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.toolbar.file): This is the label displayed
+  -  in the network table toolbar, above the "file" column. -->
+<!ENTITY netmonitorUI.toolbar.file        "File">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.toolbar.domain): This is the label displayed
+  -  in the network table toolbar, above the "domain" column. -->
+<!ENTITY netmonitorUI.toolbar.domain      "Domain">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.toolbar.type): This is the label displayed
+  -  in the network table toolbar, above the "type" column. -->
+<!ENTITY netmonitorUI.toolbar.type        "Type">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.toolbar.size): This is the label displayed
+  -  in the network table toolbar, above the "size" column. -->
+<!ENTITY netmonitorUI.toolbar.size        "Size">
+
+<!-- LOCALIZATION NOTE (netmonitorUI.toolbar.waterfall): This is the label displayed
+  -  in the network table toolbar, above the "waterfall" column. -->
+<!ENTITY netmonitorUI.toolbar.waterfall   "Timeline">
+
+<!-- LOCALIZATION NOTE (debuggerUI.tab.headers): This is the label displayed
+  -  in the network details pane identifying the headers tab. -->
+<!ENTITY netmonitorUI.tab.headers         "Headers">
+
+<!-- LOCALIZATION NOTE (debuggerUI.tab.cookies): This is the label displayed
+  -  in the network details pane identifying the cookies tab. -->
+<!ENTITY netmonitorUI.tab.cookies         "Cookies">
+
+<!-- LOCALIZATION NOTE (debuggerUI.tab.params): This is the label displayed
+  -  in the network details pane identifying the params tab. -->
+<!ENTITY netmonitorUI.tab.params          "Params">
+
+<!-- LOCALIZATION NOTE (debuggerUI.tab.response): This is the label displayed
+  -  in the network details pane identifying the response tab. -->
+<!ENTITY netmonitorUI.tab.response        "Response">
+
+<!-- LOCALIZATION NOTE (debuggerUI.tab.timings): This is the label displayed
+  -  in the network details pane identifying the timings tab. -->
+<!ENTITY netmonitorUI.tab.timings         "Timings">
+
+<!-- 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:">
+
+<!-- LOCALIZATION NOTE (debuggerUI.summary.method): This is the label displayed
+  -  in the network details headers tab identifying the method. -->
+<!ENTITY netmonitorUI.summary.method      "Request method:">
+
+<!-- LOCALIZATION NOTE (debuggerUI.summary.status): This is the label displayed
+  -  in the network details headers tab identifying the status code. -->
+<!ENTITY netmonitorUI.summary.status      "Status code:">
+
+<!-- LOCALIZATION NOTE (debuggerUI.summary.version): This is the label displayed
+  -  in the network details headers tab identifying the http version. -->
+<!ENTITY netmonitorUI.summary.version     "Version:">
+
+<!-- LOCALIZATION NOTE (debuggerUI.summary.size): This is the label displayed
+  -  in the network details headers tab identifying the headers size. -->
+<!ENTITY netmonitorUI.summary.size        "Headers size:">
+
+<!-- LOCALIZATION NOTE (debuggerUI.response.name): This is the label displayed
+  -  in the network details response tab identifying an image's file name. -->
+<!ENTITY netmonitorUI.response.name       "Name:">
+
+<!-- LOCALIZATION NOTE (debuggerUI.response.dimensions): This is the label displayed
+  -  in the network details response tab identifying an image's dimensions. -->
+<!ENTITY netmonitorUI.response.dimensions "Dimensions:">
+
+<!-- LOCALIZATION NOTE (debuggerUI.response.mime): This is the label displayed
+  -  in the network details response tab identifying an image's mime. -->
+<!ENTITY netmonitorUI.response.mime       "MIME Type:">
+
+<!-- LOCALIZATION NOTE (debuggerUI.response.encoding): This is the label displayed
+  -  in the network details response tab identifying an image's encoding. -->
+<!ENTITY netmonitorUI.response.encoding   "Encoding:">
+
+<!-- LOCALIZATION NOTE (debuggerUI.timings.blocked): This is the label displayed
+  -  in the network details timings tab identifying the amount of time spent
+  -  in a "blocked" state. -->
+<!ENTITY netmonitorUI.timings.blocked     "Blocked:">
+
+<!-- LOCALIZATION NOTE (debuggerUI.timings.dns): This is the label displayed
+  -  in the network details timings tab identifying the amount of time spent
+  -  in a "dns" state. -->
+<!ENTITY netmonitorUI.timings.dns         "DNS resolution:">
+
+<!-- LOCALIZATION NOTE (debuggerUI.timings.connect): This is the label displayed
+  -  in the network details timings tab identifying the amount of time spent
+  -  in a "connect" state. -->
+<!ENTITY netmonitorUI.timings.connect     "Connecting:">
+
+<!-- LOCALIZATION NOTE (debuggerUI.timings.send): This is the label displayed
+  -  in the network details timings tab identifying the amount of time spent
+  -  in a "send" state. -->
+<!ENTITY netmonitorUI.timings.send        "Sending:">
+
+<!-- LOCALIZATION NOTE (debuggerUI.timings.wait): This is the label displayed
+  -  in the network details timings tab identifying the amount of time spent
+  -  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:">
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.properties
@@ -0,0 +1,98 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Network Monitor
+# which is available from the Web Developer sub-menu -> 'Network Monitor'.
+# 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 (netmonitor.label):
+# This string is displayed in the title of the tab when the Network Monitor is
+# displayed inside the developer tools window and in the Developer Tools Menu.
+netmonitor.label=Network
+
+# LOCALIZATION NOTE (netmonitor.commandkey, netmonitor.accesskey)
+# Used for the menuitem in the tool menu
+netmonitor.commandkey=Q
+netmonitor.accesskey=N
+
+# LOCALIZATION NOTE (netmonitor.tooltip):
+# This string is displayed in the tooltip of the tab when the Network Monitor is
+# displayed inside the developer tools window.
+netmonitor.tooltip=Network Monitor
+
+# LOCALIZATION NOTE (collapseDetailsPane): This is the tooltip for the button
+# that collapses the network details pane in the UI.
+collapseDetailsPane=Hide request details
+
+# LOCALIZATION NOTE (expandDetailsPane): This is the tooltip for the button
+# that expands the network details pane in the UI.
+expandDetailsPane=Show request details
+
+# LOCALIZATION NOTE (headersEmptyText): This is the text displayed in the
+# headers tab of the network details pane when there are no headers available.
+headersEmptyText=No headers for this request
+
+# LOCALIZATION NOTE (headersFilterText): This is the text displayed in the
+# headers tab of the network details pane for the filtering input.
+headersFilterText=Filter headers
+
+# LOCALIZATION NOTE (cookiesEmptyText): This is the text displayed in the
+# cookies tab of the network details pane when there are no cookies available.
+cookiesEmptyText=No cookies for this request
+
+# LOCALIZATION NOTE (cookiesFilterText): This is the text displayed in the
+# cookies tab of the network details pane for the filtering input.
+cookiesFilterText=Filter cookies
+
+# LOCALIZATION NOTE (paramsEmptyText): This is the text displayed in the
+# params tab of the network details pane when there are no params available.
+paramsEmptyText=No parameters for this request
+
+# LOCALIZATION NOTE (paramsFilterText): This is the text displayed in the
+# params tab of the network details pane for the filtering input.
+paramsFilterText=Filter request parameters
+
+# LOCALIZATION NOTE (paramsQueryString): This is the label displayed
+# in the network details params tab identifying the query string.
+paramsQueryString=Query string
+
+# LOCALIZATION NOTE (paramsFormData): This is the label displayed
+# in the network details params tab identifying the form data.
+paramsFormData=Form data
+
+# LOCALIZATION NOTE (paramsPostPayload): This is the label displayed
+# in the network details params tab identifying the request payload.
+paramsPostPayload=Request payload
+
+# LOCALIZATION NOTE (requestHeaders): This is the label displayed
+# in the network details headers tab identifying the request headers.
+requestHeaders=Request headers
+
+# LOCALIZATION NOTE (responseHeaders): This is the label displayed
+# in the network details headers tab identifying the response headers.
+responseHeaders=Response headers
+
+# LOCALIZATION NOTE (requestCookies): This is the label displayed
+# in the network details params tab identifying the request cookies.
+requestCookies=Request cookies
+
+# LOCALIZATION NOTE (responseCookies): This is the label displayed
+# in the network details params tab identifying the response cookies.
+responseCookies=Response cookies
+
+# LOCALIZATION NOTE (jsonFilterText): This is the text displayed
+# in the response tab of the network details pane for the JSON filtering input.
+jsonFilterText=Filter properties
+
+# LOCALIZATION NOTE (networkMenu.size): This is the label displayed
+# in the network menu specifying the size of a request (in kb).
+networkMenu.size=%Skb
+
+# LOCALIZATION NOTE (networkMenu.total): This is the label displayed
+# in the network menu specifying the time for a request to finish (in ms).
+networkMenu.total=→ %Sms
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -19,16 +19,18 @@
     locale/browser/syncProgress.dtd                (%chrome/browser/syncProgress.dtd)
     locale/browser/aboutSyncTabs.dtd               (%chrome/browser/aboutSyncTabs.dtd)
 #endif
     locale/browser/browser.dtd                     (%chrome/browser/browser.dtd)
     locale/browser/baseMenuOverlay.dtd             (%chrome/browser/baseMenuOverlay.dtd)
     locale/browser/browser.properties              (%chrome/browser/browser.properties)
     locale/browser/devtools/debugger.dtd              (%chrome/browser/devtools/debugger.dtd)
     locale/browser/devtools/debugger.properties       (%chrome/browser/devtools/debugger.properties)
+    locale/browser/devtools/netmonitor.dtd            (%chrome/browser/devtools/netmonitor.dtd)
+    locale/browser/devtools/netmonitor.properties     (%chrome/browser/devtools/netmonitor.properties)
     locale/browser/devtools/gcli.properties           (%chrome/browser/devtools/gcli.properties)
     locale/browser/devtools/gclicommands.properties   (%chrome/browser/devtools/gclicommands.properties)
     locale/browser/devtools/webconsole.properties     (%chrome/browser/devtools/webconsole.properties)
     locale/browser/devtools/inspector.properties      (%chrome/browser/devtools/inspector.properties)
     locale/browser/devtools/tilt.properties           (%chrome/browser/devtools/tilt.properties)
     locale/browser/devtools/scratchpad.properties     (%chrome/browser/devtools/scratchpad.properties)
     locale/browser/devtools/scratchpad.dtd            (%chrome/browser/devtools/scratchpad.dtd)
     locale/browser/devtools/styleeditor.properties    (%chrome/browser/devtools/styleeditor.properties)
--- a/browser/themes/linux/devtools/common.css
+++ b/browser/themes/linux/devtools/common.css
@@ -251,17 +251,17 @@
 
 .devtools-sidebar-tabs > tabs > .tabs-right,
 .devtools-sidebar-tabs > tabs > .tabs-left {
   display: none;
 }
 
 .devtools-sidebar-tabs > tabs > tab {
   -moz-appearance: none;
-  /* We want to match the height of a toolbar with a toolbarbutton 
+  /* We want to match the height of a toolbar with a toolbarbutton
    * First, we need to replicated the padding of toolbar (4px),
    * then, the padding of the button itself from toolbarbutton.css (3px),
    * Also, we need to take the border of the buttons into accout (1px).
    * Padding-bottom is one pixel shorter because we need to include the
    * black border.
    */
   padding: 8px 3px 7px;
   -moz-padding-start: 6px;
--- a/browser/themes/linux/devtools/debugger.css
+++ b/browser/themes/linux/devtools/debugger.css
@@ -12,30 +12,16 @@
 #sources-pane {
   min-width: 50px;
 }
 
 #sources-pane + .devtools-side-splitter {
   -moz-border-start-color: transparent;
 }
 
-/* Watch expressions, variables and other instruments pane */
-
-#instruments-pane {
-  min-width: 50px;
-  -moz-margin-start: 0px !important;
-  /* Unfortunately, transitions don't work properly with locale-aware properties,
-     so both the left and right margins are set via js, while the start margin
-     is always overridden here. */
-}
-
-#instruments-pane[animated] {
-  transition: margin 0.25s ease-in-out;
-}
-
 /* ListWidget items */
 
 .list-widget-item {
   padding: 2px;
 }
 
 .list-widget-item:not(.selected):not(.empty):hover {
   background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
@@ -305,19 +291,19 @@
   border-radius: 3px;
   margin: 0 3px;
 }
 
 #instruments-pane-toggle {
   background: none;
   box-shadow: none;
   border: none;
-  list-style-image: url("chrome://browser/skin/devtools/debugger-expand.png");
+  list-style-image: url("chrome://browser/skin/devtools/debugger-collapse.png");
   -moz-image-region: rect(0px,16px,16px,0px);
 }
 
-#instruments-pane-toggle:not([toggled]) {
-  list-style-image: url("chrome://browser/skin/devtools/debugger-collapse.png");
+#instruments-pane-toggle[pane-collapsed] {
+  list-style-image: url("chrome://browser/skin/devtools/debugger-expand.png");
 }
 
 #instruments-pane-toggle:active {
   -moz-image-region: rect(0px,32px,16px,16px);
 }
new file mode 100644
--- /dev/null
+++ b/browser/themes/linux/devtools/netmonitor.css
@@ -0,0 +1,304 @@
+/* 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/. */
+
+/* Network requests table */
+
+#body {
+  background: url(background-noise-toolbar.png), hsl(208,11%,27%);
+}
+
+.requests-menu-empty-notice {
+  background: url(background-noise-toolbar.png), hsl(208,11%,27%);
+  padding: 12px;
+  font-size: 110%;
+  color: #fff;
+}
+
+#requests-menu-toolbar {
+  -moz-padding-start: 4px;
+}
+
+.requests-menu-header {
+  text-align: center;
+}
+
+.requests-menu-subitem {
+  padding: 0 4px;
+}
+
+.requests-menu-header:not(:last-of-type),
+.requests-menu-subitem:not(:last-child) {
+  -moz-border-end: 1px solid hsla(210,8%,5%,.25);
+  box-shadow: 1px 0 0 hsla(210,16%,76%,.1);
+}
+
+.requests-menu-status-and-method {
+  width: 6em;
+}
+
+.requests-menu-status {
+  background: #fff;
+  width: 10px;
+  height: 10px;
+  -moz-margin-start: 4px;
+  -moz-margin-end: 2px;
+  border-radius: 20px;
+  box-shadow:
+    0 0 0 1px rgba(255,255,255,0.4) inset,
+    0 -6px 4px 0 rgba(32,32,32,1.0) inset,
+    0 0 8px 0 rgba(32,0,0,0.4);
+  transition: box-shadow 0.5s ease-in-out;
+}
+
+.requests-menu-status[code^="1"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(0,0,64,1.0) inset,
+    0 0 8px 0 rgba(0,0,128,1.0);
+}
+
+.requests-menu-status[code^="2"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(0,64,0,1.0) inset,
+    0 0 8px 0 rgba(0,128,0,1.0);
+}
+
+.requests-menu-status[code^="3"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(64,32,0,1.0) inset,
+    0 0 8px 0 rgba(128,128,0,1.0);
+}
+
+.requests-menu-status[code^="4"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(64,0,0,1.0) inset,
+    0 0 8px 0 rgba(128,0,0,1.0);
+}
+
+.requests-menu-status[code^="5"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(64,0,64,1.0) inset,
+    0 0 8px 0 rgba(128,0,128,1.0);
+}
+
+.requests-menu-method {
+  text-align: center;
+  font-weight: 600;
+}
+
+.requests-menu-file {
+  width: 14em;
+}
+
+.requests-menu-domain {
+  width: 14em;
+}
+
+.requests-menu-type {
+  text-align: center;
+  width: 4em;
+}
+
+.requests-menu-size {
+  text-align: center;
+  width: 6em;
+}
+
+.requests-menu-header.requests-menu-waterfall {
+  -moz-padding-start: 8px;
+  -moz-padding-end: 8px;
+  text-align: center;
+}
+
+/* Network request waterfall */
+
+.requests-menu-subitem.requests-menu-waterfall {
+  -moz-padding-start: 4px;
+  -moz-padding-end: 4px;
+  background-size: 5px;
+  background-image:
+    -moz-linear-gradient(left,
+      transparent 25%,
+      rgba(255,255,255,0.02) 25%,
+      rgba(255,255,255,0.02) 75%,
+      transparent 75%);
+}
+
+.requests-menu-timings {
+  transform-origin: left center;
+}
+
+.requests-menu-timings-total {
+  -moz-padding-start: 4px;
+  font-size: 85%;
+  font-weight: 600;
+  transform-origin: -4px center; /* negative cap size */
+}
+
+.requests-menu-timings-cap {
+  width: 4px;
+  height: 10px;
+  border: 1px solid #fff;
+}
+
+.requests-menu-timings-cap.start {
+  -moz-border-end: none;
+  border-radius: 4px 0 0 4px;
+  transform-origin: right center;
+}
+
+.requests-menu-timings-cap.end {
+  -moz-border-start: none;
+  border-radius: 0 4px 4px 0;
+  transform-origin: left center;
+}
+
+.requests-menu-timings-box {
+  height: 10px;
+  border-top: 1px solid #fff;
+  border-bottom: 1px solid #fff;
+}
+
+.requests-menu-timings-box.blocked,
+.requests-menu-timings-cap.blocked {
+  background-color: rgba(255,32,32,0.8);
+  box-shadow: 0 0 8px 0 rgba(128,32,32,0.8),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.dns,
+.requests-menu-timings-cap.dns {
+  background-color: rgba(255,128,255,0.6);
+  box-shadow: 0 0 8px 0 rgba(128,128,255,1.0),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.connect,
+.requests-menu-timings-cap.connect {
+  background-color: rgba(255,128,16,0.4);
+  box-shadow: 0 0 8px 0 rgba(128,128,16,0.8),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.send,
+.requests-menu-timings-cap.send {
+  background-color: rgba(255,255,128,0.4);
+  box-shadow: 0 0 8px 0 rgba(128,255,128,0.8),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.wait,
+.requests-menu-timings-cap.wait {
+  background-color: rgba(255,255,255,0.2);
+  box-shadow: 0 0 8px 0 rgba(128,255,255,0.4),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.receive,
+.requests-menu-timings-cap.receive {
+  background-color: rgba(255,255,255,1.0);
+  box-shadow: 0 0 8px 0 rgba(128,255,255,1.0),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.side-menu-widget-container,
+.side-menu-widget-item-or-group {
+  box-shadow: none !important;
+}
+
+.side-menu-widget-item:nth-child(even) {
+  background: rgba(255,255,255,0.05);
+}
+
+/* Network request details */
+
+#details-pane {
+  background: hsl(208,11%,27%);
+  max-width: 500px;
+}
+
+#details-pane-toggle {
+  background: none;
+  box-shadow: none;
+  border-color: transparent;
+  list-style-image: url("chrome://browser/skin/devtools/debugger-collapse.png");
+  -moz-image-region: rect(0px,16px,16px,0px);
+}
+
+#details-pane-toggle[pane-collapsed] {
+  list-style-image: url("chrome://browser/skin/devtools/debugger-expand.png");
+}
+
+#details-pane-toggle:active {
+  -moz-image-region: rect(0px,32px,16px,16px);
+}
+
+/* Network request details tabpanels */
+
+.tabpanel-content {
+  background: url(background-noise-toolbar.png), #3e4750;
+  box-shadow: 0 1px 0 hsla(204,45%,98%,.05) inset;
+  color: #fff;
+}
+
+.tabpanel-summary-container {
+  padding: 1px;
+}
+
+.tabpanel-summary-label {
+  -moz-padding-start: 4px;
+  -moz-padding-end: 3px;
+  font-weight: 600;
+  text-shadow: 0 1px 0 #000;
+  color: hsl(210,30%,85%);
+}
+
+.tabpanel-summary-value {
+  -moz-padding-start: 3px;
+  font-family: Menlo, Monaco, monospace;
+}
+
+/* Headers tabpanel */
+
+#headers-summary-status,
+#headers-summary-version {
+  padding-bottom: 2px;
+}
+
+#headers-summary-size {
+  padding-top: 2px;
+}
+
+/* Response tabpanel */
+
+#response-content-image-box {
+  padding-top: 10px;
+  padding-bottom: 10px;
+}
+
+#response-content-image {
+  background: #fff;
+  border: 1px dashed GrayText;
+  margin-bottom: 10px;
+}
+
+/* Timings tabpanel */
+
+#timings-tabpanel .tabpanel-summary-label {
+  width: 10em;
+}
+
+#timings-tabpanel .requests-menu-timings-box {
+  transition: transform 0.2s ease-out;
+  min-width: 1px;
+}
+
+#timings-tabpanel .requests-menu-timings-total {
+  transition: transform 0.2s ease-out;
+}
--- a/browser/themes/linux/devtools/widgets.css
+++ b/browser/themes/linux/devtools/widgets.css
@@ -1,13 +1,27 @@
 /* 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/. */
 
+/* Generic pane helpers */
+
+.generic-toggled-side-pane {
+  min-width: 50px;
+  -moz-margin-start: 0px !important;
+  /* Unfortunately, transitions don't work properly with locale-aware properties,
+     so both the left and right margins are set via js, while the start margin
+     is always overridden here. */
+}
+
+.generic-toggled-side-pane[animated] {
+  transition: margin 0.25s ease-in-out;
+}
+
 /* BreacrumbsWidget */
 
 .breadcrumbs-widget-container {
   -moz-margin-end: 3px;
   /* A fake 1px-shadow is included in the border-images of the
      breadcrumbs-widget-items, to match toolbar-buttons style.
      This negative margin compensates the extra row of pixels created
      by the shadow.*/
@@ -293,17 +307,17 @@
   cursor: pointer;
 }
 
 .side-menu-widget-item:last-of-type {
   box-shadow: 0 1px 0 hsla(210,16%,76%,.1);
 }
 
 .side-menu-widget-item.selected {
-  background: -moz-linear-gradient(hsl(206,61%,40%), hsl(206,61%,31%)) repeat-x top left;
+  background: -moz-linear-gradient(hsl(206,61%,40%), hsl(206,61%,31%)) repeat-x top left !important;
   box-shadow: inset 0 1px 0 hsla(210,40%,83%,.15),
               inset 0 -1px 0 hsla(210,40%,83%,.05);
   border-top: 0;
   padding-top: 1px;
 }
 
 .side-menu-widget-item-arrow {
   -moz-margin-start: -8px;
@@ -325,17 +339,17 @@
   background-image: url(itemArrow-rtl.png), -moz-linear-gradient(left, black, black);
   background-position: center left, top left;
 }
 
 .side-menu-widget-item-contents {
   padding: 4px;
 }
 
-.side-menu-widget-item-label {
+.side-menu-widget-item label {
   cursor: inherit;
 }
 
 .side-menu-widget-item-other {
   background: url(background-noise-toolbar.png), hsla(208,11%,27%, 0.65);
   margin: 0 -4px;
   -moz-padding-start: 5px;
 }
@@ -355,17 +369,16 @@
   margin-top: 4px;
 }
 
 .side-menu-widget-item-other:last-of-type {
   margin-bottom: -4px;
 }
 
 .side-menu-widget-item-other > label {
-  cursor: inherit;
   color: #f5f7fa;
   text-shadow: 0 1px 1px #111;
 }
 
 .side-menu-widget-empty-notice-container {
   background: url(background-noise-toolbar.png), hsl(208,11%,27%);
   padding: 12px;
   color: #fff;
@@ -393,24 +406,25 @@
 }
 
 .variables-view-scope > .variables-view-element-details:not(:empty) {
   margin-top: 1px;
   -moz-margin-start: 2px;
   -moz-margin-end: 1px;
 }
 
-.variables-view-scope > .variables-view-element-details.nonenum:not(:empty) {
-  border-top: 1px solid #ddd;
+.variables-view-scope > .variables-view-element-details.enum:not(:empty) {
+  border-bottom: 1px solid #ddd;
 }
 
 /* Generic traits applied to both variables and properties */
 
 .variable-or-property {
   transition: background 1s ease-in-out;
+  color: #000;
 }
 
 .variable-or-property[changed] {
   background: rgba(255,255,0,0.65);
   transition-duration: 0.4s;
 }
 
 .variable-or-property > .title > .value {
@@ -466,17 +480,16 @@
   color: #777;
 }
 
 .variable-or-property:not(:focus) > .title > .token-number {
   color: #c40a16;
 }
 
 .variable-or-property:not(:focus) > .title > .token-string {
-  max-width: 30em;
   color: #1c00cf;
 }
 
 .variable-or-property:not(:focus) > .title > .token-other {
   color: #333;
 }
 
 /* Non enumerable, configurable and writable variables and properties */
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -160,16 +160,17 @@ browser.jar:
   skin/classic/browser/devtools/breadcrumbs/rtl-middle.png                   (devtools/breadcrumbs/rtl-middle.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-pressed.png            (devtools/breadcrumbs/rtl-start-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-selected-pressed.png   (devtools/breadcrumbs/rtl-start-selected-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start.png                    (devtools/breadcrumbs/rtl-start.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-selected.png           (devtools/breadcrumbs/rtl-start-selected.png)
   skin/classic/browser/devtools/splitview.css         (devtools/splitview.css)
   skin/classic/browser/devtools/styleeditor.css       (devtools/styleeditor.css)
   skin/classic/browser/devtools/debugger.css          (devtools/debugger.css)
+  skin/classic/browser/devtools/netmonitor.css        (devtools/netmonitor.css)
   skin/classic/browser/devtools/magnifying-glass.png  (devtools/magnifying-glass.png)
   skin/classic/browser/devtools/option-icon.png       (devtools/option-icon.png)
   skin/classic/browser/devtools/itemToggle.png        (devtools/itemToggle.png)
   skin/classic/browser/devtools/itemArrow-rtl.png     (devtools/itemArrow-rtl.png)
   skin/classic/browser/devtools/itemArrow-ltr.png     (devtools/itemArrow-ltr.png)
   skin/classic/browser/devtools/background-noise-toolbar.png (devtools/background-noise-toolbar.png)
   skin/classic/browser/devtools/inspect-button.png    (devtools/inspect-button.png)
   skin/classic/browser/devtools/dropmarker.png        (devtools/dropmarker.png)
--- a/browser/themes/osx/devtools/common.css
+++ b/browser/themes/osx/devtools/common.css
@@ -264,17 +264,17 @@
 
 .devtools-sidebar-tabs > tabs > .tabs-right,
 .devtools-sidebar-tabs > tabs > .tabs-left {
   display: none;
 }
 
 .devtools-sidebar-tabs > tabs > tab {
   -moz-appearance: none;
-  /* We want to match the height of a toolbar with a toolbarbutton 
+  /* We want to match the height of a toolbar with a toolbarbutton
    * First, we need to replicated the padding of toolbar (4px),
    * then, the padding of the button itself from toolbarbutton.css (3px),
    * Also, we need to take the border of the buttons into accout (1px).
    * Minus the tab-text margin (2px).
    * Padding-bottom is one pixel shorter because we need to include the
    * black border.
    */
   padding: 6px 3px 5px !important;
--- a/browser/themes/osx/devtools/debugger.css
+++ b/browser/themes/osx/devtools/debugger.css
@@ -14,30 +14,16 @@
 #sources-pane {
   min-width: 50px;
 }
 
 #sources-pane + .devtools-side-splitter {
   -moz-border-start-color: transparent;
 }
 
-/* Watch expressions, variables and other instruments pane */
-
-#instruments-pane {
-  min-width: 50px;
-  -moz-margin-start: 0px !important;
-  /* Unfortunately, transitions don't work properly with locale-aware properties,
-     so both the left and right margins are set via js, while the start margin
-     is always overridden here. */
-}
-
-#instruments-pane[animated] {
-  transition: margin 0.25s ease-in-out;
-}
-
 /* ListWidget items */
 
 .list-widget-item {
   padding: 2px;
 }
 
 .list-widget-item:not(.selected):not(.empty):hover {
   background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
@@ -307,19 +293,19 @@
   border-radius: @toolbarbuttonCornerRadius@;
   margin: 0 3px;
 }
 
 #instruments-pane-toggle {
   background: none;
   box-shadow: none;
   border: none;
-  list-style-image: url("chrome://browser/skin/devtools/debugger-expand.png");
+  list-style-image: url("chrome://browser/skin/devtools/debugger-collapse.png");
   -moz-image-region: rect(0px,16px,16px,0px);
 }
 
-#instruments-pane-toggle:not([toggled]) {
-  list-style-image: url("chrome://browser/skin/devtools/debugger-collapse.png");
+#instruments-pane-toggle[pane-collapsed] {
+  list-style-image: url("chrome://browser/skin/devtools/debugger-expand.png");
 }
 
 #instruments-pane-toggle:active {
   -moz-image-region: rect(0px,32px,16px,16px);
 }
new file mode 100644
--- /dev/null
+++ b/browser/themes/osx/devtools/netmonitor.css
@@ -0,0 +1,304 @@
+/* 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/. */
+
+/* Network requests table */
+
+#body {
+  background: url(background-noise-toolbar.png), hsl(208,11%,27%);
+}
+
+.requests-menu-empty-notice {
+  background: url(background-noise-toolbar.png), hsl(208,11%,27%);
+  padding: 12px;
+  font-size: 110%;
+  color: #fff;
+}
+
+#requests-menu-toolbar {
+  -moz-padding-start: 4px;
+}
+
+.requests-menu-header {
+  text-align: center;
+}
+
+.requests-menu-subitem {
+  padding: 0 4px;
+}
+
+.requests-menu-header:not(:last-of-type),
+.requests-menu-subitem:not(:last-child) {
+  -moz-border-end: 1px solid hsla(210,8%,5%,.25);
+  box-shadow: 1px 0 0 hsla(210,16%,76%,.1);
+}
+
+.requests-menu-status-and-method {
+  width: 8em;
+}
+
+.requests-menu-status {
+  background: #fff;
+  width: 10px;
+  height: 10px;
+  -moz-margin-start: 4px;
+  -moz-margin-end: 2px;
+  border-radius: 20px;
+  box-shadow:
+    0 0 0 1px rgba(255,255,255,0.4) inset,
+    0 -6px 4px 0 rgba(32,32,32,1.0) inset,
+    0 0 8px 0 rgba(32,0,0,0.4);
+  transition: box-shadow 0.5s ease-in-out;
+}
+
+.requests-menu-status[code^="1"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(0,0,64,1.0) inset,
+    0 0 8px 0 rgba(0,0,128,1.0);
+}
+
+.requests-menu-status[code^="2"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(0,64,0,1.0) inset,
+    0 0 8px 0 rgba(0,128,0,1.0);
+}
+
+.requests-menu-status[code^="3"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(64,32,0,1.0) inset,
+    0 0 8px 0 rgba(128,128,0,1.0);
+}
+
+.requests-menu-status[code^="4"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(64,0,0,1.0) inset,
+    0 0 8px 0 rgba(128,0,0,1.0);
+}
+
+.requests-menu-status[code^="5"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(64,0,64,1.0) inset,
+    0 0 8px 0 rgba(128,0,128,1.0);
+}
+
+.requests-menu-method {
+  text-align: center;
+  font-weight: 600;
+}
+
+.requests-menu-file {
+  width: 16em;
+}
+
+.requests-menu-domain {
+  width: 16em;
+}
+
+.requests-menu-type {
+  text-align: center;
+  width: 4em;
+}
+
+.requests-menu-size {
+  text-align: center;
+  width: 8em;
+}
+
+.requests-menu-header.requests-menu-waterfall {
+  -moz-padding-start: 8px;
+  -moz-padding-end: 8px;
+  text-align: center;
+}
+
+/* Network request waterfall */
+
+.requests-menu-subitem.requests-menu-waterfall {
+  -moz-padding-start: 4px;
+  -moz-padding-end: 4px;
+  background-size: 5px;
+  background-image:
+    -moz-linear-gradient(left,
+      transparent 25%,
+      rgba(255,255,255,0.02) 25%,
+      rgba(255,255,255,0.02) 75%,
+      transparent 75%);
+}
+
+.requests-menu-timings {
+  transform-origin: left center;
+}
+
+.requests-menu-timings-total {
+  -moz-padding-start: 4px;
+  font-size: 85%;
+  font-weight: 600;
+  transform-origin: -4px center; /* negative cap size */
+}
+
+.requests-menu-timings-cap {
+  width: 4px;
+  height: 10px;
+  border: 1px solid #fff;
+}
+
+.requests-menu-timings-cap.start {
+  -moz-border-end: none;
+  border-radius: 4px 0 0 4px;
+  transform-origin: right center;
+}
+
+.requests-menu-timings-cap.end {
+  -moz-border-start: none;
+  border-radius: 0 4px 4px 0;
+  transform-origin: left center;
+}
+
+.requests-menu-timings-box {
+  height: 10px;
+  border-top: 1px solid #fff;
+  border-bottom: 1px solid #fff;
+}
+
+.requests-menu-timings-box.blocked,
+.requests-menu-timings-cap.blocked {
+  background-color: rgba(255,32,32,0.8);
+  box-shadow: 0 0 8px 0 rgba(128,32,32,0.8),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.dns,
+.requests-menu-timings-cap.dns {
+  background-color: rgba(255,128,255,0.6);
+  box-shadow: 0 0 8px 0 rgba(128,128,255,1.0),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.connect,
+.requests-menu-timings-cap.connect {
+  background-color: rgba(255,128,16,0.4);
+  box-shadow: 0 0 8px 0 rgba(128,128,16,0.8),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.send,
+.requests-menu-timings-cap.send {
+  background-color: rgba(255,255,128,0.4);
+  box-shadow: 0 0 8px 0 rgba(128,255,128,0.8),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.wait,
+.requests-menu-timings-cap.wait {
+  background-color: rgba(255,255,255,0.2);
+  box-shadow: 0 0 8px 0 rgba(128,255,255,0.4),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.receive,
+.requests-menu-timings-cap.receive {
+  background-color: rgba(255,255,255,1.0);
+  box-shadow: 0 0 8px 0 rgba(128,255,255,1.0),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.side-menu-widget-container,
+.side-menu-widget-item-or-group {
+  box-shadow: none !important;
+}
+
+.side-menu-widget-item:nth-child(even) {
+  background: rgba(255,255,255,0.05);
+}
+
+/* Network request details */
+
+#details-pane {
+  background: hsl(208,11%,27%);
+  max-width: 500px;
+}
+
+#details-pane-toggle {
+  background: none;
+  box-shadow: none;
+  border-color: transparent;
+  list-style-image: url("chrome://browser/skin/devtools/debugger-collapse.png");
+  -moz-image-region: rect(0px,16px,16px,0px);
+}
+
+#details-pane-toggle[pane-collapsed] {
+  list-style-image: url("chrome://browser/skin/devtools/debugger-expand.png");
+}
+
+#details-pane-toggle:active {
+  -moz-image-region: rect(0px,32px,16px,16px);
+}
+
+/* Network request details tabpanels */
+
+.tabpanel-content {
+  background: url(background-noise-toolbar.png), #3e4750;
+  box-shadow: 0 1px 0 hsla(204,45%,98%,.05) inset;
+  color: #fff;
+}
+
+.tabpanel-summary-container {
+  padding: 1px;
+}
+
+.tabpanel-summary-label {
+  -moz-padding-start: 4px;
+  -moz-padding-end: 3px;
+  font-weight: 600;
+  text-shadow: 0 1px 0 #000;
+  color: hsl(210,30%,85%);
+}
+
+.tabpanel-summary-value {
+  -moz-padding-start: 3px;
+  font-family: Menlo, Monaco, monospace;
+}
+
+/* Headers tabpanel */
+
+#headers-summary-status,
+#headers-summary-version {
+  padding-bottom: 2px;
+}
+
+#headers-summary-size {
+  padding-top: 2px;
+}
+
+/* Response tabpanel */
+
+#response-content-image-box {
+  padding-top: 10px;
+  padding-bottom: 10px;
+}
+
+#response-content-image {
+  background: #fff;
+  border: 1px dashed GrayText;
+  margin-bottom: 10px;
+}
+
+/* Timings tabpanel */
+
+#timings-tabpanel .tabpanel-summary-label {
+  width: 10em;
+}
+
+#timings-tabpanel .requests-menu-timings-box {
+  transition: transform 0.2s ease-out;
+  min-width: 1px;
+}
+
+#timings-tabpanel .requests-menu-timings-total {
+  transition: transform 0.2s ease-out;
+}
--- a/browser/themes/osx/devtools/widgets.css
+++ b/browser/themes/osx/devtools/widgets.css
@@ -1,13 +1,27 @@
 /* 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/. */
 
+/* Generic pane helpers */
+
+.generic-toggled-side-pane {
+  min-width: 50px;
+  -moz-margin-start: 0px !important;
+  /* Unfortunately, transitions don't work properly with locale-aware properties,
+     so both the left and right margins are set via js, while the start margin
+     is always overridden here. */
+}
+
+.generic-toggled-side-pane[animated] {
+  transition: margin 0.25s ease-in-out;
+}
+
 /* BreacrumbsWidget */
 
 .breadcrumbs-widget-container {
   -moz-margin-end: 3px;
   /* A fake 1px-shadow is included in the border-images of the
      breadcrumbs-widget-items, to match toolbar-buttons style.
      This negative margin compensates the extra row of pixels created
      by the shadow.*/
@@ -293,17 +307,17 @@
   cursor: pointer;
 }
 
 .side-menu-widget-item:last-of-type {
   box-shadow: 0 1px 0 hsla(210,16%,76%,.1);
 }
 
 .side-menu-widget-item.selected {
-  background: -moz-linear-gradient(hsl(206,61%,40%), hsl(206,61%,31%)) repeat-x top left;
+  background: -moz-linear-gradient(hsl(206,61%,40%), hsl(206,61%,31%)) repeat-x top left !important;
   box-shadow: inset 0 1px 0 hsla(210,40%,83%,.15),
               inset 0 -1px 0 hsla(210,40%,83%,.05);
   border-top: 0;
   padding-top: 1px;
 }
 
 .side-menu-widget-item-arrow {
   -moz-margin-start: -8px;
@@ -325,17 +339,17 @@
   background-image: url(itemArrow-rtl.png), -moz-linear-gradient(left, black, black);
   background-position: center left, top left;
 }
 
 .side-menu-widget-item-contents {
   padding: 4px;
 }
 
-.side-menu-widget-item-label {
+.side-menu-widget-item label {
   cursor: inherit;
 }
 
 .side-menu-widget-item-other {
   background: url(background-noise-toolbar.png), hsla(208,11%,27%, 0.65);
   margin: 0 -4px;
   -moz-padding-start: 5px;
 }
@@ -355,17 +369,16 @@
   margin-top: 4px;
 }
 
 .side-menu-widget-item-other:last-of-type {
   margin-bottom: -4px;
 }
 
 .side-menu-widget-item-other > label {
-  cursor: inherit;
   color: #f5f7fa;
   text-shadow: 0 1px 1px #111;
 }
 
 .side-menu-widget-empty-notice-container {
   background: url(background-noise-toolbar.png), hsl(208,11%,27%);
   padding: 12px;
   color: #fff;
@@ -393,24 +406,25 @@
 }
 
 .variables-view-scope > .variables-view-element-details:not(:empty) {
   margin-top: 1px;
   -moz-margin-start: 2px;
   -moz-margin-end: 1px;
 }
 
-.variables-view-scope > .variables-view-element-details.nonenum:not(:empty) {
-  border-top: 1px solid #ddd;
+.variables-view-scope > .variables-view-element-details.enum:not(:empty) {
+  border-bottom: 1px solid #ddd;
 }
 
 /* Generic traits applied to both variables and properties */
 
 .variable-or-property {
   transition: background 1s ease-in-out;
+  color: #000;
 }
 
 .variable-or-property[changed] {
   background: rgba(255,255,0,0.65);
   transition-duration: 0.4s;
 }
 
 .variable-or-property > .title > .value {
@@ -466,17 +480,16 @@
   color: #777;
 }
 
 .variable-or-property:not(:focus) > .title > .token-number {
   color: #c40a16;
 }
 
 .variable-or-property:not(:focus) > .title > .token-string {
-  max-width: 30em;
   color: #1c00cf;
 }
 
 .variable-or-property:not(:focus) > .title > .token-other {
   color: #333;
 }
 
 /* Non enumerable, configurable and writable variables and properties */
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -245,16 +245,17 @@ browser.jar:
   skin/classic/browser/devtools/breadcrumbs/rtl-middle.png                   (devtools/breadcrumbs/rtl-middle.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-pressed.png            (devtools/breadcrumbs/rtl-start-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-selected-pressed.png   (devtools/breadcrumbs/rtl-start-selected-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start.png                    (devtools/breadcrumbs/rtl-start.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-selected.png           (devtools/breadcrumbs/rtl-start-selected.png)
   skin/classic/browser/devtools/splitview.css               (devtools/splitview.css)
   skin/classic/browser/devtools/styleeditor.css             (devtools/styleeditor.css)
 * skin/classic/browser/devtools/debugger.css                (devtools/debugger.css)
+  skin/classic/browser/devtools/netmonitor.css              (devtools/netmonitor.css)
   skin/classic/browser/devtools/magnifying-glass.png        (devtools/magnifying-glass.png)
   skin/classic/browser/devtools/option-icon.png             (devtools/option-icon.png)
   skin/classic/browser/devtools/itemToggle.png              (devtools/itemToggle.png)
   skin/classic/browser/devtools/itemArrow-rtl.png           (devtools/itemArrow-rtl.png)
   skin/classic/browser/devtools/itemArrow-ltr.png           (devtools/itemArrow-ltr.png)
   skin/classic/browser/devtools/background-noise-toolbar.png (devtools/background-noise-toolbar.png)
   skin/classic/browser/devtools/inspect-button.png          (devtools/inspect-button.png)
   skin/classic/browser/devtools/dropmarker.png              (devtools/dropmarker.png)
--- a/browser/themes/windows/devtools/common.css
+++ b/browser/themes/windows/devtools/common.css
@@ -271,17 +271,17 @@
 
 .devtools-sidebar-tabs > tabs > .tabs-right,
 .devtools-sidebar-tabs > tabs > .tabs-left {
   display: none;
 }
 
 .devtools-sidebar-tabs > tabs > tab {
   -moz-appearance: none;
-  /* We want to match the height of a toolbar with a toolbarbutton 
+  /* We want to match the height of a toolbar with a toolbarbutton
    * First, we need to replicated the padding of toolbar (4px),
    * then, the padding of the button itself from toolbarbutton.css (3px),
    * Also, we need to take the border of the buttons into accout (1px).
    * Padding-bottom is one pixel shorter because we need to include the
    * black border.
    */
   padding: 8px 3px 7px;
   -moz-padding-start: 6px;
--- a/browser/themes/windows/devtools/debugger.css
+++ b/browser/themes/windows/devtools/debugger.css
@@ -12,30 +12,16 @@
 #sources-pane {
   min-width: 50px;
 }
 
 #sources-pane + .devtools-side-splitter {
   -moz-border-start-color: transparent;
 }
 
-/* Watch expressions, variables and other instruments pane */
-
-#instruments-pane {
-  min-width: 50px;
-  -moz-margin-start: 0px !important;
-  /* Unfortunately, transitions don't work properly with locale-aware properties,
-     so both the left and right margins are set via js, while the start margin
-     is always overridden here. */
-}
-
-#instruments-pane[animated] {
-  transition: margin 0.25s ease-in-out;
-}
-
 /* ListWidget items */
 
 .list-widget-item {
   padding: 2px;
 }
 
 .list-widget-item:not(.selected):not(.empty):hover {
   background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
@@ -306,22 +292,22 @@
   border-radius: 3px;
   margin: 0 3px;
 }
 
 #instruments-pane-toggle {
   background: none;
   box-shadow: none;
   border: none;
-  list-style-image: url("chrome://browser/skin/devtools/debugger-expand.png");
+  list-style-image: url("chrome://browser/skin/devtools/debugger-collapse.png");
   -moz-image-region: rect(0px,16px,16px,0px);
 }
 
-#instruments-pane-toggle:not([toggled]) {
-  list-style-image: url("chrome://browser/skin/devtools/debugger-collapse.png");
+#instruments-pane-toggle[pane-collapsed] {
+  list-style-image: url("chrome://browser/skin/devtools/debugger-expand.png");
 }
 
 #instruments-pane-toggle:hover {
   -moz-image-region: rect(0px,32px,16px,16px);
 }
 
 #instruments-pane-toggle:hover:active {
   -moz-image-region: rect(0px,48px,16px,32px);
new file mode 100644
--- /dev/null
+++ b/browser/themes/windows/devtools/netmonitor.css
@@ -0,0 +1,304 @@
+/* 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/. */
+
+/* Network requests table */
+
+#body {
+  background: url(background-noise-toolbar.png), hsl(208,11%,27%);
+}
+
+.requests-menu-empty-notice {
+  background: url(background-noise-toolbar.png), hsl(208,11%,27%);
+  padding: 12px;
+  font-size: 110%;
+  color: #fff;
+}
+
+#requests-menu-toolbar {
+  -moz-padding-start: 4px;
+}
+
+.requests-menu-header {
+  text-align: center;
+}
+
+.requests-menu-subitem {
+  padding: 0 4px;
+}
+
+.requests-menu-header:not(:last-of-type),
+.requests-menu-subitem:not(:last-child) {
+  -moz-border-end: 1px solid hsla(210,8%,5%,.25);
+  box-shadow: 1px 0 0 hsla(210,16%,76%,.1);
+}
+
+.requests-menu-status-and-method {
+  width: 8em;
+}
+
+.requests-menu-status {
+  background: #fff;
+  width: 10px;
+  height: 10px;
+  -moz-margin-start: 4px;
+  -moz-margin-end: 2px;
+  border-radius: 20px;
+  box-shadow:
+    0 0 0 1px rgba(255,255,255,0.4) inset,
+    0 -6px 4px 0 rgba(32,32,32,1.0) inset,
+    0 0 8px 0 rgba(32,0,0,0.4);
+  transition: box-shadow 0.5s ease-in-out;
+}
+
+.requests-menu-status[code^="1"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(0,0,64,1.0) inset,
+    0 0 8px 0 rgba(0,0,128,1.0);
+}
+
+.requests-menu-status[code^="2"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(0,64,0,1.0) inset,
+    0 0 8px 0 rgba(0,128,0,1.0);
+}
+
+.requests-menu-status[code^="3"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(64,32,0,1.0) inset,
+    0 0 8px 0 rgba(128,128,0,1.0);
+}
+
+.requests-menu-status[code^="4"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(64,0,0,1.0) inset,
+    0 0 8px 0 rgba(128,0,0,1.0);
+}
+
+.requests-menu-status[code^="5"] {
+  box-shadow:
+    0 0 2px 1px rgba(255,255,255,0.8) inset,
+    0 -6px 4px 0 rgba(64,0,64,1.0) inset,
+    0 0 8px 0 rgba(128,0,128,1.0);
+}
+
+.requests-menu-method {
+  text-align: center;
+  font-weight: 600;
+}
+
+.requests-menu-file {
+  width: 16em;
+}
+
+.requests-menu-domain {
+  width: 16em;
+}
+
+.requests-menu-type {
+  text-align: center;
+  width: 4em;
+}
+
+.requests-menu-size {
+  text-align: center;
+  width: 8em;
+}
+
+.requests-menu-header.requests-menu-waterfall {
+  -moz-padding-start: 8px;
+  -moz-padding-end: 8px;
+  text-align: center;
+}
+
+/* Network request waterfall */
+
+.requests-menu-subitem.requests-menu-waterfall {
+  -moz-padding-start: 4px;
+  -moz-padding-end: 4px;
+  background-size: 5px;
+  background-image:
+    -moz-linear-gradient(left,
+      transparent 25%,
+      rgba(255,255,255,0.02) 25%,
+      rgba(255,255,255,0.02) 75%,
+      transparent 75%);
+}
+
+.requests-menu-timings {
+  transform-origin: left center;
+}
+
+.requests-menu-timings-total {
+  -moz-padding-start: 4px;
+  font-size: 85%;
+  font-weight: 600;
+  transform-origin: -4px center; /* negative cap size */
+}
+
+.requests-menu-timings-cap {
+  width: 4px;
+  height: 10px;
+  border: 1px solid #fff;
+}
+
+.requests-menu-timings-cap.start {
+  -moz-border-end: none;
+  border-radius: 4px 0 0 4px;
+  transform-origin: right center;
+}
+
+.requests-menu-timings-cap.end {
+  -moz-border-start: none;
+  border-radius: 0 4px 4px 0;
+  transform-origin: left center;
+}
+
+.requests-menu-timings-box {
+  height: 10px;
+  border-top: 1px solid #fff;
+  border-bottom: 1px solid #fff;
+}
+
+.requests-menu-timings-box.blocked,
+.requests-menu-timings-cap.blocked {
+  background-color: rgba(255,32,32,0.8);
+  box-shadow: 0 0 8px 0 rgba(128,32,32,0.8),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.dns,
+.requests-menu-timings-cap.dns {
+  background-color: rgba(255,128,255,0.6);
+  box-shadow: 0 0 8px 0 rgba(128,128,255,1.0),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.connect,
+.requests-menu-timings-cap.connect {
+  background-color: rgba(255,128,16,0.4);
+  box-shadow: 0 0 8px 0 rgba(128,128,16,0.8),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.send,
+.requests-menu-timings-cap.send {
+  background-color: rgba(255,255,128,0.4);
+  box-shadow: 0 0 8px 0 rgba(128,255,128,0.8),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.wait,
+.requests-menu-timings-cap.wait {
+  background-color: rgba(255,255,255,0.2);
+  box-shadow: 0 0 8px 0 rgba(128,255,255,0.4),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.requests-menu-timings-box.receive,
+.requests-menu-timings-cap.receive {
+  background-color: rgba(255,255,255,1.0);
+  box-shadow: 0 0 8px 0 rgba(128,255,255,1.0),
+              0 0 4px 0 rgba(255,255,255,1.0) inset;
+}
+
+.side-menu-widget-container,
+.side-menu-widget-item-or-group {
+  box-shadow: none !important;
+}
+
+.side-menu-widget-item:nth-child(even) {
+  background: rgba(255,255,255,0.05);
+}
+
+/* Network request details */
+
+#details-pane {
+  background: hsl(208,11%,27%);
+  max-width: 500px;
+}
+
+#details-pane-toggle {
+  background: none;
+  box-shadow: none;
+  border-color: transparent;
+  list-style-image: url("chrome://browser/skin/devtools/debugger-collapse.png");
+  -moz-image-region: rect(0px,16px,16px,0px);
+}
+
+#details-pane-toggle[pane-collapsed] {
+  list-style-image: url("chrome://browser/skin/devtools/debugger-expand.png");
+}
+
+#details-pane-toggle:active {
+  -moz-image-region: rect(0px,32px,16px,16px);
+}
+
+/* Network request details tabpanels */
+
+.tabpanel-content {
+  background: url(background-noise-toolbar.png), #3e4750;
+  box-shadow: 0 1px 0 hsla(204,45%,98%,.05) inset;
+  color: #fff;
+}
+
+.tabpanel-summary-container {
+  padding: 1px;
+}
+
+.tabpanel-summary-label {
+  -moz-padding-start: 4px;
+  -moz-padding-end: 3px;
+  font-weight: 600;
+  text-shadow: 0 1px 0 #000;
+  color: hsl(210,30%,85%);
+}
+
+.tabpanel-summary-value {
+  -moz-padding-start: 3px;
+  font-family: Menlo, Monaco, monospace;
+}
+
+/* Headers tabpanel */
+
+#headers-summary-status,
+#headers-summary-version {
+  padding-bottom: 2px;
+}
+
+#headers-summary-size {
+  padding-top: 2px;
+}
+
+/* Response tabpanel */
+
+#response-content-image-box {
+  padding-top: 10px;
+  padding-bottom: 10px;
+}
+
+#response-content-image {
+  background: #fff;
+  border: 1px dashed GrayText;
+  margin-bottom: 10px;
+}
+
+/* Timings tabpanel */
+
+#timings-tabpanel .tabpanel-summary-label {
+  width: 10em;
+}
+
+#timings-tabpanel .requests-menu-timings-box {
+  transition: transform 0.2s ease-out;
+  min-width: 1px;
+}
+
+#timings-tabpanel .requests-menu-timings-total {
+  transition: transform 0.2s ease-out;
+}
--- a/browser/themes/windows/devtools/widgets.css
+++ b/browser/themes/windows/devtools/widgets.css
@@ -1,13 +1,27 @@
 /* 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/. */
 
+/* Generic pane helpers */
+
+.generic-toggled-side-pane {
+  min-width: 50px;
+  -moz-margin-start: 0px !important;
+  /* Unfortunately, transitions don't work properly with locale-aware properties,
+     so both the left and right margins are set via js, while the start margin
+     is always overridden here. */
+}
+
+.generic-toggled-side-pane[animated] {
+  transition: margin 0.25s ease-in-out;
+}
+
 /* BreacrumbsWidget */
 
 .breadcrumbs-widget-container {
   -moz-margin-end: 3px;
   /* A fake 1px-shadow is included in the border-images of the
      breadcrumbs-widget-items, to match toolbar-buttons style.
      This negative margin compensates the extra row of pixels created
      by the shadow.*/
@@ -297,17 +311,17 @@
   cursor: pointer;
 }
 
 .side-menu-widget-item:last-of-type {
   box-shadow: 0 1px 0 hsla(210,16%,76%,.1);
 }
 
 .side-menu-widget-item.selected {
-  background: -moz-linear-gradient(hsl(206,61%,40%), hsl(206,61%,31%)) repeat-x top left;
+  background: -moz-linear-gradient(hsl(206,61%,40%), hsl(206,61%,31%)) repeat-x top left !important;
   box-shadow: inset 0 1px 0 hsla(210,40%,83%,.15),
               inset 0 -1px 0 hsla(210,40%,83%,.05);
   border-top: 0;
   padding-top: 1px;
 }
 
 .side-menu-widget-item-arrow {
   -moz-margin-start: -8px;
@@ -329,17 +343,17 @@
   background-image: url(itemArrow-rtl.png), -moz-linear-gradient(left, black, black);
   background-position: center left, top left;
 }
 
 .side-menu-widget-item-contents {
   padding: 4px;
 }
 
-.side-menu-widget-item-label {
+.side-menu-widget-item label {
   cursor: inherit;
 }
 
 .side-menu-widget-item-other {
   background: url(background-noise-toolbar.png), hsla(208,11%,27%, 0.65);
   margin: 0 -4px;
   -moz-padding-start: 5px;
 }
@@ -359,19 +373,17 @@
   margin-top: 4px;
 }
 
 .side-menu-widget-item-other:last-of-type {
   margin-bottom: -4px;
 }
 
 .side-menu-widget-item-other > label {
-  cursor: inherit;
   color: #f5f7fa;
-  text-shadow: 0 1px 1px #111;
 }
 
 .side-menu-widget-empty-notice-container {
   background: url(background-noise-toolbar.png), hsl(208,11%,27%);
   padding: 12px;
   color: #fff;
   font-weight: 600;
 }
@@ -397,24 +409,25 @@
 }
 
 .variables-view-scope > .variables-view-element-details:not(:empty) {
   margin-top: 1px;
   -moz-margin-start: 2px;
   -moz-margin-end: 1px;
 }
 
-.variables-view-scope > .variables-view-element-details.nonenum:not(:empty) {
-  border-top: 1px solid #ddd;
+.variables-view-scope > .variables-view-element-details.enum:not(:empty) {
+  border-bottom: 1px solid #ddd;
 }
 
 /* Generic traits applied to both variables and properties */
 
 .variable-or-property {
   transition: background 1s ease-in-out;
+  color: #000;
 }
 
 .variable-or-property[changed] {
   background: rgba(255,255,0,0.65);
   transition-duration: 0.4s;
 }
 
 .variable-or-property > .title > .value {
@@ -470,17 +483,16 @@
   color: #777;
 }
 
 .variable-or-property:not(:focus) > .title > .token-number {
   color: #c40a16;
 }
 
 .variable-or-property:not(:focus) > .title > .token-string {
-  max-width: 30em;
   color: #1c00cf;
 }
 
 .variable-or-property:not(:focus) > .title > .token-other {
   color: #333;
 }
 
 /* Non enumerable, configurable and writable variables and properties */
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -187,16 +187,17 @@ browser.jar:
         skin/classic/browser/devtools/breadcrumbs/rtl-middle.png                   (devtools/breadcrumbs/rtl-middle.png)
         skin/classic/browser/devtools/breadcrumbs/rtl-start-pressed.png            (devtools/breadcrumbs/rtl-start-pressed.png)
         skin/classic/browser/devtools/breadcrumbs/rtl-start-selected-pressed.png   (devtools/breadcrumbs/rtl-start-selected-pressed.png)
         skin/classic/browser/devtools/breadcrumbs/rtl-start.png                    (devtools/breadcrumbs/rtl-start.png)
         skin/classic/browser/devtools/breadcrumbs/rtl-start-selected.png           (devtools/breadcrumbs/rtl-start-selected.png)
         skin/classic/browser/devtools/splitview.css                 (devtools/splitview.css)
         skin/classic/browser/devtools/styleeditor.css               (devtools/styleeditor.css)
         skin/classic/browser/devtools/debugger.css                  (devtools/debugger.css)
+        skin/classic/browser/devtools/netmonitor.css                (devtools/netmonitor.css)
         skin/classic/browser/devtools/magnifying-glass.png          (devtools/magnifying-glass.png)
         skin/classic/browser/devtools/option-icon.png               (devtools/option-icon.png)
         skin/classic/browser/devtools/itemToggle.png                (devtools/itemToggle.png)
         skin/classic/browser/devtools/itemArrow-rtl.png             (devtools/itemArrow-rtl.png)
         skin/classic/browser/devtools/itemArrow-ltr.png             (devtools/itemArrow-ltr.png)
         skin/classic/browser/devtools/background-noise-toolbar.png  (devtools/background-noise-toolbar.png)
         skin/classic/browser/devtools/inspect-button.png            (devtools/inspect-button.png)
         skin/classic/browser/devtools/dropmarker.png                (devtools/dropmarker.png)
@@ -427,16 +428,17 @@ browser.jar:
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-middle.png                   (devtools/breadcrumbs/rtl-middle.png)
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-start-pressed.png            (devtools/breadcrumbs/rtl-start-pressed.png)
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-start-selected-pressed.png   (devtools/breadcrumbs/rtl-start-selected-pressed.png)
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-start.png                    (devtools/breadcrumbs/rtl-start.png)
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-start-selected.png           (devtools/breadcrumbs/rtl-start-selected.png)
         skin/classic/aero/browser/devtools/splitview.css             (devtools/splitview.css)
         skin/classic/aero/browser/devtools/styleeditor.css           (devtools/styleeditor.css)
         skin/classic/aero/browser/devtools/debugger.css              (devtools/debugger.css)
+        skin/classic/aero/browser/devtools/netmonitor.css            (devtools/netmonitor.css)
         skin/classic/aero/browser/devtools/magnifying-glass.png      (devtools/magnifying-glass.png)
         skin/classic/aero/browser/devtools/option-icon.png           (devtools/option-icon.png)
         skin/classic/aero/browser/devtools/itemToggle.png            (devtools/itemToggle.png)
         skin/classic/aero/browser/devtools/itemArrow-rtl.png         (devtools/itemArrow-rtl.png)
         skin/classic/aero/browser/devtools/background-noise-toolbar.png (devtools/background-noise-toolbar.png)
         skin/classic/aero/browser/devtools/itemArrow-ltr.png         (devtools/itemArrow-ltr.png)
         skin/classic/aero/browser/devtools/inspect-button.png        (devtools/inspect-button.png)
         skin/classic/aero/browser/devtools/dropmarker.png            (devtools/dropmarker.png)
--- a/toolkit/devtools/webconsole/NetworkHelper.jsm
+++ b/toolkit/devtools/webconsole/NetworkHelper.jsm
@@ -82,24 +82,21 @@ this.NetworkHelper =
    *        Text to convert.
    * @param string aCharset
    *        Charset to convert the text to.
    * @returns string
    *          Converted text.
    */
   convertToUnicode: function NH_convertToUnicode(aText, aCharset)
   {
-    if (!aCharset) {
-      return aText;
-    }
-
     let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                createInstance(Ci.nsIScriptableUnicodeConverter);
-    conv.charset = aCharset;
-
+    if (aCharset) {
+      conv.charset = aCharset;
+    }
     try {
       return conv.ConvertToUnicode(aText);
     }
     catch (ex) {
       return aText;
     }
   },