Merge mozilla-central to b2g-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 16 Apr 2015 12:52:54 +0200
changeset 239451 91978119c83a77aee6f9aa8e10db1adbdd83f33d
parent 239450 3f531d2ecf6727c6be55b651353d49af9728c2aa (current diff)
parent 239438 ec1351f9bc587dc0bf91b56db0b881decdf05d76 (diff)
child 239452 068805894e366a1614cb86e3502da217f0325716
push id28598
push userryanvm@gmail.com
push dateThu, 16 Apr 2015 19:55:20 +0000
treeherdermozilla-central@7f3383ba40cb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone40.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 mozilla-central to b2g-inbound
testing/web-platform/meta/workers/interfaces/WorkerGlobalScope/location/members.html.ini
testing/web-platform/meta/workers/interfaces/WorkerGlobalScope/location/redirect.html.ini
testing/web-platform/meta/workers/interfaces/WorkerGlobalScope/location/setting-members.html.ini
testing/web-platform/meta/workers/interfaces/WorkerGlobalScope/location/worker-separate-file.html.ini
testing/web-platform/meta/workers/interfaces/WorkerUtils/navigator/007.html.ini
--- a/browser/components/readinglist/Sync.jsm
+++ b/browser/components/readinglist/Sync.jsm
@@ -134,28 +134,66 @@ SyncImpl.prototype = {
    */
   promise: null,
 
   /**
    * See the document linked above that describes the sync algorithm.
    */
   _start: Task.async(function* () {
     log.info("Starting sync");
+    yield this._logDiagnostics();
     yield this._uploadStatusChanges();
     yield this._uploadNewItems();
     yield this._uploadDeletedItems();
     yield this._downloadModifiedItems();
 
     // TODO: "Repeat [this phase] until no conflicts occur," says the doc.
     yield this._uploadMaterialChanges();
 
     log.info("Sync done");
   }),
 
   /**
+   * Phase 0 - for debugging we log some stuff about the local store before
+   * we start syncing.
+   * We only do this when the log level is "Trace" or lower as the info (a)
+   * may be expensive to generate, (b) generate alot of output and (c) may
+   * contain private information.
+   */
+  _logDiagnostics: Task.async(function* () {
+    // Sadly our log is likely to have Log.Level.All, so loop over our
+    // appenders looking for the effective level.
+    let smallestLevel = log.appenders.reduce(
+      (prev, appender) => Math.min(prev, appender.level),
+      Log.Level.Error);
+
+    if (smallestLevel > Log.Level.Trace) {
+      return;
+    }
+
+    let localItems = [];
+    yield this.list.forEachItem(localItem => localItems.push(localItem));
+    log.trace("Have " + localItems.length + " local item(s)");
+    for (let localItem of localItems) {
+      // We need to use .record so we get access to a couple of the "internal" fields.
+      let record = localItem._record;
+      let redacted = {};
+      for (let attr of ["guid", "url", "resolvedURL", "serverLastModified", "syncStatus"]) {
+        redacted[attr] = record[attr];
+      }
+      log.trace(JSON.stringify(redacted));
+    }
+    // and the GUIDs of deleted items.
+    let deletedGuids = []
+    yield this.list.forEachSyncedDeletedGUID(guid => deletedGuids.push(guid));
+    // This might be a huge line, but that's OK.
+    log.trace("Have ${num} deleted item(s): ${deletedGuids}", {num: deletedGuids.length, deletedGuids});
+  }),
+
+  /**
    * Phase 1 part 1
    *
    * Uploads not-new items with status-only changes.  By design, status-only
    * changes will never conflict with what's on the server.
    */
   _uploadStatusChanges: Task.async(function* () {
     log.debug("Phase 1 part 1: Uploading status changes");
     yield this._uploadChanges(ReadingList.SyncStatus.CHANGED_STATUS,
@@ -191,17 +229,17 @@ SyncImpl.prototype = {
         defaults: {
           method: "PATCH",
         },
         requests: requests,
       },
     };
     let batchResponse = yield this._postBatch(request);
     if (batchResponse.status != 200) {
-      this._handleUnexpectedResponse("uploading changes", batchResponse);
+      this._handleUnexpectedResponse(true, "uploading changes", batchResponse);
       return;
     }
 
     // Update local items based on the response.
     for (let response of batchResponse.body.responses) {
       if (response.status == 404) {
         // item deleted
         yield this._deleteItemForGUID(response.body.id);
@@ -210,17 +248,17 @@ SyncImpl.prototype = {
       if (response.status == 409) {
         // "Conflict": A change violated a uniqueness constraint.  Mark the item
         // as having material changes, and reconcile and upload it in the
         // material-changes phase.
         // TODO
         continue;
       }
       if (response.status != 200) {
-        this._handleUnexpectedResponse("uploading a change", response);
+        this._handleUnexpectedResponse(false, "uploading a change", response);
         continue;
       }
       // Don't assume the local record and the server record aren't materially
       // different.  Reconcile the differences.
       // TODO
 
       let item = yield this._itemForGUID(response.body.id);
       yield this._updateItemWithServerRecord(item, response.body);
@@ -254,17 +292,17 @@ SyncImpl.prototype = {
           method: "POST",
           path: "/articles",
         },
         requests: requests,
       },
     };
     let batchResponse = yield this._postBatch(request);
     if (batchResponse.status != 200) {
-      this._handleUnexpectedResponse("uploading new items", batchResponse);
+      this._handleUnexpectedResponse(true, "uploading new items", batchResponse);
       return;
     }
 
     // Update local items based on the response.
     for (let response of batchResponse.body.responses) {
       if (response.status == 303) {
         // "See Other": An item with the URL already exists.  Mark the item as
         // having material changes, and reconcile and upload it in the
@@ -276,17 +314,17 @@ SyncImpl.prototype = {
       // exists, but we shouldn't be uploading identical items in this phase in
       // normal usage. But if something goes wrong locally (eg, we upload but
       // get some error even though the upload worked) we will see this.
       // So allow 200 but log a warning.
       if (response.status == 200) {
         log.debug("Attempting to upload a new item found the server already had it", response);
         // but we still process it.
       } else if (response.status != 201) {
-        this._handleUnexpectedResponse("uploading a new item", response);
+        this._handleUnexpectedResponse(false, "uploading a new item", response);
         continue;
       }
       let item = yield this.list.itemForURL(response.body.url);
       yield this._updateItemWithServerRecord(item, response.body);
     }
   }),
 
   /**
@@ -315,26 +353,26 @@ SyncImpl.prototype = {
         defaults: {
           method: "DELETE",
         },
         requests: requests,
       },
     };
     let batchResponse = yield this._postBatch(request);
     if (batchResponse.status != 200) {
-      this._handleUnexpectedResponse("uploading deleted items", batchResponse);
+      this._handleUnexpectedResponse(true, "uploading deleted items", batchResponse);
       return;
     }
 
     // Delete local items based on the response.
     for (let response of batchResponse.body.responses) {
       // A 404 means the item was already deleted on the server, which is OK.
       // We still need to make sure it's deleted locally, though.
       if (response.status != 200 && response.status != 404) {
-        this._handleUnexpectedResponse("uploading a deleted item", response);
+        this._handleUnexpectedResponse(false, "uploading a deleted item", response);
         continue;
       }
       yield this._deleteItemForGUID(response.body.id);
     }
   }),
 
   /**
    * Phase 2
@@ -350,17 +388,17 @@ SyncImpl.prototype = {
       path += "?_since=" + this._serverLastModifiedHeader;
     }
     let request = {
       method: "GET",
       path: path,
     };
     let response = yield this._sendRequest(request);
     if (response.status != 200) {
-      this._handleUnexpectedResponse("downloading modified items", response);
+      this._handleUnexpectedResponse(true, "downloading modified items", response);
       return;
     }
 
     // Update local items based on the response.
     for (let serverRecord of response.body.items) {
       if (serverRecord.deleted) {
         // _deleteItemForGUID is a no-op if no item exists with the GUID.
         yield this._deleteItemForGUID(serverRecord.id);
@@ -544,18 +582,26 @@ SyncImpl.prototype = {
       body: {
         responses: allSubResponses,
       },
     };
     log.debug("All batch requests successfully sent");
     return bigResponse;
   }),
 
-  _handleUnexpectedResponse(contextMsgFragment, response) {
+  _handleUnexpectedResponse(isTopLevel, contextMsgFragment, response) {
     log.error(`Unexpected response ${contextMsgFragment}`, response);
+    // We want to throw in some cases so the sync engine knows there was an
+    // error and retries using the error schedule. 401 implies an auth issue
+    // (possibly transient, possibly not) - but things like 404 might just
+    // relate to a single item and need not throw.  Any 5XX implies a
+    // (hopefully transient) server error.
+    if (isTopLevel && (response.status == 401 || response.status >= 500)) {
+      throw new Error("Sync aborted due to " + response.status + " server response.");
+    }
   },
 
   // TODO: Wipe this pref when user logs out.
   get _serverLastModifiedHeader() {
     if (!("__serverLastModifiedHeader" in this)) {
       this.__serverLastModifiedHeader =
         Preferences.get(SERVER_LAST_MODIFIED_HEADER_PREF, undefined);
     }
--- a/browser/devtools/animationinspector/test/head.js
+++ b/browser/devtools/animationinspector/test/head.js
@@ -115,37 +115,61 @@ let selectNode = Task.async(function*(da
     nodeFront = yield getNodeFront(data, inspector);
   }
   let updated = inspector.once("inspector-updated");
   inspector.selection.setNodeFront(nodeFront, reason);
   yield updated;
 });
 
 /**
+ * Takes an Inspector panel that was just created, and waits
+ * for a "inspector-updated" event as well as the animation inspector
+ * sidebar to be ready. Returns a promise once these are completed.
+ *
+ * @param {InspectorPanel} inspector
+ * @return {Promise}
+ */
+let waitForAnimationInspectorReady = Task.async(function*(inspector) {
+  let win = inspector.sidebar.getWindowForTab("animationinspector");
+  let updated = inspector.once("inspector-updated");
+
+  // In e10s, if we wait for underlying toolbox actors to
+  // load (by setting gDevTools.testing to true), we miss the "animationinspector-ready"
+  // event on the sidebar, so check to see if the iframe
+  // is already loaded.
+  let tabReady = win.document.readyState === "complete" ?
+                 promise.resolve() :
+                 inspector.sidebar.once("animationinspector-ready");
+
+  return promise.all([updated, tabReady]);
+});
+
+/**
  * Open the toolbox, with the inspector tool visible and the animationinspector
  * sidebar selected.
  * @return a promise that resolves when the inspector is ready
  */
 let openAnimationInspector = Task.async(function*() {
   let target = TargetFactory.forTab(gBrowser.selectedTab);
 
   info("Opening the toolbox with the inspector selected");
   let toolbox = yield gDevTools.showToolbox(target, "inspector");
-  yield waitForToolboxFrameFocus(toolbox);
 
   info("Switching to the animationinspector");
   let inspector = toolbox.getPanel("inspector");
-  let initPromises = [
-    inspector.once("inspector-updated"),
-    inspector.sidebar.once("animationinspector-ready")
-  ];
+
+  let panelReady = waitForAnimationInspectorReady(inspector);
+
+  info("Waiting for toolbox focus");
+  yield waitForToolboxFrameFocus(toolbox);
+
   inspector.sidebar.select("animationinspector");
 
   info("Waiting for the inspector and sidebar to be ready");
-  yield promise.all(initPromises);
+  yield panelReady;
 
   let win = inspector.sidebar.getWindowForTab("animationinspector");
   let {AnimationsController, AnimationsPanel} = win;
 
   info("Waiting for the animation controller and panel to be ready");
   if (AnimationsPanel.initialized) {
     yield AnimationsPanel.initialized;
   } else {
--- a/browser/devtools/framework/gDevTools.jsm
+++ b/browser/devtools/framework/gDevTools.jsm
@@ -9,25 +9,22 @@ 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://gre/modules/devtools/Loader.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "promise",
                                   "resource://gre/modules/Promise.jsm", "Promise");
-
 XPCOMUtils.defineLazyModuleGetter(this, "console",
                                   "resource://gre/modules/devtools/Console.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
                                   "resource://gre/modules/devtools/dbg-server.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient",
                                   "resource://gre/modules/devtools/dbg-client.jsm");
 
 const EventEmitter = devtools.require("devtools/toolkit/event-emitter");
 const Telemetry = devtools.require("devtools/shared/telemetry");
 
 const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR";
 const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR";
@@ -1208,26 +1205,16 @@ let gDevToolsBrowser = {
         broadcaster.setAttribute("checked", "true");
       } else {
         broadcaster.removeAttribute("checked");
       }
     }
   },
 
   /**
-   * Connects to the SPS profiler when the developer tools are open. This is
-   * necessary because of the WebConsole's `profile` and `profileEnd` methods.
-   */
-  _connectToProfiler: function DT_connectToProfiler(event, toolbox) {
-    let SharedPerformanceUtils = devtools.require("devtools/performance/front");
-    let connection = SharedPerformanceUtils.getPerformanceActorsConnection(toolbox.target);
-    connection.open();
-  },
-
-  /**
    * Remove the menuitem for a tool to all open browser windows.
    *
    * @param {string} toolId
    *        id of the tool to remove
    */
   _removeToolFromWindows: function DT_removeToolFromWindows(toolId) {
     for (let win of gDevToolsBrowser._trackedBrowserWindows) {
       gDevToolsBrowser._removeToolFromMenu(toolId, win.document);
@@ -1325,17 +1312,16 @@ let gDevToolsBrowser = {
         gDevToolsBrowser._updateMenuCheckbox();
     }
   },
 
   /**
    * All browser windows have been closed, tidy up remaining objects.
    */
   destroy: function() {
-    gDevTools.off("toolbox-ready", gDevToolsBrowser._connectToProfiler);
     Services.prefs.removeObserver("devtools.", gDevToolsBrowser);
     Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application");
   },
 }
 
 this.gDevToolsBrowser = gDevToolsBrowser;
 
 gDevTools.on("tool-registered", function(ev, toolId) {
@@ -1346,15 +1332,14 @@ gDevTools.on("tool-registered", function
 gDevTools.on("tool-unregistered", function(ev, toolId) {
   if (typeof toolId != "string") {
     toolId = toolId.id;
   }
   gDevToolsBrowser._removeToolFromWindows(toolId);
 });
 
 gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox);
-gDevTools.on("toolbox-ready", gDevToolsBrowser._connectToProfiler);
 gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox);
 
 Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false);
 
 // Load the browser devtools main module as the loader's main module.
 devtools.main("main");
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -46,16 +46,17 @@ loader.lazyGetter(this, "toolboxStrings"
       return null;
     }
   };
 });
 
 loader.lazyGetter(this, "Selection", () => require("devtools/framework/selection").Selection);
 loader.lazyGetter(this, "InspectorFront", () => require("devtools/server/actors/inspector").InspectorFront);
 loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/toolkit/DevToolsUtils");
+loader.lazyRequireGetter(this, "getPerformanceActorsConnection", "devtools/performance/front", true);
 
 XPCOMUtils.defineLazyGetter(this, "screenManager", () => {
   return Cc["@mozilla.org/gfx/screenmanager;1"].getService(Ci.nsIScreenManager);
 });
 
 XPCOMUtils.defineLazyGetter(this, "oscpu", () => {
   return Cc["@mozilla.org/network/protocol;1?name=http"]
            .getService(Ci.nsIHttpProtocolHandler).oscpu;
@@ -305,90 +306,90 @@ Toolbox.prototype = {
    */
   get splitConsole() {
     return this._splitConsole;
   },
 
   /**
    * Open the toolbox
    */
-  open: function() {
-    let deferred = promise.defer();
-
-    return this._host.create().then(iframe => {
-      let deferred = promise.defer();
-
-      let domReady = () => {
-        this.isReady = true;
-
-        let framesPromise = this._listFrames();
-
-        this.closeButton = this.doc.getElementById("toolbox-close");
-        this.closeButton.addEventListener("command", this.destroy, true);
-
-        gDevTools.on("pref-changed", this._prefChanged);
-
-        let framesMenu = this.doc.getElementById("command-button-frames");
-        framesMenu.addEventListener("command", this.selectFrame, true);
-
-        this._buildDockButtons();
-        this._buildOptions();
-        this._buildTabs();
-        this._applyCacheSettings();
-        this._applyServiceWorkersTestingSettings();
-        this._addKeysToWindow();
-        this._addReloadKeys();
-        this._addHostListeners();
-        if (this._hostOptions && this._hostOptions.zoom === false) {
-          this._disableZoomKeys();
-        } else {
-          this._addZoomKeys();
-          this._loadInitialZoom();
-        }
-
-        this.webconsolePanel = this.doc.querySelector("#toolbox-panel-webconsole");
-        this.webconsolePanel.height =
-          Services.prefs.getIntPref(SPLITCONSOLE_HEIGHT_PREF);
-        this.webconsolePanel.addEventListener("resize",
-          this._saveSplitConsoleHeight);
-
-        let buttonsPromise = this._buildButtons();
-
-        this._pingTelemetry();
-
-        this.selectTool(this._defaultToolId).then(panel => {
-
-          // Wait until the original tool is selected so that the split
-          // console input will receive focus.
-          let splitConsolePromise = promise.resolve();
-          if (Services.prefs.getBoolPref(SPLITCONSOLE_ENABLED_PREF)) {
-            splitConsolePromise = this.openSplitConsole();
-          }
-
-          promise.all([
-            splitConsolePromise,
-            buttonsPromise,
-            framesPromise
-          ]).then(() => {
-            this.emit("ready");
-            deferred.resolve();
-          }, deferred.reject);
-        });
-      };
+  open: function () {
+    return Task.spawn(function*() {
+      let iframe = yield this._host.create();
+      let domReady = promise.defer();
 
       // Load the toolbox-level actor fronts and utilities now
-      this._target.makeRemote().then(() => {
-        iframe.setAttribute("src", this._URL);
-        iframe.setAttribute("aria-label", toolboxStrings("toolbox.label"));
-        let domHelper = new DOMHelpers(iframe.contentWindow);
-        domHelper.onceDOMReady(domReady);
-      });
+      yield this._target.makeRemote();
+      iframe.setAttribute("src", this._URL);
+      iframe.setAttribute("aria-label", toolboxStrings("toolbox.label"));
+      let domHelper = new DOMHelpers(iframe.contentWindow);
+      domHelper.onceDOMReady(() => domReady.resolve());
+
+      yield domReady.promise;
+
+      this.isReady = true;
+      let framesPromise = this._listFrames();
+
+      this.closeButton = this.doc.getElementById("toolbox-close");
+      this.closeButton.addEventListener("command", this.destroy, true);
+
+      gDevTools.on("pref-changed", this._prefChanged);
+
+      let framesMenu = this.doc.getElementById("command-button-frames");
+      framesMenu.addEventListener("command", this.selectFrame, true);
+
+      this._buildDockButtons();
+      this._buildOptions();
+      this._buildTabs();
+      this._applyCacheSettings();
+      this._applyServiceWorkersTestingSettings();
+      this._addKeysToWindow();
+      this._addReloadKeys();
+      this._addHostListeners();
+      if (this._hostOptions && this._hostOptions.zoom === false) {
+        this._disableZoomKeys();
+      } else {
+        this._addZoomKeys();
+        this._loadInitialZoom();
+      }
 
-      return deferred.promise;
-    }).then(null, console.error.bind(console));
+      this.webconsolePanel = this.doc.querySelector("#toolbox-panel-webconsole");
+      this.webconsolePanel.height = Services.prefs.getIntPref(SPLITCONSOLE_HEIGHT_PREF);
+      this.webconsolePanel.addEventListener("resize", this._saveSplitConsoleHeight);
+
+      let buttonsPromise = this._buildButtons();
+
+      this._pingTelemetry();
+
+      let panel = yield this.selectTool(this._defaultToolId);
+
+      // Wait until the original tool is selected so that the split
+      // console input will receive focus.
+      let splitConsolePromise = promise.resolve();
+      if (Services.prefs.getBoolPref(SPLITCONSOLE_ENABLED_PREF)) {
+        splitConsolePromise = this.openSplitConsole();
+      }
+
+      yield promise.all([
+        splitConsolePromise,
+        buttonsPromise,
+        framesPromise
+      ]);
+
+      let profilerReady = this._connectProfiler();
+
+      // Only wait for the profiler initialization during tests. Otherwise,
+      // lazily load this. This is to intercept console.profile calls; the performance
+      // tools will explicitly wait for the connection opening when opened.
+      if (gDevTools.testing) {
+        yield profilerReady;
+      }
+
+      this.emit("ready");
+    }.bind(this)).then(null, console.error.bind(console));
   },
 
   _pingTelemetry: function() {
     this._telemetry.toolOpened("toolbox");
 
     this._telemetry.logOncePerBrowserVersion(OS_HISTOGRAM,
                                              this._getOsCpu());
     this._telemetry.logOncePerBrowserVersion(OS_IS_64_BITS, is64Bit ? 1 : 0);
@@ -1685,16 +1686,17 @@ Toolbox.prototype = {
     }
 
     this.emit("destroy");
 
     this._target.off("navigate", this._refreshHostTitle);
     this._target.off("frame-update", this._updateFrames);
     this.off("select", this._refreshHostTitle);
     this.off("host-changed", this._refreshHostTitle);
+    this.off("ready", this._showDevEditionPromo);
 
     gDevTools.off("tool-registered", this._toolRegistered);
     gDevTools.off("tool-unregistered", this._toolUnregistered);
 
     gDevTools.off("pref-changed", this._prefChanged);
 
     this._lastFocusedElement = null;
     if (this.webconsolePanel) {
@@ -1730,16 +1732,19 @@ Toolbox.prototype = {
     outstanding.push(this.destroyInspector().then(() => {
       // Removing buttons
       if (this._pickerButton) {
         this._pickerButton.removeEventListener("command", this._togglePicker, false);
         this._pickerButton = null;
       }
     }));
 
+    // Destroy the profiler connection
+    outstanding.push(this._disconnectProfiler());
+
     // We need to grab a reference to win before this._host is destroyed.
     let win = this.frame.ownerGlobal;
 
     if (this._requisition) {
       this._requisition.destroy();
     }
     this._telemetry.toolClosed("toolbox");
     this._telemetry.destroy();
@@ -1810,10 +1815,44 @@ Toolbox.prototype = {
    */
   _showDevEditionPromo: function() {
     // Do not display in browser toolbox
     if (this.target.chrome) {
       return;
     }
     let window = this.frame.contentWindow;
     showDoorhanger({ window, type: "deveditionpromo" });
-  }
+  },
+
+  getPerformanceActorsConnection: function() {
+    if (!this._performanceConnection) {
+      this._performanceConnection = getPerformanceActorsConnection(this.target);
+    }
+    return this._performanceConnection;
+  },
+
+  /**
+   * Connects to the SPS profiler when the developer tools are open. This is
+   * necessary because of the WebConsole's `profile` and `profileEnd` methods.
+   */
+  _connectProfiler: Task.async(function*() {
+    // If target does not have profiler actor (addons), do not
+    // even register the shared performance connection.
+    if (!this.target.hasActor("profiler")) {
+      return;
+    }
+
+    yield this.getPerformanceActorsConnection().open();
+    // Emit an event when connected, but don't wait on startup for this.
+    this.emit("profiler-connected");
+  }),
+
+  /**
+   * Disconnects the underlying Performance Actor Connection.
+   */
+  _disconnectProfiler: Task.async(function*() {
+    if (!this._performanceConnection) {
+      return;
+    }
+    yield this._performanceConnection.destroy();
+    this._performanceConnection = null;
+  }),
 };
--- a/browser/devtools/performance/modules/front.js
+++ b/browser/devtools/performance/modules/front.js
@@ -1,16 +1,17 @@
 /* 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, Ci, Cu, Cr } = require("chrome");
 const { Task } = require("resource://gre/modules/Task.jsm");
 const { extend } = require("sdk/util/object");
+const { RecordingModel } = require("devtools/performance/recording-model");
 
 loader.lazyRequireGetter(this, "Services");
 loader.lazyRequireGetter(this, "promise");
 loader.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 loader.lazyRequireGetter(this, "TimelineFront",
   "devtools/server/actors/timeline", true);
 loader.lazyRequireGetter(this, "MemoryFront",
@@ -21,20 +22,32 @@ loader.lazyRequireGetter(this, "compatib
   "devtools/performance/compatibility");
 
 loader.lazyImporter(this, "gDevTools",
   "resource:///modules/devtools/gDevTools.jsm");
 loader.lazyImporter(this, "setTimeout",
   "resource://gre/modules/Timer.jsm");
 loader.lazyImporter(this, "clearTimeout",
   "resource://gre/modules/Timer.jsm");
+loader.lazyImporter(this, "Promise",
+  "resource://gre/modules/Promise.jsm");
+
 
 // How often do we pull allocation sites from the memory actor.
 const DEFAULT_ALLOCATION_SITES_PULL_TIMEOUT = 200; // ms
 
+// Events to pipe from PerformanceActorsConnection to the PerformanceFront
+const CONNECTION_PIPE_EVENTS = [
+  "console-profile-start", "console-profile-ending", "console-profile-end",
+  "timeline-data", "profiler-already-active", "profiler-activated"
+];
+
+// Events to listen to from the profiler actor
+const PROFILER_EVENTS = ["console-api-profiler", "profiler-stopped"];
+
 /**
  * A cache of all PerformanceActorsConnection instances.
  * The keys are Target objects.
  */
 let SharedPerformanceActors = new WeakMap();
 
 /**
  * Instantiates a shared PerformanceActorsConnection for the specified target.
@@ -66,16 +79,26 @@ SharedPerformanceActors.forTarget = func
  *        The target owning this connection.
  */
 function PerformanceActorsConnection(target) {
   EventEmitter.decorate(this);
 
   this._target = target;
   this._client = this._target.client;
   this._request = this._request.bind(this);
+  this._pendingConsoleRecordings = [];
+  this._sitesPullTimeout = 0;
+  this._recordings = [];
+
+  this._onTimelineMarkers = this._onTimelineMarkers.bind(this);
+  this._onTimelineFrames = this._onTimelineFrames.bind(this);
+  this._onTimelineMemory = this._onTimelineMemory.bind(this);
+  this._onTimelineTicks = this._onTimelineTicks.bind(this);
+  this._onProfilerEvent = this._onProfilerEvent.bind(this);
+  this._pullAllocationSites = this._pullAllocationSites.bind(this);
 
   Services.obs.notifyObservers(null, "performance-actors-connection-created", null);
 }
 
 PerformanceActorsConnection.prototype = {
 
   // Properties set when mocks are being used
   _usingMockMemory: false,
@@ -84,41 +107,55 @@ PerformanceActorsConnection.prototype = 
   /**
    * Initializes a connection to the profiler and other miscellaneous actors.
    * If in the process of opening, or already open, nothing happens.
    *
    * @return object
    *         A promise that is resolved once the connection is established.
    */
   open: Task.async(function*() {
-    if (this._connected) {
-      return;
+    if (this._connecting) {
+      return this._connecting.promise;
     }
 
+    // Create a promise that gets resolved upon connecting, so that
+    // other attempts to open the connection use the same resolution promise
+    this._connecting = Promise.defer();
+
     // Local debugging needs to make the target remote.
     yield this._target.makeRemote();
 
     // Sets `this._profiler`, `this._timeline` and `this._memory`.
     // Only initialize the timeline and memory fronts if the respective actors
     // are available. Older Gecko versions don't have existing implementations,
     // in which case all the methods we need can be easily mocked.
     yield this._connectProfilerActor();
     yield this._connectTimelineActor();
     yield this._connectMemoryActor();
 
+    yield this._registerListeners();
+
     this._connected = true;
 
+    this._connecting.resolve();
     Services.obs.notifyObservers(null, "performance-actors-connection-opened", null);
   }),
 
   /**
    * Destroys this connection.
    */
   destroy: Task.async(function*() {
+    if (this._connecting && !this._connected) {
+      console.warn("Attempting to destroy SharedPerformanceActorsConnection before initialization completion. If testing, ensure `gDevTools.testing` is set.");
+    }
+
+    yield this._unregisterListeners();
     yield this._disconnectActors();
+
+    this._memory = this._timeline = this._profiler = this._target = this._client = null;
     this._connected = false;
   }),
 
   /**
    * Initializes a connection to the profiler actor.
    */
   _connectProfilerActor: Task.async(function*() {
     // Chrome and content process targets already have obtained a reference
@@ -159,16 +196,45 @@ PerformanceActorsConnection.prototype = 
       this._memory = new MemoryFront(this._target.client, this._target.form);
     } else {
       this._usingMockMemory = true;
       this._memory = new compatibility.MockMemoryFront();
     }
   }),
 
   /**
+   * Registers listeners on events from the underlying
+   * actors, so the connection can handle them.
+   */
+  _registerListeners: Task.async(function*() {
+    // Pipe events from TimelineActor to the PerformanceFront
+    this._timeline.on("markers", this._onTimelineMarkers);
+    this._timeline.on("frames", this._onTimelineFrames);
+    this._timeline.on("memory", this._onTimelineMemory);
+    this._timeline.on("ticks", this._onTimelineTicks);
+
+    // Register events on the profiler actor to hook into `console.profile*` calls.
+    yield this._request("profiler", "registerEventNotifications", { events: PROFILER_EVENTS });
+    this._client.addListener("eventNotification", this._onProfilerEvent);
+  }),
+
+  /**
+   * Unregisters listeners on events on the underlying actors.
+   */
+  _unregisterListeners: Task.async(function*() {
+    this._timeline.off("markers", this._onTimelineMarkers);
+    this._timeline.off("frames", this._onTimelineFrames);
+    this._timeline.off("memory", this._onTimelineMemory);
+    this._timeline.off("ticks", this._onTimelineTicks);
+
+    yield this._request("profiler", "unregisterEventNotifications", { events: PROFILER_EVENTS });
+    this._client.removeListener("eventNotification", this._onProfilerEvent);
+  }),
+
+  /**
    * Closes the connections to non-profiler actors.
    */
   _disconnectActors: Task.async(function* () {
     yield this._timeline.destroy();
     yield this._memory.destroy();
   }),
 
   /**
@@ -199,96 +265,229 @@ PerformanceActorsConnection.prototype = 
     if (actor == "timeline") {
       return this._timeline[method].apply(this._timeline, args);
     }
 
     // Handle requests to the memory actor.
     if (actor == "memory") {
       return this._memory[method].apply(this._memory, args);
     }
-  }
-};
+  },
 
-/**
- * A thin wrapper around a shared PerformanceActorsConnection for the parent target.
- * Handles manually starting and stopping a recording.
- *
- * @param PerformanceActorsConnection connection
- *        The shared instance for the parent target.
- */
-function PerformanceFront(connection) {
-  EventEmitter.decorate(this);
+  /**
+   * Invoked whenever a registered event was emitted by the profiler actor.
+   *
+   * @param object response
+   *        The data received from the backend.
+   */
+  _onProfilerEvent: function (_, { topic, subject, details }) {
+    if (topic === "console-api-profiler") {
+      if (subject.action === "profile") {
+        this._onConsoleProfileStart(details);
+      } else if (subject.action === "profileEnd") {
+        this._onConsoleProfileEnd(details);
+      }
+    } else if (topic === "profiler-stopped") {
+      this._onProfilerUnexpectedlyStopped();
+    }
+  },
 
-  this._request = connection._request;
+  /**
+   * TODO handle bug 1144438
+   */
+  _onProfilerUnexpectedlyStopped: function () {
+
+  },
 
-  // Pipe events from TimelineActor to the PerformanceFront
-  connection._timeline.on("markers", markers => this.emit("markers", markers));
-  connection._timeline.on("frames", (delta, frames) => this.emit("frames", delta, frames));
-  connection._timeline.on("memory", (delta, measurement) => this.emit("memory", delta, measurement));
-  connection._timeline.on("ticks", (delta, timestamps) => this.emit("ticks", delta, timestamps));
+  /**
+   * Invoked whenever `console.profile` is called.
+   *
+   * @param string profileLabel
+   *        The provided string argument if available; undefined otherwise.
+   * @param number currentTime
+   *        The time (in milliseconds) when the call was made, relative to when
+   *        the nsIProfiler module was started.
+   */
+  _onConsoleProfileStart: Task.async(function *({ profileLabel, currentTime: startTime }) {
+    let recordings = this._recordings;
 
-  // Set when mocks are being used
-  this._usingMockMemory = connection._usingMockMemory;
-  this._usingMockTimeline = connection._usingMockTimeline;
+    // Abort if a profile with this label already exists.
+    if (recordings.find(e => e.getLabel() === profileLabel)) {
+      return;
+    }
 
-  this._pullAllocationSites = this._pullAllocationSites.bind(this);
-  this._sitesPullTimeout = 0;
-}
+    // Ensure the performance front is set up and ready.
+    // Slight performance overhead for this, should research some more.
+    // This is to ensure that there is a front to receive the events for
+    // the console profiles.
+    yield gDevTools.getToolbox(this._target).loadTool("performance");
 
-PerformanceFront.prototype = {
+    let model = yield this.startRecording(extend(getRecordingModelPrefs(), {
+      console: true,
+      label: profileLabel
+    }));
+
+    this.emit("console-profile-start", model);
+  }),
 
   /**
-   * Manually begins a recording session.
+   * Invoked whenever `console.profileEnd` is called.
+   *
+   * @param object profilerData
+   *        The dump of data from the profiler triggered by this console.profileEnd call.
+   */
+  _onConsoleProfileEnd: Task.async(function *(profilerData) {
+    let pending = this._recordings.filter(r => r.isConsole() && r.isRecording());
+    if (pending.length === 0) {
+      return;
+    }
+
+    let model;
+    // Try to find the corresponding `console.profile` call if
+    // a label was used in profileEnd(). If no matches, abort.
+    if (profilerData.profileLabel) {
+      model = pending.find(e => e.getLabel() === profilerData.profileLabel);
+    }
+    // If no label supplied, pop off the most recent pending console recording
+    else {
+      model = pending[pending.length - 1];
+    }
+
+    // If `profileEnd()` was called with a label, and there are no matching
+    // sessions, abort.
+    if (!model) {
+      Cu.reportError("console.profileEnd() called with label that does not match a recording.");
+      return;
+    }
+
+    this.emit("console-profile-ending", model);
+    yield this.stopRecording(model);
+    this.emit("console-profile-end", model);
+  }),
+
+  /**
+   * Handlers for TimelineActor events. All pipe to `_onTimelineData`
+   * with the appropriate event name.
+   */
+  _onTimelineMarkers: function (markers) { this._onTimelineData("markers", markers); },
+  _onTimelineFrames: function (delta, frames) { this._onTimelineData("frames", delta, frames); },
+  _onTimelineMemory: function (delta, measurement) { this._onTimelineData("memory", delta, measurement); },
+  _onTimelineTicks: function (delta, timestamps) { this._onTimelineData("ticks", delta, timestamps); },
+
+  /**
+   * Called whenever there is timeline data of any of the following types:
+   * - markers
+   * - frames
+   * - memory
+   * - ticks
+   * - allocations
+   *
+   * Populate our internal store of recordings for all currently recording sessions.
+   */
+
+  _onTimelineData: function (...data) {
+    this._recordings.forEach(e => e.addTimelineData.apply(e, data));
+    this.emit("timeline-data", ...data);
+  },
+
+  /**
+   * Begins a recording session
    *
    * @param object options
    *        An options object to pass to the actors. Supported properties are
-   *        `withTicks`, `withMemory` and `withAllocations`.
+   *        `withTicks`, `withMemory` and `withAllocations`, `probability`, and `maxLogLength`.
    * @return object
    *         A promise that is resolved once recording has started.
    */
   startRecording: Task.async(function*(options = {}) {
+    let model = new RecordingModel(options);
     // All actors are started asynchronously over the remote debugging protocol.
     // Get the corresponding start times from each one of them.
     let profilerStartTime = yield this._startProfiler();
     let timelineStartTime = yield this._startTimeline(options);
     let memoryStartTime = yield this._startMemory(options);
 
-    return {
+    let data = {
       profilerStartTime,
       timelineStartTime,
       memoryStartTime
     };
+
+    // Signify to the model that the recording has started,
+    // populate with data and store the recording model here.
+    model.populate(data);
+    this._recordings.push(model);
+
+    return model;
   }),
 
   /**
-   * Manually ends the current recording session.
+   * Manually ends the recording session for the corresponding RecordingModel.
    *
-   * @param object options
-   *        @see PerformanceFront.prototype.startRecording
-   * @return object
-   *         A promise that is resolved once recording has stopped,
-   *         with the profiler and memory data, along with all the end times.
+   * @param RecordingModel model
+   *        The corresponding RecordingModel that belongs to the recording session wished to stop.
+   * @return RecordingModel
+   *         Returns the same model, populated with the profiling data.
    */
-  stopRecording: Task.async(function*(options = {}) {
-    let memoryEndTime = yield this._stopMemory(options);
-    let timelineEndTime = yield this._stopTimeline(options);
+  stopRecording: Task.async(function*(model) {
+    // If model isn't in the PerformanceActorsConnections internal store,
+    // then do nothing.
+    if (!this._recordings.includes(model)) {
+      return;
+    }
+
+    // Currently there are two ways profiles stop recording. Either manually in the
+    // performance tool, or via console.profileEnd. Once a recording is done,
+    // we want to deliver the model to the performance tool (either as a return
+    // from the PerformanceFront or via `console-profile-end` event) and then
+    // remove it from the internal store.
+    //
+    // In the case where a console.profile is generated via the console (so the tools are
+    // open), we initialize the Performance tool so it can listen to those events.
+    this._recordings.splice(this._recordings.indexOf(model), 1);
+
+    let config = model.getConfiguration();
     let profilerData = yield this._request("profiler", "getProfile");
+    let memoryEndTime = Date.now();
+    let timelineEndTime = Date.now();
 
-    return {
+    // Only if there are no more sessions recording do we stop
+    // the underlying memory and timeline actors. If we're still recording,
+    // juse use Date.now() for the memory and timeline end times, as those
+    // are only used in tests.
+    if (!this.isRecording()) {
+      memoryEndTime = yield this._stopMemory(config);
+      timelineEndTime = yield this._stopTimeline(config);
+    }
+
+    // Set the results on the RecordingModel itself.
+    model._onStopRecording({
       // Data available only at the end of a recording.
       profile: profilerData.profile,
 
       // End times for all the actors.
       profilerEndTime: profilerData.currentTime,
       timelineEndTime: timelineEndTime,
       memoryEndTime: memoryEndTime
-    };
+    });
+
+    return model;
   }),
 
   /**
+   * Checks all currently stored recording models and returns a boolean
+   * if there is a session currently being recorded.
+   *
+   * @return Boolean
+   */
+  isRecording: function () {
+    return this._recordings.some(recording => recording.isRecording());
+  },
+
+  /**
    * Starts the profiler actor, if necessary.
    */
   _startProfiler: Task.async(function *() {
     // Start the profiler only if it wasn't already active. The built-in
     // nsIPerformance module will be kept recording, because it's the same instance
     // for all targets and interacts with the whole platform, so we don't want
     // to affect other clients by stopping (or restarting) it.
     let profilerStatus = yield this._request("profiler", "isActive");
@@ -384,29 +583,84 @@ PerformanceFront.prototype = {
 
     let isDetached = (yield this._request("memory", "getState")) !== "attached";
     if (isDetached) {
       deferred.resolve();
       return;
     }
 
     let memoryData = yield this._request("memory", "getAllocations");
-    this.emit("allocations", {
+
+    this._onTimelineData("allocations", {
       sites: memoryData.allocations,
       timestamps: memoryData.allocationsTimestamps,
       frames: memoryData.frames,
       counts: memoryData.counts
     });
 
     let delay = DEFAULT_ALLOCATION_SITES_PULL_TIMEOUT;
     this._sitesPullTimeout = setTimeout(this._pullAllocationSites, delay);
 
     deferred.resolve();
   }),
 
+  toString: () => "[object PerformanceActorsConnection]"
+};
+
+/**
+ * A thin wrapper around a shared PerformanceActorsConnection for the parent target.
+ * Handles manually starting and stopping a recording.
+ *
+ * @param PerformanceActorsConnection connection
+ *        The shared instance for the parent target.
+ */
+function PerformanceFront(connection) {
+  EventEmitter.decorate(this);
+
+  this._connection = connection;
+  this._request = connection._request;
+
+  // Set when mocks are being used
+  this._usingMockMemory = connection._usingMockMemory;
+  this._usingMockTimeline = connection._usingMockTimeline;
+
+  // Pipe the console profile events from the connection
+  // to the front so that the UI can listen.
+  CONNECTION_PIPE_EVENTS.forEach(eventName => this._connection.on(eventName, () => this.emit.apply(this, arguments)));
+}
+
+PerformanceFront.prototype = {
+
+  /**
+   * Manually begins a recording session and creates a RecordingModel.
+   * Calls the underlying PerformanceActorsConnection's startRecording method.
+   *
+   * @param object options
+   *        An options object to pass to the actors. Supported properties are
+   *        `withTicks`, `withMemory` and `withAllocations`, `probability` and `maxLogLength`.
+   * @return object
+   *         A promise that is resolved once recording has started.
+   */
+  startRecording: function (options) {
+    return this._connection.startRecording(options);
+  },
+
+  /**
+   * Manually ends the recording session for the corresponding RecordingModel.
+   * Calls the underlying PerformanceActorsConnection's
+   *
+   * @param RecordingModel model
+   *        The corresponding RecordingModel that belongs to the recording session wished to stop.
+   * @return RecordingModel
+   *         Returns the same model, populated with the profiling data.
+   */
+  stopRecording: function (model) {
+    return this._connection.stopRecording(model);
+  },
+
   /**
    * Returns an object indicating if mock actors are being used or not.
    */
   getMocksInUse: function () {
     return {
       memory: this._usingMockMemory,
       timeline: this._usingMockTimeline
     };
@@ -418,10 +672,23 @@ PerformanceFront.prototype = {
  * provided thread client.
  */
 function listTabs(client) {
   let deferred = promise.defer();
   client.listTabs(deferred.resolve);
   return deferred.promise;
 }
 
+/**
+ * Creates an object of configurations based off of preferences for a RecordingModel.
+ */
+function getRecordingModelPrefs () {
+  return {
+    withMemory: Services.prefs.getBoolPref("devtools.performance.ui.enable-memory"),
+    withTicks: Services.prefs.getBoolPref("devtools.performance.ui.enable-framerate"),
+    withAllocations: Services.prefs.getBoolPref("devtools.performance.ui.enable-memory"),
+    allocationsSampleProbability: +Services.prefs.getCharPref("devtools.performance.memory.sample-probability"),
+    allocationsMaxLogLength: Services.prefs.getIntPref("devtools.performance.memory.max-log-length")
+  };
+}
+
 exports.getPerformanceActorsConnection = target => SharedPerformanceActors.forTarget(target);
 exports.PerformanceFront = PerformanceFront;
--- a/browser/devtools/performance/modules/recording-model.js
+++ b/browser/devtools/performance/modules/recording-model.js
@@ -1,42 +1,43 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { Cc, Ci, Cu, Cr } = require("chrome");
+const { Task } = require("resource://gre/modules/Task.jsm");
 
 loader.lazyRequireGetter(this, "PerformanceIO",
   "devtools/performance/io", true);
 loader.lazyRequireGetter(this, "RecordingUtils",
   "devtools/performance/recording-utils", true);
 
 /**
  * Model for a wholistic profile, containing the duration, profiling data,
  * frames data, timeline (marker, tick, memory) data, and methods to mark
  * a recording as 'in progress' or 'finished'.
  */
 
 const RecordingModel = function (options={}) {
-  this._front = options.front;
-  this._performance = options.performance;
   this._label = options.label || "";
+  this._console = options.console || false;
 
   this._configuration = {
     withTicks: options.withTicks || false,
     withMemory: options.withMemory || false,
     withAllocations: options.withAllocations || false,
     allocationsSampleProbability: options.allocationsSampleProbability || 0,
     allocationsMaxLogLength: options.allocationsMaxLogLength || 0
   };
 };
 
 RecordingModel.prototype = {
   // Private fields, only needed when a recording is started or stopped.
+  _console: false,
   _imported: false,
   _recording: false,
   _profilerStartTime: 0,
   _timelineStartTime: 0,
   _memoryStartTime: 0,
   _configuration: {},
 
   // Serializable fields, necessary and sufficient for import and export.
@@ -76,43 +77,43 @@ RecordingModel.prototype = {
    *        The file to stream the data into.
    */
   exportRecording: Task.async(function *(file) {
     let recordingData = this.getAllData();
     yield PerformanceIO.saveRecordingToFile(recordingData, file);
   }),
 
   /**
-   * Starts recording with the PerformanceFront.
+   * Sets up the instance with data from the SharedPerformanceConnection when
+   * starting a recording. Should only be called by SharedPerformanceConnection.
    */
-  startRecording: Task.async(function *() {
+  populate: function (info) {
     // Times must come from the actor in order to be self-consistent.
     // However, we also want to update the view with the elapsed time
     // even when the actor is not generating data. To do this we get
     // the local time and use it to compute a reasonable elapsed time.
-    this._localStartTime = this._performance.now();
+    this._localStartTime = Date.now()
 
-    let info = yield this._front.startRecording(this.getConfiguration());
     this._profilerStartTime = info.profilerStartTime;
     this._timelineStartTime = info.timelineStartTime;
     this._memoryStartTime = info.memoryStartTime;
     this._recording = true;
 
     this._markers = [];
     this._frames = [];
     this._memory = [];
     this._ticks = [];
     this._allocations = { sites: [], timestamps: [], frames: [], counts: [] };
-  }),
+  },
 
   /**
-   * Stops recording with the PerformanceFront.
+   * Sets results available from stopping a recording from SharedPerformanceConnection.
+   * Should only be called by SharedPerformanceConnection.
    */
-  stopRecording: Task.async(function *() {
-    let info = yield this._front.stopRecording(this.getConfiguration());
+  _onStopRecording: Task.async(function *(info) {
     this._profile = info.profile;
     this._duration = info.profilerEndTime - this._profilerStartTime;
     this._recording = false;
 
     // We'll need to filter out all samples that fall out of current profile's
     // range since the profiler is continuously running. Because of this, sample
     // times are not guaranteed to have a zero epoch, so offset the timestamps.
     RecordingUtils.filterSamples(this._profile, this._profilerStartTime);
@@ -135,17 +136,17 @@ RecordingModel.prototype = {
    * Gets duration of this recording, in milliseconds.
    * @return number
    */
   getDuration: function () {
     // Compute an approximate ending time for the current recording if it is
     // still in progress. This is needed to ensure that the view updates even
     // when new data is not being generated.
     if (this._recording) {
-      return this._performance.now() - this._localStartTime;
+      return Date.now() - this._localStartTime;
     } else {
       return this._duration;
     }
   },
 
   /**
    * Returns configuration object of specifying whether the recording
    * was started withTicks, withMemory and withAllocations.
@@ -215,16 +216,32 @@ RecordingModel.prototype = {
     let ticks = this.getTicks();
     let allocations = this.getAllocations();
     let profile = this.getProfile();
     return { label, duration, markers, frames, memory, ticks, allocations, profile };
   },
 
   /**
    * Returns a boolean indicating whether or not this recording model
+   * was imported via file.
+   */
+  isImported: function () {
+    return this._imported;
+  },
+
+  /**
+   * Returns a boolean indicating whether or not this recording model
+   * was started via a `console.profile` call.
+   */
+  isConsole: function () {
+    return this._console;
+  },
+
+  /**
+   * Returns a boolean indicating whether or not this recording model
    * is recording.
    */
   isRecording: function () {
     return this._recording;
   },
 
   /**
    * Fired whenever the PerformanceFront emits markers, memory or ticks.
--- a/browser/devtools/performance/panel.js
+++ b/browser/devtools/performance/panel.js
@@ -1,17 +1,17 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {Cc, Ci, Cu, Cr} = require("chrome");
-const { PerformanceFront, getPerformanceActorsConnection } = require("devtools/performance/front");
+const { PerformanceFront } = require("devtools/performance/front");
 
 Cu.import("resource://gre/modules/Task.jsm");
 
 loader.lazyRequireGetter(this, "promise");
 loader.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 
 function PerformancePanel(iframeWindow, toolbox) {
@@ -30,17 +30,21 @@ PerformancePanel.prototype = {
    * @return object
    *         A promise that is resolved when the Performance tool
    *         completes opening.
    */
   open: Task.async(function*() {
     this.panelWin.gToolbox = this._toolbox;
     this.panelWin.gTarget = this.target;
 
-    this._connection = getPerformanceActorsConnection(this.target);
+    // Connection is already created in the toolbox; reuse
+    // the same connection.
+    this._connection = this.panelWin.gToolbox.getPerformanceActorsConnection();
+    // The toolbox will also open the connection, but attempt to open it again
+    // incase it's still in the process of opening.
     yield this._connection.open();
 
     this.panelWin.gFront = new PerformanceFront(this._connection);
 
     yield this.panelWin.startupPerformance();
 
     this.isReady = true;
     this.emit("ready");
@@ -52,16 +56,13 @@ PerformancePanel.prototype = {
   get target() this._toolbox.target,
 
   destroy: Task.async(function*() {
     // Make sure this panel is not already destroyed.
     if (this._destroyed) {
       return;
     }
 
-    // Destroy the connection to ensure packet handlers are removed from client.
-    yield this._connection.destroy();
-
     yield this.panelWin.shutdownPerformance();
     this.emit("destroyed");
     this._destroyed = true;
   })
 };
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -64,16 +64,23 @@ const BRANCH_NAME = "devtools.performanc
 // Events emitted by various objects in the panel.
 const EVENTS = {
   // Fired by the PerformanceController and OptionsView when a pref changes.
   PREF_CHANGED: "Performance:PrefChanged",
 
   // Fired by the PerformanceController when the devtools theme changes.
   THEME_CHANGED: "Performance:ThemeChanged",
 
+  // When the SharedPerformanceConnection handles profiles created via `console.profile()`,
+  // the controller handles those events and emits the below events for consumption
+  // by other views.
+  CONSOLE_RECORDING_STARTED: "Performance:ConsoleRecordingStarted",
+  CONSOLE_RECORDING_WILL_STOP: "Performance:ConsoleRecordingWillStop",
+  CONSOLE_RECORDING_STOPPED: "Performance:ConsoleRecordingStopped",
+
   // Emitted by the PerformanceView when the state (display mode) changes,
   // for example when switching between "empty", "recording" or "recorded".
   // This causes certain panels to be hidden or visible.
   UI_STATE_CHANGED: "Performance:UI:StateChanged",
 
   // Emitted by the PerformanceView on clear button click
   UI_CLEAR_RECORDINGS: "Performance:UI:ClearRecordings",
 
@@ -98,18 +105,16 @@ const EVENTS = {
 
   // When recordings have been cleared out
   RECORDINGS_CLEARED: "Performance:RecordingsCleared",
 
   // When a recording is imported or exported via the PerformanceController
   RECORDING_IMPORTED: "Performance:RecordingImported",
   RECORDING_EXPORTED: "Performance:RecordingExported",
 
-  // When the PerformanceController has new recording data
-  TIMELINE_DATA: "Performance:TimelineData",
 
   // Emitted by the JITOptimizationsView when it renders new optimization
   // data and clears the optimization data
   OPTIMIZATIONS_RESET: "Performance:UI:OptimizationsReset",
   OPTIMIZATIONS_RENDERED: "Performance:UI:OptimizationsRendered",
 
   // Emitted by the OverviewView when more data has been rendered
   OVERVIEW_RENDERED: "Performance:UI:OverviewRendered",
@@ -183,70 +188,68 @@ let PerformanceController = {
    * main UI events.
    */
   initialize: Task.async(function* () {
     this.startRecording = this.startRecording.bind(this);
     this.stopRecording = this.stopRecording.bind(this);
     this.importRecording = this.importRecording.bind(this);
     this.exportRecording = this.exportRecording.bind(this);
     this.clearRecordings = this.clearRecordings.bind(this);
-    this._onTimelineData = this._onTimelineData.bind(this);
     this._onRecordingSelectFromView = this._onRecordingSelectFromView.bind(this);
     this._onPrefChanged = this._onPrefChanged.bind(this);
     this._onThemeChanged = this._onThemeChanged.bind(this);
+    this._onConsoleProfileStart = this._onConsoleProfileStart.bind(this);
+    this._onConsoleProfileEnd = this._onConsoleProfileEnd.bind(this);
+    this._onConsoleProfileEnding = this._onConsoleProfileEnding.bind(this);
 
     // All boolean prefs should be handled via the OptionsView in the
     // ToolbarView, so that they may be accessible via the "gear" menu.
     // Every other pref should be registered here.
     this._nonBooleanPrefs = new ViewHelpers.Prefs("devtools.performance", {
       "hidden-markers": ["Json", "timeline.hidden-markers"],
       "memory-sample-probability": ["Float", "memory.sample-probability"],
       "memory-max-log-length": ["Int", "memory.max-log-length"]
     });
 
     this._nonBooleanPrefs.registerObserver();
     this._nonBooleanPrefs.on("pref-changed", this._onPrefChanged);
 
+    gFront.on("console-profile-start", this._onConsoleProfileStart);
+    gFront.on("console-profile-ending", this._onConsoleProfileEnding);
+    gFront.on("console-profile-end", this._onConsoleProfileEnd);
     ToolbarView.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
     PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
     PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
     PerformanceView.on(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
     RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     RecordingsView.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
 
     gDevTools.on("pref-changed", this._onThemeChanged);
-    gFront.on("markers", this._onTimelineData); // timeline markers
-    gFront.on("frames", this._onTimelineData); // stack frames
-    gFront.on("memory", this._onTimelineData); // memory measurements
-    gFront.on("ticks", this._onTimelineData); // framerate
-    gFront.on("allocations", this._onTimelineData); // memory allocations
   }),
 
   /**
    * Remove events handled by the PerformanceController
    */
   destroy: function() {
     this._nonBooleanPrefs.unregisterObserver();
     this._nonBooleanPrefs.off("pref-changed", this._onPrefChanged);
 
+    gFront.off("console-profile-start", this._onConsoleProfileStart);
+    gFront.off("console-profile-ending", this._onConsoleProfileEnding);
+    gFront.off("console-profile-end", this._onConsoleProfileEnd);
     ToolbarView.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
     PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
     PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
     PerformanceView.off(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
     RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     RecordingsView.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
 
     gDevTools.off("pref-changed", this._onThemeChanged);
-    gFront.off("markers", this._onTimelineData);
-    gFront.off("frames", this._onTimelineData);
-    gFront.off("memory", this._onTimelineData);
-    gFront.off("ticks", this._onTimelineData);
-    gFront.off("allocations", this._onTimelineData);
   },
 
   /**
    * Returns the current devtools theme.
    */
   getTheme: function () {
     return Services.prefs.getCharPref("devtools.theme");
   },
@@ -280,40 +283,41 @@ let PerformanceController = {
     this._nonBooleanPrefs[prefName] = prefValue;
   },
 
   /**
    * Starts recording with the PerformanceFront. Emits `EVENTS.RECORDING_STARTED`
    * when the front has started to record.
    */
   startRecording: Task.async(function *() {
-    let recording = this._createRecording({
+    let options = {
       withMemory: this.getOption("enable-memory"),
       withTicks: this.getOption("enable-framerate"),
       withAllocations: this.getOption("enable-memory"),
       allocationsSampleProbability: this.getPref("memory-sample-probability"),
       allocationsMaxLogLength: this.getPref("memory-max-log-length")
-    });
+    };
+
+    this.emit(EVENTS.RECORDING_WILL_START);
 
-    this.emit(EVENTS.RECORDING_WILL_START, recording);
-    yield recording.startRecording();
+    let recording = yield gFront.startRecording(options);
+    this._recordings.push(recording);
+
     this.emit(EVENTS.RECORDING_STARTED, recording);
-
-    this.setCurrentRecording(recording);
   }),
 
   /**
    * Stops recording with the PerformanceFront. Emits `EVENTS.RECORDING_STOPPED`
    * when the front has stopped recording.
    */
   stopRecording: Task.async(function *() {
-    let recording = this.getLatestRecording();
+    let recording = this.getLatestManualRecording();
 
     this.emit(EVENTS.RECORDING_WILL_STOP, recording);
-    yield recording.stopRecording();
+    yield gFront.stopRecording(recording);
     this.emit(EVENTS.RECORDING_STOPPED, recording);
   }),
 
   /**
    * Saves the given recording to a file. Emits `EVENTS.RECORDING_EXPORTED`
    * when the file was saved.
    *
    * @param RecordingModel recording
@@ -326,17 +330,17 @@ let PerformanceController = {
     this.emit(EVENTS.RECORDING_EXPORTED, recording);
   }),
 
   /**
    * Clears all recordings from the list as well as the current recording.
    * Emits `EVENTS.RECORDINGS_CLEARED` when complete so other components can clean up.
    */
   clearRecordings: Task.async(function* () {
-    let latest = this.getLatestRecording();
+    let latest = this.getLatestManualRecording();
 
     if (latest && latest.isRecording()) {
       yield this.stopRecording();
     }
 
     this._recordings.length = 0;
     this.setCurrentRecording(null);
     this.emit(EVENTS.RECORDINGS_CLEARED);
@@ -345,42 +349,27 @@ let PerformanceController = {
   /**
    * Loads a recording from a file, adding it to the recordings list. Emits
    * `EVENTS.RECORDING_IMPORTED` when the file was loaded.
    *
    * @param nsILocalFile file
    *        The file to import the data from.
    */
   importRecording: Task.async(function*(_, file) {
-    let recording = this._createRecording();
+    let recording = new RecordingModel();
+    this._recordings.push(recording);
     yield recording.importRecording(file);
 
     this.emit(EVENTS.RECORDING_IMPORTED, recording);
   }),
 
   /**
-   * Creates a new RecordingModel, fires events and stores it
-   * internally in the controller.
-   *
-   * @param object options
-   *        @see PerformanceFront.prototype.startRecording
-   * @return RecordingModel
-   *         The newly created recording model.
-   */
-  _createRecording: function (options={}) {
-    let recording = new RecordingModel(Heritage.extend(options, {
-      front: gFront,
-      performance: window.performance
-    }));
-    this._recordings.push(recording);
-    return recording;
-  },
-
-  /**
-   * Sets the currently active RecordingModel.
+   * Sets the currently active RecordingModel. Should rarely be called directly,
+   * as RecordingsView handles this when manually selected a recording item. Exceptions
+   * are when clearing the view.
    * @param RecordingModel recording
    */
   setCurrentRecording: function (recording) {
     if (this._currentRecording !== recording) {
       this._currentRecording = recording;
       this.emit(EVENTS.RECORDING_SELECTED, recording);
     }
   },
@@ -392,42 +381,37 @@ let PerformanceController = {
   getCurrentRecording: function () {
     return this._currentRecording;
   },
 
   /**
    * Get most recently added recording that was triggered manually (via UI).
    * @return RecordingModel
    */
-  getLatestRecording: function () {
+  getLatestManualRecording: function () {
     for (let i = this._recordings.length - 1; i >= 0; i--) {
-      return this._recordings[i];
+      let model = this._recordings[i];
+      if (!model.isConsole() && !model.isImported()) {
+        return this._recordings[i];
+      }
     }
     return null;
   },
 
   /**
    * Gets the current timeline blueprint without the hidden markers.
    * @return object
    */
   getTimelineBlueprint: function() {
     let blueprint = TIMELINE_BLUEPRINT;
     let hiddenMarkers = this.getPref("hidden-markers");
     return RecordingUtils.getFilteredBlueprint({ blueprint, hiddenMarkers });
   },
 
   /**
-   * Fired whenever the PerformanceFront emits markers, memory or ticks.
-   */
-  _onTimelineData: function (...data) {
-    this._recordings.forEach(e => e.addTimelineData.apply(e, data));
-    this.emit(EVENTS.TIMELINE_DATA, ...data);
-  },
-
-  /**
    * Fired from RecordingsView, we listen on the PerformanceController so we can
    * set it here and re-emit on the controller, where all views can listen.
    */
   _onRecordingSelectFromView: function (_, recording) {
     this.setCurrentRecording(recording);
   },
 
   /**
@@ -446,16 +430,47 @@ let PerformanceController = {
     // but this could change in the future.
     if (data.pref !== "devtools.theme") {
       return;
     }
 
     this.emit(EVENTS.THEME_CHANGED, data.newValue);
   },
 
+  /**
+   * Fired when `console.profile()` is executed.
+   */
+  _onConsoleProfileStart: function (_, recording) {
+    this._recordings.push(recording);
+    this.emit(EVENTS.CONSOLE_RECORDING_STARTED, recording);
+  },
+
+  /**
+   * Fired when `console.profileEnd()` is executed, and the profile
+   * is stopping soon, as it fetches profiler data.
+   */
+  _onConsoleProfileEnding: function (_, recording) {
+    this.emit(EVENTS.CONSOLE_RECORDING_WILL_STOP, recording);
+  },
+
+  /**
+   * Fired when `console.profileEnd()` is executed, and
+   * has a corresponding `console.profile()` session.
+   */
+  _onConsoleProfileEnd: function (_, recording) {
+    this.emit(EVENTS.CONSOLE_RECORDING_STOPPED, recording);
+  },
+
+  /**
+   * Returns the internal store of recording models.
+   */
+  getRecordings: function () {
+    return this._recordings;
+  },
+
   toString: () => "[object PerformanceController]"
 };
 
 /**
  * Convenient way of emitting events from the controller.
  */
 EventEmitter.decorate(PerformanceController);
 
--- a/browser/devtools/performance/performance-view.js
+++ b/browser/devtools/performance/performance-view.js
@@ -15,16 +15,20 @@ let PerformanceView = {
   states: {
     empty: [
       { deck: "#performance-view", pane: "#empty-notice" }
     ],
     recording: [
       { deck: "#performance-view", pane: "#performance-view-content" },
       { deck: "#details-pane-container", pane: "#recording-notice" }
     ],
+    "console-recording": [
+      { deck: "#performance-view", pane: "#performance-view-content" },
+      { deck: "#details-pane-container", pane: "#console-recording-notice" }
+    ],
     recorded: [
       { deck: "#performance-view", pane: "#performance-view-content" },
       { deck: "#details-pane-container", pane: "#details-pane" }
     ]
   },
 
   /**
    * Sets up the view with event binding and main subviews.
@@ -50,16 +54,17 @@ let PerformanceView = {
     this._importButton.addEventListener("click", this._onImportButtonClick);
     this._clearButton.addEventListener("click", this._onClearButtonClick);
 
     // Bind to controller events to unlock the record button
     PerformanceController.on(EVENTS.RECORDING_WILL_START, this._onRecordingWillStart);
     PerformanceController.on(EVENTS.RECORDING_WILL_STOP, this._onRecordingWillStop);
     PerformanceController.on(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.on(EVENTS.CONSOLE_RECORDING_STOPPED, this._onRecordingStopped);
     PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
 
     this.setState("empty");
 
     // Initialize the ToolbarView first, because other views may need access
     // to the OptionsView via the controller, to read prefs.
     yield ToolbarView.initialize();
     yield RecordingsView.initialize();
@@ -76,38 +81,46 @@ let PerformanceView = {
     }
     this._importButton.removeEventListener("click", this._onImportButtonClick);
     this._clearButton.removeEventListener("click", this._onClearButtonClick);
 
     PerformanceController.off(EVENTS.RECORDING_WILL_START, this._onRecordingWillStart);
     PerformanceController.off(EVENTS.RECORDING_WILL_STOP, this._onRecordingWillStop);
     PerformanceController.off(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.off(EVENTS.CONSOLE_RECORDING_STOPPED, this._onRecordingStopped);
     PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
 
     yield ToolbarView.destroy();
     yield RecordingsView.destroy();
     yield OverviewView.destroy();
     yield DetailsView.destroy();
   }),
 
   /**
    * Sets the state of the profiler view. Possible options are "empty",
-   * "recording", "recorded".
+   * "recording", "console-recording", "recorded".
    */
   setState: function (state) {
     let viewConfig = this.states[state];
     if (!viewConfig) {
       throw new Error(`Invalid state for PerformanceView: ${state}`);
     }
     for (let { deck, pane } of viewConfig) {
       $(deck).selectedPanel = $(pane);
     }
 
     this._state = state;
+
+    if (state === "console-recording") {
+      let recording = PerformanceController.getCurrentRecording();
+      let label = recording.getLabel() || "";
+      $(".console-profile-recording-notice").value = L10N.getFormatStr("consoleProfile.recordingNotice", label);
+      $(".console-profile-stop-notice").value = L10N.getFormatStr("consoleProfile.stopCommand", label);
+    }
     this.emit(EVENTS.UI_STATE_CHANGED, state);
   },
 
   /**
    * Returns the state of the PerformanceView.
    */
   getState: function () {
     return this._state;
@@ -143,21 +156,24 @@ let PerformanceView = {
     this._lockRecordButton();
     this._recordButton.removeAttribute("checked");
   },
 
   /**
    * When a recording is complete.
    */
   _onRecordingStopped: function (_, recording) {
-    this._unlockRecordButton();
+    // A stopped recording can be from `console.profileEnd` -- only unlock
+    // the button if it's the main recording that was started via UI.
+    if (!recording.isConsole()) {
+      this._unlockRecordButton();
+    }
 
-    // If this recording stopped is the current recording, set the
-    // state to "recorded". A stopped recording doesn't necessarily
-    // have to be the current recording (console.profileEnd, for example)
+    // If the currently selected recording is the one that just stopped,
+    // switch state to "recorded".
     if (recording === PerformanceController.getCurrentRecording()) {
       this.setState("recorded");
     }
   },
 
   /**
    * Handler for clicking the clear button.
    */
@@ -191,16 +207,18 @@ let PerformanceView = {
   },
 
   /**
    * Fired when a recording is selected. Used to toggle the profiler view state.
    */
   _onRecordingSelected: function (_, recording) {
     if (!recording) {
       this.setState("empty");
+    } else if (recording.isRecording() && recording.isConsole()) {
+      this.setState("console-recording");
     } else if (recording.isRecording()) {
       this.setState("recording");
     } else {
       this.setState("recorded");
     }
   },
 
   toString: () => "[object PerformanceView]"
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -153,16 +153,26 @@
                   pack="center"
                   flex="1">
               <label value="&profilerUI.stopNotice1;"/>
               <button class="devtools-toolbarbutton record-button"
                       standalone="true"
                       checked="true" />
               <label value="&profilerUI.stopNotice2;"/>
             </hbox>
+            <hbox id="console-recording-notice"
+                  class="notice-container"
+                  align="center"
+                  pack="center"
+                  flex="1">
+                  <vbox>
+                    <label class="console-profile-recording-notice" />
+                    <label class="console-profile-stop-notice" />
+                  </vbox>
+            </hbox>
             <deck id="details-pane" flex="1">
               <hbox id="waterfall-view" flex="1">
                 <vbox id="waterfall-breakdown" flex="1" />
                 <splitter class="devtools-side-splitter"/>
                 <vbox id="waterfall-details"
                       class="theme-sidebar"
                       width="150"
                       height="150"/>
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -18,16 +18,24 @@ support-files =
 [browser_perf-compatibility-01.js]
 [browser_perf-compatibility-02.js]
 [browser_perf-compatibility-03.js]
 [browser_perf-compatibility-04.js]
 [browser_perf-clear-01.js]
 [browser_perf-clear-02.js]
 [browser_perf-columns-js-calltree.js]
 [browser_perf-columns-memory-calltree.js]
+[browser_perf-console-record-01.js]
+[browser_perf-console-record-02.js]
+[browser_perf-console-record-03.js]
+[browser_perf-console-record-04.js]
+[browser_perf-console-record-05.js]
+[browser_perf-console-record-06.js]
+[browser_perf-console-record-07.js]
+[browser_perf-console-record-08.js]
 [browser_perf-data-massaging-01.js]
 [browser_perf-data-samples.js]
 [browser_perf-details-calltree-render.js]
 [browser_perf-details-flamegraph-render.js]
 [browser_perf-details-memory-calltree-render.js]
 [browser_perf-details-memory-flamegraph-render.js]
 [browser_perf-details-waterfall-render.js]
 [browser_perf-details-01.js]
--- a/browser/devtools/performance/test/browser_markers-parse-html.js
+++ b/browser/devtools/performance/test/browser_markers-parse-html.js
@@ -4,25 +4,27 @@
 /**
  * Test that we get a "Parse HTML" marker when a page sets innerHTML.
  */
 
 const TEST_URL = EXAMPLE_URL + "doc_innerHTML.html"
 
 function* getMarkers(front) {
   const { promise, resolve } = Promise.defer();
-  const handler = (_, markers) => {
-    resolve(markers);
+  const handler = (_, name, markers) => {
+    if (name === "markers") {
+      resolve(markers);
+    }
   };
-  front.on("markers", handler);
+  front.on("timeline-data", handler);
 
   yield front.startRecording({ withTicks: true });
 
   const markers = yield promise;
-  front.off("markers", handler);
+  front.off("timeline-data", handler);
   yield front.stopRecording();
 
   return markers;
 }
 
 function* spawnTest () {
   let { target, front } = yield initBackend(TEST_URL);
 
--- a/browser/devtools/performance/test/browser_perf-compatibility-01.js
+++ b/browser/devtools/performance/test/browser_perf-compatibility-01.js
@@ -13,54 +13,36 @@ function spawnTest () {
     TEST_MOCK_TIMELINE_ACTOR: true
   });
   Services.prefs.setBoolPref(MEMORY_PREF, true);
 
   let { memory, timeline } = front.getMocksInUse();
   ok(memory, "memory should be mocked.");
   ok(timeline, "timeline should be mocked.");
 
-  let {
-    profilerStartTime,
-    timelineStartTime,
-    memoryStartTime
-  } = yield front.startRecording({
+  let recording = yield front.startRecording({
     withTicks: true,
     withMemory: true,
     withAllocations: true,
     allocationsSampleProbability: +Services.prefs.getCharPref(MEMORY_SAMPLE_PROB_PREF),
     allocationsMaxLogLength: Services.prefs.getIntPref(MEMORY_MAX_LOG_LEN_PREF)
   });
 
-  ok(typeof profilerStartTime === "number",
-    "The front.startRecording() emits a profiler start time.");
-  ok(typeof timelineStartTime === "number",
-    "The front.startRecording() emits a timeline start time.");
-  ok(typeof memoryStartTime === "number",
-    "The front.startRecording() emits a memory start time.");
+  ok(typeof recording._profilerStartTime === "number",
+    "The front.startRecording() returns a recording with a profiler start time");
+  ok(typeof recording._timelineStartTime === "number",
+    "The front.startRecording() returns a recording with a timeline start time");
+  ok(typeof recording._memoryStartTime === "number",
+    "The front.startRecording() returns a recording with a memory start time");
 
   yield busyWait(WAIT_TIME);
 
-  let {
-    profilerEndTime,
-    timelineEndTime,
-    memoryEndTime
-  } = yield front.stopRecording({
-    withAllocations: true
-  });
+  yield front.stopRecording(recording);
 
-  ok(typeof profilerEndTime === "number",
-    "The front.stopRecording() emits a profiler end time.");
-  ok(typeof timelineEndTime === "number",
-    "The front.stopRecording() emits a timeline end time.");
-  ok(typeof memoryEndTime === "number",
-    "The front.stopRecording() emits a memory end time.");
+  ok(typeof recording.getDuration() === "number",
+    "The front.stopRecording() allows recording to get a duration.");
 
-  ok(profilerEndTime > profilerStartTime,
+  ok(recording.getDuration() >= 0,
     "The profilerEndTime is after profilerStartTime.");
-  is(timelineEndTime, timelineStartTime,
-    "The timelineEndTime is the same as timelineStartTime.");
-  is(memoryEndTime, memoryStartTime,
-    "The memoryEndTime is the same as memoryStartTime.");
 
   yield removeTab(target.tab);
   finish();
 }
--- a/browser/devtools/performance/test/browser_perf-compatibility-03.js
+++ b/browser/devtools/performance/test/browser_perf-compatibility-03.js
@@ -12,54 +12,36 @@ function spawnTest () {
     TEST_MOCK_MEMORY_ACTOR: true
   });
   Services.prefs.setBoolPref(MEMORY_PREF, true);
 
   let { memory, timeline } = front.getMocksInUse();
   ok(memory, "memory should be mocked.");
   ok(!timeline, "timeline should not be mocked.");
 
-  let {
-    profilerStartTime,
-    timelineStartTime,
-    memoryStartTime
-  } = yield front.startRecording({
+  let recording = yield front.startRecording({
     withTicks: true,
     withMemory: true,
     withAllocations: true,
     allocationsSampleProbability: +Services.prefs.getCharPref(MEMORY_SAMPLE_PROB_PREF),
     allocationsMaxLogLength: Services.prefs.getIntPref(MEMORY_MAX_LOG_LEN_PREF)
   });
 
-  ok(typeof profilerStartTime === "number",
-    "The front.startRecording() emits a profiler start time.");
-  ok(typeof timelineStartTime === "number",
-    "The front.startRecording() emits a timeline start time.");
-  ok(typeof memoryStartTime === "number",
-    "The front.startRecording() emits a memory start time.");
+  ok(typeof recording._profilerStartTime === "number",
+    "The front.startRecording() returns a recording with a profiler start time");
+  ok(typeof recording._timelineStartTime === "number",
+    "The front.startRecording() returns a recording with a timeline start time");
+  ok(typeof recording._memoryStartTime === "number",
+    "The front.startRecording() returns a recording with a memory start time");
 
   yield busyWait(WAIT_TIME);
 
-  let {
-    profilerEndTime,
-    timelineEndTime,
-    memoryEndTime
-  } = yield front.stopRecording({
-    withAllocations: true
-  });
+  yield front.stopRecording(recording);
 
-  ok(typeof profilerEndTime === "number",
-    "The front.stopRecording() emits a profiler end time.");
-  ok(typeof timelineEndTime === "number",
-    "The front.stopRecording() emits a timeline end time.");
-  ok(typeof memoryEndTime === "number",
-    "The front.stopRecording() emits a memory end time.");
+  ok(typeof recording.getDuration() === "number",
+    "The front.stopRecording() allows recording to get a duration.");
 
-  ok(profilerEndTime > profilerStartTime,
+  ok(recording.getDuration() >= 0,
     "The profilerEndTime is after profilerStartTime.");
-  ok(timelineEndTime > timelineStartTime,
-    "The timelineEndTime is after timelineStartTime.");
-  is(memoryEndTime, memoryStartTime,
-    "The memoryEndTime is the same as memoryStartTime.");
 
   yield removeTab(target.tab);
   finish();
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-console-record-01.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the profiler is populated by console recordings that have finished
+ * before it was opened.
+ */
+
+let { getPerformanceActorsConnection } = devtools.require("devtools/performance/front");
+let WAIT_TIME = 10;
+
+function spawnTest () {
+  let profilerConnected = waitForProfilerConnection();
+  let { target, toolbox, console } = yield initConsole(SIMPLE_URL);
+  yield profilerConnected;
+  let connection = getPerformanceActorsConnection(target);
+
+  let profileStart = once(connection, "console-profile-start");
+  console.profile("rust");
+  yield profileStart;
+
+  busyWait(WAIT_TIME);
+  let profileEnd = once(connection, "console-profile-end");
+  console.profileEnd("rust");
+  yield profileEnd;
+
+  yield gDevTools.showToolbox(target, "performance");
+  let panel = toolbox.getCurrentPanel();
+  let { panelWin: { PerformanceController, RecordingsView }} = panel;
+
+  let recordings = PerformanceController.getRecordings();
+  is(recordings.length, 1, "one recording found in the performance panel.");
+  is(recordings[0].isConsole(), true, "recording came from console.profile.");
+  is(recordings[0].getLabel(), "rust", "correct label in the recording model.");
+
+  is(RecordingsView.selectedItem.attachment, recordings[0],
+    "The profile from console should be selected as its the only one in the RecordingsView.");
+
+  is(RecordingsView.selectedItem.attachment.getLabel(), "rust",
+    "The profile label for the first recording is correct.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-console-record-02.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the profiler is populated by in-progress console recordings
+ * when it is opened.
+ */
+
+let { getPerformanceActorsConnection } = devtools.require("devtools/performance/front");
+let WAIT_TIME = 10;
+
+function spawnTest () {
+  let profilerConnected = waitForProfilerConnection();
+  let { target, toolbox, console } = yield initConsole(SIMPLE_URL);
+  yield profilerConnected;
+  let connection = getPerformanceActorsConnection(target);
+
+  let profileStart = once(connection, "console-profile-start");
+  console.profile("rust");
+  yield profileStart;
+  profileStart = once(connection, "console-profile-start");
+  console.profile("rust2");
+  yield profileStart;
+
+  yield gDevTools.showToolbox(target, "performance");
+  let panel = toolbox.getCurrentPanel();
+  let { panelWin: { PerformanceController, RecordingsView }} = panel;
+
+  let recordings = PerformanceController.getRecordings();
+  is(recordings.length, 2, "two recordings found in the performance panel.");
+  is(recordings[0].isConsole(), true, "recording came from console.profile (1).");
+  is(recordings[0].getLabel(), "rust", "correct label in the recording model (1).");
+  is(recordings[0].isRecording(), true, "recording is still recording (1).");
+  is(recordings[1].isConsole(), true, "recording came from console.profile (2).");
+  is(recordings[1].getLabel(), "rust2", "correct label in the recording model (2).");
+  is(recordings[1].isRecording(), true, "recording is still recording (2).");
+
+  is(RecordingsView.selectedItem.attachment, recordings[0],
+    "The first console recording should be selected.");
+
+  let profileEnd = once(connection, "console-profile-end");
+  console.profileEnd("rust");
+  yield profileEnd;
+  profileEnd = once(connection, "console-profile-end");
+  console.profileEnd("rust2");
+  yield profileEnd;
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-console-record-03.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the profiler is populated by in-progress console recordings, and
+ * also console recordings that have finished before it was opened.
+ */
+
+let { getPerformanceActorsConnection } = devtools.require("devtools/performance/front");
+let WAIT_TIME = 10;
+
+function spawnTest () {
+  let profilerConnected = waitForProfilerConnection();
+  let { target, toolbox, console } = yield initConsole(SIMPLE_URL);
+  yield profilerConnected;
+  let connection = getPerformanceActorsConnection(target);
+
+  let profileStart = once(connection, "console-profile-start");
+  console.profile("rust");
+  yield profileStart;
+
+  let profileEnd = once(connection, "console-profile-end");
+  console.profileEnd("rust");
+  yield profileEnd;
+
+  profileStart = once(connection, "console-profile-start");
+  console.profile("rust2");
+  yield profileStart;
+
+  yield gDevTools.showToolbox(target, "performance");
+  let panel = toolbox.getCurrentPanel();
+  let { panelWin: { PerformanceController, RecordingsView }} = panel;
+
+  let recordings = PerformanceController.getRecordings();
+  is(recordings.length, 2, "two recordings found in the performance panel.");
+  is(recordings[0].isConsole(), true, "recording came from console.profile (1).");
+  is(recordings[0].getLabel(), "rust", "correct label in the recording model (1).");
+  is(recordings[0].isRecording(), false, "recording is still recording (1).");
+  is(recordings[1].isConsole(), true, "recording came from console.profile (2).");
+  is(recordings[1].getLabel(), "rust2", "correct label in the recording model (2).");
+  is(recordings[1].isRecording(), true, "recording is still recording (2).");
+
+  is(RecordingsView.selectedItem.attachment, recordings[0],
+    "The first console recording should be selected.");
+
+  profileEnd = once(connection, "console-profile-end");
+  console.profileEnd("rust2");
+  yield profileEnd;
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-console-record-04.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the profiler can handle creation and stopping of console profiles
+ * after being opened.
+ */
+
+function spawnTest () {
+  loadFrameScripts();
+  let { target, toolbox, panel } = yield initPerformance(SIMPLE_URL);
+  let { $, EVENTS, gFront, PerformanceController, OverviewView, RecordingsView } = panel.panelWin;
+
+  yield consoleProfile(panel.panelWin, "rust");
+
+  let recordings = PerformanceController.getRecordings();
+  is(recordings.length, 1, "a recordings found in the performance panel.");
+  is(recordings[0].isConsole(), true, "recording came from console.profile.");
+  is(recordings[0].getLabel(), "rust", "correct label in the recording model.");
+  is(recordings[0].isRecording(), true, "recording is still recording.");
+
+  is(RecordingsView.selectedItem.attachment, recordings[0],
+    "The first console recording should be selected.");
+
+  // Ensure overview is still rendering
+  yield once(OverviewView, EVENTS.OVERVIEW_RENDERED);
+  yield once(OverviewView, EVENTS.OVERVIEW_RENDERED);
+  yield once(OverviewView, EVENTS.OVERVIEW_RENDERED);
+
+  yield consoleProfileEnd(panel.panelWin, "rust");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-console-record-05.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that multiple recordings with the same label (non-overlapping) appear
+ * in the recording list.
+ */
+
+function spawnTest () {
+  loadFrameScripts();
+  let { target, toolbox, panel } = yield initPerformance(SIMPLE_URL);
+  let { $, EVENTS, gFront, PerformanceController, OverviewView, RecordingsView } = panel.panelWin;
+
+  yield consoleProfile(panel.panelWin, "rust");
+
+  let recordings = PerformanceController.getRecordings();
+  is(recordings.length, 1, "a recordings found in the performance panel.");
+  is(recordings[0].isConsole(), true, "recording came from console.profile.");
+  is(recordings[0].getLabel(), "rust", "correct label in the recording model.");
+  is(recordings[0].isRecording(), true, "recording is still recording.");
+
+  is(RecordingsView.selectedItem.attachment, recordings[0],
+    "The first console recording should be selected.");
+
+  // Ensure overview is still rendering
+  yield once(OverviewView, EVENTS.OVERVIEW_RENDERED);
+  yield once(OverviewView, EVENTS.OVERVIEW_RENDERED);
+  yield once(OverviewView, EVENTS.OVERVIEW_RENDERED);
+
+  yield consoleProfileEnd(panel.panelWin, "rust");
+
+  yield consoleProfile(panel.panelWin, "rust");
+  recordings = PerformanceController.getRecordings();
+  is(recordings.length, 2, "a recordings found in the performance panel.");
+  is(recordings[1].isConsole(), true, "recording came from console.profile.");
+  is(recordings[1].getLabel(), "rust", "correct label in the recording model.");
+  is(recordings[1].isRecording(), true, "recording is still recording.");
+
+  yield consoleProfileEnd(panel.panelWin, "rust");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-console-record-06.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that console recordings can overlap (not completely nested).
+ */
+
+function spawnTest () {
+  loadFrameScripts();
+  let { target, toolbox, panel } = yield initPerformance(SIMPLE_URL);
+  let { $, EVENTS, gFront, PerformanceController, OverviewView, RecordingsView, WaterfallView } = panel.panelWin;
+
+  yield consoleProfile(panel.panelWin, "rust");
+
+  let recordings = PerformanceController.getRecordings();
+  is(recordings.length, 1, "a recording found in the performance panel.");
+  is(RecordingsView.selectedItem.attachment, recordings[0],
+    "The first console recording should be selected.");
+
+  yield consoleProfile(panel.panelWin, "golang");
+
+  recordings = PerformanceController.getRecordings();
+  is(recordings.length, 2, "two recordings found in the performance panel.");
+  is(RecordingsView.selectedItem.attachment, recordings[0],
+    "The first console recording should still be selected.");
+
+  // Ensure overview is still rendering
+  yield once(OverviewView, EVENTS.OVERVIEW_RENDERED);
+  yield once(OverviewView, EVENTS.OVERVIEW_RENDERED);
+  yield once(OverviewView, EVENTS.OVERVIEW_RENDERED);
+
+  let detailsRendered = once(WaterfallView, EVENTS.WATERFALL_RENDERED);
+  yield consoleProfileEnd(panel.panelWin, "rust");
+  yield detailsRendered;
+
+  recordings = PerformanceController.getRecordings();
+  is(recordings.length, 2, "two recordings found in the performance panel.");
+  is(RecordingsView.selectedItem.attachment, recordings[0],
+    "The first console recording should still be selected.");
+  is(RecordingsView.selectedItem.attachment.isRecording(), false,
+    "The first console recording should no longer be recording.");
+
+  detailsRendered = once(WaterfallView, EVENTS.WATERFALL_RENDERED);
+  yield consoleProfileEnd(panel.panelWin, "golang");
+  yield detailsRendered;
+
+  recordings = PerformanceController.getRecordings();
+  is(recordings.length, 2, "two recordings found in the performance panel.");
+  is(RecordingsView.selectedItem.attachment, recordings[0],
+    "The first console recording should still be selected.");
+  is(recordings[1].isRecording(), false,
+    "The second console recording should no longer be recording.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-console-record-07.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that a call to console.profileEnd() with no label ends the
+ * most recent console recording, and console.profileEnd() with a label that does not
+ * match any pending recordings does nothing.
+ */
+
+function spawnTest () {
+  loadFrameScripts();
+  let { target, toolbox, panel } = yield initPerformance(SIMPLE_URL);
+  let { $, EVENTS, gFront, PerformanceController, OverviewView, RecordingsView, WaterfallView } = panel.panelWin;
+
+  yield consoleProfile(panel.panelWin);
+  yield consoleProfile(panel.panelWin, "1");
+  yield consoleProfile(panel.panelWin, "2");
+
+  let recordings = PerformanceController.getRecordings();
+  is(recordings.length, 3, "3 recordings found");
+  is(RecordingsView.selectedItem.attachment, recordings[0],
+    "The first console recording should be selected.");
+
+  yield consoleProfileEnd(panel.panelWin);
+
+  // First off a label-less profileEnd to make sure no other recordings close
+  consoleProfileEnd(panel.panelWin, "fxos");
+  yield idleWait(500);
+
+  recordings = PerformanceController.getRecordings();
+  is(recordings.length, 3, "3 recordings found");
+
+  is(recordings[0].getLabel(), "", "Checking label of recording 1");
+  is(recordings[1].getLabel(), "1", "Checking label of recording 2");
+  is(recordings[2].getLabel(), "2", "Checking label of recording 3");
+  is(recordings[0].isRecording(), true,
+    "The not most recent recording should not stop when calling console.profileEnd with no args.");
+  is(recordings[1].isRecording(), true,
+    "The not most recent recording should not stop when calling console.profileEnd with no args.");
+  is(recordings[2].isRecording(), false,
+    "Only thw most recent recording should stop when calling console.profileEnd with no args.");
+
+  let detailsRendered = once(WaterfallView, EVENTS.WATERFALL_RENDERED);
+  yield consoleProfileEnd(panel.panelWin);
+  yield consoleProfileEnd(panel.panelWin);
+
+  is(recordings[0].isRecording(), false,
+    "All recordings should now be ended. (1)");
+  is(recordings[1].isRecording(), false,
+    "All recordings should now be ended. (2)");
+  is(recordings[2].isRecording(), false,
+    "All recordings should now be ended. (3)");
+
+  yield detailsRendered;
+
+  consoleProfileEnd(panel.panelWin);
+  yield idleWait(500);
+  ok(true, "Calling additional console.profileEnd() with no argument and no pending recordings does not throw.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-console-record-08.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the profiler can correctly handle simultaneous console and manual
+ * recordings (via `console.profile` and clicking the record button).
+ */
+
+let C = 1; // is console
+let R = 2; // is recording
+let S = 4; // is selected
+
+function testRecordings (win, expected) {
+  let recordings = win.PerformanceController.getRecordings();
+  let current = win.PerformanceController.getCurrentRecording();
+  is(recordings.length, expected.length, "expected number of recordings");
+  recordings.forEach((recording, i) => {
+    ok(recording.isConsole() == !!(expected[i] & C), `recording ${i+1} has expected console state.`);
+    ok(recording.isRecording() == !!(expected[i] & R), `recording ${i+1} has expected console state.`);
+    ok((recording === current) == !!(expected[i] & S), `recording ${i+1} has expected selected state.`);
+  });
+}
+
+function spawnTest () {
+  loadFrameScripts();
+  let { target, toolbox, panel } = yield initPerformance(SIMPLE_URL);
+  let win = panel.panelWin;
+  let { $, EVENTS, gFront, PerformanceController, OverviewView, RecordingsView, WaterfallView } = win;
+
+  info("Starting console.profile()...");
+  yield consoleProfile(win);
+  testRecordings(win, [C+S+R]);
+  info("Starting manual recording...");
+  yield startRecording(panel);
+  testRecordings(win, [C+R, R+S]);
+  info("Starting console.profile(\"3\")...");
+  yield consoleProfile(win, "3");
+  testRecordings(win, [C+R, R+S, C+R]);
+  info("Starting console.profile(\"3\")...");
+  yield consoleProfile(win, "4");
+  testRecordings(win, [C+R, R+S, C+R, C+R]);
+
+  info("Ending console.profileEnd()...");
+  yield consoleProfileEnd(win);
+
+  testRecordings(win, [C+R, R+S, C+R, C]);
+  ok(OverviewView.isRendering(), "still rendering overview with manual recorded selected.");
+
+  let onSelected = once(WaterfallView, EVENTS.WATERFALL_RENDERED);
+  info("Select last recording...");
+  RecordingsView.selectedIndex = 3;
+  yield onSelected;
+  testRecordings(win, [C+R, R, C+R, C+S]);
+  ok(!OverviewView.isRendering(), "stop rendering overview when selected completed recording.");
+
+  info("Manually stop manual recording...");
+  yield stopRecording(panel);
+  testRecordings(win, [C+R, S, C+R, C]);
+  ok(!OverviewView.isRendering(), "stop rendering overview when selected completed recording.");
+
+  onSelected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+  info("Select first recording...");
+  RecordingsView.selectedIndex = 0;
+  yield onSelected;
+  testRecordings(win, [C+R+S, 0, C+R, C]);
+  yield once(OverviewView, EVENTS.OVERVIEW_RENDERED);
+  ok(OverviewView.isRendering(), "should be rendering overview when selected recording in progress.");
+
+  info("Ending console.profileEnd()...");
+  yield consoleProfileEnd(win);
+  testRecordings(win, [C+R+S, 0, C, C]);
+  ok(OverviewView.isRendering(), "should still be rendering overview when selected recording in progress.");
+  info("Start one more manual recording...");
+  yield startRecording(panel);
+  testRecordings(win, [C+R, 0, C, C, R+S]);
+  ok(OverviewView.isRendering(), "should be rendering overview when selected recording in progress.");
+  info("Stop manual recording...");
+  yield stopRecording(panel);
+  testRecordings(win, [C+R, 0, C, C, S]);
+  ok(!OverviewView.isRendering(), "stop rendering overview when selected completed recording.");
+
+  info("Ending console.profileEnd()...");
+  yield consoleProfileEnd(win);
+  testRecordings(win, [C, 0, C, C, S]);
+  ok(!OverviewView.isRendering(), "stop rendering overview when selected completed recording.");
+
+  yield teardown(panel);
+  finish();
+}
--- a/browser/devtools/performance/test/browser_perf-data-massaging-01.js
+++ b/browser/devtools/performance/test/browser_perf-data-massaging-01.js
@@ -9,53 +9,49 @@
 const WAIT_TIME = 1000; // ms
 
 function spawnTest () {
   let { panel } = yield initPerformance(SIMPLE_URL);
   let front = panel.panelWin.gFront;
 
   // Perform the first recording...
 
-  let firstRecordingDataStart = yield front.startRecording();
-  let firstRecordingStartTime = firstRecordingDataStart.profilerStartTime;
+  let firstRecording = yield front.startRecording();
+  let firstRecordingStartTime = firstRecording._profilerStartTime;
   info("Started profiling at: " + firstRecordingStartTime);
 
   busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity
 
-  let firstRecordingDataStop = yield front.stopRecording();
-  let firstRecordingFinishTime = firstRecordingDataStop.profilerEndTime;
+  yield front.stopRecording(firstRecording);
 
   is(firstRecordingStartTime, 0,
     "The profiling start time should be 0 for the first recording.");
-  ok(firstRecordingFinishTime - firstRecordingStartTime >= WAIT_TIME,
+  ok(firstRecording.getDuration() >= WAIT_TIME,
     "The first recording duration is correct.");
-  ok(firstRecordingFinishTime >= WAIT_TIME,
-    "The first recording finish time is correct.");
 
   // Perform the second recording...
 
-  let secondRecordingDataStart = yield front.startRecording();
-  let secondRecordingStartTime = secondRecordingDataStart.profilerStartTime;
+  let secondRecording = yield front.startRecording();
+  let secondRecordingStartTime = secondRecording._profilerStartTime;
   info("Started profiling at: " + secondRecordingStartTime);
 
   busyWait(WAIT_TIME); // allow the profiler module to sample more cpu activity
 
-  let secondRecordingDataStop = yield front.stopRecording();
-  let secondRecordingFinishTime = secondRecordingDataStop.profilerEndTime;
-  let secondRecordingProfile = secondRecordingDataStop.profile;
+  yield front.stopRecording(secondRecording);
+  let secondRecordingProfile = secondRecording.getProfile();
   let secondRecordingSamples = secondRecordingProfile.threads[0].samples;
 
-  isnot(secondRecordingStartTime, 0,
+  isnot(secondRecording._profilerStartTime, 0,
     "The profiling start time should not be 0 on the second recording.");
-  ok(secondRecordingFinishTime - secondRecordingStartTime >= WAIT_TIME,
+  ok(secondRecording.getDuration() >= WAIT_TIME,
     "The second recording duration is correct.");
 
   ok(secondRecordingSamples[0].time < secondRecordingStartTime,
     "The second recorded sample times were normalized.");
   ok(secondRecordingSamples[0].time > 0,
     "The second recorded sample times were normalized correctly.");
-  ok(!secondRecordingSamples.find(e => e.time + secondRecordingStartTime <= firstRecordingFinishTime),
+  ok(!secondRecordingSamples.find(e => e.time + secondRecordingStartTime <= firstRecording.getDuration()),
     "There should be no samples from the first recording in the second one, " +
     "even though the total number of frames did not overflow.");
 
   yield teardown(panel);
   finish();
 }
--- a/browser/devtools/performance/test/browser_perf-data-samples.js
+++ b/browser/devtools/performance/test/browser_perf-data-samples.js
@@ -8,20 +8,21 @@
  */
 
 const WAIT_TIME = 1000; // ms
 
 function spawnTest () {
   let { panel } = yield initPerformance(SIMPLE_URL);
   let front = panel.panelWin.gFront;
 
-  yield front.startRecording();
+  let rec = yield front.startRecording();
   busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity
 
-  let { profile } = yield front.stopRecording();
+  yield front.stopRecording(rec);
+  let profile = rec.getProfile();
   let sampleCount = 0;
 
   for (let thread of profile.threads) {
     info("Checking thread: " + thread.name);
 
     for (let sample of thread.samples) {
       sampleCount++;
 
--- a/browser/devtools/performance/test/browser_perf-front-01.js
+++ b/browser/devtools/performance/test/browser_perf-front-01.js
@@ -5,67 +5,48 @@
  * Test basic functionality of PerformanceFront, emitting start and endtime values
  */
 
 let WAIT_TIME = 1000;
 
 function spawnTest () {
   let { target, front } = yield initBackend(SIMPLE_URL);
 
-  let {
-    profilerStartTime,
-    timelineStartTime,
-    memoryStartTime
-  } = yield front.startRecording({
+  let recording = yield front.startRecording({
     withAllocations: true,
     allocationsSampleProbability: +Services.prefs.getCharPref(MEMORY_SAMPLE_PROB_PREF),
     allocationsMaxLogLength: Services.prefs.getIntPref(MEMORY_MAX_LOG_LEN_PREF)
   });
 
   let allocationsCount = 0;
-  let allocationsCounter = () => allocationsCount++;
+  let allocationsCounter = (_, type) => type === "allocations" && allocationsCount++;
 
   // Record allocation events to ensure it's called more than once
   // so we know it's polling
-  front.on("allocations", allocationsCounter);
+  front.on("timeline-data", allocationsCounter);
 
-  ok(typeof profilerStartTime === "number",
-    "The front.startRecording() emits a profiler start time.");
-  ok(typeof timelineStartTime === "number",
-    "The front.startRecording() emits a timeline start time.");
-  ok(typeof memoryStartTime === "number",
-    "The front.startRecording() emits a memory start time.");
+  ok(typeof recording._profilerStartTime === "number",
+    "The front.startRecording() returns a recording model with a profiler start time.");
+  ok(typeof recording._timelineStartTime === "number",
+    "The front.startRecording() returns a recording model with a timeline start time.");
+  ok(typeof recording._memoryStartTime === "number",
+    "The front.startRecording() returns a recording model with a memory start time.");
 
   yield Promise.all([
     busyWait(WAIT_TIME),
     waitUntil(() => allocationsCount > 1)
   ]);
 
-  let {
-    profilerEndTime,
-    timelineEndTime,
-    memoryEndTime
-  } = yield front.stopRecording({
-    withAllocations: true
-  });
+  yield front.stopRecording(recording);
 
-  front.off("allocations", allocationsCounter);
+  front.off("timeline-data", allocationsCounter);
 
-  ok(typeof profilerEndTime === "number",
-    "The front.stopRecording() emits a profiler end time.");
-  ok(typeof timelineEndTime === "number",
-    "The front.stopRecording() emits a timeline end time.");
-  ok(typeof memoryEndTime === "number",
-    "The front.stopRecording() emits a memory end time.");
-
-  ok(profilerEndTime > profilerStartTime,
-    "The profilerEndTime is after profilerStartTime.");
-  ok(timelineEndTime > timelineStartTime,
-    "The timelineEndTime is after timelineStartTime.");
-  ok(memoryEndTime > memoryStartTime,
-    "The memoryEndTime is after memoryStartTime.");
+  ok(typeof recording.getDuration() === "number",
+    "The front.stopRecording() gives the recording model a stop time and duration.");
+  ok(recording.getDuration() > 0,
+    "The front.stopRecording() gives a positive duration amount.");
 
   is((yield front._request("memory", "getState")), "detached",
     "Memory actor is detached when stopping recording with allocations.");
 
   yield removeTab(target.tab);
   finish();
 }
--- a/browser/devtools/performance/test/browser_perf-front-basic-profiler-01.js
+++ b/browser/devtools/performance/test/browser_perf-front-basic-profiler-01.js
@@ -5,51 +5,28 @@
  * Test basic functionality of PerformanceFront
  */
 
 let WAIT_TIME = 1000;
 
 function spawnTest () {
   let { target, front } = yield initBackend(SIMPLE_URL);
 
-  let startData = yield front.startRecording();
-  let { profilerStartTime, timelineStartTime, memoryStartTime } = startData;
+  let startModel = yield front.startRecording();
+  let { profilerStartTime, timelineStartTime, memoryStartTime } = startModel;
 
-  ok("profilerStartTime" in startData,
-    "A `profilerStartTime` property is properly set in the recording data.");
-  ok("timelineStartTime" in startData,
-    "A `timelineStartTime` property is properly set in the recording data.");
-  ok("memoryStartTime" in startData,
-    "A `memoryStartTime` property is properly set in the recording data.");
-
-  ok(profilerStartTime !== undefined,
-    "A `profilerStartTime` property exists in the recording data.");
-  ok(timelineStartTime !== undefined,
-    "A `timelineStartTime` property exists in the recording data.");
-  is(memoryStartTime, 0,
-    "A `memoryStartTime` property exists in the recording data, but it's 0.");
+  ok(startModel._profilerStartTime !== undefined,
+    "A `_profilerStartTime` property exists in the recording model.");
+  ok(startModel._timelineStartTime !== undefined,
+    "A `_timelineStartTime` property exists in the recording model.");
+  ise(startModel._memoryStartTime, 0,
+    "A `_memoryStartTime` property exists in the recording model, but it's 0.");
 
   yield busyWait(WAIT_TIME);
 
-  let stopData = yield front.stopRecording();
-  let { profile, profilerEndTime, timelineEndTime, memoryEndTime } = stopData;
+  let stopModel = yield front.stopRecording(startModel);
 
-  ok("profile" in stopData,
-    "A `profile` property is properly set in the recording data.");
-  ok("profilerEndTime" in stopData,
-    "A `profilerEndTime` property is properly set in the recording data.");
-  ok("timelineEndTime" in stopData,
-    "A `timelineEndTime` property is properly set in the recording data.");
-  ok("memoryEndTime" in stopData,
-    "A `memoryEndTime` property is properly set in the recording data.");
-
-  ok(profile,
-    "A `profile` property exists in the recording data.");
-  ok(profilerEndTime !== undefined,
-    "A `profilerEndTime` property exists in the recording data.");
-  ok(timelineEndTime !== undefined,
-    "A `timelineEndTime` property exists in the recording data.");
-  is(memoryEndTime, 0,
-    "A `memoryEndTime` property exists in the recording data, but it's 0.");
+  ok(stopModel.getProfile(), "recording model has a profile after stopping.");
+  ok(stopModel.getDuration(), "recording model has a duration after stopping.");
 
   yield removeTab(target.tab);
   finish();
 }
--- a/browser/devtools/performance/test/browser_perf-front-basic-timeline-01.js
+++ b/browser/devtools/performance/test/browser_perf-front-basic-timeline-01.js
@@ -18,65 +18,64 @@ function spawnTest () {
   };
 
   let deferreds = {
     markers: Promise.defer(),
     memory: Promise.defer(),
     ticks: Promise.defer()
   };
 
-  front.on("markers", handler);
-  front.on("memory", handler);
-  front.on("ticks", handler);
+  front.on("timeline-data", handler);
 
   yield front.startRecording({ withMemory: true, withTicks: true });
   yield Promise.all(Object.keys(deferreds).map(type => deferreds[type].promise));
   yield front.stopRecording();
+  front.off("timeline-data", handler);
 
   is(counters.markers.length, 1, "one marker event fired.");
   is(counters.memory.length, 3, "three memory events fired.");
   is(counters.ticks.length, 3, "three ticks events fired.");
 
   yield removeTab(target.tab);
   finish();
 
-  function handler (name, ...args) {
+  function handler (_, name, ...args) {
     if (name === "markers") {
+      if (counters.markers.length >= 1) { return; }
       let [markers] = args;
       ok(markers[0].start, "received atleast one marker with `start`");
       ok(markers[0].end, "received atleast one marker with `end`");
       ok(markers[0].name, "received atleast one marker with `name`");
 
       counters.markers.push(markers);
-      front.off(name, handler);
-      deferreds[name].resolve();
     }
     else if (name === "memory") {
+      if (counters.memory.length >= 3) { return; }
       let [delta, measurement] = args;
       is(typeof delta, "number", "received `delta` in memory event");
       ok(delta > lastMemoryDelta, "received `delta` in memory event");
       ok(measurement.total, "received `total` in memory event");
 
       counters.memory.push({ delta, measurement });
       lastMemoryDelta = delta;
     }
     else if (name === "ticks") {
+      if (counters.ticks.length >= 3) { return; }
       let [delta, timestamps] = args;
       ok(delta > lastTickDelta, "received `delta` in ticks event");
 
       // Timestamps aren't guaranteed to always contain tick events, since
       // they're dependent on the refresh driver, which may be blocked.
 
       counters.ticks.push({ delta, timestamps });
       lastTickDelta = delta;
     }
     else {
       throw new Error("unknown event " + name);
     }
 
     if (name === "markers" && counters[name].length === 1 ||
         name === "memory" && counters[name].length === 3 ||
         name === "ticks" && counters[name].length === 3) {
-      front.off(name, handler);
       deferreds[name].resolve();
     }
   };
 }
--- a/browser/devtools/performance/test/browser_perf-front-profiler-02.js
+++ b/browser/devtools/performance/test/browser_perf-front-profiler-02.js
@@ -10,26 +10,26 @@
 let test = Task.async(function*() {
   let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
   let front = panel.panelWin.gFront;
 
   ok(!nsIProfilerModule.IsActive(),
     "The built-in profiler module should not have been automatically started.");
 
   let activated = front.once("profiler-activated");
-  yield front.startRecording();
+  let rec = yield front.startRecording();
   yield activated;
-  yield front.stopRecording();
+  yield front.stopRecording(rec);
   ok(nsIProfilerModule.IsActive(),
     "The built-in profiler module should still be active (1).");
 
   let alreadyActive = front.once("profiler-already-active");
-  yield front.startRecording();
+  rec = yield front.startRecording();
   yield alreadyActive;
-  yield front.stopRecording();
+  yield front.stopRecording(rec);
   ok(nsIProfilerModule.IsActive(),
     "The built-in profiler module should still be active (2).");
 
   yield teardown(panel);
 
   ok(!nsIProfilerModule.IsActive(),
     "The built-in profiler module should have been automatically stoped.");
 
--- a/browser/devtools/performance/test/browser_perf-front-profiler-04.js
+++ b/browser/devtools/performance/test/browser_perf-front-profiler-04.js
@@ -14,27 +14,27 @@ let test = Task.async(function*() {
   let INTERVAL = 1;
   let FEATURES = ["js"];
   nsIProfilerModule.StartProfiler(ENTRIES, INTERVAL, FEATURES, FEATURES.length);
 
   let { panel: firstPanel } = yield initPerformance(SIMPLE_URL);
   let firstFront = firstPanel.panelWin.gFront;
 
   let firstAlreadyActive = firstFront.once("profiler-already-active");
-  let { profilerStartTime: firstStartTime } = yield firstFront.startRecording();
+  let recording = yield firstFront.startRecording();
   yield firstAlreadyActive;
-  ok(firstStartTime > 0, "The profiler was not restarted.");
+  ok(recording._profilerStartTime > 0, "The profiler was not restarted.");
 
   let { panel: secondPanel } = yield initPerformance(SIMPLE_URL);
   let secondFront = secondPanel.panelWin.gFront;
 
   let secondAlreadyActive = secondFront.once("profiler-already-active");
-  let { profilerStartTime: secondStartTime } = yield secondFront.startRecording();
+  let secondRecording = yield secondFront.startRecording();
   yield secondAlreadyActive;
-  ok(secondStartTime > 0, "The profiler was not restarted.");
+  ok(secondRecording._profilerStartTime > 0, "The profiler was not restarted.");
 
   yield teardown(firstPanel);
   ok(nsIProfilerModule.IsActive(),
     "The built-in profiler module should still be active.");
 
   yield teardown(secondPanel);
   ok(!nsIProfilerModule.IsActive(),
     "The built-in profiler module should have been automatically stoped.");
--- a/browser/devtools/performance/test/head.js
+++ b/browser/devtools/performance/test/head.js
@@ -180,18 +180,16 @@ function initBackend(aUrl, targetOps={})
 
     // Attach addition options to `target`. This is used to force mock fronts
     // to smokescreen test different servers where memory or timeline actors
     // may not exist. Possible options that will actually work:
     // TEST_MOCK_MEMORY_ACTOR = true
     // TEST_MOCK_TIMELINE_ACTOR = true
     merge(target, targetOps);
 
-    yield gDevTools.showToolbox(target, "performance");
-
     let connection = getPerformanceActorsConnection(target);
     yield connection.open();
 
     let front = new PerformanceFront(connection);
     return { target, front };
   });
 }
 
@@ -212,16 +210,61 @@ function initPerformance(aUrl, selectedT
     merge(target, targetOps);
 
     let toolbox = yield gDevTools.showToolbox(target, selectedTool);
     let panel = toolbox.getCurrentPanel();
     return { target, panel, toolbox };
   });
 }
 
+/**
+ * Initializes a webconsole panel. Returns a target, panel and toolbox reference.
+ * Also returns a console property that allows calls to `profile` and `profileEnd`.
+ */
+function initConsole(aUrl) {
+  return Task.spawn(function*() {
+    let { target, toolbox, panel } = yield initPerformance(aUrl, "webconsole");
+    let { hud } = panel;
+    return {
+      target, toolbox, panel, console: {
+        profile: (s) => consoleExecute(hud, "profile", s),
+        profileEnd: (s) => consoleExecute(hud, "profileEnd", s)
+      }
+    };
+  });
+}
+
+function consoleExecute (console, method, val) {
+  let { ui, jsterm } = console;
+  let { promise, resolve } = Promise.defer();
+  let message = `console.${method}("${val}")`;
+
+  ui.on("new-messages", handler);
+  jsterm.execute(message);
+
+  let { console: c } = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
+  function handler (event, messages) {
+    for (let msg of messages) {
+      if (msg.response._message === message) {
+        ui.off("new-messages", handler);
+        resolve();
+        return;
+      }
+    }
+  }
+  return promise;
+}
+
+function waitForProfilerConnection() {
+  let { promise, resolve } = Promise.defer();
+  Services.obs.addObserver(resolve, "performance-actors-connection-opened", false);
+  return promise.then(() =>
+    Services.obs.removeObserver(resolve, "performance-actors-connection-opened"));
+}
+
 function* teardown(panel) {
   info("Destroying the performance tool.");
 
   let tab = panel.target.tab;
   yield panel._toolbox.destroy();
   yield removeTab(tab);
 }
 
@@ -234,29 +277,37 @@ function busyWait(time) {
   let stack;
   while (Date.now() - start < time) { stack = Components.stack; }
 }
 
 function consoleMethod (...args) {
   if (!mm) {
     throw new Error("`loadFrameScripts()` must be called before using frame scripts.");
   }
+  // Terrible ugly hack -- this gets stringified when it uses the
+  // message manager, so an undefined arg in `console.profileEnd()`
+  // turns into a stringified "null", which is terrible. This method is only used
+  // for test helpers, so swap out the argument if its undefined with an empty string.
+  // Differences between empty string and undefined are tested on the front itself.
+  if (args[1] == null) {
+    args[1] = "";
+  }
   mm.sendAsyncMessage("devtools:test:console", args);
 }
 
-function* consoleProfile(connection, label) {
-  let notified = connection.once("profile");
+function* consoleProfile(win, label) {
+  let profileStart = once(win.PerformanceController, win.EVENTS.CONSOLE_RECORDING_STARTED);
   consoleMethod("profile", label);
-  yield notified;
+  yield profileStart;
 }
 
-function* consoleProfileEnd(connection) {
-  let notified = connection.once("profileEnd");
-  consoleMethod("profileEnd");
-  yield notified;
+function* consoleProfileEnd(win, label) {
+  let ended = once(win.PerformanceController, win.EVENTS.CONSOLE_RECORDING_STOPPED);
+  consoleMethod("profileEnd", label);
+  yield ended;
 }
 
 function command (button) {
   let ev = button.ownerDocument.createEvent("XULCommandEvent");
   ev.initCommandEvent("command", true, true, button.ownerDocument.defaultView, 0, false, false, false, false, null);
   button.dispatchEvent(ev);
 }
 
--- a/browser/devtools/performance/views/details-abstract-subview.js
+++ b/browser/devtools/performance/views/details-abstract-subview.js
@@ -12,30 +12,32 @@ let DetailsSubview = {
    */
   initialize: function () {
     this._onRecordingStoppedOrSelected = this._onRecordingStoppedOrSelected.bind(this);
     this._onOverviewRangeChange = this._onOverviewRangeChange.bind(this);
     this._onDetailsViewSelected = this._onDetailsViewSelected.bind(this);
     this._onPrefChanged = this._onPrefChanged.bind(this);
 
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
+    PerformanceController.on(EVENTS.CONSOLE_RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
     PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
     PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
     OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onOverviewRangeChange);
     OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onOverviewRangeChange);
     DetailsView.on(EVENTS.DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
   },
 
   /**
    * Unbinds events.
    */
   destroy: function () {
     clearNamedTimeout("range-change-debounce");
 
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
+    PerformanceController.off(EVENTS.CONSOLE_RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
     PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
     PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
     OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onOverviewRangeChange);
     OverviewView.off(EVENTS.OVERVIEW_RANGE_CLEARED, this._onOverviewRangeChange);
     DetailsView.off(EVENTS.DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
   },
 
   /**
--- a/browser/devtools/performance/views/details.js
+++ b/browser/devtools/performance/views/details.js
@@ -54,16 +54,17 @@ let DetailsView = {
 
     for (let button of $$("toolbarbutton[data-view]", this.toolbar)) {
       button.addEventListener("command", this._onViewToggle);
     }
 
     yield this.selectDefaultView();
     yield this.setAvailableViews();
 
+    PerformanceController.on(EVENTS.CONSOLE_RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
     PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
     PerformanceController.on(EVENTS.PREF_CHANGED, this.setAvailableViews);
   }),
 
   /**
    * Unbinds events, destroys subviews.
    */
@@ -71,16 +72,17 @@ let DetailsView = {
     for (let button of $$("toolbarbutton[data-view]", this.toolbar)) {
       button.removeEventListener("command", this._onViewToggle);
     }
 
     for (let [_, component] of Iterator(this.components)) {
       component.initialized && (yield component.view.destroy());
     }
 
+    PerformanceController.off(EVENTS.CONSOLE_RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
     PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
     PerformanceController.off(EVENTS.PREF_CHANGED, this.setAvailableViews);
   }),
 
   /**
    * Sets the possible views based off of prefs and server actor support by hiding/showing the
    * buttons that select them and going to default view if currently selected.
--- a/browser/devtools/performance/views/overview.js
+++ b/browser/devtools/performance/views/overview.js
@@ -44,16 +44,19 @@ let OverviewView = {
 
     PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
     PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
     PerformanceController.on(EVENTS.RECORDING_WILL_START, this._onRecordingWillStart);
     PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
     PerformanceController.on(EVENTS.RECORDING_WILL_STOP, this._onRecordingWillStop);
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
     PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
+    PerformanceController.on(EVENTS.CONSOLE_RECORDING_STARTED, this._onRecordingStarted);
+    PerformanceController.on(EVENTS.CONSOLE_RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.on(EVENTS.CONSOLE_RECORDING_WILL_STOP, this._onRecordingWillStop);
   },
 
   /**
    * Unbinds events.
    */
   destroy: Task.async(function*() {
     if (this.markersOverview) {
       yield this.markersOverview.destroy();
@@ -67,16 +70,19 @@ let OverviewView = {
 
     PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
     PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
     PerformanceController.off(EVENTS.RECORDING_WILL_START, this._onRecordingWillStart);
     PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
     PerformanceController.off(EVENTS.RECORDING_WILL_STOP, this._onRecordingWillStop);
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
     PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
+    PerformanceController.off(EVENTS.CONSOLE_RECORDING_STARTED, this._onRecordingStarted);
+    PerformanceController.off(EVENTS.CONSOLE_RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.off(EVENTS.CONSOLE_RECORDING_WILL_STOP, this._onRecordingWillStop);
   }),
 
   /**
    * Disabled in the event we're using a Timeline mock, so we'll have no
    * markers, ticks or memory data to show, so just block rendering and hide
    * the panel.
    */
   disable: function () {
@@ -274,17 +280,17 @@ let OverviewView = {
   }),
 
   /**
    * Called to refresh the timer to keep firing _onRecordingTick.
    */
   _prepareNextTick: function () {
     // Check here to see if there's still a _timeoutId, incase
     // `stop` was called before the _prepareNextTick call was executed.
-    if (this._timeoutId) {
+    if (this.isRendering()) {
       this._timeoutId = setTimeout(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
     }
   },
 
   /**
    * Fired when the graph selection has changed. Called by
    * mouseup and scroll events.
    */
@@ -298,68 +304,115 @@ let OverviewView = {
     if (interval.endTime - interval.startTime < 1) {
       this.emit(EVENTS.OVERVIEW_RANGE_CLEARED);
     } else {
       this.emit(EVENTS.OVERVIEW_RANGE_SELECTED, interval);
     }
   },
 
   /**
-   * Called when recording will start.
+   * Called when recording will start. No recording because it does not
+   * exist yet, but can just disable from here. This will only trigger for
+   * manual recordings.
    */
-  _onRecordingWillStart: Task.async(function* (_, recording) {
-    yield this._checkSelection(recording);
+  _onRecordingWillStart: Task.async(function* () {
+    this._onRecordingStateChange();
+    yield this._checkSelection();
     this.markersOverview.dropSelection();
   }),
 
   /**
    * Called when recording actually starts.
    */
   _onRecordingStarted: function (_, recording) {
-    this._timeoutId = setTimeout(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
+    this._onRecordingStateChange();
   },
 
   /**
    * Called when recording will stop.
    */
   _onRecordingWillStop: function(_, recording) {
-    clearTimeout(this._timeoutId);
-    this._timeoutId = null;
+    this._onRecordingStateChange();
   },
 
   /**
    * Called when recording actually stops.
    */
   _onRecordingStopped: Task.async(function* (_, recording) {
+    this._onRecordingStateChange();
+    // Check to see if the recording that just stopped is the current recording.
+    // If it is, render the high-res graphs. For manual recordings, it will also
+    // be the current recording, but profiles generated by `console.profile` can stop
+    // while having another profile selected -- in this case, OverviewView should keep
+    // rendering the current recording.
+    if (recording !== PerformanceController.getCurrentRecording()) {
+      return;
+    }
     this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);
     yield this._checkSelection(recording);
   }),
 
   /**
    * Called when a new recording is selected.
    */
   _onRecordingSelected: Task.async(function* (_, recording) {
     if (!recording) {
       return;
     }
-    // If timeout exists, we have something recording, so
-    // this will still tick away at rendering. Otherwise, force a render.
-    if (!this._timeoutId) {
+    this._onRecordingStateChange();
+    // If this recording is complete, render the high res graph
+    if (!recording.isRecording()) {
       yield this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);
     }
     yield this._checkSelection(recording);
     this.markersOverview.dropSelection();
   }),
 
   /**
+   * Called when a recording is starting, stopping, or about to start/stop.
+   * Checks the current recording displayed to determine whether or not
+   * the polling for rendering the overview graph needs to start or stop.
+   */
+  _onRecordingStateChange: function () {
+    let currentRecording = PerformanceController.getCurrentRecording();
+    if (!currentRecording || (this.isRendering() && !currentRecording.isRecording())) {
+      this._stopPolling();
+    } else if (currentRecording.isRecording() && !this.isRendering()) {
+      this._startPolling();
+    }
+  },
+
+  /**
+   * Start the polling for rendering the overview graph.
+   */
+  _startPolling: function () {
+    this._timeoutId = setTimeout(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
+  },
+
+  /**
+   * Stop the polling for rendering the overview graph.
+   */
+  _stopPolling: function () {
+    clearTimeout(this._timeoutId);
+    this._timeoutId = null;
+  },
+
+  /**
+   * Whether or not the overview view is in a state of polling rendering.
+   */
+  isRendering: function () {
+    return !!this._timeoutId;
+  },
+
+  /**
    * Makes sure the selection is enabled or disabled in all the graphs,
    * based on whether a recording currently exists and is not in progress.
    */
   _checkSelection: Task.async(function* (recording) {
-    let selectionEnabled = !recording.isRecording();
+    let selectionEnabled = recording ? !recording.isRecording() : false;
 
     if (yield this._markersGraphAvailable()) {
       this.markersOverview.selectionEnabled = selectionEnabled;
     }
     if (yield this._memoryGraphAvailable()) {
       this.memoryOverview.selectionEnabled = selectionEnabled;
     }
     if (yield this._framerateGraphAvailable()) {
--- a/browser/devtools/performance/views/recordings.js
+++ b/browser/devtools/performance/views/recordings.js
@@ -19,27 +19,31 @@ let RecordingsView = Heritage.extend(Wid
     this._onRecordingImported = this._onRecordingImported.bind(this);
     this._onSaveButtonClick = this._onSaveButtonClick.bind(this);
     this._onRecordingsCleared = this._onRecordingsCleared.bind(this);
 
     this.emptyText = L10N.getStr("noRecordingsText");
 
     PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.on(EVENTS.CONSOLE_RECORDING_STARTED, this._onRecordingStarted);
+    PerformanceController.on(EVENTS.CONSOLE_RECORDING_STOPPED, this._onRecordingStopped);
     PerformanceController.on(EVENTS.RECORDING_IMPORTED, this._onRecordingImported);
     PerformanceController.on(EVENTS.RECORDINGS_CLEARED, this._onRecordingsCleared);
     this.widget.addEventListener("select", this._onSelect, false);
   },
 
   /**
    * Destruction function, called when the tool is closed.
    */
   destroy: function() {
     PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.off(EVENTS.CONSOLE_RECORDING_STARTED, this._onRecordingStarted);
+    PerformanceController.off(EVENTS.CONSOLE_RECORDING_STOPPED, this._onRecordingStopped);
     PerformanceController.off(EVENTS.RECORDING_IMPORTED, this._onRecordingImported);
     PerformanceController.off(EVENTS.RECORDINGS_CLEARED, this._onRecordingsCleared);
     this.widget.removeEventListener("select", this._onSelect, false);
   },
 
   /**
    * Adds an empty recording to this container.
    *
@@ -90,67 +94,51 @@ let RecordingsView = Heritage.extend(Wid
 
   /**
    * Signals that a recording session has started.
    *
    * @param RecordingModel recording
    *        Model of the recording that was started.
    */
   _onRecordingStarted: function (_, recording) {
-    // Insert a "dummy" recording item, to hint that recording has now started.
-    let recordingItem;
-
-    // If a label is specified (e.g due to a call to `console.profile`),
-    // then try reusing a pre-existing recording item, if there is one.
-    // This is symmetrical to how `this.handleRecordingEnded` works.
-    let profileLabel = recording.getLabel();
-    if (profileLabel) {
-      recordingItem = this.getItemForAttachment(e => e.getLabel() == profileLabel);
-    }
-    // Otherwise, create a new empty recording item.
-    if (!recordingItem) {
-      recordingItem = this.addEmptyRecording(recording);
-    }
+    // TODO bug 1144388
+    // If a label is identical to an existing recording item,
+    // logically group them here.
+    // For now, insert a "dummy" recording item, to hint that recording has now started.
+    let recordingItem = this.addEmptyRecording(recording);
 
     // Mark the corresponding item as being a "record in progress".
     recordingItem.isRecording = true;
 
-    // If this is a manual recording, immediately select it.
-    if (!recording.getLabel()) {
+    // If this is a manual recording, immediately select it, or
+    // select a console profile if its the only one
+    if (!recording.isConsole() || this.selectedIndex === -1) {
       this.selectedItem = recordingItem;
     }
   },
 
   /**
    * Signals that a recording session has ended.
    *
    * @param RecordingModel recording
    *        The model of the recording that just stopped.
    */
   _onRecordingStopped: function (_, recording) {
-    let recordingItem;
-
-    // If a label is specified (e.g due to a call to `console.profileEnd`),
-    // then try reusing a pre-existing recording item, if there is one.
-    // This is symmetrical to how `this.handleRecordingStarted` works.
-    let profileLabel = recording.getLabel();
-    if (profileLabel) {
-      recordingItem = this.getItemForAttachment(e => e.getLabel() == profileLabel);
-    }
-    // Otherwise, just use the first available recording item.
-    if (!recordingItem) {
-      recordingItem = this.getItemForPredicate(e => e.isRecording);
-    }
+    let recordingItem = this.getItemForPredicate(e => e.attachment === recording);
 
     // Mark the corresponding item as being a "finished recording".
     recordingItem.isRecording = false;
 
     // Render the recording item with finalized information (timing, etc)
     this.finalizeRecording(recordingItem);
-    this.forceSelect(recordingItem);
+
+    // Select the recording if it was a manual recording only
+    if (!recording.isConsole()) {
+      this.forceSelect(recordingItem);
+    }
   },
 
   /**
    * Signals that a recording has been imported.
    *
    * @param RecordingModel model
    *        The recording model containing data on the recording session.
    */
@@ -190,31 +178,21 @@ let RecordingsView = Heritage.extend(Wid
     durationNode.setAttribute("value",
       L10N.getFormatStr("recordingsList.durationLabel", durationMillis));
   },
 
   /**
    * The select listener for this container.
    */
   _onSelect: Task.async(function*({ detail: recordingItem }) {
-    // TODO 1120699
-    // show appropriate empty/recording panels for several scenarios below
     if (!recordingItem) {
       return;
     }
 
     let model = recordingItem.attachment;
-
-    // If recording, don't abort completely, as we still want to fire an event
-    // for selection so we can continue repainting the overview graphs.
-    if (recordingItem.isRecording) {
-      this.emit(EVENTS.RECORDING_SELECTED, model);
-      return;
-    }
-
     this.emit(EVENTS.RECORDING_SELECTED, model);
   }),
 
   /**
    * The click listener for the "save" button of each item in this container.
    */
   _onSaveButtonClick: function (e) {
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
--- a/browser/devtools/shadereditor/test/head.js
+++ b/browser/devtools/shadereditor/test/head.js
@@ -31,16 +31,18 @@ const MULTIPLE_CONTEXTS_URL = EXAMPLE_UR
 const OVERLAPPING_GEOMETRY_CANVAS_URL = EXAMPLE_URL + "doc_overlapping-geometry.html";
 const BLENDED_GEOMETRY_CANVAS_URL = EXAMPLE_URL + "doc_blended-geometry.html";
 
 // All tests are asynchronous.
 waitForExplicitFinish();
 
 let gToolEnabled = Services.prefs.getBoolPref("devtools.shadereditor.enabled");
 
+gDevTools.testing = true;
+
 registerCleanupFunction(() => {
   info("finish() was called, cleaning up...");
   Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
   Services.prefs.setBoolPref("devtools.shadereditor.enabled", gToolEnabled);
 
   // These tests use a lot of memory due to GL contexts, so force a GC to help
   // fragmentation.
   info("Forcing GC after shadereditor test.");
--- a/browser/locales/en-US/chrome/browser/devtools/profiler.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/profiler.properties
@@ -131,8 +131,13 @@ jit.optimizationFailure=Optimization fai
 # frame is sampled.
 # "#1" represents the number of samples
 # example: 30 samples
 jit.samples2=#1 sample;#1 samples
 
 # LOCALIZATION NOTE (jit.empty):
 # This string is displayed when there are no JIT optimizations to display.
 jit.empty=No JIT optimizations recorded for this frame.
+
+# LOCALIZATION NOTE (consoleProfile.recordingNotice/stopCommand):
+# These strings are displayed when a recording is in progress, that was started from the console.
+consoleProfile.recordingNotice=Currently recording profile "%S".
+consoleProfile.stopCommand=Stop profiling by typing \"console.profileEnd(\'%S\')\" into the console.
--- a/browser/themes/shared/devtools/performance.inc.css
+++ b/browser/themes/shared/devtools/performance.inc.css
@@ -70,16 +70,20 @@
 }
 
 #performance-view .notice-container button {
   min-width: 30px;
   min-height: 28px;
   margin: 0;
 }
 
+#performance-view .notice-container vbox {
+  text-align: center;
+}
+
 /* Overview Panel */
 
 .record-button {
   list-style-image: url(profiler-stopwatch.svg);
 }
 
 .record-button[checked] {
   list-style-image: url(profiler-stopwatch-checked.svg);
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -7134,32 +7134,37 @@ nsContentUtils::GetInnerWindowID(nsIRequ
   }
 
   nsPIDOMWindow* inner = pwindow->IsInnerWindow() ? pwindow.get() : pwindow->GetCurrentInnerWindow();
 
   return inner ? inner->WindowID() : 0;
 }
 
 void
-nsContentUtils::GetHostOrIPv6WithBrackets(nsIURI* aURI, nsAString& aHost)
+nsContentUtils::GetHostOrIPv6WithBrackets(nsIURI* aURI, nsCString& aHost)
 {
   aHost.Truncate();
-  nsAutoCString hostname;
-  nsresult rv = aURI->GetHost(hostname);
+  nsresult rv = aURI->GetHost(aHost);
   if (NS_FAILED(rv)) { // Some URIs do not have a host
     return;
   }
 
-  if (hostname.FindChar(':') != -1) { // Escape IPv6 address
-    MOZ_ASSERT(!hostname.Length() ||
-      (hostname[0] !='[' && hostname[hostname.Length() - 1] != ']'));
-    hostname.Insert('[', 0);
-    hostname.Append(']');
-  }
-
+  if (aHost.FindChar(':') != -1) { // Escape IPv6 address
+    MOZ_ASSERT(!aHost.Length() ||
+      (aHost[0] !='[' && aHost[aHost.Length() - 1] != ']'));
+    aHost.Insert('[', 0);
+    aHost.Append(']');
+  }
+}
+
+void
+nsContentUtils::GetHostOrIPv6WithBrackets(nsIURI* aURI, nsAString& aHost)
+{
+  nsAutoCString hostname;
+  GetHostOrIPv6WithBrackets(aURI, hostname);
   CopyUTF8toUTF16(hostname, aHost);
 }
 
 void
 nsContentUtils::CallOnAllRemoteChildren(nsIMessageBroadcaster* aManager,
                                         CallOnRemoteChildFunction aCallback,
                                         void* aArg)
 {
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -2277,16 +2277,17 @@ public:
    */
   static uint64_t GetInnerWindowID(nsIRequest* aRequest);
 
   /**
    * If the hostname for aURI is an IPv6 it encloses it in brackets,
    * otherwise it just outputs the hostname in aHost.
    */
   static void GetHostOrIPv6WithBrackets(nsIURI* aURI, nsAString& aHost);
+  static void GetHostOrIPv6WithBrackets(nsIURI* aURI, nsCString& aHost);
 
   /*
    * Call the given callback on all remote children of the given top-level
    * window.
    */
   static void CallOnAllRemoteChildren(nsIDOMWindow* aWindow,
                                       CallOnRemoteChildFunction aCallback,
                                       void* aArg);
--- a/dom/base/test/test_url.html
+++ b/dom/base/test/test_url.html
@@ -1,9 +1,8 @@
-
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test URL API</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
@@ -112,17 +111,17 @@
       port: '',
       pathname: '/',
       search: '?test',
       hash: ''
     },
     { url: 'http://example.com/carrot#question%3f',
       base: undefined,
       error: false,
-      hash: '#question%3f'
+      hash: '#question?'
     },
     { url: 'https://example.com:4443?',
       base: undefined,
       error: false,
       protocol: 'https:',
       port: '4443',
       pathname: '/',
       hash: '',
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -1892,16 +1892,18 @@ addExternalIface('MozXULTemplateBuilder'
 addExternalIface('nsIBrowserDOMWindow', nativeType='nsIBrowserDOMWindow',
                  notflattened=True)
 addExternalIface('nsIControllers', nativeType='nsIControllers')
 addExternalIface('nsIDOMCrypto', nativeType='nsIDOMCrypto',
                  headerFile='Crypto.h')
 addExternalIface('nsIInputStreamCallback', nativeType='nsIInputStreamCallback',
                  headerFile='nsIAsyncInputStream.h')
 addExternalIface('nsIFile', nativeType='nsIFile', notflattened=True)
+addExternalIface('nsILoadGroup', nativeType='nsILoadGroup',
+                 headerFile='nsILoadGroup.h', notflattened=True)
 addExternalIface('nsIMessageBroadcaster', nativeType='nsIMessageBroadcaster',
                  headerFile='nsIMessageManager.h', notflattened=True)
 addExternalIface('nsISelectionListener', nativeType='nsISelectionListener')
 addExternalIface('nsIStreamListener', nativeType='nsIStreamListener', notflattened=True)
 addExternalIface('nsISupports', nativeType='nsISupports')
 addExternalIface('nsIDocShell', nativeType='nsIDocShell', notflattened=True)
 addExternalIface('nsIEditor', nativeType='nsIEditor', notflattened=True)
 addExternalIface('nsIVariant', nativeType='nsIVariant', notflattened=True)
--- a/dom/events/test/test_bug422132.html
+++ b/dom/events/test/test_bug422132.html
@@ -3,16 +3,17 @@
 <!--
 https://bugzilla.mozilla.org/show_bug.cgi?id=422132
 -->
 <head>
   <title>Test for Bug 422132</title>
   <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=422132">Mozilla Bug 422132</a>
 <p id="display"></p>
 <div id="target" style="font-size: 0; width: 200px; height: 200px; overflow: auto;">
   <div style="width: 1000px; height: 1000px;"></div>
 </div>
@@ -20,75 +21,104 @@ https://bugzilla.mozilla.org/show_bug.cg
   
 </div>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 /** Test for Bug 422132 **/
 
 SimpleTest.waitForExplicitFinish();
-SimpleTest.requestFlakyTimeout("untriaged");
 SimpleTest.waitForFocus(function() {
   SpecialPowers.pushPrefEnv({
     "set":[["general.smoothScroll", false],
            ["mousewheel.min_line_scroll_amount", 1],
+           ["mousewheel.system_scroll_override_on_root_content.enabled", false],
            ["mousewheel.transaction.timeout", 100000]]}, runTests)}, window);
 
-function hitEventLoop(aFunc, aTimes)
-{
-  if (--aTimes) {
-    setTimeout(hitEventLoop, 0, aFunc, aTimes);
-  } else {
-    setTimeout(aFunc, 20);
-  }
-}
-
 function runTests()
 {
   var target = document.getElementById("target");
 
   var scrollLeft = target.scrollLeft;
   var scrollTop = target.scrollTop;
-  synthesizeWheel(target, 10, 10,
-    { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
-      deltaX: 0.5, deltaY: 0.5, lineOrPageDeltaX: 0, lineOrPageDeltaY: 0 });
-  hitEventLoop(function () {
-    is(target.scrollLeft, scrollLeft, "scrolled to right by 0.5px delta value");
-    is(target.scrollTop, scrollTop, "scrolled to bottom by 0.5px delta value");
-    scrollLeft = target.scrollLeft;
-    scrollTop = target.scrollTop;
-    synthesizeWheel(target, 10, 10,
-      { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
-        deltaX: 0.5, deltaY: 0.5, lineOrPageDeltaX: 0, lineOrPageDeltaY: 0 });
-    hitEventLoop(function () {
-      ok(target.scrollLeft > scrollLeft,
-         "not scrolled to right by 0.5px delta value with pending 0.5px delta");
-      ok(target.scrollTop > scrollTop,
-         "not scrolled to bottom by 0.5px delta value with pending 0.5px delta");
-      scrollLeft = target.scrollLeft;
-      scrollTop = target.scrollTop;
-      synthesizeWheel(target, 10, 10,
-        { deltaMode: WheelEvent.DOM_DELTA_LINE,
-          deltaX: 0.5, deltaY: 0.5, lineOrPageDeltaX: 0, lineOrPageDeltaY: 0 });
-      hitEventLoop(function () {
-        is(target.scrollLeft, scrollLeft, "scrolled to right by 0.5 line delta value");
-        is(target.scrollTop, scrollTop, "scrolled to bottom by 0.5 line delta value");
+
+  var tests = [
+    {
+      prepare: function() {
+        scrollLeft = target.scrollLeft;
+        scrollTop = target.scrollTop;
+      },
+      event: {
+        deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+        deltaX: 0.5,
+        deltaY: 0.5,
+        lineOrPageDeltaX: 0,
+        lineOrPageDeltaY: 0
+      },
+    }, {
+      event: {
+        deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+        deltaX: 0.5,
+        deltaY: 0.5,
+        lineOrPageDeltaX: 0,
+        lineOrPageDeltaY: 0
+      },
+      check: function() {
+        is(target.scrollLeft - scrollLeft, 1,
+           "not scrolled to right by 0.5px delta value with pending 0.5px delta");
+        is(target.scrollTop - scrollTop, 1,
+           "not scrolled to bottom by 0.5px delta value with pending 0.5px delta");
+      },
+    }, {
+      prepare: function() {
         scrollLeft = target.scrollLeft;
         scrollTop = target.scrollTop;
-        synthesizeWheel(target, 10, 10,
-          { deltaMode: WheelEvent.DOM_DELTA_LINE,
-            deltaX: 0.5, deltaY: 0.5, lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 });
-        hitEventLoop(function () {
-          ok(target.scrollLeft > scrollLeft,
-             "not scrolled to right by 0.5 line delta value with pending 0.5 line delta");
-          ok(target.scrollTop > scrollTop,
-             "not scrolled to bottom by 0.5 line delta value with pending 0.5 line delta");
-          SimpleTest.finish();
-        }, 20);
-      }, 20);
-    }, 20);
-  }, 20);
+      },
+      event: {
+        deltaMode: WheelEvent.DOM_DELTA_LINE,
+        deltaX: 0.5,
+        deltaY: 0.5,
+        lineOrPageDeltaX: 0,
+        lineOrPageDeltaY: 0
+      },
+    }, {
+      event: {
+        deltaMode: WheelEvent.DOM_DELTA_LINE,
+        deltaX: 0.5,
+        deltaY: 0.5,
+        lineOrPageDeltaX: 1,
+        lineOrPageDeltaY: 1
+      },
+      check: function() {
+        is(target.scrollLeft - scrollLeft, 1,
+           "not scrolled to right by 0.5 line delta value with pending 0.5 line delta");
+        is(target.scrollTop - scrollTop, 1,
+           "not scrolled to bottom by 0.5 line delta value with pending 0.5 line delta");
+      }
+    }
+  ];
+
+  var nextTest = function() {
+    var test = tests.shift();
+    if (test.prepare) {
+      test.prepare();
+    }
+
+    sendWheelAndPaint(target, 10, 10, test.event, function() {
+      if (test.check) {
+        test.check();
+      }
+      if (tests.length == 0) {
+        SimpleTest.finish();
+        return;
+      }
+
+      setTimeout(nextTest, 0);
+    });
+  }
+
+  nextTest();
 }
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/html/test/test_hash_encoded.html
+++ b/dom/html/test/test_hash_encoded.html
@@ -2,24 +2,52 @@
 <html>
 <head>
 <title>Test link.hash attribute</title>
 <script src="/tests/SimpleTest/SimpleTest.js"></script>
 <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
-<a id="target1" href="http://www.example.com/#q=♥â¥#hello"></a>
-<a id="target2" href="http://www.example.com/#q=%E2%99%A5%C3%A2%C2%A5"></a>
-<a id="target3" href="http://www.example.com/#/search/%23important"></a>
-<a id="target4" href='http://www.example.com/#{"a":[13, 42], "b":{"key":"value"}}'></a>
-
 <pre id="test">
 
 <script>
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [['dom.url.encode_decode_hash', false]]}, runTest);
+
+function runTest() {
+  setupTest();
+  doTestEncoded();
+  SimpleTest.finish();
+}
+
+function setupTest() {
+  var target1 = document.createElement("a");
+  target1.id =  "target1";
+  target1.href = "http://www.example.com/#q=♥â¥#hello";
+  document.body.appendChild(target1);
+
+  var target2 = document.createElement("a");
+  target2.id =  "target2";
+  target2.href = "http://www.example.com/#q=%E2%99%A5%C3%A2%C2%A5";
+  document.body.appendChild(target2);
+
+  var target3 = document.createElement("a");
+  target3.id =  "target3";
+  target3.href = "http://www.example.com/#/search/%23important";
+  document.body.appendChild(target3);
+
+  var target4 = document.createElement("a");
+  target4.id =  "target4";
+  target4.href = 'http://www.example.com/#{"a":[13, 42], "b":{"key":"value"}}';
+  document.body.appendChild(target4);
+}
+
+function doTestEncoded() {
   // Tests Link::GetHash
 
   // Check that characters aren't being encoded
   var target = document.getElementById("target1");
   is(target.hash, '#q=♥â¥#hello', 'Unexpected link hash');
 
   // Check that encoded characters aren't being decoded
   target = document.getElementById("target2");
@@ -27,19 +55,17 @@
 
   // A more regular use case
   target = document.getElementById("target3");
   is(target.hash, '#/search/%23important', 'Unexpected link hash');
 
   // Some JSON
   target = document.getElementById("target4");
   is(target.hash, '#{"a":[13, 42], "b":{"key":"value"}}', 'Unexpected link hash');
-</script>
 
-<script>
   // Tests URL::GetHash
 
   var url = new URL("http://www.example.com/#q=♥â¥#hello")
   is(url.hash, '#q=♥â¥#hello', 'Unexpected url hash');
 
   url = new URL("http://www.example.com/#q=%E2%99%A5%C3%A2%C2%A5")
   is(url.hash, '#q=%E2%99%A5%C3%A2%C2%A5', 'Unexpected url hash');
 
@@ -63,29 +89,30 @@
   var parsed = JSON.parse(target.hash.substring(1));
   is(parsed.b.key, 'value', 'JSON not parsed correctly');
 
   url = new URL("http://www.example.com/test/");
   url.hash = '#{"a":[13, 42], "b":{"key":"value"}}';
   is(target.hash, '#{"a":[13, 42], "b":{"key":"value"}}', 'Unexpected url hash');
   parsed = JSON.parse(target.hash.substring(1));
   is(parsed.b.key, 'value', 'JSON not parsed correctly');
-</script>
 
-<script>
   // Tests nsLocation::GetHash
 
   window.history.pushState(1, document.title, '#q=♥â¥#hello');
   is(location.hash,'#q=♥â¥#hello', 'Unexpected location hash');
 
   window.history.pushState(1, document.title, '#q=%E2%99%A5%C3%A2%C2%A5');
   is(location.hash,'#q=%E2%99%A5%C3%A2%C2%A5', 'Unexpected location hash');
 
   window.history.pushState(1, document.title, '#/search/%23important');
   is(location.hash,'#/search/%23important', 'Unexpected location hash');
 
   window.history.pushState(1, document.title, '#{"a":[13, 42], "b":{"key":"value"}}');
   is(location.hash,'#{"a":[13, 42], "b":{"key":"value"}}', 'Unexpected location hash');
+
+}
+
 </script>
 
 </pre>
 </body>
 </html>
--- a/dom/media/IdpSandbox.jsm
+++ b/dom/media/IdpSandbox.jsm
@@ -35,26 +35,29 @@ RedirectHttpsOnly.prototype = {
 /** This class loads a resource into a single string. ResourceLoader.load() is
  * the entry point. */
 function ResourceLoader(res, rej) {
   this.resolve = res;
   this.reject = rej;
   this.data = '';
 }
 
-/** Loads the identified https:// URL.  */
-ResourceLoader.load = function(uri) {
+/** Loads the identified https:// URL. */
+ResourceLoader.load = function(uri, doc) {
   return new Promise((resolve, reject) => {
     let listener = new ResourceLoader(resolve, reject);
     let ioService = Cc['@mozilla.org/network/io-service;1']
       .getService(Ci.nsIIOService);
     let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
     // the '2' identifies this as a script load
-    let ioChannel = ioService.newChannelFromURI2(uri, null, systemPrincipal,
-                                                 systemPrincipal, 0, 2);
+    let ioChannel = ioService.newChannelFromURI2(uri, doc, doc.nodePrincipal,
+                                                 systemPrincipal, 0,
+                                                 Ci.nsIContentPolicy.TYPE_SCRIPT);
+
+    ioChannel.loadGroup = doc.documentLoadGroup.QueryInterface(Ci.nsILoadGroup);
     ioChannel.notificationCallbacks = new RedirectHttpsOnly();
     ioChannel.asyncOpen(listener, null);
   });
 };
 
 ResourceLoader.prototype = {
   onDataAvailable: function(request, context, input, offset, count) {
     let stream = Cc['@mozilla.org/scriptableinputstream;1']
@@ -105,22 +108,24 @@ function createLocationFromURI(uri) {
   };
 }
 
 /**
  * A javascript sandbox for running an IdP.
  *
  * @param domain (string) the domain of the IdP
  * @param protocol (string?) the protocol of the IdP [default: 'default']
+ * @param doc (obj) the current document
  * @throws if the domain or protocol aren't valid
  */
-function IdpSandbox(domain, protocol) {
+function IdpSandbox(domain, protocol, doc) {
   this.source = IdpSandbox.createIdpUri(domain, protocol || "default");
   this.active = null;
   this.sandbox = null;
+  this.document = doc;
 }
 
 IdpSandbox.checkDomain = function(domain) {
   if (!domain || typeof domain !== 'string') {
     throw new Error('Invalid domain for identity provider: ' +
                     'must be a non-zero length string');
   }
 };
@@ -171,17 +176,17 @@ IdpSandbox.createIdpUri = function(domai
 
 IdpSandbox.prototype = {
   isSame: function(domain, protocol) {
     return this.source.spec === IdpSandbox.createIdpUri(domain, protocol).spec;
   },
 
   start: function() {
     if (!this.active) {
-      this.active = ResourceLoader.load(this.source)
+      this.active = ResourceLoader.load(this.source, this.document)
         .then(result => this._createSandbox(result));
     }
     return this.active;
   },
 
   // Provides the sandbox with some useful facilities.  Initially, this is only
   // a minimal set; it is far easier to add more as the need arises, than to
   // take them back if we discover a mistake.
--- a/dom/media/MediaDecoder.cpp
+++ b/dom/media/MediaDecoder.cpp
@@ -1218,49 +1218,50 @@ void MediaDecoder::UpdateReadyStateForDa
   if (!mOwner || mShuttingDown || !mDecoderStateMachine) {
     return;
   }
   MediaDecoderOwner::NextFrameStatus frameStatus =
     mDecoderStateMachine->GetNextFrameStatus();
   mOwner->UpdateReadyStateForData(frameStatus);
 }
 
-void MediaDecoder::OnSeekResolvedInternal(bool aAtEnd, MediaDecoderEventVisibility aEventVisibility)
+void MediaDecoder::OnSeekResolved(SeekResolveValue aVal)
 {
   MOZ_ASSERT(NS_IsMainThread());
+  mSeekRequest.Complete();
 
   if (mShuttingDown)
     return;
 
   bool fireEnded = false;
   bool seekWasAborted = false;
   {
     ReentrantMonitorAutoEnter mon(GetReentrantMonitor());
 
     // An additional seek was requested while the current seek was
     // in operation.
     if (mRequestedSeekTarget.IsValid()) {
       ChangeState(PLAY_STATE_SEEKING);
       seekWasAborted = true;
     } else {
       UnpinForSeek();
-      fireEnded = aAtEnd;
-      if (aAtEnd) {
+      fireEnded = aVal.mAtEnd;
+      if (aVal.mAtEnd) {
         ChangeState(PLAY_STATE_ENDED);
-      } else if (aEventVisibility != MediaDecoderEventVisibility::Suppressed) {
-        ChangeState(aAtEnd ? PLAY_STATE_ENDED : mNextState);
+      } else if (aVal.mEventVisibility != MediaDecoderEventVisibility::Suppressed) {
+        ChangeState(aVal.mAtEnd ? PLAY_STATE_ENDED : mNextState);
       }
     }
   }
 
-  PlaybackPositionChanged(aEventVisibility);
+  PlaybackPositionChanged(aVal.mEventVisibility);
 
   if (mOwner) {
     UpdateReadyStateForData();
-    if (!seekWasAborted && (aEventVisibility != MediaDecoderEventVisibility::Suppressed)) {
+    if (!seekWasAborted && (aVal.mEventVisibility != MediaDecoderEventVisibility::Suppressed)) {
       mOwner->SeekCompleted();
       if (fireEnded) {
         mOwner->PlaybackEnded();
       }
     }
   }
 }
 
--- a/dom/media/MediaDecoder.h
+++ b/dom/media/MediaDecoder.h
@@ -804,31 +804,17 @@ public:
   // Acquires the monitor. Call from any thread.
   virtual bool IsExpectingMoreData();
 
   // Called when the video has completed playing.
   // Call on the main thread only.
   void PlaybackEnded();
 
   void OnSeekRejected() { mSeekRequest.Complete(); }
-  void OnSeekResolvedInternal(bool aAtEnd, MediaDecoderEventVisibility aEventVisibility);
-
-  void OnSeekResolved(SeekResolveValue aVal)
-  {
-    mSeekRequest.Complete();
-    OnSeekResolvedInternal(aVal.mAtEnd, aVal.mEventVisibility);
-  }
-
-#ifdef MOZ_AUDIO_OFFLOAD
-  // Temporary hack - see bug 1139206.
-  void SimulateSeekResolvedForAudioOffload(MediaDecoderEventVisibility aEventVisibility)
-  {
-    OnSeekResolvedInternal(false, aEventVisibility);
-  }
-#endif
+  void OnSeekResolved(SeekResolveValue aVal);
 
   // Seeking has started. Inform the element on the main
   // thread.
   void SeekingStarted(MediaDecoderEventVisibility aEventVisibility = MediaDecoderEventVisibility::Observable);
 
   // Called when the backend has changed the current playback
   // position. It dispatches a timeupdate event and invalidates the frame.
   // This must be called on the main thread only.
--- a/dom/media/PeerConnectionIdp.jsm
+++ b/dom/media/PeerConnectionIdp.jsm
@@ -51,17 +51,17 @@ PeerConnectionIdp.prototype = {
     this.protocol = protocol || 'default';
     this.username = username;
     if (this._idp) {
       if (this._idp.isSame(provider, protocol)) {
         return; // noop
       }
       this._idp.stop();
     }
-    this._idp = new IdpSandbox(provider, protocol);
+    this._idp = new IdpSandbox(provider, protocol, this._win.document);
   },
 
   // start the IdP and do some error fixup
   start: function() {
     return this._idp.start()
       .catch(e => {
         throw new this._win.DOMException(e.message, 'IdpError');
       });
--- a/dom/media/omx/AudioOffloadPlayer.cpp
+++ b/dom/media/omx/AudioOffloadPlayer.cpp
@@ -52,23 +52,20 @@ PRLogModuleInfo* gAudioOffloadPlayerLog;
 
 // maximum time in paused state when offloading audio decompression.
 // When elapsed, the AudioSink is destroyed to allow the audio DSP to power down.
 static const uint64_t OFFLOAD_PAUSE_MAX_MSECS = 60000ll;
 
 AudioOffloadPlayer::AudioOffloadPlayer(MediaOmxCommonDecoder* aObserver) :
   mStarted(false),
   mPlaying(false),
-  mSeeking(false),
   mReachedEOS(false),
-  mSeekDuringPause(false),
   mIsElementVisible(true),
   mSampleRate(0),
   mStartPosUs(0),
-  mSeekTimeUs(0),
   mPositionTimeMediaUs(-1),
   mInputBuffer(nullptr),
   mObserver(aObserver)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
 #ifdef PR_LOGGING
   if (!gAudioOffloadPlayerLog) {
@@ -194,23 +191,16 @@ status_t AudioOffloadPlayer::ChangeState
     case MediaDecoder::PLAY_STATE_PLAYING: {
       status_t err = Play();
       if (err != OK) {
         return err;
       }
       StartTimeUpdate();
     } break;
 
-    case MediaDecoder::PLAY_STATE_SEEKING: {
-      int64_t seekTimeUs
-          = mObserver->GetSeekTime();
-      SeekTo(seekTimeUs, true);
-      mObserver->ResetSeekTime();
-    } break;
-
     case MediaDecoder::PLAY_STATE_PAUSED:
     case MediaDecoder::PLAY_STATE_SHUTDOWN:
       // Just pause here during play state shutdown as well to stop playing
       // offload track immediately. Resources will be freed by
       // MediaOmxCommonDecoder
       Pause();
       break;
 
@@ -273,18 +263,22 @@ status_t AudioOffloadPlayer::Play()
 
   if (!mStarted) {
     // Last pause timed out and offloaded audio sink was reset. Start it again
     err = Start(false);
     if (err != OK) {
       return err;
     }
     // Seek to last play position only when there was no seek during last pause
-    if (!mSeeking) {
-      SeekTo(mPositionTimeMediaUs);
+    android::Mutex::Autolock autoLock(mLock);
+    if (!mSeekTarget.IsValid()) {
+      mSeekTarget = SeekTarget(mPositionTimeMediaUs,
+                               SeekTarget::Accurate,
+                               MediaDecoderEventVisibility::Suppressed);
+      DoSeek();
     }
   }
 
   if (!mPlaying) {
     CHECK(mAudioSink.get());
     err = mAudioSink->Start();
     if (err == OK) {
       mPlaying = true;
@@ -338,62 +332,64 @@ void AudioOffloadPlayer::Reset()
   mReachedEOS = false;
   mStarted = false;
   mPlaying = false;
   mStartPosUs = 0;
 
   WakeLockRelease();
 }
 
-status_t AudioOffloadPlayer::SeekTo(int64_t aTimeUs, bool aDispatchSeekEvents)
+nsRefPtr<MediaDecoder::SeekPromise> AudioOffloadPlayer::Seek(SeekTarget aTarget)
 {
   MOZ_ASSERT(NS_IsMainThread());
-  CHECK(mAudioSink.get());
-
   android::Mutex::Autolock autoLock(mLock);
 
-  AUDIO_OFFLOAD_LOG(PR_LOG_DEBUG, ("SeekTo ( %lld )", aTimeUs));
+  mSeekPromise.RejectIfExists(true, __func__);
+  mSeekTarget = aTarget;
+  nsRefPtr<MediaDecoder::SeekPromise> p = mSeekPromise.Ensure(__func__);
+  DoSeek();
+  return p;
+}
 
-  mSeeking = true;
+status_t AudioOffloadPlayer::DoSeek()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(mSeekTarget.IsValid());
+  CHECK(mAudioSink.get());
+
+  AUDIO_OFFLOAD_LOG(PR_LOG_DEBUG, ("DoSeek ( %lld )", mSeekTarget.mTime));
+
   mReachedEOS = false;
   mPositionTimeMediaUs = -1;
-  mSeekTimeUs = aTimeUs;
-  mStartPosUs = aTimeUs;
-  mDispatchSeekEvents = aDispatchSeekEvents;
+  mStartPosUs = mSeekTarget.mTime;
 
-  if (mDispatchSeekEvents) {
+  if (!mSeekPromise.IsEmpty()) {
     nsCOMPtr<nsIRunnable> nsEvent =
       NS_NewRunnableMethodWithArg<MediaDecoderEventVisibility>(
         mObserver,
         &MediaDecoder::SeekingStarted,
-        MediaDecoderEventVisibility::Observable);
+        mSeekTarget.mEventVisibility);
     NS_DispatchToCurrentThread(nsEvent);
   }
 
   if (mPlaying) {
     mAudioSink->Pause();
     mAudioSink->Flush();
     mAudioSink->Start();
 
   } else {
-    mSeekDuringPause = true;
-
     if (mStarted) {
       mAudioSink->Flush();
     }
 
-    if (mDispatchSeekEvents) {
-      mDispatchSeekEvents = false;
+    if (!mSeekPromise.IsEmpty()) {
       AUDIO_OFFLOAD_LOG(PR_LOG_DEBUG, ("Fake seek complete during pause"));
-      nsCOMPtr<nsIRunnable> nsEvent =
-        NS_NewRunnableMethodWithArg<MediaDecoderEventVisibility>(
-          mObserver,
-          &MediaDecoder::SimulateSeekResolvedForAudioOffload,
-          MediaDecoderEventVisibility::Observable);
-      NS_DispatchToCurrentThread(nsEvent);
+      // We do not reset mSeekTarget here.
+      MediaDecoder::SeekResolveValue val(mReachedEOS, mSeekTarget.mEventVisibility);
+      mSeekPromise.Resolve(val, __func__);
     }
   }
 
   return OK;
 }
 
 double AudioOffloadPlayer::GetMediaTimeSecs()
 {
@@ -402,18 +398,18 @@ double AudioOffloadPlayer::GetMediaTimeS
       static_cast<double>(USECS_PER_S));
 }
 
 int64_t AudioOffloadPlayer::GetMediaTimeUs()
 {
   android::Mutex::Autolock autoLock(mLock);
 
   int64_t playPosition = 0;
-  if (mSeeking) {
-    return mSeekTimeUs;
+  if (mSeekTarget.IsValid()) {
+    return mSeekTarget.mTime;
   }
   if (!mStarted) {
     return mPositionTimeMediaUs;
   }
 
   playPosition = GetOutputPlayPositionUs_l();
   if (!mReachedEOS) {
     mPositionTimeMediaUs = playPosition;
@@ -434,16 +430,22 @@ int64_t AudioOffloadPlayer::GetOutputPla
 
   // HAL position is relative to the first buffer we sent at mStartPosUs
   const int64_t renderedDuration = mStartPosUs + playedUs;
   return renderedDuration;
 }
 
 void AudioOffloadPlayer::NotifyAudioEOS()
 {
+  android::Mutex::Autolock autoLock(mLock);
+  // We do not reset mSeekTarget here.
+  if (!mSeekPromise.IsEmpty()) {
+    MediaDecoder::SeekResolveValue val(mReachedEOS, mSeekTarget.mEventVisibility);
+    mSeekPromise.Resolve(val, __func__);
+  }
   nsCOMPtr<nsIRunnable> nsEvent = NS_NewRunnableMethod(mObserver,
       &MediaDecoder::PlaybackEnded);
   NS_DispatchToMainThread(nsEvent);
 }
 
 void AudioOffloadPlayer::NotifyPositionChanged()
 {
   nsCOMPtr<nsIRunnable> nsEvent =
@@ -451,16 +453,25 @@ void AudioOffloadPlayer::NotifyPositionC
       mObserver,
       &MediaOmxCommonDecoder::PlaybackPositionChanged,
       MediaDecoderEventVisibility::Observable);
   NS_DispatchToMainThread(nsEvent);
 }
 
 void AudioOffloadPlayer::NotifyAudioTearDown()
 {
+  // Fallback to state machine.
+  // state machine's seeks will be done with
+  // MediaDecoderEventVisibility::Suppressed.
+  android::Mutex::Autolock autoLock(mLock);
+  // We do not reset mSeekTarget here.
+  if (!mSeekPromise.IsEmpty()) {
+    MediaDecoder::SeekResolveValue val(mReachedEOS, mSeekTarget.mEventVisibility);
+    mSeekPromise.Resolve(val, __func__);
+  }
   nsCOMPtr<nsIRunnable> nsEvent = NS_NewRunnableMethod(mObserver,
       &MediaOmxCommonDecoder::AudioOffloadTearDown);
   NS_DispatchToMainThread(nsEvent);
 }
 
 // static
 size_t AudioOffloadPlayer::AudioSinkCallback(AudioSink* aAudioSink,
                                              void* aBuffer,
@@ -501,45 +512,47 @@ size_t AudioOffloadPlayer::FillBuffer(vo
   CHECK(mAudioSink.get());
 
   if (mReachedEOS) {
     return 0;
   }
 
   size_t sizeDone = 0;
   size_t sizeRemaining = aSize;
+  int64_t seekTimeUs = -1;
   while (sizeRemaining > 0) {
     MediaSource::ReadOptions options;
     bool refreshSeekTime = false;
-
     {
       android::Mutex::Autolock autoLock(mLock);
 
-      if (mSeeking) {
-        options.setSeekTo(mSeekTimeUs);
+      if (mSeekTarget.IsValid()) {
+        seekTimeUs = mSeekTarget.mTime;
+        options.setSeekTo(seekTimeUs);
         refreshSeekTime = true;
 
         if (mInputBuffer) {
           mInputBuffer->release();
           mInputBuffer = nullptr;
         }
-        mSeeking = false;
       }
     }
 
     if (!mInputBuffer) {
-
       status_t err;
       err = mSource->read(&mInputBuffer, &options);
 
       CHECK((!err && mInputBuffer) || (err && !mInputBuffer));
 
       android::Mutex::Autolock autoLock(mLock);
 
       if (err != OK) {
+        if (mSeekTarget.IsValid()) {
+          mSeekTarget.Reset();
+        }
         AUDIO_OFFLOAD_LOG(PR_LOG_ERROR, ("Error while reading media source %d "
             "Ok to receive EOS error at end", err));
         if (!mReachedEOS) {
           // After seek there is a possible race condition if
           // OffloadThread is observing state_stopping_1 before
           // framesReady() > 0. Ensure sink stop is called
           // after last buffer is released. This ensures the
           // partial buffer is written to the driver before
@@ -559,50 +572,36 @@ size_t AudioOffloadPlayer::FillBuffer(vo
         break;
       }
 
       if(mInputBuffer->range_length() != 0) {
         CHECK(mInputBuffer->meta_data()->findInt64(
             kKeyTime, &mPositionTimeMediaUs));
       }
 
-      if (refreshSeekTime) {
-        if (mDispatchSeekEvents && !mSeekDuringPause) {
-          mDispatchSeekEvents = false;
+      if (mSeekTarget.IsValid() && seekTimeUs == mSeekTarget.mTime) {
+        MOZ_ASSERT(mSeekTarget.IsValid());
+        mSeekTarget.Reset();
+        if (!mSeekPromise.IsEmpty()) {
           AUDIO_OFFLOAD_LOG(PR_LOG_DEBUG, ("FillBuffer posting SEEK_COMPLETE"));
-          nsCOMPtr<nsIRunnable> nsEvent =
-            NS_NewRunnableMethodWithArg<MediaDecoderEventVisibility>(
-              mObserver,
-              &MediaDecoder::SimulateSeekResolvedForAudioOffload,
-              MediaDecoderEventVisibility::Observable);
-          NS_DispatchToMainThread(nsEvent, NS_DISPATCH_NORMAL);
+          MediaDecoder::SeekResolveValue val(mReachedEOS, mSeekTarget.mEventVisibility);
+          mSeekPromise.Resolve(val, __func__);
+        }
+      } else if (mSeekTarget.IsValid()) {
+        AUDIO_OFFLOAD_LOG(PR_LOG_DEBUG, ("seek is updated during unlocking mLock"));
+      }
 
-        } else if (mSeekDuringPause) {
-          // Callback is already called for seek during pause. Just reset the
-          // flag
-          AUDIO_OFFLOAD_LOG(PR_LOG_DEBUG, ("Not posting seek complete as its"
-              " already faked"));
-          mSeekDuringPause = false;
-        }
-
+      if (refreshSeekTime) {
         NotifyPositionChanged();
 
         // need to adjust the mStartPosUs for offload decoding since parser
         // might not be able to get the exact seek time requested.
         mStartPosUs = mPositionTimeMediaUs;
         AUDIO_OFFLOAD_LOG(PR_LOG_DEBUG, ("Adjust seek time to: %.2f",
             mStartPosUs / 1E6));
-
-        // clear seek time with mLock locked and once we have valid
-        // mPositionTimeMediaUs
-        // before clearing mSeekTimeUs check if a new seek request has been
-        // received while we were reading from the source with mLock released.
-        if (!mSeeking) {
-          mSeekTimeUs = 0;
-        }
       }
     }
 
     if (mInputBuffer->range_length() == 0) {
       mInputBuffer->release();
       mInputBuffer = nullptr;
       continue;
     }
--- a/dom/media/omx/AudioOffloadPlayer.h
+++ b/dom/media/omx/AudioOffloadPlayer.h
@@ -73,72 +73,58 @@ public:
     SEEK_COMPLETE
   };
 
   AudioOffloadPlayer(MediaOmxCommonDecoder* aDecoder = nullptr);
 
   ~AudioOffloadPlayer();
 
   // Caller retains ownership of "aSource".
-  void SetSource(const android::sp<MediaSource> &aSource);
+  virtual void SetSource(const android::sp<MediaSource> &aSource) override;
 
   // Start the source if it's not already started and open the AudioSink to
   // create an offloaded audio track
-  status_t Start(bool aSourceAlreadyStarted = false);
+  virtual status_t Start(bool aSourceAlreadyStarted = false) override;
+
+  virtual status_t ChangeState(MediaDecoder::PlayState aState) override;
 
-  double GetMediaTimeSecs();
+  virtual void SetVolume(double aVolume) override;
+
+  virtual double GetMediaTimeSecs() override;
 
   // To update progress bar when the element is visible
-  void SetElementVisibility(bool aIsVisible);
-
-  status_t ChangeState(MediaDecoder::PlayState aState);
-
-  void SetVolume(double aVolume);
+  virtual void SetElementVisibility(bool aIsVisible) override;;
 
   // Update ready state based on current play state. Not checking data
   // availability since offloading is currently done only when whole compressed
   // data is available
-  MediaDecoderOwner::NextFrameStatus GetNextFrameStatus();
+  virtual MediaDecoderOwner::NextFrameStatus GetNextFrameStatus() override;
+
+  virtual nsRefPtr<MediaDecoder::SeekPromise> Seek(SeekTarget aTarget) override;
 
   void TimeUpdate();
 
   // Close the audio sink, stop time updates, frees the input buffers
   void Reset();
 
 private:
   // Set when audio source is started and audioSink is initialized
   // Used only in main thread
   bool mStarted;
 
   // Set when audio sink is started. i.e. playback started
   // Used only in main thread
   bool mPlaying;
 
-  // Set when playstate is seeking and reset when FillBUffer() acknowledged
-  // seeking by seeking audio source. Used in main thread and offload
-  // callback thread, protected by Mutex mLock
-  bool mSeeking;
-
   // Once playback reached end of stream (last ~100ms), position provided by DSP
   // may be reset/corrupted. This bool is used to avoid that.
   // Used in main thread and offload callback thread, protected by Mutex
   // mLock
   bool mReachedEOS;
 
-  // Set when there is a seek request during pause.
-  // Used in main thread and offload callback thread, protected by Mutex
-  // mLock
-  bool mSeekDuringPause;
-
-  // Seek can be triggered internally or by MediaDecoder. This bool is to
-  // to track seek triggered by MediaDecoder so that we can send back
-  // SeekingStarted and SeekingStopped events.
-  // Used in main thread and offload callback thread, protected by Mutex mLock
-  bool mDispatchSeekEvents;
-
   // Set when the HTML Audio Element is visible to the user.
   // Used only in main thread
   bool mIsElementVisible;
 
   // Session id given by Android::AudioSystem and used while creating audio sink
   // Used only in main thread
   int mSessionId;
 
@@ -150,20 +136,25 @@ private:
   // different than given mSeekTimeUs, if audio source cannot find a frame at
   // that position. Store seeked position in mStartPosUs and provide
   // mStartPosUs + GetPosition() (i.e. absolute position) to
   // MediaOmxCommonDecoder
   // Used in main thread and offload callback thread, protected by Mutex
   // mLock
   int64_t mStartPosUs;
 
-  // Given seek time when there is a request to seek
+  // The target of current seek when there is a request to seek
   // Used in main thread and offload callback thread, protected by Mutex
   // mLock
-  int64_t mSeekTimeUs;
+  SeekTarget mSeekTarget;
+
+  // MediaPromise of current seek.
+  // Used in main thread and offload callback thread, protected by Mutex
+  // mLock
+  MediaPromiseHolder<MediaDecoder::SeekPromise> mSeekPromise;
 
   // Positions obtained from offlaoded tracks (DSP)
   // Used in main thread and offload callback thread, protected by Mutex
   // mLock
   int64_t mPositionTimeMediaUs;
 
   // State obtained from MediaOmxCommonDecoder. Used only in main thread
   MediaDecoder::PlayState mPlayState;
@@ -216,25 +207,25 @@ private:
   static size_t AudioSinkCallback(AudioSink *aAudioSink,
                                   void *aData,
                                   size_t aSize,
                                   void *aMe,
                                   AudioSink::cb_event_t aEvent);
 
   bool IsSeeking();
 
-  // Set mSeekTime to the given position and restart the sink. Actual seek
-  // happens in FillBuffer(). If aDispatchSeekEvents is true, send
+  // Set mSeekTarget to the given position and restart the sink. Actual seek
+  // happens in FillBuffer(). If mSeekPromise is not empty, send
   // SeekingStarted event always and SeekingStopped event when the play state is
   // paused to MediaDecoder.
   // When decoding and playing happens separately, if there is a seek during
   // pause, we can decode and keep data ready.
   // In case of offload player, no way to seek during pause. So just fake that
   // seek is done.
-  status_t SeekTo(int64_t aTimeUs, bool aDispatchSeekEvents = false);
+  status_t DoSeek();
 
   // Start/Resume the audio sink so that callback will start being called to get
   // compressed data
   status_t Play();
 
   // Stop the audio sink if we need to play till we drain the current buffer.
   // or Pause the sink in case we should stop playing immediately
   void Pause(bool aPlayPendingSamples = false);
--- a/dom/media/omx/AudioOffloadPlayerBase.h
+++ b/dom/media/omx/AudioOffloadPlayerBase.h
@@ -61,13 +61,15 @@ public:
 
   // Update ready state based on current play state. Not checking data
   // availability since offloading is currently done only when whole compressed
   // data is available
   virtual MediaDecoderOwner::NextFrameStatus GetNextFrameStatus()
   {
     return MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE;
   }
+
+  virtual nsRefPtr<MediaDecoder::SeekPromise> Seek(SeekTarget aTarget) = 0;
 };
 
 } // namespace mozilla
 
 #endif // AUDIO_OFFLOAD_PLAYER_BASE_H_
--- a/dom/media/omx/MediaOmxCommonDecoder.cpp
+++ b/dom/media/omx/MediaOmxCommonDecoder.cpp
@@ -121,18 +121,19 @@ MediaOmxCommonDecoder::ResumeStateMachin
   if (!mDecoderStateMachine) {
     return;
   }
 
   mFallbackToStateMachine = true;
   mAudioOffloadPlayer = nullptr;
   int64_t timeUsecs = 0;
   SecondsToUsecs(mCurrentTime, timeUsecs);
-  mRequestedSeekTarget = SeekTarget(timeUsecs, SeekTarget::Accurate);
-
+  mRequestedSeekTarget = SeekTarget(timeUsecs,
+                                    SeekTarget::Accurate,
+                                    MediaDecoderEventVisibility::Suppressed);
   mNextState = mPlayState;
   ChangeState(PLAY_STATE_LOADING);
   // exit dormant state
   RefPtr<nsRunnable> event =
     NS_NewRunnableMethodWithArg<bool>(
       mDecoderStateMachine,
       &MediaDecoderStateMachine::SetDormant,
       false);
@@ -188,20 +189,35 @@ void
 MediaOmxCommonDecoder::ChangeState(PlayState aState)
 {
   MOZ_ASSERT(NS_IsMainThread());
   // Keep MediaDecoder state in sync with MediaElement irrespective of offload
   // playback so it will continue to work in normal mode when offloading fails
   // in between
   MediaDecoder::ChangeState(aState);
 
-  if (mAudioOffloadPlayer) {
-    status_t err = mAudioOffloadPlayer->ChangeState(aState);
-    if (err != OK) {
-      ResumeStateMachine();
+  if (!mAudioOffloadPlayer) {
+    return;
+  }
+
+  status_t err = mAudioOffloadPlayer->ChangeState(aState);
+  if (err != OK) {
+    ResumeStateMachine();
+    return;
+  }
+
+  switch (mPlayState) {
+    case PLAY_STATE_SEEKING:
+      mSeekRequest.Begin(mAudioOffloadPlayer->Seek(mRequestedSeekTarget)
+        ->RefableThen(AbstractThread::MainThread(), __func__, static_cast<MediaDecoder*>(this),
+                      &MediaDecoder::OnSeekResolved, &MediaDecoder::OnSeekRejected));
+      mRequestedSeekTarget.Reset();
+      break;
+    default: {
+      break;
     }
   }
 }
 
 void
 MediaOmxCommonDecoder::ApplyStateToStateMachine(PlayState aState)
 {
   MOZ_ASSERT(NS_IsMainThread());
--- a/dom/media/tests/mochitest/identity/idp.js
+++ b/dom/media/tests/mochitest/identity/idp.js
@@ -1,90 +1,110 @@
 (function(global) {
-  "use strict";
+  'use strict';
 
   // rather than create a million different IdP configurations and litter the
   // world with files all containing near-identical code, let's use the hash/URL
   // fragment as a way of generating instructions for the IdP
-  var instructions = global.location.hash.replace("#", "").split(":");
+  var instructions = global.location.hash.replace('#', '').split(':');
   function is(target) {
     return function(instruction) {
       return instruction === target;
     };
   }
 
   function IDPJS() {
     this.domain = global.location.host;
     var path = global.location.pathname;
     this.protocol =
       path.substring(path.lastIndexOf('/') + 1) + global.location.hash;
+    this.id = crypto.getRandomValues(new Uint8Array(10)).join('.');
   }
 
-  function borkResult(result) {
-    if (instructions.some(is("throw"))) {
-      throw new Error('Throwing!');
-    }
-    if (instructions.some(is("fail"))) {
-      return Promise.reject(new Error('Failing!'));
-    }
-    if (instructions.some(is("loginerror"))) {
-      return Promise.reject({
-        name: 'IdpLoginError',
-        loginUrl: 'https://example.com/log/in/here'
+  IDPJS.prototype = {
+    getLogin: function() {
+      var xhr = new XMLHttpRequest();
+      xhr.open('GET', 'https://example.com/.well-known/idp-proxy/idp.sjs?' + this.id);
+      return new Promise(resolve => {
+        xhr.onload = e => resolve(xhr.status === 200);
+        xhr.send();
       });
-    }
-    if (instructions.some(is("hang"))) {
-      return new Promise(r => {});
-    }
-    dump('idp: result=' + JSON.stringify(result) + '\n');
-    return Promise.resolve(result);
-  };
+    },
+    checkLogin: function(result) {
+      return this.getLogin()
+        .then(loggedIn => {
+          if (loggedIn) {
+            return result;
+          }
+          return Promise.reject({
+            name: 'IdpLoginError',
+            loginUrl: 'https://example.com/.well-known/idp-proxy/login.html#' +
+              this.id
+          });
+        });
+    },
 
-  IDPJS.prototype = {
+    borkResult: function(result) {
+      if (instructions.some(is('throw'))) {
+        throw new Error('Throwing!');
+      }
+      if (instructions.some(is('fail'))) {
+        return Promise.reject(new Error('Failing!'));
+      }
+      if (instructions.some(is('login'))) {
+        return this.checkLogin(result);
+      }
+      if (instructions.some(is('hang'))) {
+        return new Promise(r => {});
+      }
+      dump('idp: result=' + JSON.stringify(result) + '\n');
+      return Promise.resolve(result);
+    },
+
     _selectUsername: function(usernameHint) {
-      var username = "someone@" + this.domain;
+      var username = 'someone@' + this.domain;
       if (usernameHint) {
-        var at = usernameHint.indexOf("@");
+        var at = usernameHint.indexOf('@');
         if (at < 0) {
-          username = usernameHint + "@" + this.domain;
+          username = usernameHint + '@' + this.domain;
         } else if (usernameHint.substring(at + 1) === this.domain) {
           username = usernameHint;
         }
       }
       return username;
     },
 
     generateAssertion: function(payload, origin, usernameHint) {
       dump('idp: generateAssertion(' + payload + ')\n');
       var idpDetails = {
         domain: this.domain,
         protocol: this.protocol
       };
-      if (instructions.some(is("bad-assert"))) {
+      if (instructions.some(is('bad-assert'))) {
         idpDetails = {};
       }
-      return borkResult({
+      return this.borkResult({
         idp: idpDetails,
         assertion: JSON.stringify({
           username: this._selectUsername(usernameHint),
           contents: payload
         })
       });
     },
 
     validateAssertion: function(assertion, origin) {
       dump('idp: validateAssertion(' + assertion + ')\n');
       var assertion = JSON.parse(assertion);
-      if (instructions.some(is("bad-validate"))) {
+      if (instructions.some(is('bad-validate'))) {
         assertion.contents = {};
       }
-      return borkResult({
+      return this.borkResult({
           identity: assertion.username,
           contents: assertion.contents
         });
     }
   };
 
-  if (!instructions.some(is("not_ready"))) {
+  if (!instructions.some(is('not_ready'))) {
     dump('registering idp.js' + global.location.hash + '\n');
     global.rtcIdentityProvider.register(new IDPJS());
   }
 }(this));
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/idp.sjs
@@ -0,0 +1,18 @@
+function handleRequest(request, response) {
+  var key = '/.well-known/idp-proxy/' + request.queryString;
+  dump(getState(key) + '\n');
+  if (request.method === 'GET') {
+    if (getState(key)) {
+      response.setStatusLine(request.httpVersion, 200, 'OK');
+    } else {
+      response.setStatusLine(request.httpVersion, 404, 'Not Found');
+    }
+  } else if (request.method === 'PUT') {
+    setState(key, 'OK');
+    response.setStatusLine(request.httpVersion, 200, 'OK');
+  } else {
+    response.setStatusLine(request.httpVersion, 406, 'Method Not Allowed');
+  }
+  response.setHeader('Content-Type', 'text/plain;charset=UTF-8');
+  response.write('OK');
+}
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/login.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Identity Provider Login</title>
+  <script type="application/javascript">
+  window.onload = () => {
+    var xhr = new XMLHttpRequest();
+    xhr.open("PUT", "https://example.com/.well-known/idp-proxy/idp.sjs?" +
+             window.location.hash.replace('#', ''));
+    xhr.onload = () => {
+      var isFramed = (window !== window.top);
+      var parent = isFramed ? window.parent : window.opener;
+      // Using '*' is cheating, but that's OK.
+      parent.postMessage('LOGINDONE', '*');
+      var done = document.createElement('div');
+
+      done.textContent = 'Done';
+      document.body.appendChild(done);
+
+      if (!isFramed) {
+        window.close();
+      }
+    };
+    xhr.send();
+  };
+  </script>
+</head>
+<body>
+  <div>Logging in...</div>
+</body>
+</html>
--- a/dom/media/tests/mochitest/identity/mochitest.ini
+++ b/dom/media/tests/mochitest/identity/mochitest.ini
@@ -22,11 +22,16 @@ support-files =
   /.well-known/idp-min.js
 
 [test_fingerprints.html]
 [test_getIdentityAssertion.html]
 [test_setIdentityProvider.html]
 [test_setIdentityProviderWithErrors.html]
 [test_peerConnection_peerIdentity.html]
 [test_peerConnection_asymmetricIsolation.html]
+[test_loginNeeded.html]
+support-files =
+  /.well-known/idp-proxy/login.html
+  /.well-known/idp-proxy/idp.sjs
+
 
 # Bug 950317: Hack for making a cleanup hook after finishing all WebRTC cases
 [../test_zmedia_cleanup.html]
--- a/dom/media/tests/mochitest/identity/test_fingerprints.html
+++ b/dom/media/tests/mochitest/identity/test_fingerprints.html
@@ -7,17 +7,17 @@
 <body>
   <script class="testbody" type="application/javascript">
 'use strict';
 
 // here we call the identity provider directly
 function getIdentityAssertion(fpArray) {
   var Cu = SpecialPowers.Cu;
   var rtcid = Cu.import('resource://gre/modules/media/IdpSandbox.jsm');
-  var sandbox = new rtcid.IdpSandbox('example.com', 'idp.js');
+  var sandbox = new rtcid.IdpSandbox('example.com', 'idp.js', window.document);
   return sandbox.start()
     .then(idp => SpecialPowers.wrap(idp)
                    .generateAssertion(JSON.stringify({ fingerprint: fpArray }),
                                       'https://example.com'))
     .then(assertion => {
       assertion = SpecialPowers.wrap(assertion);
       var assertionString = btoa(JSON.stringify(assertion));
       sandbox.stop();
--- a/dom/media/tests/mochitest/identity/test_getIdentityAssertion.html
+++ b/dom/media/tests/mochitest/identity/test_getIdentityAssertion.html
@@ -50,21 +50,22 @@ function theTest() {
 
     function PC_LOCAL_IDP_FAILS(t) {
       return getAssertion(t, '#fail')
         .then(a => ok(false, '#fail should not get an identity result'),
               e => is(e.name, 'IdpError', '#fail should cause rejection'));
     },
 
     function PC_LOCAL_IDP_LOGIN_ERROR(t) {
-      return getAssertion(t, '#loginerror')
-        .then(a => ok(false, '#loginerror should not work'),
+      return getAssertion(t, '#login')
+        .then(a => ok(false, '#login should not work'),
               e => {
                 is(e.name, 'IdpLoginError', 'name is IdpLoginError');
-                is(t.pcLocal._pc.idpLoginUrl, 'https://example.com/log/in/here',
+                is(t.pcLocal._pc.idpLoginUrl.split('#')[0],
+                   'https://example.com/.well-known/idp-proxy/login.html',
                    'got the right login URL from the IdP');
               });
     },
 
     function PC_LOCAL_IDP_NOT_READY(t) {
       return getAssertion(t, '#not_ready')
         .then(a => ok(false, '#not_ready should not get an identity result'),
               e => is(e.name, 'IdpError', '#not_ready should cause rejection'));
--- a/dom/media/tests/mochitest/identity/test_idpproxy.html
+++ b/dom/media/tests/mochitest/identity/test_idpproxy.html
@@ -21,47 +21,47 @@ function test_domain_sandbox() {
     toString : function() {
       return 'example.com/path';
     }
   };
   var domains = [ 'ex/foo', 'user@ex', 'user:pass@ex', 'ex#foo', 'ex?foo',
                   '', 12, null, diabolical, true ];
   domains.forEach(function(domain) {
     try {
-      var idp = new IdpSandbox(domain);
+      var idp = new IdpSandbox(domain, undefined, window.document);
       ok(false, 'IdpSandbox allowed a bad domain: ' + domain);
     } catch (e) {
       var str = (typeof domain === 'string') ? domain : typeof domain;
       ok(true, 'Evil domain "' + str + '" raises exception');
     }
   });
 }
 
 function test_protocol_sandbox() {
   var protos = [ '../evil/proto', '..%2Fevil%2Fproto',
                  '\\evil', '%5cevil', 12, true, {} ];
   protos.forEach(function(proto) {
     try {
-      var idp = new IdpSandbox('example.com', proto);
+      var idp = new IdpSandbox('example.com', proto, window.document);
       ok(false, 'IdpSandbox allowed a bad protocol: ' + proto);
     } catch (e) {
       var str = (typeof proto === 'string') ? proto : typeof proto;
       ok(true, 'Evil protocol "' + proto + '" raises exception');
     }
   });
 }
 
 function idpName(hash) {
   return 'idp.js' + (hash ? ('#' + hash) : '');
 }
 
 function makeSandbox(js) {
   var name = js || idpName();
   info('Creating a sandbox for the protocol: ' + name);
-  var sandbox = new IdpSandbox('example.com', name);
+  var sandbox = new IdpSandbox('example.com', name, window.document);
   return sandbox.start().then(idp => SpecialPowers.wrap(idp));
 }
 
 function test_generate_assertion() {
   return makeSandbox()
     .then(idp => idp.generateAssertion(dummyPayload,
                                        'https://example.net'))
     .then(response => {
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/identity/test_loginNeeded.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript">var scriptRelativePath = "../";</script>
+  <script type="application/javascript" src="../pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    title: 'RTCPeerConnection identity with login',
+    bug: '1153314'
+  });
+
+function waitForLoginDone() {
+  return new Promise(resolve => {
+    window.addEventListener('message', function listener(e) {
+      is(e.origin, 'https://example.com', 'got the right message origin');
+      is(e.data, 'LOGINDONE', 'got the right message');
+      window.removeEventListener('message', listener);
+      resolve();
+    }, false);
+  });
+}
+
+function checkLogin(t, name, onLoginNeeded) {
+  t.pcLocal.setIdentityProvider('example.com', 'idp.js#login:' + name);
+  return t.pcLocal._pc.getIdentityAssertion()
+    .then(a => ok(false, 'should request login'),
+          e => {
+            is(e.name, 'IdpLoginError', 'name is IdpLoginError');
+            is(t.pcLocal._pc.idpLoginUrl.split('#')[0],
+               'https://example.com/.well-known/idp-proxy/login.html',
+               'got the right login URL from the IdP');
+            return t.pcLocal._pc.idpLoginUrl;
+          })
+    .then(onLoginNeeded)
+    .then(waitForLoginDone)
+    .then(() => t.pcLocal._pc.getIdentityAssertion())
+    .then(a => ok(a, 'got assertion'));
+}
+
+function theTest() {
+  var test = new PeerConnectionTest();
+  test.setMediaConstraints([{audio: true}], [{audio: true}]);
+  test.chain.removeAfter('PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE');
+  test.chain.append([
+    function PC_LOCAL_IDENTITY_ASSERTION_WITH_IFRAME_LOGIN(t) {
+      return checkLogin(t, 'iframe', loginUrl => {
+        var iframe = document.createElement('iframe');
+        iframe.setAttribute('src', loginUrl);
+        iframe.frameBorder = 0;
+        iframe.width = 400;
+        iframe.height = 60;
+        document.getElementById('display').appendChild(iframe);
+      });
+    },
+    function PC_LOCAL_IDENTITY_ASSERTION_WITH_WINDOW_LOGIN(t) {
+      return checkLogin(t, 'openwin', loginUrl => {
+        window.open(loginUrl, 'login', 'width=400,height=60');
+      });
+    }
+  ]);
+  test.run();
+}
+runNetworkTest(theTest);
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/webidl/Document.webidl
+++ b/dom/webidl/Document.webidl
@@ -5,16 +5,17 @@
  *
  * http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/core/nsIDOMDocument.idl
  */
 
 interface WindowProxy;
 interface nsISupports;
 interface URI;
 interface nsIDocShell;
+interface nsILoadGroup;
 
 enum VisibilityState { "hidden", "visible" };
 
 /* http://dom.spec.whatwg.org/#interface-document */
 [Constructor]
 interface Document : Node {
   [Throws]
   readonly attribute DOMImplementation implementation;
@@ -349,16 +350,18 @@ partial interface Document {
   [ChromeOnly, Throws]
   void obsoleteSheet(URI sheetURI);
   [ChromeOnly, Throws]
   void obsoleteSheet(DOMString sheetURI);
 
   [ChromeOnly] readonly attribute nsIDocShell? docShell;
 
   [ChromeOnly] readonly attribute DOMString contentLanguage;
+
+  [ChromeOnly] readonly attribute nsILoadGroup? documentLoadGroup;
 };
 
 // Extension to give chrome JS the ability to determine when a document was
 // created to satisfy an iframe with srcdoc attribute.
 partial interface Document {
   [ChromeOnly] readonly attribute boolean isSrcdocDocument;
 };
 
--- a/dom/webidl/Navigator.webidl
+++ b/dom/webidl/Navigator.webidl
@@ -42,16 +42,17 @@ interface NavigatorID {
   [Constant, Cached]
   readonly attribute DOMString platform;
   [Constant, Cached]
   readonly attribute DOMString userAgent;
   [Constant, Cached]
   readonly attribute DOMString product; // constant "Gecko"
 
   // Everyone but WebKit/Blink supports this.  See bug 679971.
+  [Exposed=Window]
   boolean taintEnabled(); // constant false
 };
 
 [NoInterfaceObject, Exposed=(Window,Worker)]
 interface NavigatorLanguage {
 
   // These 2 values are cached. They are updated when pref
   // intl.accept_languages is changed.
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -3991,27 +3991,26 @@ WorkerPrivateParent<Derived>::SetBaseURI
   }
 
   mLoadInfo.mBaseURI = aBaseURI;
 
   if (NS_FAILED(aBaseURI->GetSpec(mLocationInfo.mHref))) {
     mLocationInfo.mHref.Truncate();
   }
 
-  if (NS_FAILED(aBaseURI->GetHost(mLocationInfo.mHostname))) {
-    mLocationInfo.mHostname.Truncate();
-  }
-
-  if (NS_FAILED(aBaseURI->GetPath(mLocationInfo.mPathname))) {
+  mLocationInfo.mHostname.Truncate();
+  nsContentUtils::GetHostOrIPv6WithBrackets(aBaseURI, mLocationInfo.mHostname);
+
+  nsCOMPtr<nsIURL> url(do_QueryInterface(aBaseURI));
+  if (!url || NS_FAILED(url->GetFilePath(mLocationInfo.mPathname))) {
     mLocationInfo.mPathname.Truncate();
   }
 
   nsCString temp;
 
-  nsCOMPtr<nsIURL> url(do_QueryInterface(aBaseURI));
   if (url && NS_SUCCEEDED(url->GetQuery(temp)) && !temp.IsEmpty()) {
     mLocationInfo.mSearch.Assign('?');
     mLocationInfo.mSearch.Append(temp);
   }
 
   if (NS_SUCCEEDED(aBaseURI->GetRef(temp)) && !temp.IsEmpty()) {
     nsCOMPtr<nsITextToSubURI> converter =
       do_GetService(NS_ITEXTTOSUBURI_CONTRACTID);
--- a/dom/workers/test/navigator_worker.js
+++ b/dom/workers/test/navigator_worker.js
@@ -6,17 +6,16 @@
 // IMPORTANT: Do not change the list below without review from a DOM peer!
 var supportedProps = [
   "appCodeName",
   "appName",
   "appVersion",
   { name: "getDataStores", b2g: true },
   "platform",
   "product",
-  "taintEnabled",
   "userAgent",
   "onLine",
   "language",
   "languages",
 ];
 
 self.onmessage = function(event) {
   if (!event || !event.data) {
@@ -61,19 +60,17 @@ function startTest(isB2G) {
     }
 
     if (typeof navigator[prop] == "undefined") {
       throw "Navigator has no '" + prop + "' property!";
     }
 
     obj = { name:  prop };
 
-    if (prop === "taintEnabled") {
-      obj.value = navigator[prop]();
-    } else if (prop === "getDataStores") {
+    if (prop === "getDataStores") {
       obj.value = typeof navigator[prop];
     } else {
       obj.value = navigator[prop];
     }
 
     postMessage(JSON.stringify(obj));
   }
 
--- a/dom/workers/test/test_navigator.html
+++ b/dom/workers/test/test_navigator.html
@@ -30,21 +30,16 @@ Tests of DOM Worker Navigator
       return;
     }
 
     if (typeof navigator[args.name] == "undefined") {
       ok(false, "Navigator has no '" + args.name + "' property!");
       return;
     }
 
-    if (args.name === "taintEnabled") {
-      is(navigator[args.name](), args.value, args.name + "() returns false.");
-      return;
-    }
-
     if (args.name === "getDataStores") {
       var type = typeof navigator[args.name];
       is(type, args.value, "getDataStores() exists and it's a function.");
       return;
     }
 
     if (args.name === "languages") {
       is(navigator.languages.toString(), args.value.toString(), "languages matches");
--- a/dom/workers/test/urlApi_worker.js
+++ b/dom/workers/test/urlApi_worker.js
@@ -106,17 +106,17 @@ onmessage = function() {
       port: '',
       pathname: '/',
       search: '?test',
       hash: ''
     },
     { url: 'http://example.com/carrot#question%3f',
       base: undefined,
       error: false,
-      hash: '#question%3f'
+      hash: '#question?'
     },
     { url: 'https://example.com:4443?',
       base: undefined,
       error: false,
       protocol: 'https:',
       port: '4443',
       pathname: '/',
       hash: '',
--- a/js/src/builtin/AtomicsObject.cpp
+++ b/js/src/builtin/AtomicsObject.cpp
@@ -50,18 +50,18 @@
 #include "mozilla/Atomics.h"
 #include "mozilla/FloatingPoint.h"
 
 #include "jsapi.h"
 #include "jsfriendapi.h"
 
 #include "prmjtime.h"
 
+#include "asmjs/AsmJSModule.h"
 #include "jit/AtomicOperations.h"
-
 #include "js/Class.h"
 #include "vm/GlobalObject.h"
 #include "vm/SharedTypedArrayObject.h"
 #include "vm/TypedArrayObject.h"
 
 #include "jsobjinlines.h"
 #include "jit/AtomicOperations-inl.h"
 
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -1498,17 +1498,17 @@ js::testingFunc_bailout(JSContext* cx, u
     return true;
 }
 
 bool
 js::testingFunc_inJit(JSContext* cx, unsigned argc, jsval* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
-    if (!IsBaselineEnabled(cx)) {
+    if (!jit::IsBaselineEnabled(cx)) {
         JSString* error = JS_NewStringCopyZ(cx, "Baseline is disabled.");
         if(!error)
             return false;
 
         args.rval().setString(error);
         return true;
     }
 
@@ -1526,17 +1526,17 @@ js::testingFunc_inJit(JSContext* cx, uns
     return true;
 }
 
 bool
 js::testingFunc_inIon(JSContext* cx, unsigned argc, jsval* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
-    if (!IsIonEnabled(cx)) {
+    if (!jit::IsIonEnabled(cx)) {
         JSString* error = JS_NewStringCopyZ(cx, "Ion is disabled.");
         if (!error)
             return false;
 
         args.rval().setString(error);
         return true;
     }
 
--- a/js/src/ds/IdValuePair.h
+++ b/js/src/ds/IdValuePair.h
@@ -2,18 +2,19 @@
  * vim: set ts=8 sts=4 et sw=4 tw=99:
  * 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/. */
 
 #ifndef ds_IdValuePair_h
 #define ds_IdValuePair_h
 
+#include "jsapi.h"
+
 #include "NamespaceImports.h"
-
 #include "js/Id.h"
 
 namespace js {
 
 struct IdValuePair
 {
     jsid id;
     Value value;
--- a/js/src/frontend/BytecodeEmitter.cpp
+++ b/js/src/frontend/BytecodeEmitter.cpp
@@ -972,23 +972,23 @@ BytecodeEmitter::leaveNestedScope(StmtIn
     MOZ_ASSERT_IF(!stmt->isBlockScope, staticScope->is<StaticWithObject>());
 #endif
 
     popStatement();
 
     if (!emit1(stmt->isBlockScope ? JSOP_DEBUGLEAVEBLOCK : JSOP_LEAVEWITH))
         return false;
 
-    blockScopeList.recordEnd(blockScopeIndex, offset());
-
     if (stmt->isBlockScope && stmt->staticScope->as<StaticBlockObject>().needsClone()) {
         if (!emit1(JSOP_POPBLOCKSCOPE))
             return false;
     }
 
+    blockScopeList.recordEnd(blockScopeIndex, offset());
+
     return true;
 }
 
 bool
 BytecodeEmitter::emitIndex32(JSOp op, uint32_t index)
 {
     MOZ_ASSERT(checkStrictOrSloppy(op));
 
--- a/js/src/gc/Allocator.cpp
+++ b/js/src/gc/Allocator.cpp
@@ -341,17 +341,17 @@ GCRuntime::refillFreeListOffMainThread(E
     return nullptr;
 }
 
 TenuredCell*
 ArenaLists::allocateFromArena(JS::Zone* zone, AllocKind thingKind,
                               AutoMaybeStartBackgroundAllocation& maybeStartBGAlloc)
 {
     JSRuntime* rt = zone->runtimeFromAnyThread();
-    Maybe<AutoLockGC> maybeLock;
+    mozilla::Maybe<AutoLockGC> maybeLock;
 
     // See if we can proceed without taking the GC lock.
     if (backgroundFinalizeState[thingKind] != BFS_DONE)
         maybeLock.emplace(rt);
 
     ArenaList& al = arenaLists[thingKind];
     ArenaHeader* aheader = al.takeNextArena();
     if (aheader) {
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-environment-05.js
@@ -0,0 +1,9 @@
+// Test that Debugger.Frame.prototype.environment works at all pcs of a script
+// with an aliased block scope.
+
+var g = newGlobal();
+var dbg = new Debugger(g);
+dbg.onDebuggerStatement = function (frame) {
+  frame.onStep = (function ()  { frame.environment; });
+};
+g.eval("debugger; for (let i of [1,2,3]) print(i);");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/bug1147939.js
@@ -0,0 +1,8 @@
+// |jit-test| error: TypeError
+var g = newGlobal();
+g.debuggeeGlobal = this;
+g.eval("(" + function () {
+        dbg = new Debugger(debuggeeGlobal);
+        dbg.onExceptionUnwind = Map;
+} + ")();");
+throw new Error("oops");
--- a/js/src/jit/JSONSpewer.cpp
+++ b/js/src/jit/JSONSpewer.cpp
@@ -3,16 +3,17 @@
  * 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/. */
 
 #include "jit/JSONSpewer.h"
 
 #include <stdarg.h>
 
+#include "jit/BacktrackingAllocator.h"
 #include "jit/LIR.h"
 #include "jit/MIR.h"
 #include "jit/MIRGraph.h"
 #include "jit/RangeAnalysis.h"
 
 using namespace js;
 using namespace js::jit;
 
--- a/js/src/jit/JitcodeMap.cpp
+++ b/js/src/jit/JitcodeMap.cpp
@@ -1527,18 +1527,18 @@ JitcodeIonTable::WriteIonTable(CompactBu
 
 } // namespace jit
 } // namespace js
 
 
 JS_PUBLIC_API(JS::ProfilingFrameIterator::FrameKind)
 JS::GetProfilingFrameKindFromNativeAddr(JSRuntime* rt, void* addr)
 {
-    JitcodeGlobalTable* table = rt->jitRuntime()->getJitcodeGlobalTable();
-    JitcodeGlobalEntry entry;
+    js::jit::JitcodeGlobalTable* table = rt->jitRuntime()->getJitcodeGlobalTable();
+    js::jit::JitcodeGlobalEntry entry;
     table->lookupInfallible(addr, &entry, rt);
     MOZ_ASSERT(entry.isIon() || entry.isIonCache() || entry.isBaseline());
 
     if (entry.isBaseline())
         return JS::ProfilingFrameIterator::Frame_Baseline;
 
     return JS::ProfilingFrameIterator::Frame_Ion;
 }
--- a/js/src/jit/x86-shared/Architecture-x86-shared.cpp
+++ b/js/src/jit/x86-shared/Architecture-x86-shared.cpp
@@ -4,18 +4,20 @@
  * 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/. */
 
 #include "jit/x86-shared/Architecture-x86-shared.h"
 #if !defined(JS_CODEGEN_X86) && !defined(JS_CODEGEN_X64)
 # error "Wrong architecture. Only x86 and x64 should build this file!"
 #endif
 
+#include "jit/RegisterSets.h"
+
 const char*
-FloatRegister::name() const {
+js::jit::FloatRegister::name() const {
     static const char* const names[] = {
 
 #ifdef JS_CODEGEN_X64
 #define FLOAT_REGS_(TYPE) \
         "%xmm0" TYPE, "%xmm1" TYPE, "%xmm2" TYPE, "%xmm3" TYPE, \
         "%xmm4" TYPE, "%xmm5" TYPE, "%xmm6" TYPE, "%xmm7" TYPE, \
         "%xmm8" TYPE, "%xmm9" TYPE, "%xmm10" TYPE, "%xmm11" TYPE, \
         "%xmm12" TYPE, "%xmm13" TYPE, "%xmm14" TYPE, "%xmm15" TYPE
@@ -33,18 +35,18 @@ FloatRegister::name() const {
         FLOAT_REGS_(".s4")
 #undef FLOAT_REGS_
 
     };
     MOZ_ASSERT(size_t(code()) < mozilla::ArrayLength(names));
     return names[size_t(code())];
 }
 
-FloatRegisterSet
-FloatRegister::ReduceSetForPush(const FloatRegisterSet& s)
+js::jit::FloatRegisterSet
+js::jit::FloatRegister::ReduceSetForPush(const FloatRegisterSet& s)
 {
     SetType bits = s.bits();
 
     // Ignore all SIMD register, if not supported.
     if (!JitSupportsSimd())
         bits &= Codes::AllPhysMask * Codes::SpreadScalar;
 
     // Exclude registers which are already pushed with a larger type. High bits
@@ -53,17 +55,17 @@ FloatRegister::ReduceSetForPush(const Fl
     bits &= ~(bits >> (1 * Codes::TotalPhys));
     bits &= ~(bits >> (2 * Codes::TotalPhys));
     bits &= ~(bits >> (3 * Codes::TotalPhys));
 
     return FloatRegisterSet(bits);
 }
 
 uint32_t
-FloatRegister::GetPushSizeInBytes(const FloatRegisterSet& s)
+js::jit::FloatRegister::GetPushSizeInBytes(const FloatRegisterSet& s)
 {
     SetType all = s.bits();
     SetType float32x4Set =
         (all >> (uint32_t(Codes::Float32x4) * Codes::TotalPhys)) & Codes::AllPhysMask;
     SetType int32x4Set =
         (all >> (uint32_t(Codes::Int32x4) * Codes::TotalPhys)) & Codes::AllPhysMask;
     SetType doubleSet =
         (all >> (uint32_t(Codes::Double) * Codes::TotalPhys)) & Codes::AllPhysMask;
@@ -87,12 +89,12 @@ FloatRegister::GetPushSizeInBytes(const 
     count32b += count32b & 1;
 #endif
 
     return mozilla::CountPopulation32(set128b) * (4 * sizeof(int32_t))
         + mozilla::CountPopulation32(set64b) * sizeof(double)
         + count32b * sizeof(float);
 }
 uint32_t
-FloatRegister::getRegisterDumpOffsetInBytes()
+js::jit::FloatRegister::getRegisterDumpOffsetInBytes()
 {
     return uint32_t(encoding()) * sizeof(FloatRegisters::RegisterContent);
 }
--- a/js/src/jit/x86-shared/Architecture-x86-shared.h
+++ b/js/src/jit/x86-shared/Architecture-x86-shared.h
@@ -6,16 +6,20 @@
 
 #ifndef jit_x86_shared_Architecture_x86_h
 #define jit_x86_shared_Architecture_x86_h
 
 #if !defined(JS_CODEGEN_X86) && !defined(JS_CODEGEN_X64)
 # error "Unsupported architecture!"
 #endif
 
+#include "mozilla/MathAlgorithms.h"
+
+#include <string.h>
+
 #include "jit/x86-shared/Constants-x86-shared.h"
 
 namespace js {
 namespace jit {
 
 #if defined(JS_CODEGEN_X86)
 // In bytes: slots needed for potential memory->memory move spills.
 //   +8 for cycles
--- a/js/src/jit/x86-shared/Constants-x86-shared.h
+++ b/js/src/jit/x86-shared/Constants-x86-shared.h
@@ -2,16 +2,21 @@
  * vim: set ts=8 sts=4 et sw=4 tw=99:
  * 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/. */
 
 #ifndef jit_x86_shared_Constants_x86_shared_h
 #define jit_x86_shared_Constants_x86_shared_h
 
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Assertions.h"
+
+#include <stddef.h>
+
 namespace js {
 namespace jit {
 
 namespace X86Encoding {
 
 enum RegisterID {
     rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi
 #ifdef JS_CODEGEN_X64
--- a/js/src/jsapi-tests/testForwardSetProperty.cpp
+++ b/js/src/jsapi-tests/testForwardSetProperty.cpp
@@ -40,17 +40,17 @@ BEGIN_TEST(testForwardSetProperty)
     EXEC("function assertEq(a, b, msg) \n"
          "{ \n"
          "  if (!Object.is(a, b)) \n"
          "    throw new Error('Assertion failure: ' + msg); \n"
          "}");
 
     // Non-strict setter
 
-    ObjectOpResult result;
+    JS::ObjectOpResult result;
     CHECK(JS_ForwardSetPropertyTo(cx, obj2, prop, setval, v3, result));
     CHECK(result);
 
     EXEC("assertEq(foundValue, obj3, 'wrong receiver passed to setter');");
 
     CHECK(JS_ForwardSetPropertyTo(cx, obj2, prop, setval, setval, result));
     CHECK(result);
 
--- a/js/src/jsapi-tests/testJSEvaluateScript.cpp
+++ b/js/src/jsapi-tests/testJSEvaluateScript.cpp
@@ -1,14 +1,16 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
  * vim: set ts=8 sts=4 et sw=4 tw=99:
  */
 
 #include "jsapi-tests/tests.h"
 
+using mozilla::ArrayLength;
+
 BEGIN_TEST(testJSEvaluateScript)
 {
     JS::RootedObject obj(cx, JS_NewPlainObject(cx));
     CHECK(obj);
 
     CHECK(JS::RuntimeOptionsRef(cx).varObjFix());
 
     static const char16_t src[] = MOZ_UTF16("var x = 5;");
--- a/js/src/jsapi-tests/tests.h
+++ b/js/src/jsapi-tests/tests.h
@@ -2,16 +2,17 @@
  * vim: set ts=8 sts=4 et sw=4 tw=99:
  * 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/. */
 
 #ifndef jsapi_tests_tests_h
 #define jsapi_tests_tests_h
 
+#include "mozilla/ArrayUtils.h"
 #include "mozilla/TypeTraits.h"
 
 #include <errno.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 
 #include "jsalloc.h"
--- a/js/src/jsexn.h
+++ b/js/src/jsexn.h
@@ -120,15 +120,15 @@ class AutoClearPendingException
     JSContext* cx;
 
   public:
     explicit AutoClearPendingException(JSContext* cxArg)
       : cx(cxArg)
     { }
 
     ~AutoClearPendingException() {
-        cx->clearPendingException();
+        JS_ClearPendingException(cx);
     }
 };
 
 } // namespace js
 
 #endif /* jsexn_h */
--- a/js/src/jsopcode.cpp
+++ b/js/src/jsopcode.cpp
@@ -1748,17 +1748,23 @@ FindStartPC(JSContext* cx, const FrameIt
     if (!parser.parse())
         return false;
 
     if (spindex < 0 && spindex + int(parser.stackDepthAtPC(current)) < 0)
         spindex = JSDVG_SEARCH_STACK;
 
     if (spindex == JSDVG_SEARCH_STACK) {
         size_t index = iter.numFrameSlots();
-        MOZ_ASSERT(index >= size_t(parser.stackDepthAtPC(current)));
+
+        // The decompiler may be called from inside functions that are not
+        // called from script, but via the C++ API directly, such as
+        // Invoke. In that case, the youngest script frame may have a
+        // completely unrelated pc and stack depth, so we give up.
+        if (index < size_t(parser.stackDepthAtPC(current)))
+            return true;
 
         // We search from fp->sp to base to find the most recently calculated
         // value matching v under assumption that it is the value that caused
         // the exception.
         int stackHits = 0;
         Value s;
         do {
             if (!index)
--- a/js/src/jsscript.h
+++ b/js/src/jsscript.h
@@ -1629,16 +1629,19 @@ class JSScript : public js::gc::TenuredC
     inline js::RegExpObject* getRegExp(jsbytecode* pc);
 
     const js::Value& getConst(size_t index) {
         js::ConstArray* arr = consts();
         MOZ_ASSERT(index < arr->length);
         return arr->vector[index];
     }
 
+    // The following 3 functions find the static scope just before the
+    // execution of the instruction pointed to by pc.
+
     js::NestedScopeObject* getStaticBlockScope(jsbytecode* pc);
 
     // Returns the innermost static scope at pc if it falls within the extent
     // of the script. Returns nullptr otherwise.
     JSObject* innermostStaticScopeInScript(jsbytecode* pc);
 
     // As innermostStaticScopeInScript, but returns the enclosing static scope
     // if the innermost static scope falls without the extent of the script.
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -2198,16 +2198,40 @@ TryNotes(JSContext* cx, HandleScript scr
         Sprint(sp, " %-7s %6u %8u %8u\n",
                TryNoteNames[tn->kind], tn->stackDepth,
                tn->start, tn->start + tn->length);
     } while (++tn != tnlimit);
     return true;
 }
 
 static bool
+BlockNotes(JSContext* cx, HandleScript script, Sprinter* sp)
+{
+    if (!script->hasBlockScopes())
+        return true;
+
+    Sprint(sp, "\nBlock table:\n   index   parent    start      end\n");
+
+    BlockScopeArray* scopes = script->blockScopes();
+    for (uint32_t i = 0; i < scopes->length; i++) {
+        const BlockScopeNote* note = &scopes->vector[i];
+        if (note->index == BlockScopeNote::NoBlockScopeIndex)
+            Sprint(sp, "%8s ", "(none)");
+        else
+            Sprint(sp, "%8u ", note->index);
+        if (note->parent == BlockScopeNote::NoBlockScopeIndex)
+            Sprint(sp, "%8s ", "(none)");
+        else
+            Sprint(sp, "%8u ", note->parent);
+        Sprint(sp, "%8u %8u\n", note->start, note->start + note->length);
+    }
+    return true;
+}
+
+static bool
 DisassembleScript(JSContext* cx, HandleScript script, HandleFunction fun, bool lines,
                   bool recursive, Sprinter* sp)
 {
     if (fun) {
         Sprint(sp, "flags:");
         if (fun->isLambda())
             Sprint(sp, " LAMBDA");
         if (fun->isHeavyweight())
@@ -2224,16 +2248,17 @@ DisassembleScript(JSContext* cx, HandleS
             Sprint(sp, " ARROW");
         Sprint(sp, "\n");
     }
 
     if (!Disassemble(cx, script, lines, sp))
         return false;
     SrcNotes(cx, script, sp);
     TryNotes(cx, script, sp);
+    BlockNotes(cx, script, sp);
 
     if (recursive && script->hasObjects()) {
         ObjectArray* objects = script->objects();
         for (unsigned i = 0; i != objects->length; ++i) {
             JSObject* obj = objects->vector[i];
             if (obj->is<JSFunction>()) {
                 Sprint(sp, "\n");
                 RootedFunction fun(cx, &obj->as<JSFunction>());
@@ -2294,16 +2319,17 @@ DisassembleToSprinter(JSContext* cx, uns
         /* Without arguments, disassemble the current script. */
         RootedScript script(cx, GetTopScript(cx));
         if (script) {
             JSAutoCompartment ac(cx, script);
             if (!Disassemble(cx, script, p.lines, sprinter))
                 return false;
             SrcNotes(cx, script, sprinter);
             TryNotes(cx, script, sprinter);
+            BlockNotes(cx, script, sprinter);
         }
     } else {
         for (unsigned i = 0; i < p.argc; i++) {
             RootedFunction fun(cx);
             RootedScript script (cx, ValueToScript(cx, p.argv[i], fun.address()));
             if (!script)
                 return false;
             if (!DisassembleScript(cx, script, fun, p.lines, p.recursive, sprinter))
@@ -4309,17 +4335,17 @@ SetSharedArrayBuffer(JSContext* cx, unsi
         oldBuffer->dropReference();
     sharedArrayBufferMailbox = newBuffer;
     PR_Unlock(sharedArrayBufferMailboxLock);
 
     args.rval().setUndefined();
     return true;
 }
 
-class SprintOptimizationTypeInfoOp : public ForEachTrackedOptimizationTypeInfoOp
+class SprintOptimizationTypeInfoOp : public JS::ForEachTrackedOptimizationTypeInfoOp
 {
     Sprinter* sp;
     bool startedTypes_;
 
   public:
     explicit SprintOptimizationTypeInfoOp(Sprinter* sp)
       : sp(sp),
         startedTypes_(false)
@@ -4340,42 +4366,42 @@ class SprintOptimizationTypeInfoOp : pub
             PutEscapedString(buf, mozilla::ArrayLength(buf), location, strlen(location), '"');
             Sprint(sp, ",\"location\":%s", buf);
         }
         if (lineno != UINT32_MAX)
             Sprint(sp, ",\"line\":%u", lineno);
         Sprint(sp, "},");
     }
 
-    void operator()(TrackedTypeSite site, const char* mirType) override {
+    void operator()(JS::TrackedTypeSite site, const char* mirType) override {
         if (startedTypes_) {
             // Clear trailing ,
             if ((*sp)[sp->getOffset() - 1] == ',')
                 (*sp)[sp->getOffset() - 1] = ' ';
             Sprint(sp, "],");
             startedTypes_ = false;
         } else {
             Sprint(sp, "{");
         }
 
         Sprint(sp, "\"site\":\"%s\",\"mirType\":\"%s\"},",
                TrackedTypeSiteString(site), mirType);
     }
 };
 
-class SprintOptimizationAttemptsOp : public ForEachTrackedOptimizationAttemptOp
+class SprintOptimizationAttemptsOp : public JS::ForEachTrackedOptimizationAttemptOp
 {
     Sprinter* sp;
 
   public:
     explicit SprintOptimizationAttemptsOp(Sprinter* sp)
       : sp(sp)
     { }
 
-    void operator()(TrackedStrategy strategy, TrackedOutcome outcome) override {
+    void operator()(JS::TrackedStrategy strategy, JS::TrackedOutcome outcome) override {
         Sprint(sp, "{\"strategy\":\"%s\",\"outcome\":\"%s\"},",
                TrackedStrategyString(strategy), TrackedOutcomeString(outcome));
     }
 };
 
 static bool
 ReflectTrackedOptimizations(JSContext* cx, unsigned argc, Value* vp)
 {
--- a/js/src/vm/ScopeObject.cpp
+++ b/js/src/vm/ScopeObject.cpp
@@ -2485,16 +2485,17 @@ js::GetDebugScopeForFunction(JSContext* 
 }
 
 JSObject*
 js::GetDebugScopeForFrame(JSContext* cx, AbstractFramePtr frame, jsbytecode* pc)
 {
     assertSameCompartment(cx, frame);
     if (CanUseDebugScopeMaps(cx) && !DebugScopes::updateLiveScopes(cx))
         return nullptr;
+
     ScopeIter si(cx, frame, pc);
     return GetDebugScope(cx, si);
 }
 
 // See declaration and documentation in jsfriendapi.h
 JS_FRIEND_API(JSObject*)
 js::GetObjectEnvironmentObjectForFunction(JSFunction* fun)
 {
--- a/layout/generic/nsRubyBaseContainerFrame.cpp
+++ b/layout/generic/nsRubyBaseContainerFrame.cpp
@@ -377,20 +377,16 @@ nsRubyBaseContainerFrame::Reflow(nsPresC
   if (!NS_INLINE_IS_BREAK_BEFORE(aStatus) &&
       NS_FRAME_IS_COMPLETE(aStatus) && hasSpan) {
     // Reflow spans
     ReflowState reflowState = {
       false, false, textContainers, aReflowState, reflowStates
     };
     nscoord spanISize = ReflowSpans(reflowState);
     isize = std::max(isize, spanISize);
-    if (isize > aReflowState.AvailableISize() &&
-        aReflowState.mLineLayout->HasOptionalBreakPosition()) {
-      aStatus = NS_INLINE_LINE_BREAK_BEFORE();
-    }
   }
 
   for (uint32_t i = 0; i < rtcCount; i++) {
     // It happens before the ruby text container is reflowed, and that
     // when it is reflowed, it will just use this size.
     nsRubyTextContainerFrame* textContainer = textContainers[i];
     nsLineLayout* lineLayout = lineLayouts[i].get();
 
--- a/layout/style/html.css
+++ b/layout/style/html.css
@@ -798,26 +798,30 @@ marquee[direction="up"], marquee[directi
     display: ruby-base;
     white-space: nowrap;
   }
   rp {
     display: none;
   }
   rt {
     display: ruby-text;
-    font-variant-east-asian: ruby;
-    text-emphasis: none;
   }
   rtc {
     display: ruby-text-container;
   }
   rtc, rt {
     white-space: nowrap;
     font-size: 50%;
     line-height: 1;
+    font-variant-east-asian: ruby;
+  }
+  @supports (text-emphasis: none) {
+    rtc, rt {
+      text-emphasis: none;
+    }
   }
   rtc:lang(zh), rt:lang(zh) {
     ruby-align: center;
   }
   rtc:lang(zh-TW), rt:lang(zh-TW) {
     font-size: 30%; /* bopomofo */
   }
   rtc > rt {
--- a/mobile/android/components/LoginManagerPrompter.js
+++ b/mobile/android/components/LoginManagerPrompter.js
@@ -135,30 +135,27 @@ LoginManagerPrompter.prototype = {
         Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION").add(PROMPT_DISPLAYED);
     },
 
 
     /*
      * _showLoginNotification
      *
      * Displays a notification doorhanger.
-     * @param aName
-     *        Name of notification
      * @param aTitle
      *        Object with title and optional resource to display with the title, such as a favicon key
      * @param aBody
      *        String message to be displayed in the doorhanger
      * @param aButtons
      *        Buttons to display with the doorhanger
      * @param aActionText
      *        Object with text to be displayed as clickable, along with a bundle to create an action
      *
      */
-    _showLoginNotification : function (aName, aTitle, aBody, aButtons, aActionText) {
-        this.log("Adding new " + aName + " notification bar");
+    _showLoginNotification : function (aTitle, aBody, aButtons, aActionText) {
         let notifyWin = this._window.top;
         let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
         let browser = chromeWin.BrowserApp.getBrowserForWindow(notifyWin);
         let tabID = chromeWin.BrowserApp.getTabForBrowser(browser).id;
 
         // The page we're going to hasn't loaded yet, so we want to persist
         // across the first location change.
 
@@ -171,17 +168,17 @@ LoginManagerPrompter.prototype = {
             persistWhileVisible: true,
             timeout: Date.now() + 10000,
             title: aTitle,
             actionText: aActionText
         }
 
         var nativeWindow = this._getNativeWindow();
         if (nativeWindow)
-            nativeWindow.doorhanger.show(aBody, aName, aButtons, tabID, options, "LOGIN");
+            nativeWindow.doorhanger.show(aBody, "password", aButtons, tabID, options, "LOGIN");
     },
 
 
     /*
      * _showSaveLoginNotification
      *
      * Displays a notification doorhanger (rather than a popup), to allow the user to
      * save the specified login. This allows the user to see the results of
@@ -227,17 +224,17 @@ LoginManagerPrompter.prototype = {
                         aLogin.password = response["password"] || aLogin.password;
                     }
                     pwmgr.addLogin(aLogin);
                     promptHistogram.add(PROMPT_ADD);
                 }
             }
         ];
 
-        this._showLoginNotification("password-save", title, notificationText, buttons, actionText);
+        this._showLoginNotification(title, notificationText, buttons, actionText);
     },
 
     /*
      * promptToChangePassword
      *
      * Called when we think we detect a password change for an existing
      * login, when the form being submitted contains multiple password
      * fields.
@@ -284,17 +281,17 @@ LoginManagerPrompter.prototype = {
                 label: this._getLocalizedString("updateButton"),
                 callback:  function() {
                     self._updateLogin(aOldLogin, aNewPassword);
                     promptHistogram.add(PROMPT_UPDATE);
                 }
             }
         ];
 
-        this._showLoginNotification("password-change", title, notificationText, buttons);
+        this._showLoginNotification(title, notificationText, buttons);
     },
 
 
     /*
      * promptToChangePasswordWithUsernames
      *
      * Called when we detect a password change in a form submission, but we
      * don't know which existing login (username) it's for. Asks the user
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -166,17 +166,17 @@ pref("dom.keyboardevent.code.enabled", t
 // even if this is true).
 pref("dom.keyboardevent.dispatch_during_composition", false);
 
 // Whether the UndoManager API is enabled
 pref("dom.undo_manager.enabled", false);
 
 // Whether URL,nsLocation,Link::GetHash should be percent encoded
 // in setter and percent decoded in getter (old behaviour = true)
-pref("dom.url.encode_decode_hash", false);
+pref("dom.url.encode_decode_hash", true);
 
 // Whether to run add-on code in different compartments from browser code. This
 // causes a separate compartment for each (addon, global) combination, which may
 // significantly increase the number of compartments in the system.
 #ifdef NIGHTLY_BUILD
 pref("dom.compartment_per_addon", true);
 #else
 pref("dom.compartment_per_addon", false);
--- a/testing/web-platform/meta/url/a-element.html.ini
+++ b/testing/web-platform/meta/url/a-element.html.ini
@@ -337,8 +337,22 @@
     expected: FAIL
 
   [Parsing: <http://[google.com\]> against <http://other.com/>]
     expected: FAIL
 
   [Parsing: <x> against <test:test>]
     expected: FAIL
 
+  [Parsing: <http://f:21/ b ? d # e > against <http://example.org/foo/bar>]
+    expected: FAIL
+
+  [Parsing: <#\xce\xb2> against <http://example.org/foo/bar>]
+    expected: FAIL
+
+  [Parsing: <http://www.google.com/foo?bar=baz# \xc2\xbb> against <about:blank>]
+    expected: FAIL
+
+  [Parsing: <#β> against <http://example.org/foo/bar>]
+    expected: FAIL
+
+  [Parsing: <http://www.google.com/foo?bar=baz# »> against <about:blank>]
+    expected: FAIL
--- a/testing/web-platform/meta/url/a-element.xhtml.ini
+++ b/testing/web-platform/meta/url/a-element.xhtml.ini
@@ -356,8 +356,22 @@
     expected: FAIL
 
   [Parsing: <http://[google.com\]> against <http://other.com/>]
     expected: FAIL
 
   [Parsing: <x> against <test:test>]
     expected: FAIL
 
+  [Parsing: <http://f:21/ b ? d # e > against <http://example.org/foo/bar>]
+    expected: FAIL
+
+  [Parsing: <#\xce\xb2> against <http://example.org/foo/bar>]
+    expected: FAIL
+
+  [Parsing: <http://www.google.com/foo?bar=baz# \xc2\xbb> against <about:blank>]
+    expected: FAIL
+
+  [Parsing: <#β> against <http://example.org/foo/bar>]
+    expected: FAIL
+
+  [Parsing: <http://www.google.com/foo?bar=baz# »> against <about:blank>]
+    expected: FAIL
--- a/testing/web-platform/meta/workers/MessagePort_initial_disabled.htm.ini
+++ b/testing/web-platform/meta/workers/MessagePort_initial_disabled.htm.ini
@@ -1,5 +1,6 @@
 [MessagePort_initial_disabled.htm]
   type: testharness
   [ MessageChannel: port message queue is initially disabled ]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=952139
 
--- a/testing/web-platform/meta/workers/MessagePort_onmessage_start.htm.ini
+++ b/testing/web-platform/meta/workers/MessagePort_onmessage_start.htm.ini
@@ -1,5 +1,6 @@
 [MessagePort_onmessage_start.htm]
   type: testharness
   [ MessageChannel: port.onmessage enables message queue ]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=952139
 
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/workers/WorkerLocation_hash_encoding.htm.ini
@@ -0,0 +1,4 @@
+[WorkerLocation_hash_encoding.htm]
+  type: testharness
+  [ WorkerLocation.hash with url encoding string ]
+    expected: FAIL
--- a/testing/web-platform/meta/workers/interfaces.worker.js.ini
+++ b/testing/web-platform/meta/workers/interfaces.worker.js.ini
@@ -1,12 +1,13 @@
 [interfaces.worker]
   type: testharness
   [WorkerGlobalScope interface: attribute onlanguagechange]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1154779
 
   [WorkerGlobalScope interface: operation importScripts(DOMString)]
     expected: FAIL
 
   [WorkerGlobalScope interface: operation setTimeout(Function,long,any)]
     expected: FAIL
 
   [WorkerGlobalScope interface: operation setTimeout(DOMString,long,any)]
@@ -24,12 +25,13 @@
   [DedicatedWorkerGlobalScope interface: calling postMessage(any,[object Object\]) on self with too few arguments must throw TypeError]
     expected: FAIL
 
   [DedicatedWorkerGlobalScope interface: self must inherit property "onmessage" with the proper type (1)]
     expected: FAIL
 
   [WorkerGlobalScope interface: self must inherit property "onlanguagechange" with the proper type (4)]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1154779
 
   [WorkerGlobalScope interface: calling importScripts(DOMString) on self with too few arguments must throw TypeError]
     expected: FAIL
 
deleted file mode 100644
--- a/testing/web-platform/meta/workers/interfaces/WorkerGlobalScope/location/members.html.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[members.html]
-  type: testharness
-  [members of WorkerLocation]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/workers/interfaces/WorkerGlobalScope/location/redirect.html.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[redirect.html]
-  type: testharness
-  [location with a worker in separate file that redirects]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/workers/interfaces/WorkerGlobalScope/location/setting-members.html.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[setting-members.html]
-  type: testharness
-  [setting members of WorkerLocation]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/workers/interfaces/WorkerGlobalScope/location/worker-separate-file.html.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[worker-separate-file.html]
-  type: testharness
-  [location with a worker in separate file]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/workers/interfaces/WorkerUtils/navigator/007.html.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[007.html]
-  type: testharness
-  [readonlyness of members of Navigator]
-    expected: FAIL
-
--- a/testing/web-platform/meta/workers/postMessage_event_properties.htm.ini
+++ b/testing/web-platform/meta/workers/postMessage_event_properties.htm.ini
@@ -1,5 +1,6 @@
 [postMessage_event_properties.htm]
   type: testharness
   [ postMessage(): MessageEvent properties ]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=952139
 
--- a/testing/web-platform/tests/html/webappapis/system-state-and-capabilities/the-navigator-object/NavigatorID.js
+++ b/testing/web-platform/tests/html/webappapis/system-state-and-capabilities/the-navigator-object/NavigatorID.js
@@ -18,20 +18,16 @@ function run_test() {
                   "navigator.platform should be a string");
   }, "platform");
 
   test(function() {
     assert_equals(navigator.product, "Gecko");
   }, "product");
 
   test(function() {
-    assert_false(navigator.taintEnabled());
-  }, "taintEnabled");
-
-  test(function() {
     assert_equals(typeof navigator.userAgent, "string",
                   "navigator.userAgent should be a string");
   }, "userAgent type");
 
   test(function() {
     assert_equals(navigator.vendorSub, "");
   }, "vendorSub");
 
--- a/testing/web-platform/tests/workers/interfaces.idl
+++ b/testing/web-platform/tests/workers/interfaces.idl
@@ -91,17 +91,16 @@ WorkerNavigator implements NavigatorOnLi
 
 [NoInterfaceObject/*, Exposed=(Window,Worker)*/]
 interface NavigatorID {
   readonly attribute DOMString appCodeName; // constant "Mozilla"
   readonly attribute DOMString appName;
   readonly attribute DOMString appVersion;
   readonly attribute DOMString platform;
   readonly attribute DOMString product; // constant "Gecko"
-  boolean taintEnabled(); // constant false
   readonly attribute DOMString userAgent;
 };
 
 [NoInterfaceObject/*, Exposed=(Window,Worker)*/]
 interface NavigatorLanguage {
   readonly attribute DOMString? language;
   readonly attribute DOMString[] languages;
 };
--- a/testing/web-platform/tests/workers/interfaces/WorkerGlobalScope/location/redirect.html
+++ b/testing/web-platform/tests/workers/interfaces/WorkerGlobalScope/location/redirect.html
@@ -4,17 +4,17 @@
 <!doctype html>
 <title>location with a worker in separate file that redirects</title>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <div id="log"></div>
 <script>
 async_test(function() {
   var worker = new Worker('helper-redirect.py?fail');
-  worker.onmessage = this.step_func(function(e) {
+  worker.onmessage = this.step_func_done(function(e) {
     assert_equals(e.data[0], location.href.replace(/\/[^\/]+$/, '/post-location-members.js?a'));
     assert_equals(e.data[1], location.protocol);
     assert_equals(e.data[2], location.host);
     assert_equals(e.data[3], location.hostname);
     assert_equals(e.data[4], location.port);
     assert_equals(e.data[5], location.pathname.replace(/\/[^\/]+$/, '/post-location-members.js'));
     assert_equals(e.data[6], '?a');
     assert_equals(e.data[7], '');
--- a/toolkit/components/addoncompat/CompatWarning.jsm
+++ b/toolkit/components/addoncompat/CompatWarning.jsm
@@ -17,55 +17,74 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 function section(number, url)
 {
   const baseURL = "https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/Limitations_of_chrome_scripts";
   return { number, url: baseURL + url };
 }
 
 let CompatWarning = {
-  warn: function(msg, addon, warning) {
-    if (addon) {
-      let histogram = Services.telemetry.getKeyedHistogramById("ADDON_SHIM_USAGE");
-      histogram.add(addon, warning ? warning.number : 0);
-    }
-
-    if (!Preferences.get("dom.ipc.shims.enabledWarnings", false))
-      return;
-
+  // Sometimes we want to generate a warning, but put off issuing it
+  // until later. For example, if someone registers a listener, we
+  // might only want to warn about it if the listener actually
+  // fires. However, we want the warning to show a stack for the
+  // registration site.
+  delayedWarning: function(msg, addon, warning) {
     function isShimLayer(filename) {
       return filename.indexOf("CompatWarning.jsm") != -1 ||
         filename.indexOf("RemoteAddonsParent.jsm") != -1 ||
         filename.indexOf("RemoteAddonsChild.jsm") != -1 ||
         filename.indexOf("multiprocessShims.js") != -1;
     };
 
     let stack = Components.stack;
     while (stack && isShimLayer(stack.filename))
       stack = stack.caller;
 
-    let error = Cc['@mozilla.org/scripterror;1'].createInstance(Ci.nsIScriptError);
-    if (!error || !Services.console) {
-      // Too late during shutdown to use the nsIConsole
-      return;
-    }
+    let alreadyWarned = false;
+
+    return function() {
+      if (alreadyWarned) {
+        return;
+      }
+      alreadyWarned = true;
 
-    let message = `Warning: ${msg}`;
-    if (warning)
-      message += `\nMore info at: ${url}`;
+      if (addon) {
+        let histogram = Services.telemetry.getKeyedHistogramById("ADDON_SHIM_USAGE");
+        histogram.add(addon, warning ? warning.number : 0);
+      }
+
+      if (!Preferences.get("dom.ipc.shims.enabledWarnings", false))
+        return;
 
-    error.init(
-               /*message*/ message,
-               /*sourceName*/ stack ? stack.filename : "",
-               /*sourceLine*/ stack ? stack.sourceLine : "",
-               /*lineNumber*/ stack ? stack.lineNumber : 0,
-               /*columnNumber*/ 0,
-               /*flags*/ Ci.nsIScriptError.warningFlag,
-               /*category*/ "chrome javascript");
-    Services.console.logMessage(error);
+      let error = Cc['@mozilla.org/scripterror;1'].createInstance(Ci.nsIScriptError);
+      if (!error || !Services.console) {
+        // Too late during shutdown to use the nsIConsole
+        return;
+      }
+
+      let message = `Warning: ${msg}`;
+      if (warning)
+        message += `\nMore info at: ${warning.url}`;
+
+      error.init(
+                 /*message*/ message,
+                 /*sourceName*/ stack ? stack.filename : "",
+                 /*sourceLine*/ stack ? stack.sourceLine : "",
+                 /*lineNumber*/ stack ? stack.lineNumber : 0,
+                 /*columnNumber*/ 0,
+                 /*flags*/ Ci.nsIScriptError.warningFlag,
+                 /*category*/ "chrome javascript");
+      Services.console.logMessage(error);
+    };
+  },
+
+  warn: function(msg, addon, warning) {
+    let delayed = this.delayedWarning(msg, addon, warning);
+    delayed();
   },
 
   warnings: {
     content: section(1, "#gBrowser.contentWindow.2C_window.content..."),
     limitations_of_CPOWs: section(2, "#Limitations_of_CPOWs"),
     nsIContentPolicy: section(3, "#nsIContentPolicy"),
     nsIWebProgressListener: section(4, "#nsIWebProgressListener"),
     observers: section(5, "#Observers_in_the_chrome_process"),
--- a/toolkit/components/addoncompat/RemoteAddonsParent.jsm
+++ b/toolkit/components/addoncompat/RemoteAddonsParent.jsm
@@ -370,21 +370,22 @@ let TOPIC_WHITELIST = [
 
 // This interposition listens for
 // nsIObserverService.{add,remove}Observer.
 let ObserverInterposition = new Interposition("ObserverInterposition");
 
 ObserverInterposition.methods.addObserver =
   function(addon, target, observer, topic, ownsWeak) {
     if (TOPIC_WHITELIST.indexOf(topic) >= 0) {
+      CompatWarning.warn(`${topic} observer should be added from the child process only.`,
+                         addon, CompatWarning.warnings.observers);
+
       ObserverParent.addObserver(addon, observer, topic);
     }
 
-    CompatWarning.warn(`${topic} observer should be added from the child process only.`,
-                       addon, CompatWarning.warnings.observers);
     target.addObserver(observer, topic, ownsWeak);
   };
 
 ObserverInterposition.methods.removeObserver =
   function(addon, target, observer, topic) {
     if (TOPIC_WHITELIST.indexOf(topic) >= 0) {
       ObserverParent.removeObserver(addon, observer, topic);
     }
@@ -441,17 +442,17 @@ let EventTargetParent = {
   // When a given event fires in the child, we fire it on the
   // <browser> element and the window since those are the two possible
   // results of redirectEventTarget.
   getTargets: function(browser) {
     let window = browser.ownerDocument.defaultView;
     return [browser, window];
   },
 
-  addEventListener: function(addon, target, type, listener, useCapture, wantsUntrusted) {
+  addEventListener: function(addon, target, type, listener, useCapture, wantsUntrusted, delayedWarning) {
     let newTarget = this.redirectEventTarget(target);
     if (!newTarget) {
       return;
     }
 
     useCapture = useCapture || false;
     wantsUntrusted = wantsUntrusted || false;
 
@@ -472,17 +473,18 @@ let EventTargetParent = {
           forType[i].wantsUntrusted === wantsUntrusted) {
         return;
       }
     }
 
     forType.push({listener: listener,
                   target: target,
                   wantsUntrusted: wantsUntrusted,
-                  useCapture: useCapture});
+                  useCapture: useCapture,
+                  delayedWarning: delayedWarning});
   },
 
   removeEventListener: function(addon, target, type, listener, useCapture) {
     let newTarget = this.redirectEventTarget(target);
     if (!newTarget) {
       return;
     }
 
@@ -522,18 +524,21 @@ let EventTargetParent = {
       let listeners = this._listeners.get(target);
       if (!listeners) {
         continue;
       }
       let forType = setDefault(listeners, type, []);
 
       // Make a copy in case they call removeEventListener in the listener.
       let handlers = [];
-      for (let {listener, target, wantsUntrusted, useCapture} of forType) {
+      for (let {listener, target, wantsUntrusted, useCapture, delayedWarning} of forType) {
         if ((wantsUntrusted || isTrusted) && useCapture == capturing) {
+          // Issue a warning for this listener.
+          delayedWarning();
+
           handlers.push([listener, target]);
         }
       }
 
       for (let [handler, target] of handlers) {
         let EventProxy = {
           get: function(knownProps, name) {
             if (knownProps.hasOwnProperty(name))
@@ -610,20 +615,22 @@ function makeFilteringListener(eventType
 }
 
 // This interposition redirects addEventListener and
 // removeEventListener to EventTargetParent.
 let EventTargetInterposition = new Interposition("EventTargetInterposition");
 
 EventTargetInterposition.methods.addEventListener =
   function(addon, target, type, listener, useCapture, wantsUntrusted) {
-    CompatWarning.warn("Registering an event listener on content DOM nodes" +
-                        " needs to happen in the content process.",
-                       addon, CompatWarning.warnings.DOM_events);
-    EventTargetParent.addEventListener(addon, target, type, listener, useCapture, wantsUntrusted);
+    let delayed = CompatWarning.delayedWarning(
+      "Registering an event listener on content DOM nodes" +
+        " needs to happen in the content process.",
+      addon, CompatWarning.warnings.DOM_events);
+
+    EventTargetParent.addEventListener(addon, target, type, listener, useCapture, wantsUntrusted, delayed);
     target.addEventListener(type, makeFilteringListener(type, listener), useCapture, wantsUntrusted);
   };
 
 EventTargetInterposition.methods.removeEventListener =
   function(addon, target, type, listener, useCapture) {
     EventTargetParent.removeEventListener(addon, target, type, listener, useCapture);
     target.removeEventListener(type, makeFilteringListener(type, listener), useCapture);
   };
--- a/toolkit/components/addoncompat/tests/browser/browser_addonShims.js
+++ b/toolkit/components/addoncompat/tests/browser/browser_addonShims.js
@@ -43,12 +43,17 @@ function removeAddon(addon)
       }
     };
     AddonManager.addAddonListener(listener);
     addon.uninstall();
   });
 }
 
 add_task(function* test_addon_shims() {
+  yield new Promise(resolve => {
+    SpecialPowers.pushPrefEnv({set: [["dom.ipc.shims.enabledWarnings", true]]},
+                             resolve);
+  });
+
   let addon = yield addAddon(ADDON_URL);
   yield window.runAddonShimTests({ok: ok, is: is, info: info});
   yield removeAddon(addon);
 });