Merge m-c to m-i
authorPhil Ringnalda <philringnalda@gmail.com>
Sun, 30 Jun 2013 08:40:17 -0700
changeset 136964 fe2f77d51bf19b62b9a2944cf711652b01a7574c
parent 136950 3f5669a7cd1491ad20f1c2dcfadef2edf789b420 (current diff)
parent 136963 e24391ffbe7e56ac975d7564aa8d3126c62e9529 (diff)
child 136965 3c993adfab9cb2e5206d20be87aee4672134aac3
push id30337
push userphilringnalda@gmail.com
push dateSun, 30 Jun 2013 15:40:31 +0000
treeherdermozilla-inbound@fe2f77d51bf1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to m-i
browser/devtools/profiler/ProfilerController.jsm
browser/devtools/profiler/ProfilerHelpers.jsm
browser/devtools/profiler/ProfilerPanel.jsm
browser/devtools/profiler/cmd-profiler.jsm
browser/devtools/profiler/test/browser_profiler_bug_830664_multiple_profiles.js
browser/devtools/profiler/test/browser_profiler_profiles.js
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "a65adcda68175ff7a05ad337f1ac8a746a408b45", 
+    "revision": "c201d63425a6866b1703ab4efbc95789e61748ff", 
     "repo_path": "/integration/gaia-central"
 }
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -258,16 +258,19 @@ let SessionStoreInternal = {
     Ci.nsIDOMEventListener,
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference
   ]),
 
   // set default load state
   _loadState: STATE_STOPPED,
 
+  // initial state to restore after startup
+  _initialState: null,
+
   // During the initial restore and setBrowserState calls tracks the number of
   // windows yet to be restored
   _restoreCount: -1,
 
   // whether a setBrowserState call is in progress
   _browserSetState: false,
 
   // time in milliseconds (Date.now()) when the session was last written to file
@@ -753,32 +756,33 @@ let SessionStoreInternal = {
 
       // restore a crashed session resp. resume the last session if requested
       if (this._initialState) {
         if (isPrivateWindow) {
           // We're starting with a single private window. Save the state we
           // actually wanted to restore so that we can do it later in case
           // the user opens another, non-private window.
           this._deferredInitialState = this._initialState;
-          delete this._initialState;
+          this._initialState = null;
 
           // Nothing to restore now, notify observers things are complete.
           Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
         } else {
           TelemetryTimestamps.add("sessionRestoreRestoring");
           // make sure that the restored tabs are first in the window
           this._initialState._firstTabs = true;
           this._restoreCount = this._initialState.windows ? this._initialState.windows.length : 0;
           this.restoreWindow(aWindow, this._initialState,
                              this._isCmdLineEmpty(aWindow, this._initialState));
-          delete this._initialState;
 
           // _loadState changed from "stopped" to "running"
           // force a save operation so that crashes happening during startup are correctly counted
-          this.saveState(true);
+          this._initialState.session.state = STATE_RUNNING_STR;
+          this._saveStateObject(this._initialState);
+          this._initialState = null;
         }
       }
       else {
         // Nothing to restore, notify observers things are complete.
         Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
 
         // the next delayed save request should execute immediately
         this._lastSaveTime -= this._interval;
--- a/browser/devtools/commandline/Commands.jsm
+++ b/browser/devtools/commandline/Commands.jsm
@@ -1,17 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-
-this.EXPORTED_SYMBOLS = [ ];
+this.EXPORTED_SYMBOLS = [];
 
 const Cu = Components.utils;
+const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
 
 Cu.import("resource:///modules/devtools/BuiltinCommands.jsm");
 Cu.import("resource:///modules/devtools/CmdDebugger.jsm");
 Cu.import("resource:///modules/devtools/CmdEdit.jsm");
 Cu.import("resource:///modules/devtools/CmdInspect.jsm");
 Cu.import("resource:///modules/devtools/CmdResize.jsm");
 Cu.import("resource:///modules/devtools/CmdTilt.jsm");
 Cu.import("resource:///modules/devtools/CmdScratchpad.jsm");
-Cu.import("resource:///modules/devtools/cmd-profiler.jsm");
+
+require("devtools/profiler/commands.js");
--- a/browser/devtools/debugger/DebuggerPanel.jsm
+++ b/browser/devtools/debugger/DebuggerPanel.jsm
@@ -88,14 +88,17 @@ DebuggerPanel.prototype = {
   },
 
   getAllBreakpoints: function() {
     return this._bkp.store;
   },
 
   highlightWhenPaused: function() {
     this._toolbox.highlightTool("jsdebugger");
+    // Also raise the toolbox window if it is undocked or select the
+    // corresponding tab when toolbox is docked.
+    this._toolbox.raise();
   },
 
   unhighlightWhenResumed: function() {
     this._toolbox.unhighlightTool("jsdebugger");
   }
 };
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -90,16 +90,17 @@ MOCHITEST_BROWSER_TESTS = \
 	browser_dbg_bug740825_conditional-breakpoints-01.js \
 	browser_dbg_bug740825_conditional-breakpoints-02.js \
 	browser_dbg_bug727429_watch-expressions-01.js \
 	browser_dbg_bug727429_watch-expressions-02.js \
 	browser_dbg_bug731394_editor-contextmenu.js \
 	browser_dbg_bug737803_editor_actual_location.js \
 	browser_dbg_bug786070_hide_nonenums.js \
 	browser_dbg_bug868163_highight_on_pause.js \
+	browser_dbg_bug883220_raise_on_pause.js \
 	browser_dbg_displayName.js \
 	browser_dbg_pause-exceptions.js \
 	browser_dbg_multiple-windows.js \
 	browser_dbg_iframes.js \
 	browser_dbg_bfcache.js \
 	browser_dbg_progress-listener-bug.js \
 	browser_dbg_chrome-debugging.js \
 	browser_dbg_source_maps-01.js \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_bug883220_raise_on_pause.js
@@ -0,0 +1,134 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that debugger's tab is highlighted when it is paused and not the
+// currently selected tool.
+
+var gTab = null;
+var gTab2 = null;
+var gDebugger = null;
+var gToolbox = null;
+var gToolboxTab = null;
+var gFocusedWindow = null;
+Promise._reportErrors = true;
+
+function test() {
+  debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
+    gTab = aTab;
+    gDebugger = aPane.panelWin;
+    gToolbox = aPane._toolbox;
+    gToolboxTab = gToolbox.doc.getElementById("toolbox-tab-jsdebugger");
+    gBrowser.selectedTab = gTab2 = gBrowser.addTab();
+    executeSoon(function() {
+      is(gBrowser.selectedTab, gTab2, "Debugger's tab is not the selected tab.");
+      gFocusedWindow = window;
+      testPause();
+    });
+  });
+}
+
+function focusMainWindow() {
+  // Make sure toolbox is not focused.
+  window.addEventListener("focus", onFocus, true);
+
+  // execute soon to avoid any race conditions between toolbox and main window
+  // getting focused.
+  executeSoon(() => {
+    window.focus();
+  });
+}
+
+function onFocus() {
+  window.removeEventListener("focus", onFocus, true);
+  info("main window focused.")
+  gFocusedWindow = window;
+  testPause();
+}
+
+function testPause() {
+  is(gDebugger.DebuggerController.activeThread.paused, false,
+    "Should be running after debug_tab_pane.");
+
+  is(gFocusedWindow, window, "Main window is the top level window before pause");
+
+  if (gToolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
+    gToolbox._host._window.onfocus = () => {
+      gFocusedWindow = gToolbox._host._window;
+    };
+  }
+
+  gDebugger.DebuggerController.activeThread.addOneTimeListener("paused", function() {
+    Services.tm.currentThread.dispatch({ run: function() {
+
+      if (gToolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
+        is(gFocusedWindow, gToolbox._host._window,
+           "Toolbox window is the top level window on pause.");
+      }
+      else {
+        is(gBrowser.selectedTab, gTab, "Debugger's tab got selected.");
+      }
+      gToolbox.selectTool("webconsole").then(() => {
+        ok(gToolboxTab.classList.contains("highlighted"),
+           "The highlighted class is present");
+        ok(!gToolboxTab.hasAttribute("selected") ||
+           gToolboxTab.getAttribute("selected") != "true",
+           "The tab is not selected");
+      }).then(() => gToolbox.selectTool("jsdebugger")).then(() => {
+        ok(gToolboxTab.classList.contains("highlighted"),
+           "The highlighted class is present");
+        ok(gToolboxTab.hasAttribute("selected") &&
+           gToolboxTab.getAttribute("selected") == "true",
+           "and the tab is selected, so the orange glow will not be present.");
+      }).then(testResume);
+    }}, 0);
+  });
+
+  EventUtils.sendMouseEvent({ type: "mousedown" },
+    gDebugger.document.getElementById("resume"),
+    gDebugger);
+}
+
+function testResume() {
+  gDebugger.DebuggerController.activeThread.addOneTimeListener("resumed", function() {
+    Services.tm.currentThread.dispatch({ run: function() {
+
+      gToolbox.selectTool("webconsole").then(() => {
+        ok(!gToolboxTab.classList.contains("highlighted"),
+           "The highlighted class is not present now after the resume");
+        ok(!gToolboxTab.hasAttribute("selected") ||
+           gToolboxTab.getAttribute("selected") != "true",
+           "The tab is not selected");
+      }).then(maybeEndTest);
+    }}, 0);
+  });
+
+  EventUtils.sendMouseEvent({ type: "mousedown" },
+    gDebugger.document.getElementById("resume"),
+    gDebugger);
+}
+
+function maybeEndTest() {
+  if (gToolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
+    gToolbox.switchHost(devtools.Toolbox.HostType.BOTTOM)
+            .then(closeDebuggerAndFinish);
+  }
+  else {
+    info("switching to toolbox window.")
+    gToolbox.switchHost(devtools.Toolbox.HostType.WINDOW).then(focusMainWindow).then(null, console.error);
+  }
+}
+
+registerCleanupFunction(function() {
+  Services.prefs.setCharPref("devtools.toolbox.host", devtools.Toolbox.HostType.BOTTOM);
+  removeTab(gTab);
+  removeTab(gTab2);
+  gTab = null;
+  gTab2 = null;
+  gDebugger = null;
+  gToolbox = null;
+  gToolboxTab = null;
+  gFocusedWindow = null;
+});
--- a/browser/devtools/framework/gDevTools.jsm
+++ b/browser/devtools/framework/gDevTools.jsm
@@ -8,17 +8,18 @@ this.EXPORTED_SYMBOLS = [ "gDevTools", "
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
-Cu.import("resource:///modules/devtools/ProfilerController.jsm");
+
+var ProfilerController = devtools.require("devtools/profiler/controller");
 
 const FORBIDDEN_IDS = new Set(["toolbox", ""]);
 const MAX_ORDINAL = 99;
 
 /**
  * DevTools is a class that represents a set of developer tools, it holds a
  * set of tools and keeps track of open toolboxes in the browser.
  */
--- a/browser/devtools/framework/test/Makefile.in
+++ b/browser/devtools/framework/test/Makefile.in
@@ -23,11 +23,12 @@ MOCHITEST_BROWSER_FILES = \
 		browser_toolbox_sidebar.js \
 		browser_toolbox_window_shortcuts.js \
 		browser_toolbox_window_title_changes.js \
 		browser_toolbox_options.js \
 		browser_toolbox_options_disablejs.js \
 		browser_toolbox_options_disablejs.html \
 		browser_toolbox_options_disablejs_iframe.html \
 		browser_toolbox_highlight.js \
+		browser_toolbox_raise.js \
 		$(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_raise.js
@@ -0,0 +1,87 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/Services.jsm");
+let temp = {}
+Cu.import("resource:///modules/devtools/gDevTools.jsm", temp);
+let DevTools = temp.DevTools;
+
+Cu.import("resource://gre/modules/devtools/Loader.jsm", temp);
+let devtools = temp.devtools;
+
+let Toolbox = devtools.Toolbox;
+
+let toolbox, target, tab1, tab2;
+
+function test() {
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = tab1 = gBrowser.addTab();
+  tab2 = gBrowser.addTab();
+  target = TargetFactory.forTab(gBrowser.selectedTab);
+
+  gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+    gDevTools.showToolbox(target)
+             .then(testBottomHost, console.error)
+             .then(null, console.error);
+  }, true);
+
+  content.location = "data:text/html,test for opening toolbox in different hosts";
+}
+
+function testBottomHost(aToolbox) {
+  toolbox = aToolbox;
+
+  // switch to another tab and test toolbox.raise()
+  gBrowser.selectedTab = tab2;
+  executeSoon(function() {
+    is(gBrowser.selectedTab, tab2, "Correct tab is selected before calling raise");
+    toolbox.raise();
+    executeSoon(function() {
+      is(gBrowser.selectedTab, tab1, "Correct tab was selected after calling raise");
+
+      toolbox.switchHost(Toolbox.HostType.WINDOW).then(testWindowHost).then(null, console.error);
+    });
+  });
+}
+
+function testWindowHost() {
+  // Make sure toolbox is not focused.
+  window.addEventListener("focus", onFocus, true);
+
+  // Need to wait for focus  as otherwise window.focus() is overridden by
+  // toolbox window getting focused first on Linux and Mac.
+  let onToolboxFocus = () => {
+    toolbox._host._window.removeEventListener("focus", onToolboxFocus, true);
+    info("focusing main window.");
+    window.focus()
+  };
+  // Need to wait for toolbox window to get focus.
+  toolbox._host._window.addEventListener("focus", onToolboxFocus, true);
+}
+
+function onFocus() {
+  info("Main window is focused before calling toolbox.raise()")
+  window.removeEventListener("focus", onFocus, true);
+
+  // Check if toolbox window got focus.
+  toolbox._host._window.onfocus = () => {
+    ok(true, "Toolbox window is the focused window after calling toolbox.raise()");
+    cleanup();
+  };
+  // Now raise toolbox.
+  toolbox.raise();
+}
+
+function cleanup() {
+  Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM);
+
+  toolbox.destroy().then(function() {
+    DevTools = Toolbox = toolbox = target = null;
+    gBrowser.removeCurrentTab();
+    gBrowser.removeCurrentTab();
+    finish();
+  });
+}
--- a/browser/devtools/inspector/breadcrumbs.js
+++ b/browser/devtools/inspector/breadcrumbs.js
@@ -146,23 +146,18 @@ HTMLBreadcrumbs.prototype = {
 
     if (aNode.className) {
       let classList = aNode.className.split(/\s+/);
       for (let i = 0; i < classList.length; i++) {
         text += "." + classList[i];
       }
     }
 
-    // XXX: needs updating when pseudoclass-lock is remotable
-    let rawNode = aNode.rawNode();
-    for (let i = 0; i < PSEUDO_CLASSES.length; i++) {
-      let pseudo = PSEUDO_CLASSES[i];
-      if (DOMUtils.hasPseudoClassLock(rawNode, pseudo)) {
-        text += pseudo;
-      }
+    for (let pseudo of aNode.pseudoClassLocks) {
+      text += pseudo;
     }
 
     return text;
   },
 
 
   /**
    * Build <label>s that represent the node:
@@ -197,22 +192,21 @@ HTMLBreadcrumbs.prototype = {
       let classList = aNode.className.split(/\s+/);
       for (let i = 0; i < classList.length; i++) {
         classesText += "." + classList[i];
       }
       classesLabel.textContent = classesText;
     }
 
     // XXX: Until we have pseudoclass lock in the node.
-    let rawNode = aNode.rawNode();
+    for (let pseudo of aNode.pseudoClassLocks) {
 
-    let pseudos = PSEUDO_CLASSES.filter(function(pseudo) {
-      return DOMUtils.hasPseudoClassLock(rawNode, pseudo);
-    }, this);
-    pseudosLabel.textContent = pseudos.join("");
+    }
+
+    pseudosLabel.textContent = aNode.pseudoClassLocks.join("");
 
     fragment.appendChild(tagLabel);
     fragment.appendChild(idLabel);
     fragment.appendChild(classesLabel);
     fragment.appendChild(pseudosLabel);
 
     return fragment;
   },
@@ -348,28 +342,16 @@ HTMLBreadcrumbs.prototype = {
     }
   },
 
   /**
    * Remove nodes and delete properties.
    */
   destroy: function BC_destroy()
   {
-    this.nodeHierarchy.forEach(function(crumb) {
-      // This node might have already been destroyed during
-      // shutdown.  Will clean this up when pseudo-class lock
-      // is ported to the walker.
-      if (crumb.node.actorID) {
-        let rawNode = crumb.node.rawNode();
-        if (LayoutHelpers.isNodeConnected(rawNode)) {
-          DOMUtils.clearPseudoClassLocks(rawNode);
-        }
-      }
-    });
-
     this.selection.off("new-node-front", this.update);
     this.selection.off("pseudoclass", this.updateSelectors);
     this.selection.off("attribute-changed", this.updateSelectors);
 
     this.container.removeEventListener("underflow", this.onscrollboxreflow, false);
     this.container.removeEventListener("overflow", this.onscrollboxreflow, false);
     this.onscrollboxreflow = null;
 
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -478,17 +478,17 @@ InspectorPanel.prototype = {
    * Disable the delete item if needed. Update the pseudo classes.
    */
   _setupNodeMenu: function InspectorPanel_setupNodeMenu() {
     // Set the pseudo classes
     for (let name of ["hover", "active", "focus"]) {
       let menu = this.panelDoc.getElementById("node-menu-pseudo-" + name);
 
       if (this.selection.isElementNode()) {
-        let checked = DOMUtils.hasPseudoClassLock(this.selection.node, ":" + name);
+        let checked = this.selection.nodeFront.hasPseudoClassLock(":" + name);
         menu.setAttribute("checked", checked);
         menu.removeAttribute("disabled");
       } else {
         menu.setAttribute("disabled", "true");
       }
     }
 
     // Disable delete item if needed
@@ -575,44 +575,34 @@ InspectorPanel.prototype = {
     }
   },
 
   /**
    * Toggle a pseudo class.
    */
   togglePseudoClass: function InspectorPanel_togglePseudoClass(aPseudo) {
     if (this.selection.isElementNode()) {
-      if (DOMUtils.hasPseudoClassLock(this.selection.node, aPseudo)) {
-        this.breadcrumbs.nodeHierarchy.forEach(function(crumb) {
-          DOMUtils.removePseudoClassLock(crumb.node.rawNode(), aPseudo);
-        });
-      } else {
-        let hierarchical = aPseudo == ":hover" || aPseudo == ":active";
-        let node = this.selection.node;
-        do {
-          DOMUtils.addPseudoClassLock(node, aPseudo);
-          node = node.parentNode;
-        } while (hierarchical && node.parentNode)
+      let node = this.selection.nodeFront;
+      if (node.hasPseudoClassLock(aPseudo)) {
+        return this.walker.removePseudoClassLock(node, aPseudo, { parents: true });
       }
+
+      let hierarchical = aPseudo == ":hover" || aPseudo == ":active";
+      return this.walker.addPseudoClassLock(node, aPseudo, { parents: hierarchical });
     }
-    this.selection.emit("pseudoclass");
-    this.breadcrumbs.scroll();
   },
 
   /**
    * Clear any pseudo-class locks applied to the current hierarchy.
    */
   clearPseudoClasses: function InspectorPanel_clearPseudoClasses() {
-    this.breadcrumbs.nodeHierarchy.forEach(function(crumb) {
-      try {
-        DOMUtils.clearPseudoClassLocks(crumb.node.rawNode());
-      } catch(e) {
-       // Ignore dead nodes after navigation.
-      }
-    });
+    if (!this.walker) {
+      return;
+    }
+    return this.walker.clearPseudoClassLocks().then(null, console.error);
   },
 
   /**
    * Toggle the highlighter when ruleview is hovered.
    */
   toggleHighlighter: function InspectorPanel_toggleHighlighter(event)
   {
     if (!this.highlighter) {
--- a/browser/devtools/inspector/selection.js
+++ b/browser/devtools/inspector/selection.js
@@ -68,32 +68,38 @@ function Selection(walker, node=null, tr
 exports.Selection = Selection;
 
 Selection.prototype = {
   _walker: null,
   _node: null,
 
   _onMutations: function(mutations) {
     let attributeChange = false;
+    let pseudoChange = false;
     let detached = false;
     let parentNode = null;
     for (let m of mutations) {
       if (!attributeChange && m.type == "attributes") {
         attributeChange = true;
       }
       if (m.type == "childList") {
         if (!detached && !this.isConnected()) {
           parentNode = m.target;
           detached = true;
         }
       }
+      if (m.type == "pseudoClassLock"){
+        pseudoChange = true;
+      }
     }
 
     if (attributeChange)
       this.emit("attribute-changed");
+    if (pseudoChange)
+      this.emit("pseudoclass");
     if (detached) {
       this.emit("detached", parentNode ? parentNode.rawNode() : null);
       this.emit("detached-front", parentNode);
     }
   },
 
   destroy: function SN_destroy() {
     this.setNode(null);
--- a/browser/devtools/inspector/test/browser_inspector_pseudoClass_menu.js
+++ b/browser/devtools/inspector/test/browser_inspector_pseudoClass_menu.js
@@ -46,26 +46,34 @@ function test() {
     menu.addEventListener("popupshowing", testMenuItems, true);
     menu.openPopup();
   }
 
   function testMenuItems()
   {
     menu.removeEventListener("popupshowing", testMenuItems, true);
 
-    for each (let pseudo in pseudos) {
+    var tryNext = () => {
+      if (pseudos.length === 0) {
+        finishUp();
+        return;
+      }
+      let pseudo = pseudos.shift();
+
       let menuitem = inspector.panelDoc.getElementById("node-menu-pseudo-" + pseudo);
       ok(menuitem, ":" + pseudo + " menuitem exists");
 
       menuitem.doCommand();
-
-      is(DOMUtils.hasPseudoClassLock(div, ":" + pseudo), true,
-        "pseudo-class lock has been applied");
+      inspector.selection.once("pseudoclass", () => {
+        is(DOMUtils.hasPseudoClassLock(div, ":" + pseudo), true,
+          "pseudo-class lock has been applied");
+        tryNext();
+      });
     }
-    finishUp();
+    tryNext();
   }
 
   function finishUp()
   {
     gBrowser.removeCurrentTab();
     finish();
   }
 }
--- a/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
+++ b/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
@@ -59,55 +59,67 @@ function selectNode(aInspector)
   });
 }
 
 function performTests()
 {
   // toggle the class
   inspector.togglePseudoClass(pseudo);
 
-  testAdded();
-
-  // toggle the lock off
-  inspector.togglePseudoClass(pseudo);
+  // Wait for the "pseudoclass" event so we know the
+  // inspector has been told of the pseudoclass lock change.
+  inspector.selection.once("pseudoclass", () => {
+    // Give the rule view time to update.
+    executeSoon(() => {
+      testAdded();
 
-  testRemoved();
-  testRemovedFromUI();
+      // toggle the lock off and wait for the pseudoclass event again.
+      inspector.togglePseudoClass(pseudo);
+      inspector.selection.once("pseudoclass", () => {
+        // Give the rule view time to update.
+        executeSoon(() => {
+          testRemoved();
+          testRemovedFromUI();
 
-  // toggle it back on
-  inspector.togglePseudoClass(pseudo);
-
-  testNavigate(() => {
-   // close the inspector
-    finishUp();
+          // toggle it back on
+          inspector.togglePseudoClass(pseudo);
+          inspector.selection.once("pseudoclass", () => {
+            testNavigate(() => {
+              // close the inspector
+              finishUp();
+            });
+          });
+        });
+      });
+    });
   });
 }
 
 function testNavigate(callback)
 {
   inspector.selection.setNode(parentDiv);
   inspector.once("inspector-updated", () => {
 
     // make sure it's still on after naving to parent
     is(DOMUtils.hasPseudoClassLock(div, pseudo), true,
          "pseudo-class lock is still applied after inspecting ancestor");
 
     inspector.selection.setNode(div2);
-
-    inspector.once("inspector-updated", () => {
-
+    inspector.selection.once("pseudoclass", () => {
       // make sure it's removed after naving to a non-hierarchy node
       is(DOMUtils.hasPseudoClassLock(div, pseudo), false,
            "pseudo-class lock is removed after inspecting sibling node");
 
       // toggle it back on
       inspector.selection.setNode(div);
       inspector.once("inspector-updated", () => {
         inspector.togglePseudoClass(pseudo);
-        callback();
+        inspector.selection.once("pseudoclass", () => {
+          callback();
+        });
       });
     });
   });
 }
 
 function testAdded()
 {
   // lock is applied to it and ancestors
--- a/browser/devtools/main.js
+++ b/browser/devtools/main.js
@@ -20,17 +20,17 @@ Object.defineProperty(exports, "TargetFa
 loader.lazyGetter(this, "osString", () => Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS);
 
 // Panels
 loader.lazyGetter(this, "OptionsPanel", function() require("devtools/framework/toolbox-options").OptionsPanel);
 loader.lazyGetter(this, "InspectorPanel", function() require("devtools/inspector/inspector-panel").InspectorPanel);
 loader.lazyImporter(this, "WebConsolePanel", "resource:///modules/WebConsolePanel.jsm");
 loader.lazyImporter(this, "DebuggerPanel", "resource:///modules/devtools/DebuggerPanel.jsm");
 loader.lazyImporter(this, "StyleEditorPanel", "resource:///modules/devtools/StyleEditorPanel.jsm");
-loader.lazyImporter(this, "ProfilerPanel", "resource:///modules/devtools/ProfilerPanel.jsm");
+loader.lazyGetter(this, "ProfilerPanel", function() require("devtools/profiler/panel"));
 loader.lazyImporter(this, "NetMonitorPanel", "resource:///modules/devtools/NetMonitorPanel.jsm");
 
 // Strings
 const toolboxProps = "chrome://browser/locale/devtools/toolbox.properties";
 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";
--- a/browser/devtools/profiler/Makefile.in
+++ b/browser/devtools/profiler/Makefile.in
@@ -7,9 +7,9 @@ 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
+	$(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/profiler
deleted file mode 100644
--- a/browser/devtools/profiler/ProfilerController.jsm
+++ /dev/null
@@ -1,394 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
-Cu.import("resource://gre/modules/devtools/Console.jsm");
-Cu.import("resource://gre/modules/AddonManager.jsm");
-
-let EXPORTED_SYMBOLS = ["ProfilerController"];
-
-XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
-  "resource:///modules/devtools/gDevTools.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
-  "resource://gre/modules/devtools/dbg-server.jsm");
-
-/**
- * Data structure that contains information that has
- * to be shared between separate ProfilerController
- * instances.
- */
-const sharedData = {
-  data: new WeakMap(),
-  controllers: new WeakMap(),
-};
-
-/**
- * Makes a structure representing an individual profile.
- */
-function makeProfile(name, def={}) {
-  if (def.timeStarted == null)
-    def.timeStarted = null;
-
-  if (def.timeEnded == null)
-    def.timeEnded = null;
-
-  return {
-    name: name,
-    timeStarted: def.timeStarted,
-    timeEnded: def.timeEnded
-  };
-}
-
-// Three functions below all operate with sharedData
-// structure defined above. They should be self-explanatory.
-
-function addTarget(target) {
-  sharedData.data.set(target, new Map());
-}
-
-function getProfiles(target) {
-  return sharedData.data.get(target);
-}
-
-/**
- * Object to control the JavaScript Profiler over the remote
- * debugging protocol.
- *
- * @param Target target
- *        A target object as defined in Target.jsm
- */
-function ProfilerController(target) {
-  if (sharedData.controllers.has(target)) {
-    return sharedData.controllers.get(target);
-  }
-
-  this.target = target;
-  this.client = target.client;
-  this.isConnected = false;
-  this.consoleProfiles = [];
-
-  addTarget(target);
-
-  // Chrome debugging targets have already obtained a reference
-  // to the profiler actor.
-  if (target.chrome) {
-    this.isConnected = true;
-    this.actor = target.form.profilerActor;
-  }
-
-  sharedData.controllers.set(target, this);
-};
-
-ProfilerController.prototype = {
-  /**
-   * Return a map of profile results for the current target.
-   *
-   * @return Map
-   */
-  get profiles() {
-    return getProfiles(this.target);
-  },
-
-  /**
-   * Checks whether the profile is currently recording.
-   *
-   * @param object profile
-   *        An object made by calling makeProfile function.
-   * @return boolean
-   */
-  isProfileRecording: function PC_isProfileRecording(profile) {
-    return profile.timeStarted !== null && profile.timeEnded === null;
-  },
-
-  /**
-   * A listener that fires whenever console.profile or console.profileEnd
-   * is called.
-   *
-   * @param string type
-   *        Type of a call. Either 'profile' or 'profileEnd'.
-   * @param object data
-   *        Event data.
-   * @param object panel
-   *        A reference to the ProfilerPanel in the current tab.
-   */
-  onConsoleEvent: function (type, data, panel) {
-    let name = data.extra.name;
-
-    let profileStart = () => {
-      if (name && this.profiles.has(name))
-        return;
-
-      // Add profile to the UI (createProfile will return
-      // an automatically generated name if 'name' is falsey).
-      let profile = panel.createProfile(name);
-      profile.start((name, cb) => cb());
-
-      // Add profile structure to shared data.
-      this.profiles.set(profile.name, makeProfile(profile.name, {
-        timeStarted: data.extra.currentTime
-      }));
-      this.consoleProfiles.push(profile.name);
-    };
-
-    let profileEnd = () => {
-      if (!name && !this.consoleProfiles.length)
-        return;
-
-      if (!name)
-        name = this.consoleProfiles.pop();
-      else
-        this.consoleProfiles.filter((n) => n !== name);
-
-      if (!this.profiles.has(name))
-        return;
-
-      let profile = this.profiles.get(name);
-      if (!this.isProfileRecording(profile))
-        return;
-
-      let profileData = data.extra.profile;
-      profile.timeEnded = data.extra.currentTime;
-
-      profileData.threads = profileData.threads.map((thread) => {
-        let samples = thread.samples.filter((sample) => {
-          return sample.time >= profile.timeStarted;
-        });
-
-        return { samples: samples };
-      });
-
-      let ui = panel.getProfileByName(name);
-      ui.data = profileData;
-      ui.parse(profileData, () => panel.emit("parsed"));
-      ui.stop((name, cb) => cb());
-    };
-
-    if (type === "profile")
-      profileStart();
-
-    if (type === "profileEnd")
-      profileEnd();
-  },
-
-  /**
-   * Connects to the client unless we're already connected.
-   *
-   * @param function cb
-   *        Function to be called once we're connected. If
-   *        the controller is already connected, this function
-   *        will be called immediately (synchronously).
-   */
-  connect: function (cb=function(){}) {
-    if (this.isConnected) {
-      return void cb();
-    }
-
-    // Check if we already have a grip to the listTabs response object
-    // and, if we do, use it to get to the profilerActor. Otherwise,
-    // call listTabs. The problem is that if we call listTabs twice
-    // webconsole tests fail (see bug 872826).
-
-    let register = () => {
-      let data = { events: ["console-api-profiler"] };
-
-      // Check if Gecko Profiler Addon [1] is installed and, if it is,
-      // don't register our own console event listeners. Gecko Profiler
-      // Addon takes care of console.profile and console.profileEnd methods
-      // and we don't want to break it.
-      //
-      // [1] - https://github.com/bgirard/Gecko-Profiler-Addon/
-
-      AddonManager.getAddonByID("jid0-edalmuivkozlouyij0lpdx548bc@jetpack", (addon) => {
-        if (addon && !addon.userDisabled && !addon.softDisabled)
-          return void cb();
-
-        this.request("registerEventNotifications", data, (resp) => {
-          this.client.addListener("eventNotification", (type, resp) => {
-            let toolbox = gDevTools.getToolbox(this.target);
-            if (toolbox == null)
-              return;
-
-            let panel = toolbox.getPanel("jsprofiler");
-            if (panel)
-              return void this.onConsoleEvent(resp.subject.action, resp.data, panel);
-
-            // Can't use a promise here because of a race condition when the promise
-            // is resolved only after -ready event is fired when creating a new panel
-            // and during the -ready event when waiting for a panel to be created:
-            //
-            // console.profile();    // creates a new panel, waits for the promise
-            // console.profileEnd(); // panel is not created yet but loading
-            //
-            // -> jsprofiler-ready event is fired which triggers a promise for profileEnd
-            // -> a promise for profile is triggered.
-            //
-            // And it should be the other way around. Hence the event.
-
-            toolbox.once("jsprofiler-ready", (_, panel) => {
-              this.onConsoleEvent(resp.subject.action, resp.data, panel);
-            });
-
-            toolbox.loadTool("jsprofiler");
-          });
-        });
-
-        cb();
-      });
-    };
-
-    if (this.target.root) {
-      this.actor = this.target.root.profilerActor;
-      this.isConnected = true;
-      return void register();
-    }
-
-    this.client.listTabs((resp) => {
-      this.actor = resp.profilerActor;
-      this.isConnected = true;
-      register();
-    });
-  },
-
-  /**
-   * Adds actor and type information to data and sends the request over
-   * the remote debugging protocol.
-   *
-   * @param string type
-   *        Method to call on the other side
-   * @param object data
-   *        Data to send with the request
-   * @param function cb
-   *        A callback function
-   */
-  request: function (type, data, cb) {
-    data.to = this.actor;
-    data.type = type;
-    this.client.request(data, cb);
-  },
-
-  /**
-   * Checks whether the profiler is active.
-   *
-   * @param function cb
-   *        Function to be called with a response from the
-   *        client. It will be called with two arguments:
-   *        an error object (may be null) and a boolean
-   *        value indicating if the profiler is active or not.
-   */
-  isActive: function (cb) {
-    this.request("isActive", {}, (resp) => {
-      cb(resp.error, resp.isActive, resp.currentTime);
-    });
-  },
-
-  /**
-   * Creates a new profile and starts the profiler, if needed.
-   *
-   * @param string name
-   *        Name of the profile.
-   * @param function cb
-   *        Function to be called once the profiler is started
-   *        or we get an error. It will be called with a single
-   *        argument: an error object (may be null).
-   */
-  start: function PC_start(name, cb) {
-    if (this.profiles.has(name)) {
-      return;
-    }
-
-    let profile = makeProfile(name);
-    this.consoleProfiles.push(name);
-    this.profiles.set(name, profile);
-
-    // If profile is already running, no need to do anything.
-    if (this.isProfileRecording(profile)) {
-      return void cb();
-    }
-
-    this.isActive((err, isActive, currentTime) => {
-      if (isActive) {
-        profile.timeStarted = currentTime;
-        return void cb();
-      }
-
-      let params = {
-        entries: 1000000,
-        interval: 1,
-        features: ["js"],
-      };
-
-      this.request("startProfiler", params, (resp) => {
-        if (resp.error) {
-          return void cb(resp.error);
-        }
-
-        profile.timeStarted = 0;
-        cb();
-      });
-    });
-  },
-
-  /**
-   * Stops the profiler. NOTE, that we don't stop the actual
-   * SPS Profiler here. It will be stopped as soon as all
-   * clients disconnect from the profiler actor.
-   *
-   * @param string name
-   *        Name of the profile that needs to be stopped.
-   * @param function cb
-   *        Function to be called once the profiler is stopped
-   *        or we get an error. It will be called with a single
-   *        argument: an error object (may be null).
-   */
-  stop: function PC_stop(name, cb) {
-    if (!this.profiles.has(name)) {
-      return;
-    }
-
-    let profile = this.profiles.get(name);
-    if (!this.isProfileRecording(profile)) {
-      return;
-    }
-
-    this.request("getProfile", {}, (resp) => {
-      if (resp.error) {
-        Cu.reportError("Failed to fetch profile data.");
-        return void cb(resp.error, null);
-      }
-
-      let data = resp.profile;
-      profile.timeEnded = resp.currentTime;
-
-      // Filter out all samples that fall out of current
-      // profile's range.
-
-      data.threads = data.threads.map((thread) => {
-        let samples = thread.samples.filter((sample) => {
-          return sample.time >= profile.timeStarted;
-        });
-
-        return { samples: samples };
-      });
-
-      cb(null, data);
-    });
-  },
-
-  /**
-   * Cleanup.
-   */
-  destroy: function PC_destroy() {
-    this.client = null;
-    this.target = null;
-    this.actor = null;
-  }
-};
deleted file mode 100644
--- a/browser/devtools/profiler/ProfilerHelpers.jsm
+++ /dev/null
@@ -1,43 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const Cu = Components.utils;
-const ProfilerProps = "chrome://browser/locale/devtools/profiler.properties";
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-this.EXPORTED_SYMBOLS = ["L10N"];
-
-/**
- * Localization helper methods.
- */
-let L10N = {
-  /**
-   * Returns a simple localized string.
-   *
-   * @param string name
-   * @return string
-   */
-  getStr: function L10N_getStr(name) {
-    return this.stringBundle.GetStringFromName(name);
-  },
-
-  /**
-   * Returns formatted localized string.
-   *
-   * @param string name
-   * @param array params
-   * @return string
-   */
-  getFormatStr: function L10N_getFormatStr(name, params) {
-    return this.stringBundle.formatStringFromName(name, params, params.length);
-  }
-};
-
-XPCOMUtils.defineLazyGetter(L10N, "stringBundle", function () {
-  return Services.strings.createBundle(ProfilerProps);
-});
\ No newline at end of file
deleted file mode 100644
--- a/browser/devtools/profiler/ProfilerPanel.jsm
+++ /dev/null
@@ -1,716 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const Cu = Components.utils;
-
-Cu.import("resource:///modules/devtools/gDevTools.jsm");
-Cu.import("resource:///modules/devtools/ProfilerController.jsm");
-Cu.import("resource:///modules/devtools/ProfilerHelpers.jsm");
-Cu.import("resource:///modules/devtools/shared/event-emitter.js");
-Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
-Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/devtools/Console.jsm");
-
-this.EXPORTED_SYMBOLS = ["ProfilerPanel"];
-
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
-    "resource://gre/modules/commonjs/sdk/core/promise.js");
-
-XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
-  "resource://gre/modules/devtools/dbg-server.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
-  "resource://gre/modules/Services.jsm");
-
-const PROFILE_IDLE = 0;
-const PROFILE_RUNNING = 1;
-const PROFILE_COMPLETED = 2;
-
-/**
- * An instance of a profile UI. Profile UI consists of
- * an iframe with Cleopatra loaded in it and some
- * surrounding meta-data (such as uids).
- *
- * Its main function is to talk to the Cleopatra instance
- * inside of the iframe.
- *
- * ProfileUI is also an event emitter. It emits the following events:
- *  - ready, when Cleopatra is done loading (you can also check the isReady
- *    property to see if a particular instance has been loaded yet.
- *  - disabled, when Cleopatra gets disabled. Happens when another ProfileUI
- *    instance starts the profiler.
- *  - enabled, when Cleopatra gets enabled. Happens when another ProfileUI
- *    instance stops the profiler.
- *
- * @param number uid
- *   Unique ID for this profile.
- * @param ProfilerPanel panel
- *   A reference to the container panel.
- */
-function ProfileUI(uid, name, panel) {
-  let doc = panel.document;
-  let win = panel.window;
-
-  EventEmitter.decorate(this);
-
-  this.isReady = false;
-  this.isStarted = false;
-  this.isFinished = false;
-
-  this.messages = [];
-  this.panel = panel;
-  this.uid = uid;
-  this.name = name;
-
-  this.iframe = doc.createElement("iframe");
-  this.iframe.setAttribute("flex", "1");
-  this.iframe.setAttribute("id", "profiler-cleo-" + uid);
-  this.iframe.setAttribute("src", "cleopatra.html?" + uid);
-  this.iframe.setAttribute("hidden", "true");
-
-  // Append our iframe and subscribe to postMessage events.
-  // They'll tell us when the underlying page is done loading
-  // or when user clicks on start/stop buttons.
-
-  doc.getElementById("profiler-report").appendChild(this.iframe);
-  win.addEventListener("message", function (event) {
-    if (parseInt(event.data.uid, 10) !== parseInt(this.uid, 10)) {
-      return;
-    }
-
-    switch (event.data.status) {
-      case "loaded":
-        this.isReady = true;
-        this.emit("ready");
-        break;
-      case "start":
-        this.start();
-        break;
-      case "stop":
-        this.stop();
-        break;
-      case "disabled":
-        this.emit("disabled");
-        break;
-      case "enabled":
-        this.emit("enabled");
-        break;
-      case "displaysource":
-        this.panel.displaySource(event.data.data);
-    }
-  }.bind(this));
-}
-
-ProfileUI.prototype = {
-  /**
-   * Returns a contentWindow of the iframe pointing to Cleopatra
-   * if it exists and can be accessed. Otherwise returns null.
-   */
-  get contentWindow() {
-    if (!this.iframe) {
-      return null;
-    }
-
-    try {
-      return this.iframe.contentWindow;
-    } catch (err) {
-      return null;
-    }
-  },
-
-  show: function PUI_show() {
-    this.iframe.removeAttribute("hidden");
-  },
-
-  hide: function PUI_hide() {
-    this.iframe.setAttribute("hidden", true);
-  },
-
-  /**
-   * Send raw profiling data to Cleopatra for parsing.
-   *
-   * @param object data
-   *   Raw profiling data from the SPS Profiler.
-   * @param function onParsed
-   *   A callback to be called when Cleopatra finishes
-   *   parsing and displaying results.
-   *
-   */
-  parse: function PUI_parse(data, onParsed) {
-    if (!this.isReady) {
-      return void this.on("ready", this.parse.bind(this, data, onParsed));
-    }
-
-    this.message({ task: "receiveProfileData", rawProfile: data }).then(() => {
-      let poll = () => {
-        let wait = this.panel.window.setTimeout.bind(null, poll, 100);
-        let trail = this.contentWindow.gBreadcrumbTrail;
-
-        if (!trail) {
-          return wait();
-        }
-
-        if (!trail._breadcrumbs || !trail._breadcrumbs.length) {
-          return wait();
-        }
-
-        onParsed();
-      };
-
-      poll();
-    });
-  },
-
-  /**
-   * Start profiling and, once started, notify the underlying page
-   * so that it could update the UI. Also, once started, we add a
-   * star to the profile name to indicate which profile is currently
-   * running.
-   *
-   * @param function startFn
-   *        A function to use instead of the default
-   *        this.panel.startProfiling. Useful when you
-   *        need mark panel as started after the profiler
-   *        has been started elsewhere. It must take two
-   *        params and call the second one.
-   */
-  start: function PUI_start(startFn) {
-    if (this.isStarted || this.isFinished) {
-      return;
-    }
-
-    startFn = startFn || this.panel.startProfiling.bind(this.panel);
-    startFn(this.name, () => {
-      this.isStarted = true;
-      this.panel.sidebar.setProfileState(this, PROFILE_RUNNING);
-      this.panel.broadcast(this.uid, {task: "onStarted"}); // Do we really need this?
-      this.emit("started");
-    });
-  },
-
-  /**
-   * Stop profiling and, once stopped, notify the underlying page so
-   * that it could update the UI and remove a star from the profile
-   * name.
-   *
-   * @param function stopFn
-   *        A function to use instead of the default
-   *        this.panel.stopProfiling. Useful when you
-   *        need mark panel as stopped after the profiler
-   *        has been stopped elsewhere. It must take two
-   *        params and call the second one.
-   */
-  stop: function PUI_stop(stopFn) {
-    if (!this.isStarted || this.isFinished) {
-      return;
-    }
-
-    stopFn = stopFn || this.panel.stopProfiling.bind(this.panel);
-    stopFn(this.name, () => {
-      this.isStarted = false;
-      this.isFinished = true;
-      this.panel.sidebar.setProfileState(this, PROFILE_COMPLETED);
-      this.panel.broadcast(this.uid, {task: "onStopped"});
-      this.emit("stopped");
-    });
-  },
-
-  /**
-   * Send a message to Cleopatra instance. If a message cannot be
-   * sent, this method queues it for later.
-   *
-   * @param object data JSON data to send (must be serializable)
-   * @return promise
-   */
-  message: function PIU_message(data) {
-    let deferred = Promise.defer();
-    let win = this.contentWindow;
-    data = JSON.stringify(data);
-
-    if (win) {
-      win.postMessage(data, "*");
-      deferred.resolve();
-    } else {
-      this.messages.push({ data: data, onSuccess: () => deferred.resolve() });
-    }
-
-    return deferred.promise;
-  },
-
-  /**
-   * Send all queued messages (see this.message for more info)
-   */
-  flushMessages: function PIU_flushMessages() {
-    if (!this.contentWindow) {
-      return;
-    }
-
-    let msg;
-    while (msg = this.messages.shift()) {
-      this.contentWindow.postMessage(msg.data, "*");
-      msg.onSuccess();
-    }
-  },
-
-  /**
-   * Destroys the ProfileUI instance.
-   */
-  destroy: function PUI_destroy() {
-    this.isReady = null
-    this.panel = null;
-    this.uid = null;
-    this.iframe = null;
-    this.messages = null;
-  }
-};
-
-function SidebarView(el) {
-  EventEmitter.decorate(this);
-  this.widget = new SideMenuWidget(el);
-}
-
-SidebarView.prototype = Heritage.extend(WidgetMethods, {
-  getItemByProfile: function (profile) {
-    return this.getItemForPredicate(item => item.attachment.uid === profile.uid);
-  },
-
-  setProfileState: function (profile, state) {
-    let item = this.getItemByProfile(profile);
-    let label = item.target.querySelector(".profiler-sidebar-item > span");
-
-    switch (state) {
-      case PROFILE_IDLE:
-        label.textContent = L10N.getStr("profiler.stateIdle");
-        break;
-      case PROFILE_RUNNING:
-        label.textContent = L10N.getStr("profiler.stateRunning");
-        break;
-      case PROFILE_COMPLETED:
-        label.textContent = L10N.getStr("profiler.stateCompleted");
-        break;
-      default: // Wrong state, do nothing.
-        return;
-    }
-
-    item.attachment.state = state;
-    this.emit("stateChanged", item);
-  }
-});
-
-/**
- * Profiler panel. It is responsible for creating and managing
- * different profile instances (see ProfileUI).
- *
- * ProfilerPanel is an event emitter. It can emit the following
- * events:
- *
- *   - ready:     after the panel is done loading everything,
- *                including the default profile instance.
- *   - started:   after the panel successfuly starts our SPS
- *                profiler.
- *   - stopped:   after the panel successfuly stops our SPS
- *                profiler and is ready to hand over profiling
- *                data
- *   - parsed:    after Cleopatra finishes parsing profiling
- *                data.
- *   - destroyed: after the panel cleans up after itself and
- *                is ready to be destroyed.
- *
- * The following events are used mainly by tests to prevent
- * accidential oranges:
- *
- *   - profileCreated:  after a new profile is created.
- *   - profileSwitched: after user switches to a different
- *                      profile.
- */
-function ProfilerPanel(frame, toolbox) {
-  this.isReady = false;
-  this.window = frame.window;
-  this.document = frame.document;
-  this.target = toolbox.target;
-
-  this.profiles = new Map();
-  this._uid = 0;
-  this._msgQueue = {};
-
-  EventEmitter.decorate(this);
-}
-
-ProfilerPanel.prototype = {
-  isReady:     null,
-  window:      null,
-  document:    null,
-  target:      null,
-  controller:  null,
-  profiles:    null,
-  sidebar:     null,
-
-  _uid:        null,
-  _activeUid:  null,
-  _runningUid: null,
-  _browserWin: null,
-  _msgQueue:   null,
-
-  get activeProfile() {
-    return this.profiles.get(this._activeUid);
-  },
-
-  set activeProfile(profile) {
-    if (this._activeUid === profile.uid)
-      return;
-
-    if (this.activeProfile)
-      this.activeProfile.hide();
-
-    this._activeUid = profile.uid;
-    profile.show();
-  },
-
-  get browserWindow() {
-    if (this._browserWin) {
-      return this._browserWin;
-    }
-
-    let win = this.window.top;
-    let type = win.document.documentElement.getAttribute("windowtype");
-
-    if (type !== "navigator:browser") {
-      win = Services.wm.getMostRecentWindow("navigator:browser");
-    }
-
-    return this._browserWin = win;
-  },
-
-  /**
-   * Open a debug connection and, on success, switch to the newly created
-   * profile.
-   *
-   * @return Promise
-   */
-  open: function PP_open() {
-    // Local profiling needs to make the target remote.
-    let target = this.target;
-    let promise = !target.isRemote ? target.makeRemote() : Promise.resolve(target);
-
-    return promise
-      .then((target) => {
-        let deferred = Promise.defer();
-
-        this.controller = new ProfilerController(this.target);
-        this.sidebar = new SidebarView(this.document.querySelector("#profiles-list"));
-        this.sidebar.widget.addEventListener("select", (ev) => {
-          if (!ev.detail)
-            return;
-
-          let profile = this.profiles.get(ev.detail.attachment.uid);
-          this.activeProfile = profile;
-
-          if (profile.isReady) {
-            profile.flushMessages();
-            return void this.emit("profileSwitched", profile.uid);
-          }
-
-          profile.once("ready", () => {
-            profile.flushMessages();
-            this.emit("profileSwitched", profile.uid);
-          });
-        });
-
-        this.controller.connect(() => {
-          let create = this.document.getElementById("profiler-create");
-          create.addEventListener("click", () => this.createProfile(), false);
-          create.removeAttribute("disabled");
-
-          let profile = this.createProfile();
-          let onSwitch = (_, uid) => {
-            if (profile.uid !== uid)
-              return;
-
-            this.off("profileSwitched", onSwitch);
-            this.isReady = true;
-            this.emit("ready");
-
-            deferred.resolve(this);
-          };
-
-          this.on("profileSwitched", onSwitch);
-          this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
-        });
-
-        return deferred.promise;
-      })
-      .then(null, (reason) =>
-        Cu.reportError("ProfilePanel open failed: " + reason.message));
-  },
-
-  /**
-   * Creates a new profile instance (see ProfileUI) and
-   * adds an appropriate item to the sidebar. Note that
-   * this method doesn't automatically switch user to
-   * the newly created profile, they have do to switch
-   * explicitly.
-   *
-   * @param string name
-   *        (optional) name of the new profile
-   *
-   * @return ProfilerPanel
-   */
-  createProfile: function PP_createProfile(name) {
-    if (name && this.getProfileByName(name)) {
-      return this.getProfileByName(name);
-    }
-
-    let uid = ++this._uid;
-
-    // If profile is anonymous, increase its UID until we get
-    // to the unused name. This way if someone manually creates
-    // a profile named say 'Profile 2' we won't create a dup
-    // with the same name. We will just skip over uid 2.
-
-    if (!name) {
-      name = L10N.getFormatStr("profiler.profileName", [uid]);
-      while (this.getProfileByName(name)) {
-        uid = ++this._uid;
-        name = L10N.getFormatStr("profiler.profileName", [uid]);
-      }
-    }
-
-    let box = this.document.createElement("vbox");
-    box.className = "profiler-sidebar-item";
-    box.id = "profile-" + uid;
-    let h3 = this.document.createElement("h3");
-    h3.textContent = name;
-    let span = this.document.createElement("span");
-    span.textContent = L10N.getStr("profiler.stateIdle");
-    box.appendChild(h3);
-    box.appendChild(span);
-
-    this.sidebar.push([box], { attachment: { uid: uid, name: name, state: PROFILE_IDLE } });
-
-    let profile = new ProfileUI(uid, name, this);
-    this.profiles.set(uid, profile);
-
-    this.emit("profileCreated", uid);
-    return profile;
-  },
-
-  /**
-   * Start collecting profile data.
-   *
-   * @param function onStart
-   *   A function to call once we get the message
-   *   that profiling had been successfuly started.
-   */
-  startProfiling: function PP_startProfiling(name, onStart) {
-    this.controller.start(name, (err) => {
-      if (err) {
-        return void Cu.reportError("ProfilerController.start: " + err.message);
-      }
-
-      onStart();
-      this.emit("started");
-    });
-  },
-
-  /**
-   * Stop collecting profile data and send it to Cleopatra
-   * for parsing.
-   *
-   * @param function onStop
-   *   A function to call once we get the message
-   *   that profiling had been successfuly stopped.
-   */
-  stopProfiling: function PP_stopProfiling(name, onStop) {
-    this.controller.isActive(function (err, isActive) {
-      if (err) {
-        Cu.reportError("ProfilerController.isActive: " + err.message);
-        return;
-      }
-
-      if (!isActive) {
-        return;
-      }
-
-      this.controller.stop(name, function (err, data) {
-        if (err) {
-          Cu.reportError("ProfilerController.stop: " + err.message);
-          return;
-        }
-
-        this.activeProfile.data = data;
-        this.activeProfile.parse(data, function onParsed() {
-          this.emit("parsed");
-        }.bind(this));
-
-        onStop();
-        this.emit("stopped", data);
-      }.bind(this));
-    }.bind(this));
-  },
-
-  /**
-   * Lookup an individual profile by its name.
-   *
-   * @param string name name of the profile
-   * @return profile object or null
-   */
-  getProfileByName: function PP_getProfileByName(name) {
-    if (!this.profiles) {
-      return null;
-    }
-
-    for (let [ uid, profile ] of this.profiles) {
-      if (profile.name === name) {
-        return profile;
-      }
-    }
-
-    return null;
-  },
-
-  /**
-   * Lookup an individual profile by its UID.
-   *
-   * @param number uid UID of the profile
-   * @return profile object or null
-   */
-  getProfileByUID: function PP_getProfileByUID(uid) {
-    if (!this.profiles) {
-      return null;
-    }
-
-    return this.profiles.get(uid) || null;
-  },
-
-  /**
-   * Iterates over each available profile and calls
-   * a callback with it as a parameter.
-   *
-   * @param function cb a callback to call
-   */
-  eachProfile: function PP_eachProfile(cb) {
-    let uid = this._uid;
-
-    if (!this.profiles) {
-      return;
-    }
-
-    while (uid >= 0) {
-      if (this.profiles.has(uid)) {
-        cb(this.profiles.get(uid));
-      }
-
-      uid -= 1;
-    }
-  },
-
-  /**
-   * Broadcast messages to all Cleopatra instances.
-   *
-   * @param number target
-   *   UID of the recepient profile. All profiles will receive the message
-   *   but the profile specified by 'target' will have a special property,
-   *   isCurrent, set to true.
-   * @param object data
-   *   An object with a property 'task' that will be sent over to Cleopatra.
-   */
-  broadcast: function PP_broadcast(target, data) {
-    if (!this.profiles) {
-      return;
-    }
-
-    if (data.task === "onStarted") {
-      this._runningUid = target;
-    } else {
-      this._runningUid = null;
-    }
-
-    this.eachProfile((profile) => {
-      profile.message({
-        uid: target,
-        isCurrent: target === profile.uid,
-        task: data.task
-      });
-    });
-  },
-
-  /**
-   * Open file specified in data in either a debugger or view-source.
-   *
-   * @param object data
-   *   An object describing the file. It must have three properties:
-   *    - uri
-   *    - line
-   *    - isChrome (chrome files are opened via view-source)
-   */
-  displaySource: function PP_displaySource(data, onOpen=function() {}) {
-    let win = this.window;
-    let panelWin, timeout;
-
-    function onSourceShown(event) {
-      if (event.detail.url !== data.uri) {
-        return;
-      }
-
-      panelWin.removeEventListener("Debugger:SourceShown", onSourceShown, false);
-      panelWin.editor.setCaretPosition(data.line - 1);
-      onOpen();
-    }
-
-    if (data.isChrome) {
-      return void this.browserWindow.gViewSourceUtils.
-        viewSource(data.uri, null, this.document, data.line);
-    }
-
-    gDevTools.showToolbox(this.target, "jsdebugger").then(function (toolbox) {
-      let dbg = toolbox.getCurrentPanel();
-      panelWin = dbg.panelWin;
-
-      let view = dbg.panelWin.DebuggerView;
-      if (view.Sources.selectedValue === data.uri) {
-        view.editor.setCaretPosition(data.line - 1);
-        onOpen();
-        return;
-      }
-
-      panelWin.addEventListener("Debugger:SourceShown", onSourceShown, false);
-      panelWin.DebuggerView.Sources.preferredSource = data.uri;
-    }.bind(this));
-  },
-
-  /**
-   * Cleanup.
-   */
-  destroy: function PP_destroy() {
-    if (this.profiles) {
-      let uid = this._uid;
-
-      while (uid >= 0) {
-        if (this.profiles.has(uid)) {
-          this.profiles.get(uid).destroy();
-          this.profiles.delete(uid);
-        }
-        uid -= 1;
-      }
-    }
-
-    if (this.controller) {
-      this.controller.destroy();
-    }
-
-    this.isReady = null;
-    this.window = null;
-    this.document = null;
-    this.target = null;
-    this.controller = null;
-    this.profiles = null;
-    this._uid = null;
-    this._activeUid = null;
-
-    this.emit("destroyed");
-  }
-};
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra.js
@@ -0,0 +1,162 @@
+/* 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";
+
+let { defer }    = require("sdk/core/promise");
+let EventEmitter = require("devtools/shared/event-emitter");
+
+const { PROFILE_IDLE, PROFILE_COMPLETED, PROFILE_RUNNING } = require("devtools/profiler/consts");
+
+/**
+ * An implementation of a profile visualization that uses Cleopatra.
+ * It consists of an iframe with Cleopatra loaded in it and some
+ * surrounding meta-data (such as UIDs).
+ *
+ * Cleopatra is also an event emitter. It emits the following events:
+ *  - ready, when Cleopatra is done loading (you can also check the isReady
+ *    property to see if a particular instance has been loaded yet.
+ *
+ * @param number uid
+ *   Unique ID for this profile.
+ * @param ProfilerPanel panel
+ *   A reference to the container panel.
+ */
+function Cleopatra(uid, name, panel) {
+  let doc = panel.document;
+  let win = panel.window;
+
+  EventEmitter.decorate(this);
+
+  this.isReady = false;
+  this.isStarted = false;
+  this.isFinished = false;
+
+  this.panel = panel;
+  this.uid = uid;
+  this.name = name;
+
+  this.iframe = doc.createElement("iframe");
+  this.iframe.setAttribute("flex", "1");
+  this.iframe.setAttribute("id", "profiler-cleo-" + uid);
+  this.iframe.setAttribute("src", "cleopatra.html?" + uid);
+  this.iframe.setAttribute("hidden", "true");
+
+  // Append our iframe and subscribe to postMessage events.
+  // They'll tell us when the underlying page is done loading
+  // or when user clicks on start/stop buttons.
+
+  doc.getElementById("profiler-report").appendChild(this.iframe);
+  win.addEventListener("message", function (event) {
+    if (parseInt(event.data.uid, 10) !== parseInt(this.uid, 10)) {
+      return;
+    }
+
+    switch (event.data.status) {
+      case "loaded":
+        this.isReady = true;
+        this.emit("ready");
+        break;
+      case "displaysource":
+        this.panel.displaySource(event.data.data);
+    }
+  }.bind(this));
+}
+
+Cleopatra.prototype = {
+  /**
+   * Returns a contentWindow of the iframe pointing to Cleopatra
+   * if it exists and can be accessed. Otherwise returns null.
+   */
+  get contentWindow() {
+    if (!this.iframe) {
+      return null;
+    }
+
+    try {
+      return this.iframe.contentWindow;
+    } catch (err) {
+      return null;
+    }
+  },
+
+  show: function () {
+    this.iframe.removeAttribute("hidden");
+  },
+
+  hide: function () {
+    this.iframe.setAttribute("hidden", true);
+  },
+
+  /**
+   * Send raw profiling data to Cleopatra for parsing.
+   *
+   * @param object data
+   *   Raw profiling data from the SPS Profiler.
+   * @param function onParsed
+   *   A callback to be called when Cleopatra finishes
+   *   parsing and displaying results.
+   *
+   */
+  parse: function (data, onParsed) {
+    if (!this.isReady) {
+      return void this.on("ready", this.parse.bind(this, data, onParsed));
+    }
+
+    this.message({ task: "receiveProfileData", rawProfile: data }).then(() => {
+      let poll = () => {
+        let wait = this.panel.window.setTimeout.bind(null, poll, 100);
+        let trail = this.contentWindow.gBreadcrumbTrail;
+
+        if (!trail) {
+          return wait();
+        }
+
+        if (!trail._breadcrumbs || !trail._breadcrumbs.length) {
+          return wait();
+        }
+
+        onParsed();
+      };
+
+      poll();
+    });
+  },
+
+  /**
+   * Send a message to Cleopatra instance. If a message cannot be
+   * sent, this method queues it for later.
+   *
+   * @param object data JSON data to send (must be serializable)
+   * @return promise
+   */
+  message: function (data) {
+    let deferred = defer();
+    data = JSON.stringify(data);
+
+    let send = () => {
+      if (!this.contentWindow)
+        setTimeout(send, 50);
+
+      this.contentWindow.postMessage(data, "*");
+      deferred.resolve();
+    };
+
+    send();
+    return deferred.promise;
+  },
+
+  /**
+   * Destroys the ProfileUI instance.
+   */
+  destroy: function () {
+    this.isReady = null;
+    this.panel = null;
+    this.uid = null;
+    this.iframe = null;
+    this.messages = null;
+  }
+};
+
+module.exports = Cleopatra;
\ No newline at end of file
--- a/browser/devtools/profiler/cleopatra/css/devtools.css
+++ b/browser/devtools/profiler/cleopatra/css/devtools.css
@@ -1,23 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-#mainarea > .controlPane {
-  font-size: 120%;
-  padding-top: 75px;
-  text-align: center;
-}
-
-#stopWrapper {
-  display: none;
-}
-
-#profilerMessage {
-  color: #999;
-  display: none;
+#mainarea {
 }
 
 /* De-emphasize chrome functions */
 .resourceIcon[data-resource^=otherhost_] + .functionName {
   color: #999;
 }
\ No newline at end of file
--- a/browser/devtools/profiler/cleopatra/js/devtools.js
+++ b/browser/devtools/profiler/cleopatra/js/devtools.js
@@ -6,20 +6,16 @@ var gInstanceUID;
 
 /**
  * Sends a message to the parent window with a status
  * update.
  *
  * @param string status
  *   Status to send to the parent page:
  *    - loaded, when page is loaded.
- *    - start, when user wants to start profiling.
- *    - stop, when user wants to stop profiling.
- *    - disabled, when the profiler was disabled
- *    - enabled, when the profiler was enabled
  *    - displaysource, when user wants to display source
  * @param object data (optional)
  *    Additional data to send to the parent page.
  */
 function notifyParent(status, data={}) {
   if (!gInstanceUID) {
     gInstanceUID = window.location.search.substr(1);
   }
@@ -104,32 +100,20 @@ function initUI() {
 
   var stopButton = document.createElement("button");
   stopButton.innerHTML = gStrings.getStr("profiler.stop");
   stopButton.addEventListener("click", function (event) {
     event.target.setAttribute("disabled", true);
     notifyParent("stop");
   }, false);
 
-  var controlPane = document.createElement("div");
-  var startProfiling = gStrings.getFormatStr("profiler.startProfiling",
-    ["<span class='btn'></span>"]);
-  var stopProfiling = gStrings.getFormatStr("profiler.stopProfiling",
-    ["<span class='btn'></span>"]);
-
-  controlPane.className = "controlPane";
-  controlPane.innerHTML =
-    "<p id='startWrapper'>" + startProfiling + "</p>" +
-    "<p id='stopWrapper'>" + stopProfiling + "</p>" +
-    "<p id='profilerMessage'></p>";
-
-  controlPane.querySelector("#startWrapper > span.btn").appendChild(startButton);
-  controlPane.querySelector("#stopWrapper > span.btn").appendChild(stopButton);
-
-  gMainArea.appendChild(controlPane);
+  var message = document.createElement("div");
+  message.className = "message";
+  message.innerHTML = "To start profiling click the button above.";
+  gMainArea.appendChild(message);
 }
 
 /**
  * Modified copy of Cleopatra's enterFinishedProfileUI.
  * By overriding the function we don't need to modify ui.js which helps
  * with updating from upstream.
  */
 function enterFinishedProfileUI() {
--- a/browser/devtools/profiler/cleopatra/js/strings.js
+++ b/browser/devtools/profiler/cleopatra/js/strings.js
@@ -1,10 +1,14 @@
 const Cu = Components.utils;
-Cu.import("resource:///modules/devtools/ProfilerHelpers.jsm");
+const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+const { L10N_BUNDLE } = require("devtools/profiler/consts");
+
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+var L10N = new ViewHelpers.L10N(L10N_BUNDLE);
 
 /**
  * Shortcuts for the L10N helper functions. Used in Cleopatra.
  */
 var gStrings = {
   // This strings are here so that Cleopatra code could use a simple object
   // lookup. This makes it easier to merge upstream changes.
   "Complete Profile": L10N.getStr("profiler.completeProfile"),
deleted file mode 100644
--- a/browser/devtools/profiler/cmd-profiler.jsm
+++ /dev/null
@@ -1,233 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-this.EXPORTED_SYMBOLS = [];
-
-Cu.import("resource://gre/modules/devtools/gcli.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/devtools/Require.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
-  "resource:///modules/devtools/gDevTools.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "console",
-  "resource://gre/modules/devtools/Console.jsm");
-
-var Promise = require('util/promise');
-
-/*
- * 'profiler' command. Doesn't do anything.
- */
-gcli.addCommand({
-  name: "profiler",
-  description: gcli.lookup("profilerDesc"),
-  manual: gcli.lookup("profilerManual")
-});
-
-/*
- * 'profiler open' command
- */
-gcli.addCommand({
-  name: "profiler open",
-  description: gcli.lookup("profilerOpenDesc"),
-  params: [],
-
-  exec: function (args, context) {
-    return gDevTools.showToolbox(context.environment.target, "jsprofiler")
-      .then(function () null);
-  }
-});
-
-/*
- * 'profiler close' command
- */
-gcli.addCommand({
-  name: "profiler close",
-  description: gcli.lookup("profilerCloseDesc"),
-  params: [],
-
-  exec: function (args, context) {
-    if (!getPanel(context, "jsprofiler"))
-      return;
-
-    return gDevTools.closeToolbox(context.environment.target)
-      .then(function () null);
-  }
-});
-
-/*
- * 'profiler start' command
- */
-gcli.addCommand({
-  name: "profiler start",
-  description: gcli.lookup("profilerStartDesc"),
-  returnType: "string",
-
-  params: [
-    {
-      name: "name",
-      type: "string",
-      manual: gcli.lookup("profilerStartManual")
-    }
-  ],
-
-  exec: function (args, context) {
-    function start() {
-      let name = args.name;
-      let panel = getPanel(context, "jsprofiler");
-      let profile = panel.getProfileByName(name) || panel.createProfile(name);
-
-      if (profile.isStarted) {
-        throw gcli.lookup("profilerAlreadyStarted");
-      }
-
-      if (profile.isFinished) {
-        throw gcli.lookup("profilerAlreadyFinished");
-      }
-
-      let item = panel.sidebar.getItemByProfile(profile);
-
-      if (panel.sidebar.selectedItem === item) {
-        profile.start();
-      } else {
-        panel.on("profileSwitched", () => profile.start());
-        panel.sidebar.selectedItem = item;
-      }
-
-      return gcli.lookup("profilerStarting2");
-    }
-
-    return gDevTools.showToolbox(context.environment.target, "jsprofiler")
-      .then(start);
-  }
-});
-
-/*
- * 'profiler stop' command
- */
-gcli.addCommand({
-  name: "profiler stop",
-  description: gcli.lookup("profilerStopDesc"),
-  returnType: "string",
-
-  params: [
-    {
-      name: "name",
-      type: "string",
-      manual: gcli.lookup("profilerStopManual")
-    }
-  ],
-
-  exec: function (args, context) {
-    function stop() {
-      let panel = getPanel(context, "jsprofiler");
-      let profile = panel.getProfileByName(args.name);
-
-      if (!profile) {
-        throw gcli.lookup("profilerNotFound");
-      }
-
-      if (profile.isFinished) {
-        throw gcli.lookup("profilerAlreadyFinished");
-      }
-
-      if (!profile.isStarted) {
-        throw gcli.lookup("profilerNotStarted2");
-      }
-
-      let item = panel.sidebar.getItemByProfile(profile);
-
-      if (panel.sidebar.selectedItem === item) {
-        profile.stop();
-      } else {
-        panel.on("profileSwitched", () => profile.stop());
-        panel.sidebar.selectedItem = item;
-      }
-
-      return gcli.lookup("profilerStopping2");
-    }
-
-    return gDevTools.showToolbox(context.environment.target, "jsprofiler")
-      .then(stop);
-  }
-});
-
-/*
- * 'profiler list' command
- */
-gcli.addCommand({
-  name: "profiler list",
-  description: gcli.lookup("profilerListDesc"),
-  returnType: "dom",
-  params: [],
-
-  exec: function (args, context) {
-    let panel = getPanel(context, "jsprofiler");
-
-    if (!panel) {
-      throw gcli.lookup("profilerNotReady");
-    }
-
-    let doc = panel.document;
-    let div = createXHTMLElement(doc, "div");
-    let ol = createXHTMLElement(doc, "ol");
-
-    for ([ uid, profile] of panel.profiles) {
-      let li = createXHTMLElement(doc, "li");
-      li.textContent = profile.name;
-      if (profile.isStarted) {
-        li.textContent += " *";
-      }
-      ol.appendChild(li);
-    }
-
-    div.appendChild(ol);
-    return div;
-  }
-});
-
-/*
- * 'profiler show' command
- */
-gcli.addCommand({
-  name: "profiler show",
-  description: gcli.lookup("profilerShowDesc"),
-
-  params: [
-    {
-      name: "name",
-      type: "string",
-      manual: gcli.lookup("profilerShowManual")
-    }
-  ],
-
-  exec: function (args, context) {
-    let panel = getPanel(context, "jsprofiler");
-
-    if (!panel) {
-      throw gcli.lookup("profilerNotReady");
-    }
-
-    let profile = panel.getProfileByName(args.name);
-    if (!profile) {
-      throw gcli.lookup("profilerNotFound");
-    }
-
-    panel.sidebar.selectedItem = panel.sidebar.getItemByProfile(profile);
-  }
-});
-
-function getPanel(context, id) {
-  if (context == null) {
-    return undefined;
-  }
-
-  let toolbox = gDevTools.getToolbox(context.environment.target);
-  return toolbox == null ? undefined : toolbox.getPanel(id);
-}
-
-function createXHTMLElement(document, tagname) {
-  return document.createElementNS("http://www.w3.org/1999/xhtml", tagname);
-}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/commands.js
@@ -0,0 +1,180 @@
+/* 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/. */
+
+const { Cu } = require("chrome");
+module.exports = [];
+
+Cu.import("resource://gre/modules/devtools/gcli.jsm");
+
+loader.lazyGetter(this, "gDevTools",
+  () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools);
+
+var Promise = require("sdk/core/promise");
+
+/*
+ * 'profiler' command. Doesn't do anything.
+ */
+gcli.addCommand({
+  name: "profiler",
+  description: gcli.lookup("profilerDesc"),
+  manual: gcli.lookup("profilerManual")
+});
+
+/*
+ * 'profiler open' command
+ */
+gcli.addCommand({
+  name: "profiler open",
+  description: gcli.lookup("profilerOpenDesc"),
+  params: [],
+
+  exec: function (args, context) {
+    return gDevTools.showToolbox(context.environment.target, "jsprofiler")
+      .then(function () null);
+  }
+});
+
+/*
+ * 'profiler close' command
+ */
+gcli.addCommand({
+  name: "profiler close",
+  description: gcli.lookup("profilerCloseDesc"),
+  params: [],
+
+  exec: function (args, context) {
+    if (!getPanel(context, "jsprofiler"))
+      return;
+
+    return gDevTools.closeToolbox(context.environment.target)
+      .then(function () null);
+  }
+});
+
+/*
+ * 'profiler start' command
+ */
+gcli.addCommand({
+  name: "profiler start",
+  description: gcli.lookup("profilerStartDesc"),
+  returnType: "string",
+  params: [],
+
+  exec: function (args, context) {
+    function start() {
+      let panel = getPanel(context, "jsprofiler");
+
+      if (panel.recordingProfile)
+        throw gcli.lookup("profilerAlreadyStarted2");
+
+      panel.toggleRecording();
+      return gcli.lookup("profilerStarted");
+    }
+
+    return gDevTools.showToolbox(context.environment.target, "jsprofiler")
+      .then(start);
+  }
+});
+
+/*
+ * 'profiler stop' command
+ */
+gcli.addCommand({
+  name: "profiler stop",
+  description: gcli.lookup("profilerStopDesc"),
+  returnType: "string",
+  params: [],
+
+  exec: function (args, context) {
+    function stop() {
+      let panel = getPanel(context, "jsprofiler");
+
+      if (!panel.recordingProfile)
+        throw gcli.lookup("profilerNotStarted3");
+
+      panel.toggleRecording();
+    }
+
+    return gDevTools.showToolbox(context.environment.target, "jsprofiler")
+      .then(stop);
+  }
+});
+
+/*
+ * 'profiler list' command
+ */
+gcli.addCommand({
+  name: "profiler list",
+  description: gcli.lookup("profilerListDesc"),
+  returnType: "dom",
+  params: [],
+
+  exec: function (args, context) {
+    let panel = getPanel(context, "jsprofiler");
+
+    if (!panel) {
+      throw gcli.lookup("profilerNotReady");
+    }
+
+    let doc = panel.document;
+    let div = createXHTMLElement(doc, "div");
+    let ol = createXHTMLElement(doc, "ol");
+
+    for ([ uid, profile] of panel.profiles) {
+      let li = createXHTMLElement(doc, "li");
+      li.textContent = profile.name;
+      if (profile.isStarted) {
+        li.textContent += " *";
+      }
+      ol.appendChild(li);
+    }
+
+    div.appendChild(ol);
+    return div;
+  }
+});
+
+/*
+ * 'profiler show' command
+ */
+gcli.addCommand({
+  name: "profiler show",
+  description: gcli.lookup("profilerShowDesc"),
+
+  params: [
+    {
+      name: "name",
+      type: "string",
+      manual: gcli.lookup("profilerShowManual")
+    }
+  ],
+
+  exec: function (args, context) {
+    let panel = getPanel(context, "jsprofiler");
+
+    if (!panel) {
+      throw gcli.lookup("profilerNotReady");
+    }
+
+    let profile = panel.getProfileByName(args.name);
+    if (!profile) {
+      throw gcli.lookup("profilerNotFound");
+    }
+
+    panel.sidebar.selectedItem = panel.sidebar.getItemByProfile(profile);
+  }
+});
+
+function getPanel(context, id) {
+  if (context == null) {
+    return undefined;
+  }
+
+  let toolbox = gDevTools.getToolbox(context.environment.target);
+  return toolbox == null ? undefined : toolbox.getPanel(id);
+}
+
+function createXHTMLElement(document, tagname) {
+  return document.createElementNS("http://www.w3.org/1999/xhtml", tagname);
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/consts.js
@@ -0,0 +1,13 @@
+/* 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";
+
+module.exports = {
+	L10N_BUNDLE:       "chrome://browser/locale/devtools/profiler.properties",
+	PROFILER_ENABLED:  "devtools.profiler.enabled",
+	PROFILE_IDLE:      0,
+	PROFILE_RUNNING:   1,
+	PROFILE_COMPLETED: 2
+};
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/controller.js
@@ -0,0 +1,411 @@
+/* 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";
+
+var isJSM = typeof require !== "function";
+
+// This code is needed because, for whatever reason, mochitest can't
+// find any requirejs module so we have to load it old school way. :(
+
+if (isJSM) {
+  var Cu = this["Components"].utils;
+  let XPCOMUtils = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}).XPCOMUtils;
+  this["loader"] = { lazyGetter: XPCOMUtils.defineLazyGetter.bind(XPCOMUtils) };
+  this["require"] = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+} else {
+  var { Cu } = require("chrome");
+}
+
+const { L10N_BUNDLE } = require("devtools/profiler/consts");
+
+var EventEmitter = require("devtools/shared/event-emitter");
+
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+Cu.import("resource://gre/modules/devtools/Console.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE));
+
+loader.lazyGetter(this, "gDevTools",
+  () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools);
+
+loader.lazyGetter(this, "DebuggerServer",
+  () => Cu.import("resource:///modules/devtools/dbg-server.jsm", {}).DebuggerServer);
+
+/**
+ * Data structure that contains information that has
+ * to be shared between separate ProfilerController
+ * instances.
+ */
+const sharedData = {
+  data: new WeakMap(),
+  controllers: new WeakMap(),
+};
+
+/**
+ * Makes a structure representing an individual profile.
+ */
+function makeProfile(name, def={}) {
+  if (def.timeStarted == null)
+    def.timeStarted = null;
+
+  if (def.timeEnded == null)
+    def.timeEnded = null;
+
+  return {
+    name: name,
+    timeStarted: def.timeStarted,
+    timeEnded: def.timeEnded,
+    fromConsole: def.fromConsole || false
+  };
+}
+
+// Three functions below all operate with sharedData
+// structure defined above. They should be self-explanatory.
+
+function addTarget(target) {
+  sharedData.data.set(target, new Map());
+}
+
+function getProfiles(target) {
+  return sharedData.data.get(target);
+}
+
+/**
+ * Object to control the JavaScript Profiler over the remote
+ * debugging protocol.
+ *
+ * @param Target target
+ *        A target object as defined in Target.jsm
+ */
+function ProfilerController(target) {
+  if (sharedData.controllers.has(target)) {
+    return sharedData.controllers.get(target);
+  }
+
+  this.target = target;
+  this.client = target.client;
+  this.isConnected = false;
+  this.consoleProfiles = [];
+  this.reservedNames = {};
+
+  addTarget(target);
+
+  // Chrome debugging targets have already obtained a reference
+  // to the profiler actor.
+  if (target.chrome) {
+    this.isConnected = true;
+    this.actor = target.form.profilerActor;
+  }
+
+  sharedData.controllers.set(target, this);
+  EventEmitter.decorate(this);
+};
+
+ProfilerController.prototype = {
+  target:          null,
+  client:          null,
+  isConnected:     null,
+  consoleProfiles: null,
+  reservedNames:   null,
+
+  /**
+   * Return a map of profile results for the current target.
+   *
+   * @return Map
+   */
+  get profiles() {
+    return getProfiles(this.target);
+  },
+
+  /**
+   * Checks whether the profile is currently recording.
+   *
+   * @param object profile
+   *        An object made by calling makeProfile function.
+   * @return boolean
+   */
+  isProfileRecording: function PC_isProfileRecording(profile) {
+    return profile.timeStarted !== null && profile.timeEnded === null;
+  },
+
+  getProfileName: function PC_getProfileName() {
+    let num = 1;
+    let name = L10N.getFormatStr("profiler.profileName", [num]);
+
+    while (this.reservedNames[name]) {
+      num += 1;
+      name = L10N.getFormatStr("profiler.profileName", [num]);
+    }
+
+    this.reservedNames[name] = true;
+    return name;
+  },
+
+  /**
+   * A listener that fires whenever console.profile or console.profileEnd
+   * is called.
+   *
+   * @param string type
+   *        Type of a call. Either 'profile' or 'profileEnd'.
+   * @param object data
+   *        Event data.
+   */
+  onConsoleEvent: function (type, data) {
+    let name = data.extra.name;
+
+    let profileStart = () => {
+      if (name && this.profiles.has(name))
+        return;
+
+      // Add profile structure to shared data.
+      let profile = makeProfile(name || this.getProfileName(), {
+        timeStarted: data.extra.currentTime,
+        fromConsole: true
+      });
+
+      this.profiles.set(profile.name, profile);
+      this.consoleProfiles.push(profile.name);
+      this.emit("profileStart", profile);
+    };
+
+    let profileEnd = () => {
+      if (!name && !this.consoleProfiles.length)
+        return;
+
+      if (!name)
+        name = this.consoleProfiles.pop();
+      else
+        this.consoleProfiles.filter((n) => n !== name);
+
+      if (!this.profiles.has(name))
+        return;
+
+      let profile = this.profiles.get(name);
+      if (!this.isProfileRecording(profile))
+        return;
+
+      let profileData = data.extra.profile;
+      profileData.threads = profileData.threads.map((thread) => {
+        let samples = thread.samples.filter((sample) => {
+          return sample.time >= profile.timeStarted;
+        });
+
+        return { samples: samples };
+      });
+
+      profile.timeEnded = data.extra.currentTime;
+      profile.data = profileData;
+
+      this.emit("profileEnd", profile);
+    };
+
+    if (type === "profile")
+      profileStart();
+
+    if (type === "profileEnd")
+      profileEnd();
+  },
+
+  /**
+   * Connects to the client unless we're already connected.
+   *
+   * @param function cb
+   *        Function to be called once we're connected. If
+   *        the controller is already connected, this function
+   *        will be called immediately (synchronously).
+   */
+  connect: function (cb=function(){}) {
+    if (this.isConnected) {
+      return void cb();
+    }
+
+    // Check if we already have a grip to the listTabs response object
+    // and, if we do, use it to get to the profilerActor. Otherwise,
+    // call listTabs. The problem is that if we call listTabs twice
+    // webconsole tests fail (see bug 872826).
+
+    let register = () => {
+      let data = { events: ["console-api-profiler"] };
+
+      // Check if Gecko Profiler Addon [1] is installed and, if it is,
+      // don't register our own console event listeners. Gecko Profiler
+      // Addon takes care of console.profile and console.profileEnd methods
+      // and we don't want to break it.
+      //
+      // [1] - https://github.com/bgirard/Gecko-Profiler-Addon/
+
+      AddonManager.getAddonByID("jid0-edalmuivkozlouyij0lpdx548bc@jetpack", (addon) => {
+        if (addon && !addon.userDisabled && !addon.softDisabled)
+          return void cb();
+
+        this.request("registerEventNotifications", data, (resp) => {
+          this.client.addListener("eventNotification", (type, resp) => {
+            let toolbox = gDevTools.getToolbox(this.target);
+            if (toolbox == null)
+              return;
+
+            this.onConsoleEvent(resp.subject.action, resp.data);
+          });
+        });
+
+        cb();
+      });
+    };
+
+    if (this.target.root) {
+      this.actor = this.target.root.profilerActor;
+      this.isConnected = true;
+      return void register();
+    }
+
+    this.client.listTabs((resp) => {
+      this.actor = resp.profilerActor;
+      this.isConnected = true;
+      register();
+    });
+  },
+
+  /**
+   * Adds actor and type information to data and sends the request over
+   * the remote debugging protocol.
+   *
+   * @param string type
+   *        Method to call on the other side
+   * @param object data
+   *        Data to send with the request
+   * @param function cb
+   *        A callback function
+   */
+  request: function (type, data, cb) {
+    data.to = this.actor;
+    data.type = type;
+    this.client.request(data, cb);
+  },
+
+  /**
+   * Checks whether the profiler is active.
+   *
+   * @param function cb
+   *        Function to be called with a response from the
+   *        client. It will be called with two arguments:
+   *        an error object (may be null) and a boolean
+   *        value indicating if the profiler is active or not.
+   */
+  isActive: function (cb) {
+    this.request("isActive", {}, (resp) => {
+      cb(resp.error, resp.isActive, resp.currentTime);
+    });
+  },
+
+  /**
+   * Creates a new profile and starts the profiler, if needed.
+   *
+   * @param string name
+   *        Name of the profile.
+   * @param function cb
+   *        Function to be called once the profiler is started
+   *        or we get an error. It will be called with a single
+   *        argument: an error object (may be null).
+   */
+  start: function PC_start(name, cb) {
+    if (this.profiles.has(name)) {
+      return;
+    }
+
+    let profile = makeProfile(name);
+    this.consoleProfiles.push(name);
+    this.profiles.set(name, profile);
+
+    // If profile is already running, no need to do anything.
+    if (this.isProfileRecording(profile)) {
+      return void cb();
+    }
+
+    this.isActive((err, isActive, currentTime) => {
+      if (isActive) {
+        profile.timeStarted = currentTime;
+        return void cb();
+      }
+
+      let params = {
+        entries: 1000000,
+        interval: 1,
+        features: ["js"],
+      };
+
+      this.request("startProfiler", params, (resp) => {
+        if (resp.error) {
+          return void cb(resp.error);
+        }
+
+        profile.timeStarted = 0;
+        cb();
+      });
+    });
+  },
+
+  /**
+   * Stops the profiler. NOTE, that we don't stop the actual
+   * SPS Profiler here. It will be stopped as soon as all
+   * clients disconnect from the profiler actor.
+   *
+   * @param string name
+   *        Name of the profile that needs to be stopped.
+   * @param function cb
+   *        Function to be called once the profiler is stopped
+   *        or we get an error. It will be called with a single
+   *        argument: an error object (may be null).
+   */
+  stop: function PC_stop(name, cb) {
+    if (!this.profiles.has(name)) {
+      return;
+    }
+
+    let profile = this.profiles.get(name);
+    if (!this.isProfileRecording(profile)) {
+      return;
+    }
+
+    this.request("getProfile", {}, (resp) => {
+      if (resp.error) {
+        Cu.reportError("Failed to fetch profile data.");
+        return void cb(resp.error, null);
+      }
+
+      let data = resp.profile;
+      profile.timeEnded = resp.currentTime;
+
+      // Filter out all samples that fall out of current
+      // profile's range.
+
+      data.threads = data.threads.map((thread) => {
+        let samples = thread.samples.filter((sample) => {
+          return sample.time >= profile.timeStarted;
+        });
+
+        return { samples: samples };
+      });
+
+      cb(null, data);
+    });
+  },
+
+  /**
+   * Cleanup.
+   */
+  destroy: function PC_destroy() {
+    this.client = null;
+    this.target = null;
+    this.actor = null;
+  }
+};
+
+if (isJSM) {
+  var EXPORTED_SYMBOLS = ["ProfilerController"];
+} else {
+  module.exports = ProfilerController;
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/panel.js
@@ -0,0 +1,474 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Cu } = require("chrome");
+const { PROFILE_IDLE, PROFILE_RUNNING, PROFILE_COMPLETED } = require("devtools/profiler/consts");
+
+var EventEmitter = require("devtools/shared/event-emitter");
+var Promise      = require("sdk/core/promise");
+var Cleopatra    = require("devtools/profiler/cleopatra");
+var Sidebar      = require("devtools/profiler/sidebar");
+var ProfilerController = require("devtools/profiler/controller");
+
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+Cu.import("resource://gre/modules/devtools/Console.jsm");
+Cu.import("resource://gre/modules/Services.jsm")
+
+/**
+ * Profiler panel. It is responsible for creating and managing
+ * different profile instances (see cleopatra.js).
+ *
+ * ProfilerPanel is an event emitter. It can emit the following
+ * events:
+ *
+ *   - ready:     after the panel is done loading everything,
+ *                including the default profile instance.
+ *   - started:   after the panel successfuly starts our SPS
+ *                profiler.
+ *   - stopped:   after the panel successfuly stops our SPS
+ *                profiler and is ready to hand over profiling
+ *                data
+ *   - parsed:    after Cleopatra finishes parsing profiling
+ *                data.
+ *   - destroyed: after the panel cleans up after itself and
+ *                is ready to be destroyed.
+ *
+ * The following events are used mainly by tests to prevent
+ * accidential oranges:
+ *
+ *   - profileCreated:  after a new profile is created.
+ *   - profileSwitched: after user switches to a different
+ *                      profile.
+ */
+function ProfilerPanel(frame, toolbox) {
+  this.isReady = false;
+  this.window = frame.window;
+  this.document = frame.document;
+  this.target = toolbox.target;
+
+  this.profiles = new Map();
+  this._uid = 0;
+  this._msgQueue = {};
+
+  EventEmitter.decorate(this);
+}
+
+ProfilerPanel.prototype = {
+  isReady:     null,
+  window:      null,
+  document:    null,
+  target:      null,
+  controller:  null,
+  profiles:    null,
+  sidebar:     null,
+
+  _uid:        null,
+  _activeUid:  null,
+  _runningUid: null,
+  _browserWin: null,
+  _msgQueue:   null,
+
+  get controls() {
+    let doc = this.document;
+
+    return {
+      get record() doc.querySelector("#profiler-start")
+    };
+  },
+
+  get activeProfile() {
+    return this.profiles.get(this._activeUid);
+  },
+
+  set activeProfile(profile) {
+    if (this._activeUid === profile.uid)
+      return;
+
+    if (this.activeProfile)
+      this.activeProfile.hide();
+
+    this._activeUid = profile.uid;
+    profile.show();
+  },
+
+  set recordingProfile(profile) {
+    let btn = this.controls.record;
+    this._runningUid = profile ? profile.uid : null;
+
+    if (this._runningUid)
+      btn.setAttribute("checked", true)
+    else
+      btn.removeAttribute("checked");
+  },
+
+  get recordingProfile() {
+    return this.profiles.get(this._runningUid);
+  },
+
+  get browserWindow() {
+    if (this._browserWin) {
+      return this._browserWin;
+    }
+
+    let win = this.window.top;
+    let type = win.document.documentElement.getAttribute("windowtype");
+
+    if (type !== "navigator:browser") {
+      win = Services.wm.getMostRecentWindow("navigator:browser");
+    }
+
+    return this._browserWin = win;
+  },
+
+  /**
+   * Open a debug connection and, on success, switch to the newly created
+   * profile.
+   *
+   * @return Promise
+   */
+  open: function PP_open() {
+    // Local profiling needs to make the target remote.
+    let target = this.target;
+    let promise = !target.isRemote ? target.makeRemote() : Promise.resolve(target);
+
+    return promise
+      .then((target) => {
+        let deferred = Promise.defer();
+
+        this.controller = new ProfilerController(this.target);
+        this.sidebar = new Sidebar(this.document.querySelector("#profiles-list"));
+
+        this.sidebar.widget.addEventListener("select", (ev) => {
+          if (!ev.detail)
+            return;
+
+          let profile = this.profiles.get(ev.detail.attachment.uid);
+          this.activeProfile = profile;
+
+          if (profile.isReady) {
+            return void this.emit("profileSwitched", profile.uid);
+          }
+
+          profile.once("ready", () => {
+            this.emit("profileSwitched", profile.uid);
+          });
+        });
+
+        this.controller.connect(() => {
+          let btn = this.controls.record;
+          btn.addEventListener("click", () => this.toggleRecording(), false);
+          btn.removeAttribute("disabled");
+
+          // Import queued profiles.
+          for (let [name, data] of this.controller.profiles) {
+            let profile = this.createProfile(name);
+            profile.isStarted = false;
+            profile.isFinished = true;
+            profile.data = data.data;
+            profile.parse(data.data, () => this.emit("parsed"));
+
+            this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
+            if (!this.sidebar.selectedItem) {
+              this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
+            }
+          }
+
+          this.isReady = true;
+          this.emit("ready");
+          deferred.resolve(this);
+        });
+
+        this.controller.on("profileEnd", (_, data) => {
+          let profile = this.createProfile(data.name);
+          profile.isStarted = false;
+          profile.isFinished = true;
+          profile.data = data.data;
+          profile.parse(data.data, () => this.emit("parsed"));
+
+          this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
+          if (!this.sidebar.selectedItem)
+            this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
+
+          if (this.recordingProfile && !data.fromConsole)
+            this.recordingProfile = null;
+
+          this.emit("stopped");
+        });
+
+        return deferred.promise;
+      })
+      .then(null, (reason) =>
+        Cu.reportError("ProfilePanel open failed: " + reason.message));
+  },
+
+  /**
+   * Creates a new profile instance (see cleopatra.js) and
+   * adds an appropriate item to the sidebar. Note that
+   * this method doesn't automatically switch user to
+   * the newly created profile, they have do to switch
+   * explicitly.
+   *
+   * @param string name
+   *        (optional) name of the new profile
+   *
+   * @return ProfilerPanel
+   */
+  createProfile: function (name) {
+    if (name && this.getProfileByName(name)) {
+      return this.getProfileByName(name);
+    }
+
+    let uid = ++this._uid;
+    let name = name || this.controller.getProfileName();
+    let profile = new Cleopatra(uid, name, this);
+
+    this.profiles.set(uid, profile);
+    this.sidebar.addProfile(profile);
+    this.emit("profileCreated", uid);
+
+    return profile;
+  },
+
+  /**
+   * Starts or stops profile recording.
+   */
+  toggleRecording: function () {
+    let profile = this.recordingProfile;
+
+    if (!profile) {
+      profile = this.createProfile();
+
+      this.startProfiling(profile.name, () => {
+        profile.isStarted = true;
+
+        this.sidebar.setProfileState(profile, PROFILE_RUNNING);
+        this.recordingProfile = profile;
+        this.emit("started");
+      });
+
+      return;
+    }
+
+    this.stopProfiling(profile.name, (data) => {
+      profile.isStarted = false;
+      profile.isFinished = true;
+      profile.data = data;
+      profile.parse(data, () => this.emit("parsed"));
+
+      this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
+      this.activeProfile = profile;
+      this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
+      this.recordingProfile = null;
+      this.emit("stopped");
+    });
+  },
+
+  /**
+   * Start collecting profile data.
+   *
+   * @param function onStart
+   *   A function to call once we get the message
+   *   that profiling had been successfuly started.
+   */
+  startProfiling: function (name, onStart) {
+    this.controller.start(name, (err) => {
+      if (err) {
+        return void Cu.reportError("ProfilerController.start: " + err.message);
+      }
+
+      onStart();
+      this.emit("started");
+    });
+  },
+
+  /**
+   * Stop collecting profile data.
+   *
+   * @param function onStop
+   *   A function to call once we get the message
+   *   that profiling had been successfuly stopped.
+   */
+  stopProfiling: function (name, onStop) {
+    this.controller.isActive((err, isActive) => {
+      if (err) {
+        Cu.reportError("ProfilerController.isActive: " + err.message);
+        return;
+      }
+
+      if (!isActive) {
+        return;
+      }
+
+      this.controller.stop(name, (err, data) => {
+        if (err) {
+          Cu.reportError("ProfilerController.stop: " + err.message);
+          return;
+        }
+
+        onStop(data);
+        this.emit("stopped", data);
+      });
+    });
+  },
+
+  /**
+   * Lookup an individual profile by its name.
+   *
+   * @param string name name of the profile
+   * @return profile object or null
+   */
+  getProfileByName: function PP_getProfileByName(name) {
+    if (!this.profiles) {
+      return null;
+    }
+
+    for (let [ uid, profile ] of this.profiles) {
+      if (profile.name === name) {
+        return profile;
+      }
+    }
+
+    return null;
+  },
+
+  /**
+   * Lookup an individual profile by its UID.
+   *
+   * @param number uid UID of the profile
+   * @return profile object or null
+   */
+  getProfileByUID: function PP_getProfileByUID(uid) {
+    if (!this.profiles) {
+      return null;
+    }
+
+    return this.profiles.get(uid) || null;
+  },
+
+  /**
+   * Iterates over each available profile and calls
+   * a callback with it as a parameter.
+   *
+   * @param function cb a callback to call
+   */
+  eachProfile: function PP_eachProfile(cb) {
+    let uid = this._uid;
+
+    if (!this.profiles) {
+      return;
+    }
+
+    while (uid >= 0) {
+      if (this.profiles.has(uid)) {
+        cb(this.profiles.get(uid));
+      }
+
+      uid -= 1;
+    }
+  },
+
+  /**
+   * Broadcast messages to all Cleopatra instances.
+   *
+   * @param number target
+   *   UID of the recepient profile. All profiles will receive the message
+   *   but the profile specified by 'target' will have a special property,
+   *   isCurrent, set to true.
+   * @param object data
+   *   An object with a property 'task' that will be sent over to Cleopatra.
+   */
+  broadcast: function PP_broadcast(target, data) {
+    if (!this.profiles) {
+      return;
+    }
+
+    this.eachProfile((profile) => {
+      profile.message({
+        uid: target,
+        isCurrent: target === profile.uid,
+        task: data.task
+      });
+    });
+  },
+
+  /**
+   * Open file specified in data in either a debugger or view-source.
+   *
+   * @param object data
+   *   An object describing the file. It must have three properties:
+   *    - uri
+   *    - line
+   *    - isChrome (chrome files are opened via view-source)
+   */
+  displaySource: function PP_displaySource(data, onOpen=function() {}) {
+    let win = this.window;
+    let panelWin, timeout;
+
+    function onSourceShown(event) {
+      if (event.detail.url !== data.uri) {
+        return;
+      }
+
+      panelWin.removeEventListener("Debugger:SourceShown", onSourceShown, false);
+      panelWin.editor.setCaretPosition(data.line - 1);
+      onOpen();
+    }
+
+    if (data.isChrome) {
+      return void this.browserWindow.gViewSourceUtils.
+        viewSource(data.uri, null, this.document, data.line);
+    }
+
+    gDevTools.showToolbox(this.target, "jsdebugger").then(function (toolbox) {
+      let dbg = toolbox.getCurrentPanel();
+      panelWin = dbg.panelWin;
+
+      let view = dbg.panelWin.DebuggerView;
+      if (view.Sources.selectedValue === data.uri) {
+        view.editor.setCaretPosition(data.line - 1);
+        onOpen();
+        return;
+      }
+
+      panelWin.addEventListener("Debugger:SourceShown", onSourceShown, false);
+      panelWin.DebuggerView.Sources.preferredSource = data.uri;
+    }.bind(this));
+  },
+
+  /**
+   * Cleanup.
+   */
+  destroy: function PP_destroy() {
+    if (this.profiles) {
+      let uid = this._uid;
+
+      while (uid >= 0) {
+        if (this.profiles.has(uid)) {
+          this.profiles.get(uid).destroy();
+          this.profiles.delete(uid);
+        }
+        uid -= 1;
+      }
+    }
+
+    if (this.controller) {
+      this.controller.destroy();
+    }
+
+    this.isReady = null;
+    this.window = null;
+    this.document = null;
+    this.target = null;
+    this.controller = null;
+    this.profiles = null;
+    this._uid = null;
+    this._activeUid = null;
+
+    this.emit("destroyed");
+  }
+};
+
+module.exports = ProfilerPanel;
\ No newline at end of file
--- a/browser/devtools/profiler/profiler.xul
+++ b/browser/devtools/profiler/profiler.xul
@@ -14,20 +14,19 @@
 <!ENTITY % profilerDTD SYSTEM "chrome://browser/locale/devtools/profiler.dtd">
   %profilerDTD;
 ]>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <box flex="1" id="profiler-chrome" class="devtools-responsive-container">
     <vbox class="profiler-sidebar">
       <toolbar class="devtools-toolbar">
-        <toolbarbutton id="profiler-create"
-                       class="devtools-toolbarbutton"
-                       label="&profilerNew.label;"
-                       disabled="true"/>
+        <hbox id="profiler-controls">
+          <toolbarbutton id="profiler-start" class="devtools-toolbarbutton"/>
+        </hbox>
       </toolbar>
 
       <vbox id="profiles-list" flex="1">
       </vbox>
     </vbox>
 
     <splitter class="devtools-side-splitter"/>
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/sidebar.js
@@ -0,0 +1,86 @@
+/* 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";
+
+let { Cu } = require("chrome");
+let EventEmitter = require("devtools/shared/event-emitter");
+
+Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+const {
+  PROFILE_IDLE,
+  PROFILE_COMPLETED,
+  PROFILE_RUNNING,
+  L10N_BUNDLE
+} = require("devtools/profiler/consts");
+
+loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE));
+
+function Sidebar(el) {
+  EventEmitter.decorate(this);
+
+  this.document = el.ownerDocument;
+  this.widget = new SideMenuWidget(el);
+  this.widget.notice = L10N.getStr("profiler.sidebarNotice");
+}
+
+Sidebar.prototype = Heritage.extend(WidgetMethods, {
+  addProfile: function (profile) {
+    let doc  = this.document;
+    let box  = doc.createElement("vbox");
+    let h3   = doc.createElement("h3");
+    let span = doc.createElement("span");
+
+    box.id = "profile-" + profile.uid;
+    box.className = "profiler-sidebar-item";
+
+    h3.textContent = profile.name;
+    span.textContent = L10N.getStr("profiler.stateIdle");
+
+    box.appendChild(h3);
+    box.appendChild(span);
+
+    this.push([box], {
+      attachment: {
+        uid:   profile.uid,
+        name:  profile.name,
+        state: PROFILE_IDLE
+      }
+    });
+  },
+
+  getElementByProfile: function (profile) {
+    return this.document.querySelector("#profile-" + profile.uid);
+  },
+
+  getItemByProfile: function (profile) {
+    return this.getItemForPredicate(item => item.attachment.uid === profile.uid);
+  },
+
+  setProfileState: function (profile, state) {
+    let item = this.getItemByProfile(profile);
+    let label = item.target.querySelector(".profiler-sidebar-item > span");
+
+    switch (state) {
+      case PROFILE_IDLE:
+        label.textContent = L10N.getStr("profiler.stateIdle");
+        break;
+      case PROFILE_RUNNING:
+        label.textContent = L10N.getStr("profiler.stateRunning");
+        break;
+      case PROFILE_COMPLETED:
+        label.textContent = L10N.getStr("profiler.stateCompleted");
+        break;
+      default: // Wrong state, do nothing.
+        return;
+    }
+
+    item.attachment.state = state;
+    this.emit("stateChanged", item);
+  }
+});
+
+module.exports = Sidebar;
\ No newline at end of file
--- a/browser/devtools/profiler/test/Makefile.in
+++ b/browser/devtools/profiler/test/Makefile.in
@@ -6,28 +6,27 @@ DEPTH          = @DEPTH@
 topsrcdir      = @top_srcdir@
 srcdir         = @srcdir@
 VPATH          = @srcdir@
 relativesrcdir = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_BROWSER_TESTS = \
-		browser_profiler_profiles.js \
 		browser_profiler_remote.js \
 		browser_profiler_bug_834878_source_buttons.js \
 		browser_profiler_cmd.js \
 		browser_profiler_run.js \
 		browser_profiler_controller.js \
-		browser_profiler_bug_830664_multiple_profiles.js \
 		browser_profiler_bug_855244_multiple_tabs.js \
 		browser_profiler_console_api.js \
 		browser_profiler_console_api_named.js \
 		browser_profiler_console_api_mixed.js \
 		browser_profiler_console_api_content.js \
+		browser_profiler_escape.js \
 		head.js \
 		$(NULL)
 
 MOCHITEST_BROWSER_PAGES = \
 		mock_profiler_bug_834878_page.html \
 		mock_profiler_bug_834878_script.js \
 		mock_console_api.html \
 		$(NULL)
deleted file mode 100644
--- a/browser/devtools/profiler/test/browser_profiler_bug_830664_multiple_profiles.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
-
-let gTab, gPanel, gUid;
-
-function test() {
-  waitForExplicitFinish();
-
-  setUp(URL, function onSetUp(tab, browser, panel) {
-    gTab = tab;
-    gPanel = panel;
-
-    gPanel.once("profileCreated", function (_, uid) {
-      gUid = uid;
-      let profile = gPanel.profiles.get(uid);
-
-      if (profile.isReady) {
-        startProfiling();
-      } else {
-        profile.once("ready", startProfiling);
-      }
-    });
-    gPanel.createProfile();
-  });
-}
-
-function startProfiling() {
-  gPanel.profiles.get(gPanel.activeProfile.uid).once("started", function () {
-    setTimeout(function () {
-      sendFromProfile(2, "start");
-      gPanel.profiles.get(2).once("started", function () setTimeout(stopProfiling, 50));
-    }, 50);
-  });
-
-  sendFromProfile(gPanel.activeProfile.uid, "start");
-}
-
-function stopProfiling() {
-  is(getSidebarItem(1).attachment.state, PROFILE_RUNNING);
-  is(getSidebarItem(2).attachment.state, PROFILE_RUNNING);
-
-  gPanel.profiles.get(gPanel.activeProfile.uid).once("stopped", function () {
-    is(getSidebarItem(1).attachment.state, PROFILE_COMPLETED);
-
-    sendFromProfile(2, "stop");
-    gPanel.profiles.get(2).once("stopped", confirmAndFinish);
-  });
-
-  sendFromProfile(gPanel.activeProfile.uid, "stop");
-}
-
-function confirmAndFinish(ev, data) {
-  is(getSidebarItem(1).attachment.state, PROFILE_COMPLETED);
-  is(getSidebarItem(2).attachment.state, PROFILE_COMPLETED);
-
-  tearDown(gTab, function onTearDown() {
-    gPanel = null;
-    gTab = null;
-    gUid = null;
-  });
-}
--- a/browser/devtools/profiler/test/browser_profiler_bug_834878_source_buttons.js
+++ b/browser/devtools/profiler/test/browser_profiler_bug_834878_source_buttons.js
@@ -4,35 +4,31 @@
 const BASE = "http://example.com/browser/browser/devtools/profiler/test/";
 const URL = BASE + "mock_profiler_bug_834878_page.html";
 const SCRIPT = BASE + "mock_profiler_bug_834878_script.js";
 
 function test() {
   waitForExplicitFinish();
 
   setUp(URL, function onSetUp(tab, browser, panel) {
-    panel.once("profileCreated", function () {
-      let data = { uri: SCRIPT, line: 5, isChrome: false };
+    let data = { uri: SCRIPT, line: 5, isChrome: false };
 
-      panel.displaySource(data, function onOpen() {
-        let target = TargetFactory.forTab(tab);
-        let dbg = gDevTools.getToolbox(target).getPanel("jsdebugger");
-        let view = dbg.panelWin.DebuggerView;
+    panel.displaySource(data, function onOpen() {
+      let target = TargetFactory.forTab(tab);
+      let dbg = gDevTools.getToolbox(target).getPanel("jsdebugger");
+      let view = dbg.panelWin.DebuggerView;
 
-        is(view.Sources.selectedValue, data.uri, "URI is different");
-        is(view.editor.getCaretPosition().line, data.line - 1,
-          "Line is different");
+      is(view.Sources.selectedValue, data.uri, "URI is different");
+      is(view.editor.getCaretPosition().line, data.line - 1,
+        "Line is different");
 
-        // Test the case where script is already loaded.
-        view.editor.setCaretPosition(1);
-        gDevTools.showToolbox(target, "jsprofiler").then(function () {
-          panel.displaySource(data, function onOpenAgain() {
-            is(view.editor.getCaretPosition().line, data.line - 1,
-              "Line is different");
-            tearDown(tab);
-          });
+      // Test the case where script is already loaded.
+      view.editor.setCaretPosition(1);
+      gDevTools.showToolbox(target, "jsprofiler").then(function () {
+        panel.displaySource(data, function onOpenAgain() {
+          is(view.editor.getCaretPosition().line, data.line - 1,
+            "Line is different");
+          tearDown(tab);
         });
       });
     });
-
-    panel.createProfile();
   });
 }
--- a/browser/devtools/profiler/test/browser_profiler_cmd.js
+++ b/browser/devtools/profiler/test/browser_profiler_cmd.js
@@ -36,64 +36,51 @@ function setupGlobals() {
   deferred.resolve();
   return deferred.promise;
 }
 
 function testProfilerStart() {
   let deferred = Promise.defer();
 
   gPanel.once("started", function () {
-    is(gPanel.profiles.size, 2, "There are two profiles");
-    ok(!gPanel.getProfileByName("Profile 1").isStarted, "Profile 1 wasn't started");
-    ok(gPanel.getProfileByName("Profile 2").isStarted, "Profile 2 was started");
-    cmd('profiler start "Profile 2"', "This profile has already been started");
+    is(gPanel.profiles.size, 1, "There is a new profile");
+    is(gPanel.getProfileByName("Profile 1"), gPanel.recordingProfile, "Recording profile is OK");
+    ok(!gPanel.activeProfile, "There's no active profile yet");
+    cmd("profiler start", gcli.lookup("profilerAlreadyStarted2"));
     deferred.resolve();
   });
 
-  cmd("profiler start", gcli.lookup("profilerStarting2"));
+  cmd("profiler start", gcli.lookup("profilerStarted"));
   return deferred.promise;
 }
 
 function testProfilerList() {
-  let deferred = Promise.defer();
-
-  cmd("profiler list", /^.*Profile\s1.*Profile\s2\s\*.*$/);
-  deferred.resolve();
-
-  return deferred.promise;
+  cmd("profiler list", /^.*Profile\s1\s\*.*$/);
 }
 
 function testProfilerStop() {
   let deferred = Promise.defer();
 
   gPanel.once("stopped", function () {
-    ok(!gPanel.getProfileByName("Profile 2").isStarted, "Profile 2 was stopped");
-    ok(gPanel.getProfileByName("Profile 2").isFinished, "Profile 2 was stopped");
-    cmd('profiler stop "Profile 2"', "This profile has already been completed. " +
-      "Use 'profile show' command to see its results");
-    cmd('profiler stop "Profile 1"', "This profile has not been started yet. " +
-      "Use 'profile start' to start profiling");
-    cmd('profiler stop "invalid"', "Profile not found")
+    is(gPanel.activeProfile, gPanel.getProfileByName("Profile 1"), "Active profile is OK");
+    ok(!gPanel.recordingProfile, "There's no recording profile");
+    cmd("profiler stop", gcli.lookup("profilerNotStarted3"));
     deferred.resolve();
   });
 
-  cmd('profiler stop "Profile 2"', gcli.lookup("profilerStopping2"));
+  cmd("profiler stop");
   return deferred.promise;
 }
 
 function testProfilerShow() {
   let deferred = Promise.defer();
 
-  is(gPanel.getProfileByName("Profile 2").uid, gPanel.activeProfile.uid,
-    "Profile 2 is active");
-
   gPanel.once("profileSwitched", function () {
-    is(gPanel.getProfileByName("Profile 1").uid, gPanel.activeProfile.uid,
-      "Profile 1 is active");
-    cmd('profile show "invalid"', "Profile not found");
+    is(gPanel.getProfileByName("Profile 1"), gPanel.activeProfile, "Profile 1 is active");
+    cmd('profile show "invalid"', gcli.lookup("profilerNotFound"));
     deferred.resolve();
   });
 
   cmd('profile show "Profile 1"');
   return deferred.promise;
 }
 
 function testProfilerClose() {
--- a/browser/devtools/profiler/test/browser_profiler_console_api.js
+++ b/browser/devtools/profiler/test/browser_profiler_console_api.js
@@ -14,51 +14,22 @@ function test() {
 
     openConsole(tab, testConsoleProfile);
   });
 }
 
 function testConsoleProfile(hud) {
   hud.jsterm.clearOutput(true);
 
-  // Here we start two named profiles and then end one of them.
-  // profileEnd, when name is not provided, simply pops the latest
-  // profile.
-
   let profilesStarted = 0;
 
-  function profileEnd(_, uid) {
-    let profile = gPanel.profiles.get(uid);
-
-    profile.once("started", () => {
-      if (++profilesStarted < 2)
-        return;
-
-      gPanel.off("profileCreated", profileEnd);
-      gPanel.profiles.get(3).once("stopped", () => {
-        openProfiler(gTab, checkProfiles);
-      });
-
-      hud.jsterm.execute("console.profileEnd()");
-    });
-  }
+  gPanel.once("parsed", () => {
+    let profile = gPanel.activeProfile;
 
-  gPanel.on("profileCreated", profileEnd);
-  hud.jsterm.execute("console.profile()");
-  hud.jsterm.execute("console.profile()");
-}
-
-function checkProfiles(toolbox) {
-  let panel = toolbox.getPanel("jsprofiler");
-
-  is(getSidebarItem(1, panel).attachment.state, PROFILE_IDLE);
-  is(getSidebarItem(2, panel).attachment.state, PROFILE_RUNNING);
-  is(getSidebarItem(3, panel).attachment.state, PROFILE_COMPLETED);
-
-  // Make sure we can still stop profiles via the UI.
-
-  gPanel.profiles.get(2).once("stopped", () => {
-    is(getSidebarItem(2, panel).attachment.state, PROFILE_COMPLETED);
+    is(profile.name, "Profile 1", "Profile name is OK");
+    is(gPanel.sidebar.selectedItem, gPanel.sidebar.getItemByProfile(profile), "Sidebar is OK");
+    is(gPanel.sidebar.selectedItem.attachment.state, PROFILE_COMPLETED);
     tearDown(gTab, () => gTab = gPanel = null);
   });
 
-  sendFromProfile(2, "stop");
+  hud.jsterm.execute("console.profile()");
+  hud.jsterm.execute("console.profileEnd()");
 }
\ No newline at end of file
--- a/browser/devtools/profiler/test/browser_profiler_console_api_content.js
+++ b/browser/devtools/profiler/test/browser_profiler_console_api_content.js
@@ -24,18 +24,17 @@ function test() {
           runTests();
         });
       });
     });
  });
 }
 
 function runTests() {
-  is(getSidebarItem(1).attachment.state, PROFILE_IDLE);
-  is(getSidebarItem(2).attachment.state, PROFILE_COMPLETED);
+  is(getSidebarItem(1).attachment.state, PROFILE_COMPLETED);
 
   gPanel.once("parsed", () => {
     function assertSampleAndFinish() {
       let [win,doc] = getProfileInternals();
       let sample = doc.getElementsByClassName("samplePercentage");
 
       if (sample.length <= 0)
         return void setTimeout(assertSampleAndFinish, 100);
--- a/browser/devtools/profiler/test/browser_profiler_console_api_mixed.js
+++ b/browser/devtools/profiler/test/browser_profiler_console_api_mixed.js
@@ -13,24 +13,25 @@ function test() {
     gPanel = panel;
 
     openProfiler(tab, runTests);
   });
 }
 
 function runTests(toolbox) {
   let panel = toolbox.getPanel("jsprofiler");
+  let record = gPanel.controls.record;
 
-  panel.profiles.get(1).once("started", () => {
+  panel.once("started", () => {
     is(getSidebarItem(1, panel).attachment.state, PROFILE_RUNNING);
 
     openConsole(gTab, (hud) => {
-      panel.profiles.get(1).once("stopped", () => {
+      panel.once("stopped", () => {
         is(getSidebarItem(1, panel).attachment.state, PROFILE_COMPLETED);
         tearDown(gTab, () => gTab = gPanel = null);
       });
 
       hud.jsterm.execute("console.profileEnd()");
     });
   });
 
-  sendFromProfile(1, "start");
+  record.click();
 }
\ No newline at end of file
--- a/browser/devtools/profiler/test/browser_profiler_console_api_named.js
+++ b/browser/devtools/profiler/test/browser_profiler_console_api_named.js
@@ -18,49 +18,39 @@ function test() {
 
 function testConsoleProfile(hud) {
   hud.jsterm.clearOutput(true);
 
   // Here we start two named profiles and then end one of them.
 
   let profilesStarted = 0;
 
-  function profileEnd(_, uid) {
-    let profile = gPanel.profiles.get(uid);
-
-    profile.once("started", () => {
-      if (++profilesStarted < 2)
-        return;
+  function endProfile() {
+    if (++profilesStarted < 2)
+      return;
 
-      gPanel.off("profileCreated", profileEnd);
-      gPanel.profiles.get(2).once("stopped", () => {
-        openProfiler(gTab, checkProfiles);
-      });
-
-      hud.jsterm.execute("console.profileEnd('Second')");
-    });
+    gPanel.controller.off("profileStart", endProfile);
+    gPanel.controller.once("profileEnd", () => openProfiler(gTab, checkProfiles));
+    hud.jsterm.execute("console.profileEnd('Second')");
   }
 
-  gPanel.on("profileCreated", profileEnd);
+  gPanel.controller.on("profileStart", endProfile);
   hud.jsterm.execute("console.profile('Second')");
   hud.jsterm.execute("console.profile('Third')");
 }
 
 function checkProfiles(toolbox) {
   let panel = toolbox.getPanel("jsprofiler");
 
-  is(getSidebarItem(1, panel).attachment.state, PROFILE_IDLE);
-  is(getSidebarItem(2, panel).attachment.name, "Second");
-  is(getSidebarItem(2, panel).attachment.state, PROFILE_COMPLETED);
-  is(getSidebarItem(3, panel).attachment.name, "Third");
-  is(getSidebarItem(3, panel).attachment.state, PROFILE_RUNNING);
+  is(getSidebarItem(1, panel).attachment.name, "Second", "Name in sidebar is OK");
+  is(getSidebarItem(1, panel).attachment.state, PROFILE_COMPLETED, "State in sidebar is OK");
 
   // Make sure we can still stop profiles via the queue pop.
 
-  gPanel.profiles.get(3).once("stopped", () => {
+  gPanel.controller.once("profileEnd", () => {
     openProfiler(gTab, () => {
-      is(getSidebarItem(3, panel).attachment.state, PROFILE_COMPLETED);
+      is(getSidebarItem(2, panel).attachment.state, PROFILE_COMPLETED, "State in sidebar is OK");
       tearDown(gTab, () => gTab = gPanel = null);
     });
   });
 
   openConsole(gTab, (hud) => hud.jsterm.execute("console.profileEnd()"));
 }
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_escape.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+
+let gTab, gPanel;
+
+function test() {
+  waitForExplicitFinish();
+
+  setUp(URL, function (tab, browser, panel) {
+    gTab = tab;
+    gPanel = panel;
+
+    let record = gPanel.controls.record;
+
+    gPanel.once("started", () => {
+      gPanel.once("stopped", () => {
+        let [ win, doc ] = getProfileInternals(gPanel.activeProfile.uid);
+
+        let expl = "<script>function f() {}</script></textarea><img/src='about:logo'>";
+        let expl2 = "<script>function f() {}</script></pre><img/src='about:logo'>";
+
+        is(win.escapeHTML(expl),
+          "&lt;script&gt;function f() {}&lt;/script&gt;&lt;/textarea&gt;&lt;img/src='about:logo'&gt;");
+
+        is(win.escapeHTML(expl2),
+          "&lt;script&gt;function f() {}&lt;/script&gt;&lt;/pre&gt;&lt;img/src='about:logo'&gt;");
+
+        tearDown(gTab, () => {
+          gTab = null;
+          gPanel = null;
+        });
+      });
+
+      setTimeout(() => {
+        record.click();
+      }, 50);
+    });
+
+    record.click();
+  });
+}
\ No newline at end of file
deleted file mode 100644
--- a/browser/devtools/profiler/test/browser_profiler_profiles.js
+++ /dev/null
@@ -1,69 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
-
-let gTab, gPanel;
-
-function test() {
-  waitForExplicitFinish();
-
-  setUp(URL, function onSetUp(tab, browser, panel) {
-    gTab = tab;
-    gPanel = panel;
-
-    panel.once("profileCreated", onProfileCreated);
-    panel.once("profileSwitched", onProfileSwitched);
-
-    testNewProfile();
-  });
-}
-
-function testNewProfile() {
-  is(gPanel.profiles.size, 1, "There is only one profile");
-
-  let btn = gPanel.document.getElementById("profiler-create");
-  ok(!btn.getAttribute("disabled"), "Create Profile button is not disabled");
-  btn.click();
-}
-
-function onProfileCreated(name, uid) {
-  is(gPanel.profiles.size, 2, "There are two profiles now");
-  ok(gPanel.activeProfile.uid !== uid, "New profile is not yet active");
-
-  let btn = gPanel.document.getElementById("profile-" + uid);
-  ok(btn, "Profile item has been added to the sidebar");
-  btn.click();
-}
-
-function onProfileSwitched(name, uid) {
-  gPanel.once("profileCreated", onNamedProfileCreated);
-  gPanel.once("profileSwitched", onNamedProfileSwitched);
-
-  ok(gPanel.activeProfile.uid === uid, "Switched to a new profile");
-  gPanel.createProfile("Custom Profile");
-}
-
-function onNamedProfileCreated(name, uid) {
-  is(gPanel.profiles.size, 3, "There are three profiles now");
-  is(gPanel.getProfileByUID(uid).name, "Custom Profile", "Name is correct");
-
-  let profile = gPanel.profiles.get(uid);
-  let data = gPanel.sidebar.getItemByProfile(profile).attachment;
-
-  is(data.uid, uid, "UID is correct");
-  is(data.name, "Custom Profile", "Name is correct on the label");
-
-  let btn = gPanel.document.getElementById("profile-" + uid);
-  ok(btn, "Profile item has been added to the sidebar");
-  btn.click();
-}
-
-function onNamedProfileSwitched(name, uid) {
-  ok(gPanel.activeProfile.uid === uid, "Switched to a new profile");
-
-  tearDown(gTab, function onTearDown() {
-    gPanel = null;
-    gTab = null;
-  });
-}
\ No newline at end of file
--- a/browser/devtools/profiler/test/browser_profiler_remote.js
+++ b/browser/devtools/profiler/test/browser_profiler_remote.js
@@ -7,17 +7,17 @@ let temp = {};
 
 Cu.import("resource://gre/modules/devtools/dbg-server.jsm", temp);
 let DebuggerServer = temp.DebuggerServer;
 
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm", temp);
 let DebuggerClient = temp.DebuggerClient;
 let debuggerSocketConnect = temp.debuggerSocketConnect;
 
-Cu.import("resource:///modules/devtools/ProfilerController.jsm", temp);
+Cu.import("resource:///modules/devtools/profiler/controller.js", temp);
 let ProfilerController = temp.ProfilerController;
 
 function test() {
   waitForExplicitFinish();
   Services.prefs.setBoolPref(REMOTE_ENABLED, true);
 
   loadTab(URL, function onTabLoad(tab, browser) {
     DebuggerServer.init(function () true);
--- a/browser/devtools/profiler/test/browser_profiler_run.js
+++ b/browser/devtools/profiler/test/browser_profiler_run.js
@@ -1,66 +1,115 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
 
-let gTab, gPanel, gAttempts = 0;
+let gTab, gPanel;
 
 function test() {
   waitForExplicitFinish();
 
   setUp(URL, function onSetUp(tab, browser, panel) {
     gTab = tab;
     gPanel = panel;
 
-    panel.once("started", onStart);
-    panel.once("parsed", onParsed);
+    function done() {
+      tearDown(gTab, () => { gPanel = null; gTab = null; });
+    }
 
-    testUI();
+    startRecording()
+      .then(stopRecording)
+      .then(startRecordingAgain)
+      .then(stopRecording)
+      .then(switchBackToTheFirstOne)
+      .then(done);
   });
 }
 
-function testUI() {
+function startRecording() {
+  let deferred = Promise.defer();
+
   ok(gPanel, "Profiler panel exists");
-  ok(gPanel.activeProfile, "Active profile exists");
+  ok(!gPanel.activeProfile, "Active profile doesn't exist");
+  ok(!gPanel.recordingProfile, "Recording profile doesn't exist");
+
+  let record = gPanel.controls.record;
+  ok(record, "Record button exists.");
+  ok(!record.getAttribute("checked"), "Record button is unchecked");
 
-  let [win, doc] = getProfileInternals();
-  let startButton = doc.querySelector(".controlPane #startWrapper button");
-  let stopButton = doc.querySelector(".controlPane #stopWrapper button");
+  gPanel.once("started", () => {
+    let item = gPanel.sidebar.getItemByProfile(gPanel.recordingProfile);
+    is(item.attachment.name, "Profile 1");
+    is(item.attachment.state, PROFILE_RUNNING);
 
-  ok(startButton, "Start button exists");
-  ok(stopButton, "Stop button exists");
+    gPanel.controller.isActive(function (err, isActive) {
+      ok(isActive, "Profiler is running");
+      deferred.resolve();
+    });
+  });
 
-  startButton.click();
+  record.click();
+  return deferred.promise;
 }
 
-function onStart() {
-  gPanel.controller.isActive(function (err, isActive) {
-    ok(isActive, "Profiler is running");
+function stopRecording() {
+  let deferred = Promise.defer();
+
+  gPanel.once("parsed", () => {
+    let item = gPanel.sidebar.getItemByProfile(gPanel.activeProfile);
+    is(item.attachment.state, PROFILE_COMPLETED);
+
+    function assertSample() {
+      let [ win, doc ] = getProfileInternals();
+      let sample = doc.getElementsByClassName("samplePercentage");
 
-    let [win, doc] = getProfileInternals();
-    let stopButton = doc.querySelector(".controlPane #stopWrapper button");
+      if (sample.length <= 0) {
+        return void setTimeout(assertSample, 100);
+      }
+
+      ok(sample.length > 0, "We have some items displayed");
+      is(sample[0].innerHTML, "100.0%", "First percentage is 100%");
 
-    setTimeout(function () stopButton.click(), 100);
+      deferred.resolve();
+    }
+
+    assertSample();
   });
+
+  setTimeout(function () gPanel.controls.record.click(), 100);
+  return deferred.promise;
 }
 
-function onParsed() {
-  function assertSample() {
-    let [win,doc] = getProfileInternals();
-    let sample = doc.getElementsByClassName("samplePercentage");
+function startRecordingAgain() {
+  let deferred = Promise.defer();
+
+  let record = gPanel.controls.record;
+  ok(!record.getAttribute("checked"), "Record button is unchecked");
 
-    if (sample.length <= 0) {
-      return void setTimeout(assertSample, 100);
-    }
+  gPanel.once("started", () => {
+    ok(gPanel.activeProfile !== gPanel.recordingProfile);
+
+    let item = gPanel.sidebar.getItemByProfile(gPanel.recordingProfile);
+    is(item.attachment.name, "Profile 2");
+    is(item.attachment.state, PROFILE_RUNNING);
+
+    deferred.resolve();
+  });
 
-    ok(sample.length > 0, "We have some items displayed");
-    is(sample[0].innerHTML, "100.0%", "First percentage is 100%");
+  record.click();
+  return deferred.promise;
+}
+
+function switchBackToTheFirstOne() {
+  let deferred = Promise.defer();
+  let button = gPanel.sidebar.getElementByProfile({ uid: 1 });
+  let item = gPanel.sidebar.getItemByProfile({ uid: 1 });
 
-    tearDown(gTab, function onTearDown() {
-      gPanel = null;
-      gTab = null;
-    });
-  }
+  gPanel.once("profileSwitched", () => {
+    is(gPanel.activeProfile.uid, 1, "activeProfile is correct");
+    is(gPanel.sidebar.selectedItem, item, "selectedItem is correct");
+    deferred.resolve();
+  });
 
-  assertSample();
-}
+  button.click();
+  return deferred.promise;
+}
\ No newline at end of file
--- a/browser/devtools/scratchpad/scratchpad-manager.jsm
+++ b/browser/devtools/scratchpad/scratchpad-manager.jsm
@@ -158,9 +158,9 @@ var ShutdownObserver = {
       this.uninit();
     }
   },
 
   uninit: function SDO_uninit()
   {
     Services.obs.removeObserver(this, "quit-application-granted");
   }
-};
\ No newline at end of file
+};
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -1394,16 +1394,17 @@ var Scratchpad = {
       if (aSaved && !Components.isSuccessCode(aStatus)) {
         shouldClose = false;
       }
 
       if (shouldClose) {
         telemetry.toolClosed("scratchpad");
         window.close();
       }
+
       if (aCallback) {
         aCallback();
       }
     });
   },
 
   _observers: [],
 
@@ -1462,26 +1463,28 @@ var Scratchpad = {
       let observer = this._observers[i];
       let handler = observer["on" + aName];
       if (handler) {
         handler.apply(observer, aArgs);
       }
     }
   },
 
+  /**
+   * Opens the MDN documentation page for Scratchpad.
+   */
   openDocumentationPage: function SP_openDocumentationPage()
   {
     let url = this.strings.GetStringFromName("help.openDocumentationPage");
     let newTab = this.gBrowser.addTab(url);
     this.browserWindow.focus();
     this.gBrowser.selectedTab = newTab;
   },
 };
 
-
 /**
  * Encapsulates management of the sidebar containing the VariablesView for
  * object inspection.
  */
 function ScratchpadSidebar(aScratchpad)
 {
   let ToolSidebar = devtools.require("devtools/framework/sidebar").ToolSidebar;
   let tabbox = document.querySelector("#scratchpad-sidebar");
--- a/browser/devtools/scratchpad/scratchpad.xul
+++ b/browser/devtools/scratchpad/scratchpad.xul
@@ -157,17 +157,16 @@
 
       <menuitem id="sp-menu-close"
                 label="&closeCmd.label;"
                 key="sp-key-close"
                 accesskey="&closeCmd.accesskey;"
                 command="sp-cmd-close"/>
     </menupopup>
   </menu>
-
   <menu id="sp-edit-menu" label="&editMenu.label;"
         accesskey="&editMenu.accesskey;">
     <menupopup id="sp-menu_editpopup"
                onpopupshowing="goUpdateSourceEditorMenuItems()">
       <menuitem id="se-menu-undo"/>
       <menuitem id="se-menu-redo"/>
       <menuseparator/>
       <menuitem id="se-menu-cut"/>
@@ -177,17 +176,16 @@
       <menuitem id="se-menu-selectAll"/>
       <menuseparator/>
       <menuitem id="se-menu-find"/>
       <menuitem id="se-menu-findAgain"/>
       <menuseparator/>
       <menuitem id="se-menu-gotoLine"/>
     </menupopup>
   </menu>
-
   <menu id="sp-execute-menu" label="&executeMenu.label;"
         accesskey="&executeMenu.accesskey;">
     <menupopup id="sp-menu_executepopup">
       <menuitem id="sp-text-run"
                 label="&run.label;"
                 accesskey="&run.accesskey;"
                 key="sp-key-run"
                 command="sp-cmd-run"/>
@@ -247,16 +245,46 @@
                 label="&documentationLink.label;"
                 accesskey="&documentationLink.accesskey;"
                 command="sp-cmd-documentationLink"
                 key="key_openHelp"/>
     </menupopup>
   </menu>
 </menubar>
 
+<toolbar id="sp-toolbar"
+         class="devtools-toolbar">
+  <toolbarbutton id="sp-toolbar-open"
+                 class="devtools-toolbarbutton"
+                 label="&openFileCmd.label;"
+                 command="sp-cmd-openFile"/>
+  <toolbarbutton id="sp-toolbar-save"
+                 class="devtools-toolbarbutton"
+                 label="&saveFileCmd.label;"
+                 command="sp-cmd-save"/>
+  <toolbarbutton id="sp-toolbar-saveAs"
+                 class="devtools-toolbarbutton"
+                 label="&saveFileAsCmd.label;"
+                 command="sp-cmd-saveas"/>
+  <toolbarspacer/>
+  <toolbarbutton id="sp-toolbar-run"
+                 class="devtools-toolbarbutton"
+                 label="&run.label;"
+                 command="sp-cmd-run"/>
+  <toolbarbutton id="sp-toolbar-inspect"
+                 class="devtools-toolbarbutton"
+                 label="&inspect.label;"
+                 command="sp-cmd-inspect"/>
+  <toolbarbutton id="sp-toolbar-display"
+                 class="devtools-toolbarbutton"
+                 label="&display.label;"
+                 command="sp-cmd-display"/>
+</toolbar>
+
+
 <popupset id="scratchpad-popups">
   <menupopup id="scratchpad-text-popup"
              onpopupshowing="goUpdateSourceEditorMenuItems()">
     <menuitem id="se-cMenu-cut"/>
     <menuitem id="se-cMenu-copy"/>
     <menuitem id="se-cMenu-paste"/>
     <menuitem id="se-cMenu-delete"/>
     <menuseparator/>
--- a/browser/devtools/webconsole/test/Makefile.in
+++ b/browser/devtools/webconsole/test/Makefile.in
@@ -136,16 +136,17 @@ MOCHITEST_BROWSER_FILES = \
 	browser_webconsole_bug_817834_add_edited_input_to_history.js \
 	browser_console_addonsdk_loader_exception.js \
 	browser_console_error_source_click.js \
 	browser_console_clear_on_reload.js \
 	browser_console_keyboard_accessibility.js \
 	browser_console_filters.js \
 	browser_console_dead_objects.js \
 	browser_console_iframe_messages.js \
+	browser_console_variables_view_while_debugging_and_inspecting.js \
 	head.js \
 	$(NULL)
 
 ifeq ($(OS_ARCH), Darwin)
 MOCHITEST_BROWSER_FILES += \
 	browser_webconsole_bug_804845_ctrl_key_nav.js \
         $(NULL)
 endif
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js
@@ -0,0 +1,127 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that makes sure web console eval works while the js debugger paused the
+// page, and while the inspector is active. See bug 886137.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html";
+
+let gWebConsole, gJSTerm, gDebuggerWin, gThread, gDebuggerController,
+    gStackframes, gVariablesView;
+
+function test()
+{
+  addTab(TEST_URI);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
+}
+
+function consoleOpened(hud)
+{
+  gWebConsole = hud;
+  gJSTerm = hud.jsterm;
+
+  info("openDebugger");
+  openDebugger().then(debuggerOpened);
+}
+
+function debuggerOpened(aResult)
+{
+  gDebuggerWin = aResult.panelWin;
+  gDebuggerController = gDebuggerWin.DebuggerController;
+  gThread = gDebuggerController.activeThread;
+  gStackframes = gDebuggerController.StackFrames;
+
+  openInspector(inspectorOpened);
+}
+
+function inspectorOpened(aPanel)
+{
+  gThread.addOneTimeListener("framesadded", onFramesAdded);
+
+  info("firstCall()");
+  content.wrappedJSObject.firstCall();
+}
+
+function onFramesAdded()
+{
+  info("onFramesAdded");
+
+  openConsole(null, () => gJSTerm.execute("fooObj", onExecuteFooObj));
+}
+
+function onExecuteFooObj()
+{
+  let msg = gWebConsole.outputNode.querySelector(".webconsole-msg-output");
+  ok(msg, "output message found");
+  isnot(msg.textContent.indexOf("[object Object]"), -1, "message text check");
+
+  gJSTerm.once("variablesview-fetched", onFooObjFetch);
+
+  EventUtils.synthesizeMouse(msg, 2, 2, {}, gWebConsole.iframeWindow);
+}
+
+function onFooObjFetch(aEvent, aVar)
+{
+  gVariablesView = aVar._variablesView;
+  ok(gVariablesView, "variables view object");
+
+  findVariableViewProperties(aVar, [
+    { name: "testProp2", value: "testValue2" },
+    { name: "testProp", value: "testValue", dontMatch: true },
+  ], { webconsole: gWebConsole }).then(onTestPropFound);
+}
+
+function onTestPropFound(aResults)
+{
+  let prop = aResults[0].matchedProp;
+  ok(prop, "matched the |testProp2| property in the variables view");
+
+  // Check that property value updates work and that jsterm functions can be
+  // used.
+  updateVariablesViewProperty({
+    property: prop,
+    field: "value",
+    string: "document.title + foo2 + $('p')",
+    webconsole: gWebConsole,
+    callback: onFooObjFetchAfterUpdate,
+  });
+}
+
+function onFooObjFetchAfterUpdate(aEvent, aVar)
+{
+  info("onFooObjFetchAfterUpdate");
+  let para = content.wrappedJSObject.document.querySelector("p");
+  let expectedValue = content.document.title + "foo2SecondCall" + para;
+
+  findVariableViewProperties(aVar, [
+    { name: "testProp2", value: expectedValue },
+  ], { webconsole: gWebConsole }).then(onUpdatedTestPropFound);
+}
+
+function onUpdatedTestPropFound(aResults)
+{
+  let prop = aResults[0].matchedProp;
+  ok(prop, "matched the updated |testProp2| property value");
+
+  // Check that testProp2 was updated.
+  gJSTerm.execute("fooObj.testProp2", onExecuteFooObjTestProp2);
+}
+
+function onExecuteFooObjTestProp2()
+{
+  let para = content.wrappedJSObject.document.querySelector("p");
+  let expected = content.document.title + "foo2SecondCall" + para;
+
+  isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+        "fooObj.testProp2 is correct");
+
+  gWebConsole = gJSTerm = gDebuggerWin = gThread = gDebuggerController =
+    gStackframes = gVariablesView = null;
+
+  finishTest();
+}
--- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
@@ -1233,42 +1233,32 @@ profilerShowDesc=Show individual profile
 # LOCALIZATION NOTE (profilerShowManual) A fuller description of the 'profile name'
 # parameter. This parameter is used to name a newly created profile or to lookup
 # an existing profile by its name.
 profilerShowManual=Name of a profile.
 
 # LOCALIZATION NOTE (profilerAlreadyStarted) A message that is displayed whenever
 # an operation cannot be completed because the profile in question has already
 # been started.
-profilerAlreadyStarted=This profile has already been started
-
-# LOCALIZATION NOTE (profilerAlreadyFinished) A message that is displayed whenever
-# an operation cannot be completed because the profile in question has already
-# been finished. It also contains a hint to use the 'profile show' command to see
-# the profiling results.
-profilerAlreadyFinished=This profile has already been completed. Use 'profile show' command to see its results
+profilerAlreadyStarted2=Profile has already been started
 
 # LOCALIZATION NOTE (profilerNotFound) A message that is displayed whenever
 # an operation cannot be completed because the profile in question could not be
 # found.
 profilerNotFound=Profile not found
 
 # LOCALIZATION NOTE (profilerNotStarted) A message that is displayed whenever
 # an operation cannot be completed because the profile in question has not been
 # started yet. It also contains a hint to use the 'profile start' command to
 # start the profiler.
-profilerNotStarted2=This profile has not been started yet. Use 'profile start' to start profiling
+profilerNotStarted3=Profiler has not been started yet. Use 'profile start' to start profiling
 
-# LOCALIZATION NOTE (profilerStarting) A very short string that indicates that
-# we're starting the profiler.
-profilerStarting2=Starting…
-
-# LOCALIZATION NOTE (profilerStopping) A very short string that indicates that
-# we're stopping the profiler.
-profilerStopping2=Stopping…
+# LOCALIZATION NOTE (profilerStarted) A very short string that indicates that
+# we have started recording.
+profilerStarted=Recording...
 
 # LOCALIZATION NOTE (profilerNotReady) A message that is displayed whenever
 # an operation cannot be completed because the profiler has not been opened yet.
 profilerNotReady=For this command to work you need to open the profiler first
 
 # LOCALIZATION NOTE (listenDesc) A very short string used to describe the
 # function of the 'listen' command.
 listenDesc=Open a remote debug port
--- a/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
@@ -8,8 +8,16 @@
   - 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 (profilerNew.label): This is the label for the
   -  button that creates a new profile. -->
 <!ENTITY profilerNew.label "New">
+
+<!-- LOCALIZATION NOTE (profilerStart.label): This is the label for the
+  -  button that starts the profiler. -->
+<!ENTITY profilerStart.label "Start">
+
+<!-- LOCALIZATION NOTE (profilerStop.label): This is the label for the
+  -  button that stops the profiler. -->
+<!ENTITY profilerStop.label "Stop">
\ No newline at end of file
--- a/browser/locales/en-US/chrome/browser/devtools/profiler.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/profiler.properties
@@ -92,8 +92,14 @@ profiler.stateIdle=Idle
 # This string is used to show that the profile in question is in RUNNING
 # state meaning that it has been started and currently gathering profile data.
 profiler.stateRunning=Running
 
 # LOCALIZATION NOTE (profiler.stateCompleted)
 # This string is used to show that the profile in question is in COMPLETED
 # state meaning that it has been started and stopped already.
 profiler.stateCompleted=Completed
+
+# LOCALIZATION NOTE (profiler.sidebarNotice)
+# This string is displayed in the profiler sidebar when there are no
+# existing profiles to show (usually happens when the user opens the
+# profiler for the first time).
+profiler.sidebarNotice=There are no profiles yet.
\ No newline at end of file
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0346458bc27816ef0e0fa782b1fd3f433aa16045
GIT binary patch
literal 1480
zc$@*q1vmPMP)<h;3K|Lk000e1NJLTq001BW000mO1^@s6cL04^000G#Nkl<Zc-p0v
zX;70_6ovy;K`Vk{n}CvyeGnBvDYzlB$fBSkQ`gp|wL%@80$Q9}7nVj0NDWOyaTz)Y
zDB@HQ#2Je$VyQ?pRg8#&3_D>-@@0VpZ_i<wNk`j1{qYR<JLi4xdB5cn8X@F=VY?yI
z5F@}Gm<I#^5##s+F4#kjq368~`w20phIH{Nt*%O?)%U1|bTXh8I6+Z_@gD(907vJq
z@?)20N*mXdP>RqhGJ*u&Rw|Z>UPz%AgBsL+FaV{|t%jj*VA$Y?fL^D1NdNf<0+?{u
zi#!%5)!YgCU8f7IAjA*wy^oBo11kv$D%a~!gIe_bBY;w)TLlApVNidcLU~OrX$anP
z-~b)LHPo(S521$i+YAC2bHjyB?g`ap{^cWN<-HNI_)@dX`9NU>YaK709nKfH#1z$f
z=5!3MfF5d4iyrijZqh@DnR-Yk#mC*&)f1ncoz3hT7zm@$XhukHzaq4>wA8V)x9>P=
zAZUk1qv4DPFn8W{_0Xy^jc)1PVd8qS>J?`r|I{L0c`DuWYd41Xe$NF*iZ9r078NYY
zc-Da$)H?0XJ&fM5yH{zWVAJkCxiSw1P63QxwYJ(q&loToRH`E=?J%FsW}EiQsa(`5
z)WdxAa>6oPJ<`REOG?yap{SvIuI~@K8D7x~9gp2z$6CvG<b<6Kb3J;m=(DVsVpi~}
zbocDmLDYKu{G<uI^Tz^cb%l6W&!tVic#U8P_&Xi$I2n4awe|H2u#OtkLZHWZ&0c#Z
z#671?>GqqP*ze6Pof~js)g0d=bY@_(ox|44(e46Ct9Oxn#33p_gXy2J(<!c4j9T}c
zb|rX2M~A^?VjLnHknezxECOm%fJrd?0oJ=@{fU>#%T2L|8mu9pnaU1H-9Epdcf_SY
z_KqEucZVgEByx6T+u25l{5|qI)NVKXiBrm>w>F`?FPJGcY21T1|8%+0M_g~nM!>sm
zB0wE%*enzZXTqIN#u^4tYKE)iDosstMusKUu!kDd0&nW->L#;7_&e;cc8u8PcD`eU
zoKCdyJGQ`j_0d`Eb!kBkS6bBeS6Vcz4QcU=Wd|K?{7!grw`XPB=XDW>oOV5Uw~w8D
zC6%ul(l;p-YA?eh=HugIf#8QYQ9})CAy_qDGuQCK*+MDB`n8QX?90E(3gWrKyLDp)
zr);tbPCjW9l$2-_khql<lIp<<I_2dMTU>=&wn$2$cXZRXj_$p93X~e{IT;}{sA2t1
zm3DZaAy~s6YEUa1lqaIs#{c*d=8xsC8JAm#!|6wz+%1<5+nsB;#m#zA%oRQ@L2zv=
zdUnfp^Th-^fn)%+%wzYP!Rt91AS)|t7Ci21c<0aIM0`_NSZLWdNCi-u;V%(fV-0(#
z0mKy*753<5EZt9MMhcQ<pOfmXE;bRn__CMGO_^sc<L};>#(#Wk8o#dSlkdw4nUUu+
z?Gh?m>7YR^deA%Z#Tz|-`rH>beggaj@Aqs=OS?CZ$1_K84K)Gw07>KP=FNCY&}-qm
zcCxk4p(S&ouUDFhUJ$DbjfBIi>E`UsyT@5CIM3Obagh^U@PM28sGojbN>GDZ>y>ey
z=%pDPyGivDi61<P%JCpi4H>W1N?*N=#E;SNU$N!FEi+)lHH@fhRVL|A^yb0_LeGRj
zFc5%s12OyM8Nr^_zWi#`qUXKe0t^uscbw7Q)w5qtY4Ty<9}wuFLZvO}?Cy;{Ef8=f
zh8qU>Xoj_s#k>s`bnkDMTZi*gX2q4fHT~%!WpqaT&N!`JKQ*~VX&HOF8GEQfEqdMy
zF!4G76B>>2F>Us2GaBvF>A1!k_WteH5I3AccQKtgKax%NiVCn^5xZxO&*2#0JF68j
iyP@WfJ=9DLNB#!Nfu_l%-@lUp0000<MNUMnLSTY@9?aMP
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -197,16 +197,17 @@ browser.jar:
   skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
   skin/classic/browser/devtools/responsive-background.png (devtools/responsive-background.png)
   skin/classic/browser/devtools/toggle-tools.png          (devtools/toggle-tools.png)
   skin/classic/browser/devtools/dock-bottom.png           (devtools/dock-bottom.png)
   skin/classic/browser/devtools/dock-side.png             (devtools/dock-side.png)
   skin/classic/browser/devtools/floating-scrollbars.css   (devtools/floating-scrollbars.css)
   skin/classic/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css)
   skin/classic/browser/devtools/inspector.css             (devtools/inspector.css)
+  skin/classic/browser/devtools/profiler-stopwatch.png    (devtools/profiler-stopwatch.png)
   skin/classic/browser/devtools/toolbox.css               (devtools/toolbox.css)
   skin/classic/browser/devtools/tool-options.png          (devtools/tool-options.png)
   skin/classic/browser/devtools/tool-webconsole.png       (devtools/tool-webconsole.png)
   skin/classic/browser/devtools/tool-debugger.png         (devtools/tool-debugger.png)
   skin/classic/browser/devtools/tool-debugger-paused.png  (devtools/tool-debugger-paused.png)
   skin/classic/browser/devtools/tool-inspector.png        (devtools/tool-inspector.png)
   skin/classic/browser/devtools/tool-styleeditor.png      (devtools/tool-styleeditor.png)
   skin/classic/browser/devtools/tool-profiler.png         (devtools/tool-profiler.png)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0346458bc27816ef0e0fa782b1fd3f433aa16045
GIT binary patch
literal 1480
zc$@*q1vmPMP)<h;3K|Lk000e1NJLTq001BW000mO1^@s6cL04^000G#Nkl<Zc-p0v
zX;70_6ovy;K`Vk{n}CvyeGnBvDYzlB$fBSkQ`gp|wL%@80$Q9}7nVj0NDWOyaTz)Y
zDB@HQ#2Je$VyQ?pRg8#&3_D>-@@0VpZ_i<wNk`j1{qYR<JLi4xdB5cn8X@F=VY?yI
z5F@}Gm<I#^5##s+F4#kjq368~`w20phIH{Nt*%O?)%U1|bTXh8I6+Z_@gD(907vJq
z@?)20N*mXdP>RqhGJ*u&Rw|Z>UPz%AgBsL+FaV{|t%jj*VA$Y?fL^D1NdNf<0+?{u
zi#!%5)!YgCU8f7IAjA*wy^oBo11kv$D%a~!gIe_bBY;w)TLlApVNidcLU~OrX$anP
z-~b)LHPo(S521$i+YAC2bHjyB?g`ap{^cWN<-HNI_)@dX`9NU>YaK709nKfH#1z$f
z=5!3MfF5d4iyrijZqh@DnR-Yk#mC*&)f1ncoz3hT7zm@$XhukHzaq4>wA8V)x9>P=
zAZUk1qv4DPFn8W{_0Xy^jc)1PVd8qS>J?`r|I{L0c`DuWYd41Xe$NF*iZ9r078NYY
zc-Da$)H?0XJ&fM5yH{zWVAJkCxiSw1P63QxwYJ(q&loToRH`E=?J%FsW}EiQsa(`5
z)WdxAa>6oPJ<`REOG?yap{SvIuI~@K8D7x~9gp2z$6CvG<b<6Kb3J;m=(DVsVpi~}
zbocDmLDYKu{G<uI^Tz^cb%l6W&!tVic#U8P_&Xi$I2n4awe|H2u#OtkLZHWZ&0c#Z
z#671?>GqqP*ze6Pof~js)g0d=bY@_(ox|44(e46Ct9Oxn#33p_gXy2J(<!c4j9T}c
zb|rX2M~A^?VjLnHknezxECOm%fJrd?0oJ=@{fU>#%T2L|8mu9pnaU1H-9Epdcf_SY
z_KqEucZVgEByx6T+u25l{5|qI)NVKXiBrm>w>F`?FPJGcY21T1|8%+0M_g~nM!>sm
zB0wE%*enzZXTqIN#u^4tYKE)iDosstMusKUu!kDd0&nW->L#;7_&e;cc8u8PcD`eU
zoKCdyJGQ`j_0d`Eb!kBkS6bBeS6Vcz4QcU=Wd|K?{7!grw`XPB=XDW>oOV5Uw~w8D
zC6%ul(l;p-YA?eh=HugIf#8QYQ9})CAy_qDGuQCK*+MDB`n8QX?90E(3gWrKyLDp)
zr);tbPCjW9l$2-_khql<lIp<<I_2dMTU>=&wn$2$cXZRXj_$p93X~e{IT;}{sA2t1
zm3DZaAy~s6YEUa1lqaIs#{c*d=8xsC8JAm#!|6wz+%1<5+nsB;#m#zA%oRQ@L2zv=
zdUnfp^Th-^fn)%+%wzYP!Rt91AS)|t7Ci21c<0aIM0`_NSZLWdNCi-u;V%(fV-0(#
z0mKy*753<5EZt9MMhcQ<pOfmXE;bRn__CMGO_^sc<L};>#(#Wk8o#dSlkdw4nUUu+
z?Gh?m>7YR^deA%Z#Tz|-`rH>beggaj@Aqs=OS?CZ$1_K84K)Gw07>KP=FNCY&}-qm
zcCxk4p(S&ouUDFhUJ$DbjfBIi>E`UsyT@5CIM3Obagh^U@PM28sGojbN>GDZ>y>ey
z=%pDPyGivDi61<P%JCpi4H>W1N?*N=#E;SNU$N!FEi+)lHH@fhRVL|A^yb0_LeGRj
zFc5%s12OyM8Nr^_zWi#`qUXKe0t^uscbw7Q)w5qtY4Ty<9}wuFLZvO}?Cy;{Ef8=f
zh8qU>Xoj_s#k>s`bnkDMTZi*gX2q4fHT~%!WpqaT&N!`JKQ*~VX&HOF8GEQfEqdMy
zF!4G76B>>2F>Us2GaBvF>A1!k_WteH5I3AccQKtgKax%NiVCn^5xZxO&*2#0JF68j
iyP@WfJ=9DLNB#!Nfu_l%-@lUp0000<MNUMnLSTY@9?aMP
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -283,16 +283,17 @@ browser.jar:
   skin/classic/browser/devtools/debugger-step-over.png      (devtools/debugger-step-over.png)
   skin/classic/browser/devtools/responsive-se-resizer.png   (devtools/responsive-se-resizer.png)
   skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
   skin/classic/browser/devtools/responsive-background.png   (devtools/responsive-background.png)
   skin/classic/browser/devtools/toggle-tools.png            (devtools/toggle-tools.png)
   skin/classic/browser/devtools/dock-bottom.png             (devtools/dock-bottom.png)
   skin/classic/browser/devtools/dock-side.png               (devtools/dock-side.png)
 * skin/classic/browser/devtools/inspector.css               (devtools/inspector.css)
+  skin/classic/browser/devtools/profiler-stopwatch.png      (devtools/profiler-stopwatch.png)
   skin/classic/browser/devtools/toolbox.css                 (devtools/toolbox.css)
   skin/classic/browser/devtools/tool-options.png            (devtools/tool-options.png)
   skin/classic/browser/devtools/tool-webconsole.png         (devtools/tool-webconsole.png)
   skin/classic/browser/devtools/tool-debugger.png           (devtools/tool-debugger.png)
   skin/classic/browser/devtools/tool-debugger-paused.png    (devtools/tool-debugger-paused.png)
   skin/classic/browser/devtools/tool-inspector.png          (devtools/tool-inspector.png)
   skin/classic/browser/devtools/tool-styleeditor.png        (devtools/tool-styleeditor.png)
   skin/classic/browser/devtools/tool-profiler.png           (devtools/tool-profiler.png)
--- a/browser/themes/shared/devtools/profiler.inc.css
+++ b/browser/themes/shared/devtools/profiler.inc.css
@@ -1,14 +1,26 @@
 %if 0
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 %endif
 
+.profiler-sidebar-empty-notice {
+  max-width: 176px;
+  padding: 10px;
+  background-color: rgb(61, 69, 76);
+  color: white;
+  font-weight: bold;
+}
+
+.devtools-toolbar {
+  min-height: 33px; 
+}
+
 .profiler-sidebar {
   min-width: 196px;
 }
 
 .profiler-sidebar + .devtools-side-splitter {
   -moz-border-start-color: transparent;
 }
 
@@ -25,19 +37,38 @@
   margin-top: 2px;
   color: rgb(140, 152, 165);
 }
 
 .selected .profiler-sidebar-item > span {
   color: rgb(128, 195, 228);
 }
 
-.devtools-toolbar {
-  height: 26px;
-  padding: 3px;
+#profiler-controls > toolbarbutton {
+  margin: 0;
+  box-shadow: none;
+  border-radius: 0;
+  border-width: 0;
+  -moz-border-end-width: 1px;
+  outline-offset: -3px;
+}
+
+#profiler-controls > toolbarbutton:last-of-type {
+  -moz-border-end-width: 0;
 }
 
-.devtools-toolbar .devtools-toolbarbutton {
-  min-width: 48px;
-  min-height: 0;
-  font-size: 11px;
-  padding: 0px 8px;
+#profiler-controls {
+  box-shadow: 0 1px 0 hsla(210,16%,76%,.15) inset,
+              0 0 0 1px hsla(210,16%,76%,.15) inset,
+              0 1px 0 hsla(210,16%,76%,.15);
+  border: 1px solid hsla(210,8%,5%,.45);
+  border-radius: 3px;
+  margin: 0 3px;
+}
+
+#profiler-start {
+  list-style-image: url("chrome://browser/skin/devtools/profiler-stopwatch.png");
+  -moz-image-region: rect(0px,16px,16px,0px);
+}
+
+#profiler-start[checked] {
+  -moz-image-region: rect(0px,32px,16px,16px);
 }
\ No newline at end of file
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0346458bc27816ef0e0fa782b1fd3f433aa16045
GIT binary patch
literal 1480
zc$@*q1vmPMP)<h;3K|Lk000e1NJLTq001BW000mO1^@s6cL04^000G#Nkl<Zc-p0v
zX;70_6ovy;K`Vk{n}CvyeGnBvDYzlB$fBSkQ`gp|wL%@80$Q9}7nVj0NDWOyaTz)Y
zDB@HQ#2Je$VyQ?pRg8#&3_D>-@@0VpZ_i<wNk`j1{qYR<JLi4xdB5cn8X@F=VY?yI
z5F@}Gm<I#^5##s+F4#kjq368~`w20phIH{Nt*%O?)%U1|bTXh8I6+Z_@gD(907vJq
z@?)20N*mXdP>RqhGJ*u&Rw|Z>UPz%AgBsL+FaV{|t%jj*VA$Y?fL^D1NdNf<0+?{u
zi#!%5)!YgCU8f7IAjA*wy^oBo11kv$D%a~!gIe_bBY;w)TLlApVNidcLU~OrX$anP
z-~b)LHPo(S521$i+YAC2bHjyB?g`ap{^cWN<-HNI_)@dX`9NU>YaK709nKfH#1z$f
z=5!3MfF5d4iyrijZqh@DnR-Yk#mC*&)f1ncoz3hT7zm@$XhukHzaq4>wA8V)x9>P=
zAZUk1qv4DPFn8W{_0Xy^jc)1PVd8qS>J?`r|I{L0c`DuWYd41Xe$NF*iZ9r078NYY
zc-Da$)H?0XJ&fM5yH{zWVAJkCxiSw1P63QxwYJ(q&loToRH`E=?J%FsW}EiQsa(`5
z)WdxAa>6oPJ<`REOG?yap{SvIuI~@K8D7x~9gp2z$6CvG<b<6Kb3J;m=(DVsVpi~}
zbocDmLDYKu{G<uI^Tz^cb%l6W&!tVic#U8P_&Xi$I2n4awe|H2u#OtkLZHWZ&0c#Z
z#671?>GqqP*ze6Pof~js)g0d=bY@_(ox|44(e46Ct9Oxn#33p_gXy2J(<!c4j9T}c
zb|rX2M~A^?VjLnHknezxECOm%fJrd?0oJ=@{fU>#%T2L|8mu9pnaU1H-9Epdcf_SY
z_KqEucZVgEByx6T+u25l{5|qI)NVKXiBrm>w>F`?FPJGcY21T1|8%+0M_g~nM!>sm
zB0wE%*enzZXTqIN#u^4tYKE)iDosstMusKUu!kDd0&nW->L#;7_&e;cc8u8PcD`eU
zoKCdyJGQ`j_0d`Eb!kBkS6bBeS6Vcz4QcU=Wd|K?{7!grw`XPB=XDW>oOV5Uw~w8D
zC6%ul(l;p-YA?eh=HugIf#8QYQ9})CAy_qDGuQCK*+MDB`n8QX?90E(3gWrKyLDp)
zr);tbPCjW9l$2-_khql<lIp<<I_2dMTU>=&wn$2$cXZRXj_$p93X~e{IT;}{sA2t1
zm3DZaAy~s6YEUa1lqaIs#{c*d=8xsC8JAm#!|6wz+%1<5+nsB;#m#zA%oRQ@L2zv=
zdUnfp^Th-^fn)%+%wzYP!Rt91AS)|t7Ci21c<0aIM0`_NSZLWdNCi-u;V%(fV-0(#
z0mKy*753<5EZt9MMhcQ<pOfmXE;bRn__CMGO_^sc<L};>#(#Wk8o#dSlkdw4nUUu+
z?Gh?m>7YR^deA%Z#Tz|-`rH>beggaj@Aqs=OS?CZ$1_K84K)Gw07>KP=FNCY&}-qm
zcCxk4p(S&ouUDFhUJ$DbjfBIi>E`UsyT@5CIM3Obagh^U@PM28sGojbN>GDZ>y>ey
z=%pDPyGivDi61<P%JCpi4H>W1N?*N=#E;SNU$N!FEi+)lHH@fhRVL|A^yb0_LeGRj
zFc5%s12OyM8Nr^_zWi#`qUXKe0t^uscbw7Q)w5qtY4Ty<9}wuFLZvO}?Cy;{Ef8=f
zh8qU>Xoj_s#k>s`bnkDMTZi*gX2q4fHT~%!WpqaT&N!`JKQ*~VX&HOF8GEQfEqdMy
zF!4G76B>>2F>Us2GaBvF>A1!k_WteH5I3AccQKtgKax%NiVCn^5xZxO&*2#0JF68j
iyP@WfJ=9DLNB#!Nfu_l%-@lUp0000<MNUMnLSTY@9?aMP
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -223,16 +223,17 @@ browser.jar:
         skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
         skin/classic/browser/devtools/responsive-background.png     (devtools/responsive-background.png)
         skin/classic/browser/devtools/toggle-tools.png              (devtools/toggle-tools.png)
         skin/classic/browser/devtools/dock-bottom.png               (devtools/dock-bottom.png)
         skin/classic/browser/devtools/dock-side.png                 (devtools/dock-side.png)
         skin/classic/browser/devtools/floating-scrollbars.css       (devtools/floating-scrollbars.css)
         skin/classic/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css)
         skin/classic/browser/devtools/inspector.css                 (devtools/inspector.css)
+        skin/classic/browser/devtools/profiler-stopwatch.png        (devtools/profiler-stopwatch.png)
         skin/classic/browser/devtools/toolbox.css                   (devtools/toolbox.css)
         skin/classic/browser/devtools/tool-options.png              (devtools/tool-options.png)
         skin/classic/browser/devtools/tool-webconsole.png           (devtools/tool-webconsole.png)
         skin/classic/browser/devtools/tool-debugger.png             (devtools/tool-debugger.png)
         skin/classic/browser/devtools/tool-debugger-paused.png      (devtools/tool-debugger-paused.png)
         skin/classic/browser/devtools/tool-inspector.png            (devtools/tool-inspector.png)
         skin/classic/browser/devtools/tool-styleeditor.png          (devtools/tool-styleeditor.png)
         skin/classic/browser/devtools/tool-profiler.png             (devtools/tool-profiler.png)
@@ -477,16 +478,17 @@ browser.jar:
         skin/classic/aero/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
         skin/classic/aero/browser/devtools/responsive-background.png (devtools/responsive-background.png)
         skin/classic/aero/browser/devtools/toggle-tools.png          (devtools/toggle-tools.png)
         skin/classic/aero/browser/devtools/dock-bottom.png           (devtools/dock-bottom.png)
         skin/classic/aero/browser/devtools/dock-side.png             (devtools/dock-side.png)
         skin/classic/aero/browser/devtools/floating-scrollbars.css   (devtools/floating-scrollbars.css)
         skin/classic/aero/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css)
         skin/classic/aero/browser/devtools/inspector.css             (devtools/inspector.css)
+        skin/classic/aero/browser/devtools/profiler-stopwatch.png    (devtools/profiler-stopwatch.png)
         skin/classic/aero/browser/devtools/toolbox.css               (devtools/toolbox.css)
         skin/classic/aero/browser/devtools/tool-options.png          (devtools/tool-options.png)
         skin/classic/aero/browser/devtools/tool-webconsole.png       (devtools/tool-webconsole.png)
         skin/classic/aero/browser/devtools/tool-debugger.png         (devtools/tool-debugger.png)
         skin/classic/aero/browser/devtools/tool-debugger-paused.png  (devtools/tool-debugger-paused.png)
         skin/classic/aero/browser/devtools/tool-inspector.png        (devtools/tool-inspector.png)
         skin/classic/aero/browser/devtools/tool-styleeditor.png      (devtools/tool-styleeditor.png)
         skin/classic/aero/browser/devtools/tool-profiler.png         (devtools/tool-profiler.png)
--- a/dom/base/DOMError.cpp
+++ b/dom/base/DOMError.cpp
@@ -16,16 +16,22 @@ namespace dom {
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_1(DOMError, mWindow)
 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMError)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMError)
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMError)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
+DOMError::DOMError(nsPIDOMWindow* aWindow)
+  : mWindow(aWindow)
+{
+  SetIsDOMBinding();
+}
+
 DOMError::DOMError(nsPIDOMWindow* aWindow, nsresult aValue)
   : mWindow(aWindow)
 {
   const char *name, *message;
   NS_GetNameAndMessageForDOMNSResult(aValue, &name, &message);
 
   mName = NS_ConvertASCIItoUTF16(name);
   mMessage = NS_ConvertASCIItoUTF16(message);
--- a/dom/base/DOMError.h
+++ b/dom/base/DOMError.h
@@ -28,16 +28,18 @@ class DOMError : public nsISupports,
 
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(DOMError)
 
   // aWindow can be null if this DOMError is not associated with a particular
   // window.
 
+  DOMError(nsPIDOMWindow* aWindow);
+
   DOMError(nsPIDOMWindow* aWindow, nsresult aValue);
 
   DOMError(nsPIDOMWindow* aWindow, const nsAString& aName);
 
   DOMError(nsPIDOMWindow* aWindow, const nsAString& aName,
            const nsAString& aMessage);
 
   virtual ~DOMError();
@@ -58,14 +60,20 @@ public:
   {
     aRetval = mName;
   }
 
   void GetMessage(nsString& aRetval) const
   {
     aRetval = mMessage;
   }
+
+  void Init(const nsAString& aName, const nsAString& aMessage)
+  {
+    mName = aName;
+    mMessage = aMessage;
+  }
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_domerror_h__
--- a/dom/network/interfaces/nsIDOMMobileConnection.idl
+++ b/dom/network/interfaces/nsIDOMMobileConnection.idl
@@ -526,8 +526,32 @@ dictionary MozCallBarringOption
   /**
    * Service for which the call barring is set up.
    *
    * It shall be one of the nsIDOMMozMobileConnection.ICC_SERVICE_CLASS_*
    * values.
    */
   unsigned short serviceClass;
 };
+
+dictionary DOMMMIResult
+{
+  /**
+   * String key that identifies the service associated with the MMI code
+   * request. The UI is supposed to handle the localization of the strings
+   * associated with this string key.
+   */
+  DOMString serviceCode;
+
+  /**
+   * String key containing the status message of the associated MMI request.
+   * The UI is supposed to handle the localization of the strings associated
+   * with this string key.
+   */
+  DOMString statusMessage;
+
+  /**
+   * Some MMI requests like call forwarding or PIN/PIN2/PUK/PUK2 related
+   * requests provide extra information along with the status message, this
+   * information can be a number, a string key or an array of string keys.
+   */
+  jsval additionalInformation;
+};
--- a/dom/network/src/MobileConnection.cpp
+++ b/dom/network/src/MobileConnection.cpp
@@ -273,41 +273,41 @@ MobileConnection::SelectNetworkAutomatic
     return NS_ERROR_FAILURE;
   }
 
   return mProvider->SelectNetworkAutomatically(GetOwner(), request);
 }
 
 NS_IMETHODIMP
 MobileConnection::SendMMI(const nsAString& aMMIString,
-                          nsIDOMDOMRequest** request)
+                          nsIDOMDOMRequest** aRequest)
 {
   if (!CheckPermission("mobileconnection")) {
     return NS_OK;
   }
 
   if (!mProvider) {
     return NS_ERROR_FAILURE;
   }
 
-  return mProvider->SendMMI(GetOwner(), aMMIString, request);
+  return mProvider->SendMMI(GetOwner(), aMMIString, aRequest);
 }
 
 NS_IMETHODIMP
-MobileConnection::CancelMMI(nsIDOMDOMRequest** request)
+MobileConnection::CancelMMI(nsIDOMDOMRequest** aRequest)
 {
   if (!CheckPermission("mobileconnection")) {
     return NS_OK;
   }
 
   if (!mProvider) {
     return NS_ERROR_FAILURE;
   }
 
-  return mProvider->CancelMMI(GetOwner(), request);
+  return mProvider->CancelMMI(GetOwner(), aRequest);
 }
 
 NS_IMETHODIMP
 MobileConnection::GetCallForwardingOption(uint16_t aReason,
                                           nsIDOMDOMRequest** aRequest)
 {
   *aRequest = nullptr;
 
--- a/dom/network/tests/marionette/test_mobile_mmi.js
+++ b/dom/network/tests/marionette/test_mobile_mmi.js
@@ -53,17 +53,22 @@ tasks.push(function testGettingIMEI() {
   log("Test *#06# ...");
 
   let request = mobileConnection.sendMMI("*#06#");
   ok(request instanceof DOMRequest,
      "request is instanceof " + request.constructor);
 
   request.onsuccess = function onsuccess(event) {
     ok(true, "request success");
-    is(event.target.result, "000000000000000", "Emulator IMEI");
+    is(typeof event.target.result, "object", "typeof result object");
+    ok(event.target.result instanceof Object, "result instanceof Object");
+    is(event.target.result.statusMessage, "000000000000000", "Emulator IMEI");
+    is(event.target.result.serviceCode, "scImei", "Service code IMEI");
+    is(event.target.result.additionalInformation, undefined,
+       "No additional information");
     tasks.next();
   }
   request.onerror = function onerror() {
     ok(false, "request success");
     tasks.abort();
   };
 });
 
--- a/dom/system/gonk/RILContentHelper.js
+++ b/dom/system/gonk/RILContentHelper.js
@@ -56,16 +56,18 @@ const MOBILECELLINFO_CID =
 const VOICEMAILSTATUS_CID=
   Components.ID("{5467f2eb-e214-43ea-9b89-67711241ec8e}");
 const MOBILECFINFO_CID=
   Components.ID("{a4756f16-e728-4d9f-8baa-8464f894888a}");
 const CELLBROADCASTMESSAGE_CID =
   Components.ID("{29474c96-3099-486f-bb4a-3c9a1da834e4}");
 const CELLBROADCASTETWSINFO_CID =
   Components.ID("{59f176ee-9dcd-4005-9d47-f6be0cd08e17}");
+const DOMMMIERROR_CID =
+  Components.ID("{6b204c42-7928-4e71-89ad-f90cd82aff96}");
 
 const RIL_IPC_MSG_NAMES = [
   "RIL:CardStateChanged",
   "RIL:IccInfoChanged",
   "RIL:VoiceInfoChanged",
   "RIL:DataInfoChanged",
   "RIL:EnumerateCalls",
   "RIL:GetAvailableNetworks",
@@ -329,16 +331,44 @@ function CallBarringOption(option) {
 }
 CallBarringOption.prototype = {
   __exposedProps__ : {program: 'r',
                       enabled: 'r',
                       password: 'r',
                       serviceClass: 'r'}
 };
 
+function DOMMMIResult(result) {
+  this.serviceCode = result.serviceCode;
+  this.statusMessage = result.statusMessage;
+  this.additionalInformation = result.additionalInformation;
+};
+DOMMMIResult.prototype = {
+  __exposedProps__: {serviceCode: 'r',
+                     statusMessage: 'r',
+                     additionalInformation: 'r'}
+};
+
+function DOMMMIError() {
+};
+DOMMMIError.prototype = {
+  classDescription: "DOMMMIError",
+  classID:          DOMMMIERROR_CID,
+  contractID:       "@mozilla.org/dom/mmi-error;1",
+  QueryInterface:   XPCOMUtils.generateQI([Ci.nsISupports]),
+  __init: function(serviceCode,
+                   name,
+                   message,
+                   additionalInformation) {
+    this.__DOM_IMPL__.init(name, message);
+    this.serviceCode = serviceCode;
+    this.additionalInformation = additionalInformation;
+  },
+};
+
 function RILContentHelper() {
   this.rilContext = {
     cardState:            RIL.GECKO_CARDSTATE_UNKNOWN,
     retryCount:           0,
     networkSelectionMode: RIL.GECKO_NETWORK_SELECTION_UNKNOWN,
     iccInfo:              new MobileICCInfo(),
     voiceConnectionInfo:  new MobileConnectionInfo(),
     dataConnectionInfo:   new MobileConnectionInfo()
@@ -588,20 +618,24 @@ RILContentHelper.prototype = {
     }
     let request = Services.DOMRequest.createRequest(window);
     info.requestId = this.getRequestId(request);
     cpmm.sendAsyncMessage("RIL:SetCardLock", info);
     return request;
   },
 
   sendMMI: function sendMMI(window, mmi) {
+    // We need to save the global window to get the proper MMIError
+    // constructor once we get the reply from the parent process.
+    this._window = window;
+
     debug("Sending MMI " + mmi);
     if (!window) {
       throw Components.Exception("Can't get window object",
-                                 Cr.NS_ERROR_EXPECTED);
+                                 Cr.NS_ERROR_UNEXPECTED);
     }
     let request = Services.DOMRequest.createRequest(window);
     let requestId = this.getRequestId(request);
     cpmm.sendAsyncMessage("RIL:SendMMI", {mmi: mmi, requestId: requestId});
     return request;
   },
 
   cancelMMI: function cancelMMI(window) {
@@ -1202,24 +1236,19 @@ RILContentHelper.prototype = {
         break;
       case "RIL:USSDReceived":
         this._deliverEvent("_mobileConnectionListeners",
                            "notifyUssdReceived",
                            [msg.json.message, msg.json.sessionEnded]);
         break;
       case "RIL:SendMMI:Return:OK":
       case "RIL:CancelMMI:Return:OK":
-        this.handleSendCancelMMIOK(msg.json);
-        break;
       case "RIL:SendMMI:Return:KO":
       case "RIL:CancelMMI:Return:KO":
-        request = this.takeRequest(msg.json.requestId);
-        if (request) {
-          Services.DOMRequest.fireError(request, msg.json.errorMsg);
-        }
+        this.handleSendCancelMMI(msg.json);
         break;
       case "RIL:StkCommand":
         this._deliverEvent("_iccListeners", "notifyStkCommand",
                            [JSON.stringify(msg.json)]);
         break;
       case "RIL:StkSessionEnd":
         this._deliverEvent("_iccListeners", "notifyStkSessionEnd", null);
         break;
@@ -1523,31 +1552,65 @@ RILContentHelper.prototype = {
 
     if (!message.success) {
       Services.DOMRequest.fireError(request, message.errorMsg);
       return;
     }
     Services.DOMRequest.fireSuccess(request, null);
   },
 
-  handleSendCancelMMIOK: function handleSendCancelMMIOK(message) {
+  handleSendCancelMMI: function handleSendCancelMMI(message) {
+    debug("handleSendCancelMMI " + JSON.stringify(message));
     let request = this.takeRequest(message.requestId);
     if (!request) {
       return;
     }
 
-    // MMI query call forwarding options request returns a set of rules that
-    // will be exposed in the form of an array of nsIDOMMozMobileCFInfo
-    // instances.
-    if (message.success && message.rules) {
-      this._cfRulesToMobileCfInfo(message.rules);
-      message.result = message.rules;
+    let success = message.success;
+
+    let result = {
+      serviceCode: message.mmiServiceCode
+    };
+
+    switch (message.mmiServiceCode) {
+      case RIL.MMI_KS_SC_IMEI:
+        // We expect to have an IMEI at this point, so getting a successful
+        // reply from the RIL without containing an actual IMEI number is
+        // considered an error.
+        if (success && message.result) {
+          result.statusMessage = message.result;
+        } else {
+          result.name = message.errorMsg ?
+            message.errorMsg : RIL.GECKO_ERROR_GENERIC_FAILURE;
+          success = false;
+        }
+        break;
+      case RIL.MMI_KS_SC_PIN:
+      case RIL.MMI_KS_SC_PIN2:
+      case RIL.MMI_KS_SC_PUK:
+      case RIL.MMI_KS_SC_PUK2:
+        // TODO: Bug 874000: Use MMIResult for PIN/PIN2/PUK related
+        //       functionality.
+        break;
+      case RIL.MMI_KS_SC_CALL_FORWARDING:
+        // TODO: Bug 884343 - Use MMIResult for Call Forwarding related
+        //       functionality.
+        break;
     }
 
-    Services.DOMRequest.fireSuccess(request, message.result);
+    if (success) {
+      let mmiResult = new DOMMMIResult(result);
+      Services.DOMRequest.fireSuccess(request, mmiResult);
+    } else {
+      let mmiError = new this._window.DOMMMIError(result.serviceCode,
+                                                  result.name,
+                                                  result.message,
+                                                  result.additionalInformation);
+      Services.DOMRequest.fireDetailedError(request, mmiError);
+    }
   },
 
   _getRandomId: function _getRandomId() {
     return gUUIDGenerator.generateUUID().toString();
   },
 
   _deliverEvent: function _deliverEvent(listenerType, name, args) {
     let thisListeners = this[listenerType];
@@ -1625,10 +1688,11 @@ RILContentHelper.prototype = {
    */
   _isValidCallBarringOption: function _isValidCallBarringOption(option) {
     return (option
             && option.serviceClass != null
             && this._isValidCallBarringProgram(option.program));
   }
 };
 
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RILContentHelper]);
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RILContentHelper,
+                                                     DOMMMIError]);
 
--- a/dom/system/gonk/RadioInterfaceLayer.manifest
+++ b/dom/system/gonk/RadioInterfaceLayer.manifest
@@ -14,10 +14,12 @@
 
 # RadioInterfaceLayer.js
 component {2d831c8d-6017-435b-a80c-e5d422810cea} RadioInterfaceLayer.js
 contract @mozilla.org/ril;1 {2d831c8d-6017-435b-a80c-e5d422810cea}
 category profile-after-change RadioInterfaceLayer @mozilla.org/ril;1
 
 # RILContentHelper.js
 component {472816e1-1fd6-4405-996c-806f9ea68174} RILContentHelper.js
+component {6b204c42-7928-4e71-89ad-f90cd82aff96} RILContentHelper.js
 contract @mozilla.org/ril/content-helper;1 {472816e1-1fd6-4405-996c-806f9ea68174}
+contract @mozilla.org/dom/mmi-error;1 {6b204c42-7928-4e71-89ad-f90cd82aff96}
 category profile-after-change RILContentHelper @mozilla.org/ril/content-helper;1
--- a/dom/system/gonk/ril_consts.js
+++ b/dom/system/gonk/ril_consts.js
@@ -2531,16 +2531,36 @@ this.MMI_SC_BAOC = "33";
 this.MMI_SC_BAOIC = "331";
 this.MMI_SC_BAOICxH = "332";
 this.MMI_SC_BAIC = "35";
 this.MMI_SC_BAICr = "351";
 this.MMI_SC_BA_ALL = "330";
 this.MMI_SC_BA_MO = "333";
 this.MMI_SC_BA_MT = "353";
 
+// MMI service code key strings.
+this.MMI_KS_SC_CALL_BARRING = "scCallBarring";
+this.MMI_KS_SC_CALL_FORWARDING = "scCallForwarding";
+this.MMI_KS_SC_CLIP = "scClip";
+this.MMI_KS_SC_CLIR = "scClir";
+this.MMI_KS_SC_PWD = "scPwd";
+this.MMI_KS_SC_CALL_WAITING = "scCallWaiting";
+this.MMI_KS_SC_PIN = "scPin";
+this.MMI_KS_SC_PIN2 = "scPin2";
+this.MMI_KS_SC_PUK = "scPuk";
+this.MMI_KS_SC_PUK2 = "scPuk2";
+this.MMI_KS_SC_IMEI = "scImei";
+this.MMI_KS_SC_USSD = "scUssd";
+
+// MMI error messages key strings.
+this.MMI_ERROR_KS_ERROR = "emMmiError";
+this.MMI_ERROR_KS_NOT_SUPPORTED = "emMmiErrorNotSupported";
+this.MMI_ERROR_KS_INVALID_ACTION = "emMmiErrorInvalidAction";
+this.MMI_ERROR_KS_MISMATCH_PIN = "emMmiErrorMismatchPin";
+
 /**
  * CDMA PDU constants
  */
 
 // SMS Message Type, as defined in 3GPP2 C.S0015-A v2.0, Table 3.4-1
 this.PDU_CDMA_MSG_TYPE_P2P = 0x00;        // Point-to-Point
 this.PDU_CDMA_MSG_TYPE_BROADCAST = 0x01;  // Broadcast
 this.PDU_CDMA_MSG_TYPE_ACK = 0x02;        // Acknowledge
--- a/dom/system/gonk/ril_worker.js
+++ b/dom/system/gonk/ril_worker.js
@@ -2178,51 +2178,54 @@ let RIL = {
 
   sendMMI: function sendMMI(options) {
     if (DEBUG) {
       debug("SendMMI " + JSON.stringify(options));
     }
     let mmiString = options.mmi;
     let mmi = this._parseMMI(mmiString);
 
-    let _sendMMIError = (function _sendMMIError(errorMsg) {
+    let _sendMMIError = (function _sendMMIError(errorMsg, mmiServiceCode) {
       options.rilMessageType = "sendMMI";
       options.errorMsg = errorMsg;
+      if (mmiServiceCode) {
+        options.mmiServiceCode = mmiServiceCode;
+      }
       this.sendDOMMessage(options);
     }).bind(this);
 
-    function _isValidPINPUKRequest() {
+    function _isValidPINPUKRequest(mmiServiceCode) {
       // The only allowed MMI procedure for ICC PIN, PIN2, PUK and PUK2 handling
       // is "Registration" (**).
       if (!mmi.procedure || mmi.procedure != MMI_PROCEDURE_REGISTRATION ) {
-        _sendMMIError("WRONG_MMI_PROCEDURE");
+        _sendMMIError(MMI_ERROR_KS_INVALID_ACTION, mmiServiceCode);
         return;
       }
 
       if (!mmi.sia || !mmi.sia.length || !mmi.sib || !mmi.sib.length ||
           !mmi.sic || !mmi.sic.length) {
-        _sendMMIError("MISSING_SUPPLEMENTARY_INFORMATION");
+        _sendMMIError(MMI_ERROR_KS_ERROR, mmiServiceCode);
         return;
       }
 
       if (mmi.sib != mmi.sic) {
-        _sendMMIError("NEW_PIN_MISMATCH");
+        _sendMMIError(MMI_ERROR_KS_MISMATCH_PIN, mmiServiceCode);
         return;
       }
 
       return true;
     }
 
     if (mmi == null) {
       if (this._ussdSession) {
         options.ussd = mmiString;
         this.sendUSSD(options);
         return;
       }
-      _sendMMIError("NO_VALID_MMI_STRING");
+      _sendMMIError(MMI_ERROR_KS_ERROR);
       return;
     }
 
     if (DEBUG) {
       debug("MMI " + JSON.stringify(mmi));
     }
 
     // We check if the MMI service code is supported and in that case we
@@ -2236,16 +2239,17 @@ let RIL = {
       case MMI_SC_CF_NO_REPLY:
       case MMI_SC_CF_NOT_REACHABLE:
       case MMI_SC_CF_ALL:
       case MMI_SC_CF_ALL_CONDITIONAL:
         // Call forwarding requires at least an action, given by the MMI
         // procedure, and a reason, given by the MMI service code, but there
         // is no way that we get this far without a valid procedure or service
         // code.
+        options.mmiServiceCode = MMI_KS_SC_CALL_FORWARDING;
         options.action = MMI_PROC_TO_CF_ACTION[mmi.procedure];
         options.rilMessageType = "sendMMI";
         options.reason = MMI_SC_TO_CF_REASON[sc];
         options.number = mmi.sia;
         options.serviceClass = this._siToServiceClass(mmi.sib);
         if (options.action == CALL_FORWARD_ACTION_QUERY_STATUS) {
           this.queryCallForwardStatus(options);
           return;
@@ -2258,118 +2262,125 @@ let RIL = {
         return;
 
       // Change the current ICC PIN number.
       case MMI_SC_PIN:
         // As defined in TS.122.030 6.6.2 to change the ICC PIN we should expect
         // an MMI code of the form **04*OLD_PIN*NEW_PIN*NEW_PIN#, where old PIN
         // should be entered as the SIA parameter and the new PIN as SIB and
         // SIC.
-        if (!_isValidPINPUKRequest()) {
+        if (!_isValidPINPUKRequest(MMI_KS_SC_PIN)) {
           return;
         }
 
+        options.mmiServiceCode = MMI_KS_SC_PIN;
         options.rilRequestType = "sendMMI";
         options.pin = mmi.sia;
         options.newPin = mmi.sib;
         this.changeICCPIN(options);
         return;
 
       // Change the current ICC PIN2 number.
       case MMI_SC_PIN2:
         // As defined in TS.122.030 6.6.2 to change the ICC PIN2 we should
         // enter and MMI code of the form **042*OLD_PIN2*NEW_PIN2*NEW_PIN2#,
         // where the old PIN2 should be entered as the SIA parameter and the
         // new PIN2 as SIB and SIC.
-        if (!_isValidPINPUKRequest()) {
+        if (!_isValidPINPUKRequest(MMI_KS_SC_PIN2)) {
           return;
         }
 
+        options.mmiServiceCode = MMI_KS_SC_PIN2;
         options.rilRequestType = "sendMMI";
         options.pin = mmi.sia;
         options.newPin = mmi.sib;
         this.changeICCPIN2(options);
         return;
 
       // Unblock ICC PIN.
       case MMI_SC_PUK:
         // As defined in TS.122.030 6.6.3 to unblock the ICC PIN we should
         // enter an MMI code of the form **05*PUK*NEW_PIN*NEW_PIN#, where PUK
         // should be entered as the SIA parameter and the new PIN as SIB and
         // SIC.
-        if (!_isValidPINPUKRequest()) {
+        if (!_isValidPINPUKRequest(MMI_KS_SC_PUK)) {
           return;
         }
 
+        options.mmiServiceCode = MMI_KS_SC_PUK;
         options.rilRequestType = "sendMMI";
         options.puk = mmi.sia;
         options.newPin = mmi.sib;
         this.enterICCPUK(options);
         return;
 
       // Unblock ICC PIN2.
       case MMI_SC_PUK2:
         // As defined in TS.122.030 6.6.3 to unblock the ICC PIN2 we should
         // enter an MMI code of the form **052*PUK2*NEW_PIN2*NEW_PIN2#, where
         // PUK2 should be entered as the SIA parameter and the new PIN2 as SIB
         // and SIC.
-        if (!_isValidPINPUKRequest()) {
+        if (!_isValidPINPUKRequest(MMI_KS_SC_PUK2)) {
           return;
         }
 
+        options.mmiServiceCode = MMI_KS_SC_PUK2;
         options.rilRequestType = "sendMMI";
         options.puk = mmi.sia;
         options.newPin = mmi.sib;
         this.enterICCPUK2(options);
         return;
 
       // IMEI
       case MMI_SC_IMEI:
         // A device's IMEI can't change, so we only need to request it once.
         if (this.IMEI == null) {
           this.getIMEI({mmi: true});
           return;
         }
         // If we already had the device's IMEI, we just send it to the DOM.
+        options.mmiServiceCode = MMI_KS_SC_IMEI;
         options.rilMessageType = "sendMMI";
         options.success = true;
         options.result = this.IMEI;
         this.sendDOMMessage(options);
         return;
 
       // Call barring
       case MMI_SC_BAOC:
       case MMI_SC_BAOIC:
       case MMI_SC_BAOICxH:
       case MMI_SC_BAIC:
       case MMI_SC_BAICr:
       case MMI_SC_BA_ALL:
       case MMI_SC_BA_MO:
       case MMI_SC_BA_MT:
-        _sendMMIError("CALL_BARRING_NOT_SUPPORTED_VIA_MMI");
-        return;
-
       // Call waiting
       case MMI_SC_CALL_WAITING:
-        _sendMMIError("CALL_WAITING_NOT_SUPPORTED_VIA_MMI");
+      // CLIP
+      case MMI_SC_CLIP:
+      // CLIR
+      case MMI_SC_CLIR:
+        _sendMMIError(MMI_ERROR_KS_NOT_SUPPORTED);
         return;
     }
 
     // If the MMI code is not a known code and is a recognized USSD request or
     // a #-string, it shall still be sent as a USSD request.
     if (mmi.fullMMI &&
         (mmiString.charAt(mmiString.length - 1) == MMI_END_OF_USSD)) {
       options.ussd = mmi.fullMMI;
+      options.mmiServiceCode = MMI_KS_SC_USSD;
       this.sendUSSD(options);
       return;
     }
 
     // At this point, the MMI string is considered as not valid MMI code and
     // not valid USSD code.
-    _sendMMIError("NOT_VALID_MMI_STRING");
+    _sendMMIError(MMI_ERROR_KS_ERROR);
   },
 
   /**
    * Send USSD.
    *
    * @param ussd
    *        String containing the USSD code.
    *
@@ -2379,16 +2390,17 @@ let RIL = {
      Buf.writeString(options.ussd);
      Buf.sendParcel();
    },
 
   /**
    * Cancel pending USSD.
    */
    cancelUSSD: function cancelUSSD(options) {
+     options.mmiServiceCode = MMI_KS_SC_USSD;
      Buf.simpleRequest(REQUEST_CANCEL_USSD, options);
    },
 
   /**
    * Queries current call forward rules.
    *
    * @param reason
    *        One of nsIDOMMozMobileCFInfo.CALL_FORWARD_REASON_* constants.
@@ -4842,16 +4854,17 @@ RIL[REQUEST_SET_CALL_WAITING] = function
 RIL[REQUEST_SMS_ACKNOWLEDGE] = null;
 RIL[REQUEST_GET_IMEI] = function REQUEST_GET_IMEI(length, options) {
   this.IMEI = Buf.readString();
   // So far we only send the IMEI back to the DOM if it was requested via MMI.
   if (!options.mmi) {
     return;
   }
 
+  options.mmiServiceCode = MMI_KS_SC_IMEI;
   options.rilMessageType = "sendMMI";
   options.success = (options.rilRequestError === 0);
   options.errorMsg = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
   if ((!options.success || this.IMEI == null) && !options.errorMsg) {
     options.errorMsg = GECKO_ERROR_GENERIC_FAILURE;
   }
   options.result = this.IMEI;
   this.sendDOMMessage(options);
--- a/dom/webidl/DOMError.webidl
+++ b/dom/webidl/DOMError.webidl
@@ -12,9 +12,12 @@
 
 [Constructor(DOMString name, optional DOMString message = "")]
 interface DOMError {
   [Constant]
   readonly attribute DOMString name;
 
   [Constant]
   readonly attribute DOMString message;
+
+  [ChromeOnly]
+  void init(DOMString name, optional DOMString message = "");
 };
new file mode 100644
--- /dev/null
+++ b/dom/webidl/DOMMMIError.webidl
@@ -0,0 +1,16 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+[JSImplementation="@mozilla.org/dom/mmi-error;1",
+ Constructor(DOMString serviceCode,
+             DOMString name,
+             optional DOMString message = "",
+             optional DOMString additionalInformation = "")]
+interface DOMMMIError : DOMError {
+  readonly attribute DOMString serviceCode;
+
+  readonly attribute any additionalInformation;
+};
--- a/dom/webidl/WebIDL.mk
+++ b/dom/webidl/WebIDL.mk
@@ -56,16 +56,17 @@ webidl_files = \
   DeviceMotionEvent.webidl \
   DeviceStorage.webidl \
   Document.webidl \
   DocumentFragment.webidl \
   DocumentType.webidl \
   DOMCursor.webidl \
   DOMError.webidl \
   DOMImplementation.webidl \
+  DOMMMIError.webidl \
   DOMParser.webidl \
   DOMRequest.webidl \
   DOMSettableTokenList.webidl \
   DOMStringMap.webidl \
   DOMTokenList.webidl \
   DOMTransaction.webidl \
   DragEvent.webidl \
   DummyBinding.webidl \
--- a/toolkit/devtools/server/actors/profiler.js
+++ b/toolkit/devtools/server/actors/profiler.js
@@ -9,16 +9,47 @@ var startTime = 0;
 
 function getCurrentTime() {
   return (new Date()).getTime() - startTime;
 }
 
 /**
  * Creates a ProfilerActor. ProfilerActor provides remote access to the
  * built-in profiler module.
+ *
+ * ProfilerActor.onGetProfile returns a JavaScript object with data
+ * generated by our built-in profiler moduele. It has the following
+ * format:
+ *
+ * {
+ *   libs: string,
+ *   meta: {
+ *     interval: number,
+ *     platform: string,
+ *     (...)
+ *   },
+ *   threads: [
+ *     {
+ *       samples: [
+ *         {
+ *           frames: [
+ *             {
+ *               line: number,
+ *               location: string
+ *             }
+ *           ],
+ *           name: string
+ *           responsiveness: number (in ms)
+ *           time: number (nspr time)
+ *         }
+ *       ]
+ *     }
+ *   ]
+ * }
+ *
  */
 function ProfilerActor(aConnection)
 {
   this._profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
   this._started = false;
   this._observedEvents = [];
 }
 
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -128,24 +128,16 @@ WebConsoleActor.prototype =
   /**
    * Web Console-related preferences.
    * @private
    * @type object
    */
   _prefs: null,
 
   /**
-   * Tells the current inner ID of |this.window|. When the page is navigated, we
-   * need to recreate the jsterm helpers.
-   * @private
-   * @type number
-   */
-  _globalWindowId: 0,
-
-  /**
    * Holds a map between inner window IDs and Debugger.Objects for the window
    * objects.
    * @private
    * @type Map
    */
   _dbgGlobals: null,
 
   /**
@@ -153,26 +145,16 @@ WebConsoleActor.prototype =
    * created with sendHTTPRequest.
    *
    * @private
    * @type Map
    */
   _netEvents: null,
 
   /**
-   * Object that holds the JSTerm API, the helper functions, for the default
-   * window object.
-   *
-   * @see this._getJSTermHelpers()
-   * @private
-   * @type object
-   */
-  _jstermHelpers: null,
-
-  /**
    * A cache of prototype chains for objects that have received a
    * prototypeAndProperties request.
    *
    * @private
    * @type Map
    * @see dbg-script-actors.js, ThreadActor._protoChains
    */
   _protoChains: null,
@@ -261,20 +243,18 @@ WebConsoleActor.prototype =
       Services.obs.removeObserver(this._onObserverNotification,
                                   "last-pb-context-exited");
     }
     this._actorPool = null;
 
     this._netEvents.clear();
     this._protoChains.clear();
     this._dbgGlobals.clear();
-    this._jstermHelpers = null;
     this.dbg.enabled = false;
     this.dbg = null;
-    this._globalWindowId = 0;
     this.conn = this._window = null;
   },
 
   /**
    * Create a grip for the given value.
    *
    * @param mixed aValue
    * @return object
@@ -736,18 +716,24 @@ WebConsoleActor.prototype =
    *        JSTerm helpers.
    * @return object
    *         The same object as |this|, but with an added |sandbox| property.
    *         The sandbox holds methods and properties that can be used as
    *         bindings during JS evaluation.
    */
   _getJSTermHelpers: function WCA__getJSTermHelpers(aDebuggerGlobal)
   {
-    let helpers = Object.create(this);
-    helpers.sandbox = Object.create(null);
+    let helpers = {
+      window: this.window,
+      chromeWindow: this.chromeWindow.bind(this),
+      makeDebuggeeValue: aDebuggerGlobal.makeDebuggeeValue.bind(aDebuggerGlobal),
+      createValueGrip: this.createValueGrip.bind(this),
+      sandbox: Object.create(null),
+      helperResult: null,
+    };
     JSTermHelpers(helpers);
 
     // Make sure the helpers can be used during eval.
     for (let name in helpers.sandbox) {
       let desc = Object.getOwnPropertyDescriptor(helpers.sandbox, name);
       if (desc.get || desc.set) {
         continue;
       }
@@ -811,101 +797,104 @@ WebConsoleActor.prototype =
    */
   evalWithDebugger: function WCA_evalWithDebugger(aString, aOptions = {})
   {
     // The help function needs to be easy to guess, so we make the () optional.
     if (aString.trim() == "help" || aString.trim() == "?") {
       aString = "help()";
     }
 
+    // Find the Debugger.Object of the given ObjectActor. This is used as
+    // a binding during eval: |_self|.
     let bindSelf = null;
-
     if (aOptions.bindObjectActor) {
       let objActor = this.getActorByID(aOptions.bindObjectActor);
       if (objActor) {
         bindSelf = objActor.obj;
       }
     }
 
+    // Find the Debugger.Frame of the given FrameActor.
     let frame = null, frameActor = null;
     if (aOptions.frameActor) {
       frameActor = this.conn.getActor(aOptions.frameActor);
       if (frameActor) {
         frame = frameActor.frame;
       }
       else {
         Cu.reportError("Web Console Actor: the frame actor was not found: " +
                        aOptions.frameActor);
       }
     }
 
-    let dbg = this.dbg;
-    let dbgWindow = null;
-    let helpers = null;
-    let found$ = false, found$$ = false;
-
     // Determine which debugger to use, depending on the presence of the
     // stackframe.
+    // This helps with avoid having bindings from a different Debugger. The
+    // Debugger.Frame comes from the jsdebugger's Debugger instance.
+    let dbg = this.dbg;
+    let dbgWindow = this._getDebuggerGlobal(this.window);
     if (frame) {
-      // Avoid having bindings from a different Debugger. The Debugger.Frame
-      // comes from the jsdebugger's Debugger instance.
       dbg = frameActor.threadActor.dbg;
       dbgWindow = dbg.addDebuggee(this.window);
-      helpers = this._getJSTermHelpers(dbgWindow);
+    }
+
+    // If we have an object to bind to |_self| we need to determine the
+    // global of the given JavaScript object.
+    if (bindSelf) {
+      let jsObj = bindSelf.unsafeDereference();
+      let global = Cu.getGlobalForObject(jsObj);
+
+      // Get the Debugger.Object for the new global.
+      if (global != this.window) {
+        dbgWindow = dbg.addDebuggee(global);
 
+        // Remove the debuggee only if the Debugger instance belongs to the
+        // console actor, to avoid breaking the ThreadActor that owns the
+        // Debugger object.
+        if (dbg == this.dbg) {
+          dbg.removeDebuggee(global);
+        }
+      }
+
+      bindSelf = dbgWindow.makeDebuggeeValue(jsObj);
+    }
+
+    // Get the JSTerm helpers for the given debugger window.
+    let helpers = this._getJSTermHelpers(dbgWindow);
+    let bindings = helpers.sandbox;
+    if (bindSelf) {
+      bindings._self = bindSelf;
+    }
+
+    // Check if the Debugger.Frame or Debugger.Object for the global include
+    // $ or $$. We will not overwrite these functions with the jsterm helpers.
+    let found$ = false, found$$ = false;
+    if (frame) {
       let env = frame.environment;
       if (env) {
         found$ = !!env.find("$");
         found$$ = !!env.find("$$");
       }
     }
     else {
-      // Use the Web Console debugger object.
-      dbgWindow = this._getDebuggerGlobal(this.window);
-
-      let windowId = WebConsoleUtils.getInnerWindowId(this.window);
-      if (this._globalWindowId != windowId) {
-        this._jstermHelpers = null;
-        this._globalWindowId = windowId;
-      }
-      if (!this._jstermHelpers) {
-        this._jstermHelpers = this._getJSTermHelpers(dbgWindow);
-      }
-
-      helpers = this._jstermHelpers;
       found$ = !!dbgWindow.getOwnPropertyDescriptor("$");
       found$$ = !!dbgWindow.getOwnPropertyDescriptor("$$");
     }
 
-    let bindings = helpers.sandbox;
-    if (bindSelf) {
-      // Determine the global of the given JavaScript object.
-      let jsObj = bindSelf.unsafeDereference();
-      let global = Cu.getGlobalForObject(jsObj);
-
-      if (global != this.window) {
-        dbgWindow = dbg.addDebuggee(global);
-        if (dbg == this.dbg) {
-          dbg.removeDebuggee(global);
-        }
-      }
-
-      bindings._self = dbgWindow.makeDebuggeeValue(jsObj);
-    }
-
     let $ = null, $$ = null;
     if (found$) {
       $ = bindings.$;
       delete bindings.$;
     }
     if (found$$) {
       $$ = bindings.$$;
       delete bindings.$$;
     }
 
+    // Ready to evaluate the string.
     helpers.evalInput = aString;
 
     let result;
     if (frame) {
       result = frame.evalWithBindings(aString, bindings);
     }
     else {
       result = dbgWindow.evalInGlobalWithBindings(aString, bindings);