Merge m-c to cedar
authorJames Graham <james@hoppipolla.co.uk>
Thu, 16 Apr 2015 22:00:41 +0100
changeset 326788 5a24a3ed407d7ac715433e2ad78dc627104396da
parent 326787 83913b163be9aa723a46897fcab47e5da4541f61 (current diff)
parent 257732 51e3cb11a258b9f22dfcdbf89ac99d1c56a1125e (diff)
child 326789 c13dbbe381bf853c9682ed32ac4cd99c054987b8
push id10169
push userdminor@mozilla.com
push dateThu, 28 Jan 2016 13:10:48 +0000
milestone40.0a1
Merge m-c to cedar
CLOBBER
dom/animation/Animation.cpp
dom/animation/Animation.h
dom/animation/AnimationEffect.cpp
dom/animation/AnimationEffect.h
dom/animation/test/css-animations/test_animation-effect-name.html
dom/animation/test/css-animations/test_animation-target.html
dom/animation/test/css-transitions/test_animation-effect-name.html
dom/animation/test/css-transitions/test_animation-target.html
dom/bluetooth/bluetooth1/BluetoothCommon.h
dom/bluetooth/bluetooth1/BluetoothInterface.cpp
dom/bluetooth/bluetooth1/BluetoothInterface.h
dom/bluetooth/bluetooth1/BluetoothInterfaceHelpers.cpp
dom/bluetooth/bluetooth1/BluetoothInterfaceHelpers.h
dom/bluetooth/bluetooth1/BluetoothUtils.cpp
dom/bluetooth/bluetooth1/BluetoothUtils.h
dom/bluetooth/bluetooth2/BluetoothCommon.h
dom/bluetooth/bluetooth2/BluetoothInterface.cpp
dom/bluetooth/bluetooth2/BluetoothInterface.h
dom/bluetooth/bluetooth2/BluetoothInterfaceHelpers.h
dom/bluetooth/bluetooth2/BluetoothUtils.cpp
dom/bluetooth/bluetooth2/BluetoothUtils.h
dom/media/webaudio/test/chrome.ini
dom/webidl/Animation.webidl
dom/webidl/AnimationEffect.webidl
editor/libeditor/tests/chrome.ini
js/src/jit-test/tests/closures/bug540136.js
js/src/jit-test/tests/closures/bug540348.js
js/src/jit-test/tests/jaeger/bug583684.js
js/src/jit-test/tests/jaeger/bug719758.js
js/src/jit/LinearScan.cpp
js/src/jit/LinearScan.h
js/src/tests/ecma_7/SIMD/float32x4abs.js
js/src/tests/ecma_7/SIMD/float32x4add.js
js/src/tests/ecma_7/SIMD/float32x4alignment.js
js/src/tests/ecma_7/SIMD/float32x4and.js
js/src/tests/ecma_7/SIMD/float32x4div.js
js/src/tests/ecma_7/SIMD/float32x4fromfloat64x2.js
js/src/tests/ecma_7/SIMD/float32x4fromfloat64x2bits.js
js/src/tests/ecma_7/SIMD/float32x4fromint32x4.js
js/src/tests/ecma_7/SIMD/float32x4fromint32x4bits.js
js/src/tests/ecma_7/SIMD/float32x4getters.js
js/src/tests/ecma_7/SIMD/float32x4handle.js
js/src/tests/ecma_7/SIMD/float32x4mul.js
js/src/tests/ecma_7/SIMD/float32x4neg.js
js/src/tests/ecma_7/SIMD/float32x4not.js
js/src/tests/ecma_7/SIMD/float32x4or.js
js/src/tests/ecma_7/SIMD/float32x4reciprocal.js
js/src/tests/ecma_7/SIMD/float32x4reciprocalsqrt.js
js/src/tests/ecma_7/SIMD/float32x4reify.js
js/src/tests/ecma_7/SIMD/float32x4setter.js
js/src/tests/ecma_7/SIMD/float32x4sqrt.js
js/src/tests/ecma_7/SIMD/float32x4sub.js
js/src/tests/ecma_7/SIMD/float32x4xor.js
js/src/tests/ecma_7/SIMD/float64x2alignment.js
js/src/tests/ecma_7/SIMD/float64x2fromfloat32x4.js
js/src/tests/ecma_7/SIMD/float64x2fromfloat32x4bits.js
js/src/tests/ecma_7/SIMD/float64x2fromint32x4.js
js/src/tests/ecma_7/SIMD/float64x2fromint32x4bits.js
js/src/tests/ecma_7/SIMD/float64x2getters.js
js/src/tests/ecma_7/SIMD/float64x2handle.js
js/src/tests/ecma_7/SIMD/float64x2reify.js
js/src/tests/ecma_7/SIMD/float64x2setter.js
js/src/tests/ecma_7/SIMD/int32x4add.js
js/src/tests/ecma_7/SIMD/int32x4alignment.js
js/src/tests/ecma_7/SIMD/int32x4and.js
js/src/tests/ecma_7/SIMD/int32x4fromfloat32x4.js
js/src/tests/ecma_7/SIMD/int32x4fromfloat32x4bits.js
js/src/tests/ecma_7/SIMD/int32x4fromfloat64x2.js
js/src/tests/ecma_7/SIMD/int32x4fromfloat64x2bits.js
js/src/tests/ecma_7/SIMD/int32x4getters.js
js/src/tests/ecma_7/SIMD/int32x4handle.js
js/src/tests/ecma_7/SIMD/int32x4mul.js
js/src/tests/ecma_7/SIMD/int32x4neg.js
js/src/tests/ecma_7/SIMD/int32x4not.js
js/src/tests/ecma_7/SIMD/int32x4or.js
js/src/tests/ecma_7/SIMD/int32x4reify.js
js/src/tests/ecma_7/SIMD/int32x4setter.js
js/src/tests/ecma_7/SIMD/int32x4sub.js
js/src/tests/ecma_7/SIMD/int32x4xor.js
layout/xul/test/chrome.ini
security/apps/Makefile.in
security/pkix/include/pkix/ScopedPtr.h
testing/docker/phone-builder/system-setup.sh
testing/mozharness/mozharness.json
testing/web-platform/harness/wptrunner/browsers/b2g.py
testing/web-platform/harness/wptrunner/browsers/firefox.py
testing/web-platform/harness/wptrunner/browsers/servo.py
testing/web-platform/harness/wptrunner/wptcommandline.py
testing/web-platform/harness/wptrunner/wptrunner.py
testing/web-platform/meta/service-workers/cache-storage/window/cache-match.https.html.ini
testing/web-platform/meta/service-workers/cache-storage/worker/cache-match.https.html.ini
testing/web-platform/meta/url/a-element.xhtml.ini
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
toolkit/modules/WindowsPrefSync.jsm
toolkit/mozapps/update/nsIUpdateTimerManager.idl
toolkit/mozapps/update/nsUpdateTimerManager.js
toolkit/mozapps/update/nsUpdateTimerManager.manifest
toolkit/mozapps/update/tests/unit_timermanager/consumerNotifications.js
toolkit/mozapps/update/tests/unit_timermanager/xpcshell.ini
toolkit/themes/windows/global/about.css
toolkit/themes/windows/global/aboutCache.css
toolkit/themes/windows/global/aboutCacheEntry.css
toolkit/themes/windows/global/aboutMemory.css
toolkit/themes/windows/global/aboutReader.css
toolkit/themes/windows/global/aboutSupport.css
toolkit/themes/windows/global/appPicker.css
tools/profiler/BreakpadSampler.cpp
tools/profiler/LulRWLock.cpp
tools/profiler/LulRWLock.h
tools/profiler/UnwinderThread2.cpp
tools/profiler/UnwinderThread2.h
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,13 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1146355: Run Bluetooth APIs v1 and v2 with same backend code
+Bug 1154235: Merge BluetoothUtils.{cpp,h} variants into single file
+
+This patch set renames and removes source files. This requires updating
+the build system's dependency information from scratch. The issue has
+been reported in bug 1154232.
--- a/accessible/tests/mochitest/common.js
+++ b/accessible/tests/mochitest/common.js
@@ -420,19 +420,19 @@ function testAccessibleTree(aAccOrElmOrI
       is(accTree[prop], acc.DOMNode.tagName, msg);
       break;
 
     case "textAttrs": {
       var prevOffset = -1;
       for (var offset in accTree[prop]) {
         if (prevOffset !=- 1) {
           var attrs = accTree[prop][prevOffset];
-          testTextAttrs(acc, prevOffset, attrs, { }, prevOffset, offset, true);
+          testTextAttrs(acc, prevOffset, attrs, { }, prevOffset, +offset, true);
         }
-        prevOffset = offset;
+        prevOffset = +offset;
       }
 
       if (prevOffset != -1) {
         var charCount = getAccessible(acc, [nsIAccessibleText]).characterCount;
         var attrs = accTree[prop][prevOffset];
         testTextAttrs(acc, prevOffset, attrs, { }, prevOffset, charCount, true);
       }
 
--- a/accessible/tests/mochitest/events/test_text_alg.html
+++ b/accessible/tests/mochitest/events/test_text_alg.html
@@ -15,18 +15,18 @@
           src="../common.js"></script>
   <script type="application/javascript"
           src="../events.js"></script>
 
   <script type="application/javascript">
     ////////////////////////////////////////////////////////////////////////////
     // Invokers
 
-    const kRemoval = 0;
-    const kInsertion = 1;
+    const kRemoval = false;
+    const kInsertion = true;
     const kUnexpected = true;
 
     function changeText(aContainerID, aValue, aEventList)
     {
       this.containerNode = getNode(aContainerID);
       this.textNode = this.containerNode.firstChild;
       this.textData = this.textNode.data;
 
--- a/accessible/tests/mochitest/events/test_tree.xul
+++ b/accessible/tests/mochitest/events/test_tree.xul
@@ -77,17 +77,17 @@
           startCol = null;
         }
         is(startCol, aStartCol,
            "Wrong 'startcolumn' of 'treeInvalidated' event on " + aMsg);
 
         try {
           var endCol = propBag.getPropertyAsInt32("endcolumn");
         } catch (e if e.name == 'NS_ERROR_NOT_AVAILABLE') {
-          startCol = null;
+          endCol = null;
         }
         is(endCol, aEndCol,
            "Wrong 'endcolumn' of 'treeInvalidated' event on " + aMsg);
       }
       this.getID = function getID()
       {
         return "TreeInvalidated on " + aMsg;
       }
--- a/accessible/tests/mochitest/states.js
+++ b/accessible/tests/mochitest/states.js
@@ -47,17 +47,17 @@ const EXT_STATE_MULTI_LINE = nsIAccessib
 const EXT_STATE_PINNED = nsIAccessibleStates.EXT_STATE_PINNED;
 const EXT_STATE_SENSITIVE = nsIAccessibleStates.EXT_STATE_SENSITIVE;
 const EXT_STATE_SINGLE_LINE = nsIAccessibleStates.EXT_STATE_SINGLE_LINE;
 const EXT_STATE_STALE = nsIAccessibleStates.EXT_STATE_STALE;
 const EXT_STATE_SUPPORTS_AUTOCOMPLETION =
   nsIAccessibleStates.EXT_STATE_SUPPORTS_AUTOCOMPLETION;
 const EXT_STATE_VERTICAL = nsIAccessibleStates.EXT_STATE_VERTICAL;
 
-const kOrdinalState = 0;
+const kOrdinalState = false;
 const kExtraState = 1;
 
 ////////////////////////////////////////////////////////////////////////////////
 // Test functions
 
 /**
  * Tests the states and extra states of the given accessible.
  * Also tests for unwanted states and extra states.
--- a/accessible/tests/mochitest/table/test_sels_listbox.xul
+++ b/accessible/tests/mochitest/table/test_sels_listbox.xul
@@ -72,17 +72,17 @@
       // getSelectedColumns
       var selColsCount = {}, selCols = {};
       aAcc.getSelectedColumnIndices(selColsCount, selCols);
 
       is(selColsCount.value, aSelCount,
          aId + ": wrong number of selected columns");
 
       if (!aSelIndexesArray) {
-        is(selCols.value, null,
+        is(selCols.value, undefined,
            aId + ": no columns should be selected");
       } else {
         for (var i = 0; i < selCols.length; i++) {
           is(selCols[i], aSelIndexesArray[i],
              aId + ": wrong selected column index " + i);
         }
       }
     }
@@ -111,17 +111,17 @@
       // getSelectedRows
       var selColsCount = {}, selCols = {};
       aAcc.getSelectedRowIndices(selColsCount, selCols);
 
       is(selColsCount.value, aSelCount,
          aId + ": wrong number of selected rows");
 
       if (!aSelIndexesArray) {
-        is(selCols.value, null,
+        is(selCols.value, undefined,
            aId + ": no row should be selected");
       } else {
         for (var i = 0; i < selCols.length; i++) {
           is(selCols[i], aSelIndexesArray[i],
              aId + ": wrong selected row index " + i);
         }
       }
     }
@@ -154,17 +154,17 @@
       // getSelectedCells
       var selColsCount = {}, selCols = {};
       aAcc.getSelectedCellIndices(selColsCount, selCols);
 
       is(selColsCount.value, aSelCount,
          aId + ": wrong number of selected cells");
 
       if (!aSelIndexesArray) {
-        is(selCols.value, null,
+        is(selCols.value, undefined,
            aId + ": no cells should be selected");
       } else {
         for (var i = 0; i < selCols.length; i++) {
           is(selCols[i], aSelIndexesArray[i],
              aId + ": wrong selected cell index " + i);
         }
       }
     }
--- a/addon-sdk/source/lib/sdk/ui/button/view.js
+++ b/addon-sdk/source/lib/sdk/ui/button/view.js
@@ -137,17 +137,17 @@ function create(options) {
         node.style.display = 'none';
 
       node.setAttribute('id', this.id);
       node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional badged-button');
       node.setAttribute('type', type);
       node.setAttribute('label', label);
       node.setAttribute('tooltiptext', label);
       node.setAttribute('image', image);
-      node.setAttribute('sdk-button', 'true');
+      node.setAttribute('constrain-size', 'true');
 
       views.set(id, {
         area: this.currentArea,
         icon: icon,
         label: label
       });
 
       node.addEventListener('command', function(event) {
--- a/addon-sdk/source/test/test-sandbox.js
+++ b/addon-sdk/source/test/test-sandbox.js
@@ -52,17 +52,17 @@ exports['test exceptions'] = function(as
   let fixture = sandbox();
   try {
     evaluate(fixture, '!' + function() {
       var message = 'boom';
       throw Error(message);
     } + '();');
   }
   catch (error) {
-    assert.equal(error.fileName, '', 'no fileName reported');
+    assert.equal(error.fileName, '[System Principal]', 'No specific fileName reported');
     assert.equal(error.lineNumber, 3, 'reports correct line number');
   }
 
   try {
     evaluate(fixture, '!' + function() {
       var message = 'boom';
       throw Error(message);
     } + '();', 'foo.js');
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -1119,28 +1119,13 @@ pref("dom.mozSettings.SettingsService.ve
 // Controlling whether we want to allow forcing some Settings
 // IndexedDB transactions to be opened as readonly or keep everything as
 // readwrite.
 pref("dom.mozSettings.allowForceReadOnly", false);
 
 // RequestSync API is enabled by default on B2G.
 pref("dom.requestSync.enabled", true);
 
-// Only enable for kit kat and above devices
-// kit kat == 19, L = 21, 20 is kit-kat for wearables
-// 15 is for the ICS emulators which will fallback to software vsync
-#if ANDROID_VERSION == 19 || ANDROID_VERSION == 21 || ANDROID_VERSION == 15
+// Use vsync aligned rendering
 pref("gfx.vsync.hw-vsync.enabled", true);
 pref("gfx.vsync.compositor", true);
 pref("gfx.touch.resample", true);
-#else
-pref("gfx.vsync.hw-vsync.enabled", false);
-pref("gfx.vsync.compositor", false);
-pref("gfx.touch.resample", false);
-#endif
-
-// Bug 1147753 - Weird issues with vsync refresh driver on L devices
-// so disable them on L, but enable on KK and ICS
-#if ANDROID_VERSION == 19 || ANDROID_VERSION == 15
 pref("gfx.vsync.refreshdriver", true);
-#else
-pref("gfx.vsync.refreshdriver", false);
-#endif
--- a/b2g/components/ContentPermissionPrompt.js
+++ b/b2g/components/ContentPermissionPrompt.js
@@ -287,60 +287,55 @@ ContentPermissionPrompt.prototype = {
        return;
     }
 
     // prompt PROMPT_ACTION request or request with options.
     typesInfo = typesInfo.filter(function(type) {
       return !type.deny && (type.action == Ci.nsIPermissionManager.PROMPT_ACTION || type.options.length > 0) ;
     });
 
-    let frame = request.element;
-
-    if (!frame) {
+    if (!request.element) {
       this.delegatePrompt(request, typesInfo);
       return;
     }
 
-    frame = frame.wrappedJSObject;
     var cancelRequest = function() {
-      frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange);
+      request.requester.onVisibilityChange = null;
       request.cancel();
     }
 
     var self = this;
-    var onVisibilityChange = function(evt) {
-      if (evt.detail.visible === true)
-        return;
-
-      self.cancelPrompt(request, typesInfo);
-      cancelRequest();
-    }
 
     // If the request was initiated from a hidden iframe
     // we don't forward it to content and cancel it right away
-    let domRequest = frame.getVisible();
-    domRequest.onsuccess = function gv_success(evt) {
-      if (!evt.target.result) {
-        cancelRequest();
-        return;
-      }
+    request.requester.getVisibility( {
+      notifyVisibility: function(isVisible) {
+        if (!isVisible) {
+          cancelRequest();
+          return;
+        }
 
-      // Monitor the frame visibility and cancel the request if the frame goes
-      // away but the request is still here.
-      frame.addEventListener("mozbrowservisibilitychange", onVisibilityChange);
+        // Monitor the frame visibility and cancel the request if the frame goes
+        // away but the request is still here.
+        request.requester.onVisibilityChange = {
+          notifyVisibility: function(isVisible) {
+            if (isVisible)
+              return;
 
-      self.delegatePrompt(request, typesInfo, function onCallback() {
-        frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange);
-      });
-    };
+            self.cancelPrompt(request, typesInfo);
+            cancelRequest();
+          }
+        }
 
-    // Something went wrong. Let's cancel the request just in case.
-    domRequest.onerror = function gv_error() {
-      cancelRequest();
-    }
+        self.delegatePrompt(request, typesInfo, function onCallback() {
+          request.requester.onVisibilityChange = null;
+        });
+      }
+    });
+
   },
 
   cancelPrompt: function(request, typesInfo) {
     this.sendToBrowserWindow("cancel-permission-prompt", request,
                              typesInfo);
   },
 
   delegatePrompt: function(request, typesInfo, callback) {
--- a/b2g/components/test/mochitest/mochitest.ini
+++ b/b2g/components/test/mochitest/mochitest.ini
@@ -11,8 +11,9 @@ support-files =
 [test_filepicker_path.html]
 [test_permission_deny.html]
 [test_permission_gum_remember.html]
 skip-if = true # Bug 1019572 - frequent timeouts
 [test_sandbox_permission.html]
 [test_screenshot.html]
 [test_systemapp.html]
 [test_presentation_device_prompt.html]
+[test_permission_visibilitychange.html]
new file mode 100644
--- /dev/null
+++ b/b2g/components/test/mochitest/test_permission_visibilitychange.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=951997
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Permission Prompt Test</title>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1020179">Permission prompt visibilitychange test</a>
+<script type="application/javascript;version=1.8">
+
+"use strict";
+
+var gUrl = SimpleTest.getTestFileURL("permission_handler_chrome.js");
+var gScript = SpecialPowers.loadChromeScript(gUrl);
+
+function testDone() {
+  gScript.sendAsyncMessage("teardown", "");
+  gScript.destroy();
+  SimpleTest.finish();
+  alert("setVisible::true");
+}
+
+function runTest() {
+  navigator.geolocation.getCurrentPosition(
+    function (pos) {
+      ok(false, "unexpected success, permission request should be canceled");
+      testDone();
+    }, function (err) {
+      ok(true, "success, permission request is canceled");
+      testDone();
+  });
+}
+
+gScript.addMessageListener("permission-request", function (detail) {
+  info("got permission-request!!!!\n");
+  alert("setVisible::false");
+});
+
+// Add permissions to this app. We use ALLOW_ACTION here. The ContentPermissionPrompt
+// should prompt for permission, not allow it without prompt.
+SpecialPowers.pushPrefEnv({"set": [["media.navigator.permission.disabled", false]]},
+  function() {
+    SpecialPowers.addPermission("geolocation",
+      SpecialPowers.Ci.nsIPermissionManager.PROMPT_ACTION, document);
+    runTest();
+  });
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,19 +10,19 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="c8cb0c0ebb8dd1f5c0c9037e38f8e4b237beb77b"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="3cd0a9facce26c2acc7be3755a17131a6358e33f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="42b56ff9f40a572e61b63a660daddc72a7a7ffcb"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="f2f2f0cbee2f2517070dd194051d509c07cdacff"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a1ba73572c713e298ae53821d000fc3a5087b5f"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,18 +14,18 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="173b3104bfcbd23fc9dccd4b0035fc49aae3d444">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="c8cb0c0ebb8dd1f5c0c9037e38f8e4b237beb77b"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="42b56ff9f40a572e61b63a660daddc72a7a7ffcb"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="3cd0a9facce26c2acc7be3755a17131a6358e33f"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="f2f2f0cbee2f2517070dd194051d509c07cdacff"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="93f9ba577f68d772093987c2f1c0a4ae293e1802"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="46e1877c0d88b085f7ebc5f432d5bb8f1e2d1f3b"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform_bionic" path="bionic" remote="b2g" revision="e2b3733ba3fa5e3f404e983d2e4142b1f6b1b846"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,18 +12,18 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="c8cb0c0ebb8dd1f5c0c9037e38f8e4b237beb77b"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="42b56ff9f40a572e61b63a660daddc72a7a7ffcb"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="3cd0a9facce26c2acc7be3755a17131a6358e33f"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="f2f2f0cbee2f2517070dd194051d509c07cdacff"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a1ba73572c713e298ae53821d000fc3a5087b5f"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,19 +10,19 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="c8cb0c0ebb8dd1f5c0c9037e38f8e4b237beb77b"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="3cd0a9facce26c2acc7be3755a17131a6358e33f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="42b56ff9f40a572e61b63a660daddc72a7a7ffcb"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="f2f2f0cbee2f2517070dd194051d509c07cdacff"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a1ba73572c713e298ae53821d000fc3a5087b5f"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
--- a/b2g/config/emulator-l/sources.xml
+++ b/b2g/config/emulator-l/sources.xml
@@ -10,19 +10,19 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="6b0721ca0e92788df30daf8f7a5fb2863544f9c8">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="c8cb0c0ebb8dd1f5c0c9037e38f8e4b237beb77b"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="3cd0a9facce26c2acc7be3755a17131a6358e33f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="42b56ff9f40a572e61b63a660daddc72a7a7ffcb"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="f2f2f0cbee2f2517070dd194051d509c07cdacff"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a1ba73572c713e298ae53821d000fc3a5087b5f"/>
   <!-- Stock Android things -->
   <project groups="pdk,linux" name="platform/prebuilts/clang/linux-x86/host/3.5" path="prebuilts/clang/linux-x86/host/3.5" revision="ffc05a232799fe8fcb3e47b7440b52b1fb4244c0"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,18 +14,18 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="173b3104bfcbd23fc9dccd4b0035fc49aae3d444">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="c8cb0c0ebb8dd1f5c0c9037e38f8e4b237beb77b"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="42b56ff9f40a572e61b63a660daddc72a7a7ffcb"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="3cd0a9facce26c2acc7be3755a17131a6358e33f"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="f2f2f0cbee2f2517070dd194051d509c07cdacff"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="93f9ba577f68d772093987c2f1c0a4ae293e1802"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="46e1877c0d88b085f7ebc5f432d5bb8f1e2d1f3b"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform_bionic" path="bionic" remote="b2g" revision="e2b3733ba3fa5e3f404e983d2e4142b1f6b1b846"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,19 +10,19 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="c8cb0c0ebb8dd1f5c0c9037e38f8e4b237beb77b"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="3cd0a9facce26c2acc7be3755a17131a6358e33f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="42b56ff9f40a572e61b63a660daddc72a7a7ffcb"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="f2f2f0cbee2f2517070dd194051d509c07cdacff"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a1ba73572c713e298ae53821d000fc3a5087b5f"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,18 +12,18 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="c8cb0c0ebb8dd1f5c0c9037e38f8e4b237beb77b"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="42b56ff9f40a572e61b63a660daddc72a7a7ffcb"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="3cd0a9facce26c2acc7be3755a17131a6358e33f"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="f2f2f0cbee2f2517070dd194051d509c07cdacff"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a1ba73572c713e298ae53821d000fc3a5087b5f"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "c8cb0c0ebb8dd1f5c0c9037e38f8e4b237beb77b", 
+        "git_revision": "3cd0a9facce26c2acc7be3755a17131a6358e33f", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "f9a4193e82db33f7717a939b769b0b90975231fe", 
+    "revision": "c5c31cccfb22048626664d063643ae1fb1e2facc", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,18 +12,18 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="c8cb0c0ebb8dd1f5c0c9037e38f8e4b237beb77b"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="42b56ff9f40a572e61b63a660daddc72a7a7ffcb"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="3cd0a9facce26c2acc7be3755a17131a6358e33f"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="f2f2f0cbee2f2517070dd194051d509c07cdacff"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a1ba73572c713e298ae53821d000fc3a5087b5f"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
--- a/b2g/config/nexus-5-l/sources.xml
+++ b/b2g/config/nexus-5-l/sources.xml
@@ -10,19 +10,19 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="6b0721ca0e92788df30daf8f7a5fb2863544f9c8">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="c8cb0c0ebb8dd1f5c0c9037e38f8e4b237beb77b"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="3cd0a9facce26c2acc7be3755a17131a6358e33f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="42b56ff9f40a572e61b63a660daddc72a7a7ffcb"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="f2f2f0cbee2f2517070dd194051d509c07cdacff"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a1ba73572c713e298ae53821d000fc3a5087b5f"/>
   <!-- Stock Android things -->
   <project groups="pdk,linux" name="platform/prebuilts/clang/linux-x86/host/3.5" path="prebuilts/clang/linux-x86/host/3.5" revision="ffc05a232799fe8fcb3e47b7440b52b1fb4244c0"/>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -246,16 +246,18 @@ pref("lightweightThemes.recommendedTheme
 pref("browser.eme.ui.enabled", false);
 
 // UI tour experience.
 pref("browser.uitour.enabled", true);
 pref("browser.uitour.loglevel", "Error");
 pref("browser.uitour.requireSecure", true);
 pref("browser.uitour.themeOrigin", "https://addons.mozilla.org/%LOCALE%/firefox/themes/");
 pref("browser.uitour.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/tour/");
+// This is used as a regexp match against the page's URL.
+pref("browser.uitour.readerViewTrigger", "^https:\/\/www\.mozilla\.org\/[^\/]+\/firefox\/reading\/start");
 
 pref("browser.customizemode.tip0.shown", false);
 pref("browser.customizemode.tip0.learnMoreUrl", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/customize");
 
 pref("keyword.enabled", true);
 pref("browser.fixup.domainwhitelist.localhost", true);
 
 pref("general.useragent.locale", "@AB_CD@");
@@ -1698,19 +1700,19 @@ pref("loop.ping.timeout", 10000);
 pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
 pref("loop.feedback.product", "Loop");
 pref("loop.debug.loglevel", "Error");
 pref("loop.debug.dispatcher", false);
 pref("loop.debug.websocket", false);
 pref("loop.debug.sdk", false);
 pref("loop.debug.twoWayMediaTelemetry", false);
 #ifdef DEBUG
-pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src *; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
+pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src * data:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
 #else
-pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src *; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
+pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src * data:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
 #endif
 pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
 pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
 pref("loop.fxa_oauth.tokendata", "");
 pref("loop.fxa_oauth.profile", "");
 pref("loop.support_url", "https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc");
 pref("loop.contacts.gravatars.show", false);
 pref("loop.contacts.gravatars.promo", true);
--- a/browser/base/content/browser-readinglist.js
+++ b/browser/base/content/browser-readinglist.js
@@ -247,17 +247,17 @@ let ReadingListUI = {
       // nothing to do if we have no button.
       return;
     }
 
     let uri;
     if (this.enabled && state == "valid") {
       uri = gBrowser.currentURI;
       if (uri.schemeIs("about"))
-        uri = ReaderParent.parseReaderUrl(uri.spec);
+        uri = ReaderMode.getOriginalUrl(uri.spec);
       else if (!uri.schemeIs("http") && !uri.schemeIs("https"))
         uri = null;
     }
 
     let msg = {topic: "UpdateActiveItem", url: null};
     if (!uri) {
       this.toolbarButton.setAttribute("hidden", true);
       if (this.isSidebarOpen)
@@ -304,17 +304,17 @@ let ReadingListUI = {
    * removing otherwise.
    *
    * @param {<xul:browser>} browser - Browser with page to toggle.
    * @returns {Promise} Promise resolved when operation has completed.
    */
   togglePageByBrowser: Task.async(function* (browser) {
     let uri = browser.currentURI;
     if (uri.spec.startsWith("about:reader?"))
-      uri = ReaderParent.parseReaderUrl(uri.spec);
+      uri = ReaderMode.getOriginalUrl(uri.spec);
     if (!uri)
       return;
 
     let item = yield ReadingList.itemForURL(uri);
     if (item) {
       yield item.delete();
     } else {
       yield ReadingList.addItemFromBrowser(browser, uri);
@@ -325,17 +325,17 @@ let ReadingListUI = {
    * Checks if a given item matches the current tab in this window.
    *
    * @param {ReadingListItem} item - Item to check
    * @returns True if match, false otherwise.
    */
   isItemForCurrentBrowser(item) {
     let currentURL = gBrowser.currentURI.spec;
     if (currentURL.startsWith("about:reader?"))
-      currentURL = ReaderParent.parseReaderUrl(currentURL);
+      currentURL = ReaderMode.getOriginalUrl(currentURL);
 
     if (item.url == currentURL || item.resolvedURL == currentURL) {
       return true;
     }
     return false;
   },
 
   /**
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -5,17 +5,16 @@
 
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 let Cc = Components.classes;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/NotificationDB.jsm");
 Cu.import("resource:///modules/RecentWindow.jsm");
-Cu.import("resource://gre/modules/WindowsPrefSync.jsm");
 
 
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
                                   "resource://gre/modules/Deprecated.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
                                   "resource:///modules/BrowserUITelemetry.jsm");
@@ -209,16 +208,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource:///modules/UITour.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "CastingApps",
   "resource:///modules/CastingApps.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
   "resource://gre/modules/SimpleServiceDiscovery.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
+  "resource://gre/modules/ReaderMode.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderParent",
   "resource:///modules/ReaderParent.jsm");
 
 let gInitialPages = [
   "about:blank",
   "about:newtab",
   "about:home",
   "about:privatebrowsing",
@@ -937,18 +939,20 @@ var gBrowserInit = {
     // loading the frame script to ensure that we don't miss any
     // message sent between when the frame script is loaded and when
     // the listener is registered.
     DOMLinkHandler.init();
     gPageStyleMenu.init();
     LanguageDetectionListener.init();
     BrowserOnClick.init();
     DevEdition.init();
+    AboutPrivateBrowsingListener.init();
 
     let mm = window.getGroupMessageManager("browsers");
+    mm.loadFrameScript("chrome://browser/content/tab-content.js", true);
     mm.loadFrameScript("chrome://browser/content/content.js", true);
     mm.loadFrameScript("chrome://browser/content/content-UITour.js", true);
 
     window.messageManager.addMessageListener("Browser:LoadURI", RedirectLoad);
 
     // initialize observers and listeners
     // and give C++ access to gBrowser
     XULBrowserWindow.init();
@@ -4194,16 +4198,17 @@ var XULBrowserWindow = {
         this.reloadCommand.removeAttribute("disabled");
       }
 
       if (gURLBar) {
         URLBarSetURI(aLocationURI);
 
         BookmarkingUI.onLocationChange();
         SocialUI.updateState(location);
+        UITour.onLocationChange(location);
       }
 
       // Utility functions for disabling find
       var shouldDisableFind = function shouldDisableFind(aDocument) {
         let docElt = aDocument.documentElement;
         return docElt && docElt.getAttribute("disablefastfind") == "true";
       }
 
@@ -6829,21 +6834,19 @@ var gIdentityHandler = {
       this.setMode(this.IDENTITY_MODE_CHROMEUI);
     } else if (unknown) {
       this.setMode(this.IDENTITY_MODE_UNKNOWN);
     } else if (state & nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
       this.setMode(this.IDENTITY_MODE_IDENTIFIED);
     } else if (state & nsIWebProgressListener.STATE_IS_SECURE) {
       this.setMode(this.IDENTITY_MODE_DOMAIN_VERIFIED);
     } else if (state & nsIWebProgressListener.STATE_IS_BROKEN) {
-      if ((state & nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) &&
-          gPrefService.getBoolPref("security.mixed_content.block_active_content")) {
+      if (state & nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) {
         this.setMode(this.IDENTITY_MODE_MIXED_ACTIVE_LOADED);
-      } else if ((state & nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) &&
-                gPrefService.getBoolPref("security.mixed_content.block_active_content")) {
+      } else if (state & nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) {
         this.setMode(this.IDENTITY_MODE_MIXED_DISPLAY_LOADED_ACTIVE_BLOCKED);
       } else {
         this.setMode(this.IDENTITY_MODE_MIXED_DISPLAY_LOADED);
       }
     } else {
       this.setMode(this.IDENTITY_MODE_UNKNOWN);
     }
 
@@ -7778,8 +7781,18 @@ let PanicButtonNotifier = {
       Cu.reportError(ex);
     }
   },
   close: function() {
     let popup = document.getElementById("panic-button-success-notification");
     popup.hidePopup();
   },
 };
+
+let AboutPrivateBrowsingListener = {
+  init: function () {
+    window.messageManager.addMessageListener(
+      "AboutPrivateBrowsing:OpenPrivateWindow",
+      msg => {
+        OpenBrowserWindow({private: true});
+    });
+  }
+};
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -1,127 +1,53 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+/* This content script should work in any browser or iframe and should not
+ * depend on the frame being contained in tabbrowser. */
+
 let {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/ContentWebRTC.jsm");
 Cu.import("resource:///modules/ContentObservers.jsm");
 Cu.import("resource://gre/modules/InlineSpellChecker.jsm");
 Cu.import("resource://gre/modules/InlineSpellCheckerContent.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
-  "resource:///modules/E10SUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ContentLinkHandler",
   "resource:///modules/ContentLinkHandler.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
   "resource://gre/modules/LoginManagerContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
   "resource://gre/modules/InsecurePasswordUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PluginContent",
   "resource:///modules/PluginContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormSubmitObserver",
   "resource:///modules/FormSubmitObserver.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "AboutReader",
-  "resource://gre/modules/AboutReader.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
-  "resource://gre/modules/ReaderMode.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata",
   "resource://gre/modules/PageMetadata.jsm");
-XPCOMUtils.defineLazyGetter(this, "SimpleServiceDiscovery", function() {
-  let ssdp = Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm", {}).SimpleServiceDiscovery;
-  // Register targets
-  ssdp.registerDevice({
-    id: "roku:ecp",
-    target: "roku:ecp",
-    factory: function(aService) {
-      Cu.import("resource://gre/modules/RokuApp.jsm");
-      return new RokuApp(aService);
-    },
-    mirror: true,
-    types: ["video/mp4"],
-    extensions: ["mp4"]
-  });
-  return ssdp;
-});
 XPCOMUtils.defineLazyGetter(this, "PageMenuChild", function() {
   let tmp = {};
   Cu.import("resource://gre/modules/PageMenu.jsm", tmp);
   return new tmp.PageMenuChild();
 });
 
 // TabChildGlobal
 var global = this;
 
 // Load the form validation popup handler
 var formSubmitObserver = new FormSubmitObserver(content, this);
 
-addMessageListener("Browser:HideSessionRestoreButton", function (message) {
-  // Hide session restore button on about:home
-  let doc = content.document;
-  let container;
-  if (doc.documentURI.toLowerCase() == "about:home" &&
-      (container = doc.getElementById("sessionRestoreContainer"))) {
-    container.hidden = true;
-  }
-});
-
-addMessageListener("Browser:Reload", function(message) {
-  /* First, we'll try to use the session history object to reload so
-   * that framesets are handled properly. If we're in a special
-   * window (such as view-source) that has no session history, fall
-   * back on using the web navigation's reload method.
-   */
-
-  let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
-  try {
-    let sh = webNav.sessionHistory;
-    if (sh)
-      webNav = sh.QueryInterface(Ci.nsIWebNavigation);
-  } catch (e) {
-  }
-
-  let reloadFlags = message.data.flags;
-  let handlingUserInput;
-  try {
-    handlingUserInput = content.QueryInterface(Ci.nsIInterfaceRequestor)
-                               .getInterface(Ci.nsIDOMWindowUtils)
-                               .setHandlingUserInput(message.data.handlingUserInput);
-    webNav.reload(reloadFlags);
-  } catch (e) {
-  } finally {
-    handlingUserInput.destruct();
-  }
-});
-
-addMessageListener("MixedContent:ReenableProtection", function() {
-  docShell.mixedContentChannel = null;
-});
-
-addMessageListener("SecondScreen:tab-mirror", function(message) {
-  if (!Services.prefs.getBoolPref("browser.casting.enabled")) {
-    return;
-  }
-  let app = SimpleServiceDiscovery.findAppForService(message.data.service);
-  if (app) {
-    let width = content.innerWidth;
-    let height = content.innerHeight;
-    let viewport = {cssWidth: width, cssHeight: height, width: width, height: height};
-    app.mirror(function() {}, content, viewport, function() {}, content);
-  }
-});
-
 addMessageListener("ContextMenu:DoCustomCommand", function(message) {
   PageMenuChild.executeMenu(message.data);
 });
 
 addEventListener("DOMFormHasPassword", function(event) {
   InsecurePasswordUtils.checkForInsecurePasswords(event.target);
   LoginManagerContent.onFormPassword(event);
 });
@@ -333,264 +259,16 @@ let AboutNetErrorListener = {
         location: contentDoc.location,
         securityInfo: serializedSecurityInfo
       });
   }
 }
 
 AboutNetErrorListener.init(this);
 
-let AboutHomeListener = {
-  init: function(chromeGlobal) {
-    chromeGlobal.addEventListener('AboutHomeLoad', this, false, true);
-  },
-
-  get isAboutHome() {
-    return content.document.documentURI.toLowerCase() == "about:home";
-  },
-
-  handleEvent: function(aEvent) {
-    if (!this.isAboutHome) {
-      return;
-    }
-    switch (aEvent.type) {
-      case "AboutHomeLoad":
-        this.onPageLoad();
-        break;
-      case "AboutHomeSearchEvent":
-        this.onSearch(aEvent);
-        break;
-      case "AboutHomeSearchPanel":
-        this.onOpenSearchPanel(aEvent);
-        break;
-      case "click":
-        this.onClick(aEvent);
-        break;
-      case "pagehide":
-        this.onPageHide(aEvent);
-        break;
-    }
-  },
-
-  receiveMessage: function(aMessage) {
-    if (!this.isAboutHome) {
-      return;
-    }
-    switch (aMessage.name) {
-      case "AboutHome:Update":
-        this.onUpdate(aMessage.data);
-        break;
-      case "AboutHome:FocusInput":
-        this.onFocusInput();
-        break;
-    }
-  },
-
-  onUpdate: function(aData) {
-    let doc = content.document;
-    if (aData.showRestoreLastSession && !PrivateBrowsingUtils.isContentWindowPrivate(content))
-      doc.getElementById("launcher").setAttribute("session", "true");
-
-    // Inject search engine and snippets URL.
-    let docElt = doc.documentElement;
-    // set the following attributes BEFORE searchEngineName, which triggers to
-    // show the snippets when it's set.
-    docElt.setAttribute("snippetsURL", aData.snippetsURL);
-    if (aData.showKnowYourRights)
-      docElt.setAttribute("showKnowYourRights", "true");
-    docElt.setAttribute("snippetsVersion", aData.snippetsVersion);
-    docElt.setAttribute("searchEngineName", aData.defaultEngineName);
-  },
-
-  onPageLoad: function() {
-    let doc = content.document;
-    if (doc.documentElement.hasAttribute("hasBrowserHandlers")) {
-      return;
-    }
-
-    doc.documentElement.setAttribute("hasBrowserHandlers", "true");
-    addMessageListener("AboutHome:Update", this);
-    addMessageListener("AboutHome:FocusInput", this);
-    addEventListener("click", this, true);
-    addEventListener("pagehide", this, true);
-
-    if (!Services.prefs.getBoolPref("browser.search.showOneOffButtons")) {
-      doc.documentElement.setAttribute("searchUIConfiguration", "oldsearchui");
-    }
-
-    sendAsyncMessage("AboutHome:RequestUpdate");
-    doc.addEventListener("AboutHomeSearchEvent", this, true, true);
-    doc.addEventListener("AboutHomeSearchPanel", this, true, true);
-  },
-
-  onClick: function(aEvent) {
-    if (!aEvent.isTrusted || // Don't trust synthetic events
-        aEvent.button == 2 || aEvent.target.localName != "button") {
-      return;
-    }
-
-    let originalTarget = aEvent.originalTarget;
-    let ownerDoc = originalTarget.ownerDocument;
-    if (ownerDoc.documentURI != "about:home") {
-      // This shouldn't happen, but we're being defensive.
-      return;
-    }
-
-    let elmId = originalTarget.getAttribute("id");
-
-    switch (elmId) {
-      case "restorePreviousSession":
-        sendAsyncMessage("AboutHome:RestorePreviousSession");
-        ownerDoc.getElementById("launcher").removeAttribute("session");
-        break;
-
-      case "downloads":
-        sendAsyncMessage("AboutHome:Downloads");
-        break;
-
-      case "bookmarks":
-        sendAsyncMessage("AboutHome:Bookmarks");
-        break;
-
-      case "history":
-        sendAsyncMessage("AboutHome:History");
-        break;
-
-      case "apps":
-        sendAsyncMessage("AboutHome:Apps");
-        break;
-
-      case "addons":
-        sendAsyncMessage("AboutHome:Addons");
-        break;
-
-      case "sync":
-        sendAsyncMessage("AboutHome:Sync");
-        break;
-
-      case "settings":
-        sendAsyncMessage("AboutHome:Settings");
-        break;
-
-      case "searchIcon":
-        sendAsyncMessage("AboutHome:OpenSearchPanel", null, { anchor: originalTarget });
-        break;
-    }
-  },
-
-  onPageHide: function(aEvent) {
-    if (aEvent.target.defaultView.frameElement) {
-      return;
-    }
-    removeMessageListener("AboutHome:Update", this);
-    removeEventListener("click", this, true);
-    removeEventListener("pagehide", this, true);
-    if (aEvent.target.documentElement) {
-      aEvent.target.documentElement.removeAttribute("hasBrowserHandlers");
-    }
-  },
-
-  onSearch: function(aEvent) {
-    sendAsyncMessage("AboutHome:Search", { searchData: aEvent.detail });
-  },
-
-  onOpenSearchPanel: function(aEvent) {
-    sendAsyncMessage("AboutHome:OpenSearchPanel");
-  },
-
-  onFocusInput: function () {
-    let searchInput = content.document.getElementById("searchText");
-    if (searchInput) {
-      searchInput.focus();
-    }
-  },
-};
-AboutHomeListener.init(this);
-
-let AboutReaderListener = {
-
-  _articlePromise: null,
-
-  init: function() {
-    addEventListener("AboutReaderContentLoaded", this, false, true);
-    addEventListener("DOMContentLoaded", this, false);
-    addEventListener("pageshow", this, false);
-    addEventListener("pagehide", this, false);
-    addMessageListener("Reader:ParseDocument", this);
-    addMessageListener("Reader:PushState", this);
-  },
-
-  receiveMessage: function(message) {
-    switch (message.name) {
-      case "Reader:ParseDocument":
-        this._articlePromise = ReaderMode.parseDocument(content.document).catch(Cu.reportError);
-        content.document.location = "about:reader?url=" + encodeURIComponent(message.data.url);
-        break;
-
-      case "Reader:PushState":
-        this.updateReaderButton();
-        break;
-    }
-  },
-
-  get isAboutReader() {
-    return content.document.documentURI.startsWith("about:reader");
-  },
-
-  handleEvent: function(aEvent) {
-    if (aEvent.originalTarget.defaultView != content) {
-      return;
-    }
-
-    switch (aEvent.type) {
-      case "AboutReaderContentLoaded":
-        if (!this.isAboutReader) {
-          return;
-        }
-
-        if (content.document.body) {
-          // Update the toolbar icon to show the "reader active" icon.
-          sendAsyncMessage("Reader:UpdateReaderButton");
-          new AboutReader(global, content, this._articlePromise);
-          this._articlePromise = null;
-        }
-        break;
-
-      case "pagehide":
-        sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false });
-        break;
-
-      case "pageshow":
-        // If a page is loaded from the bfcache, we won't get a "DOMContentLoaded"
-        // event, so we need to rely on "pageshow" in this case.
-        if (aEvent.persisted) {
-          this.updateReaderButton();
-        }
-        break;
-      case "DOMContentLoaded":
-        this.updateReaderButton();
-        break;
-
-    }
-  },
-  updateReaderButton: function() {
-    if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader ||
-        !(content.document instanceof content.HTMLDocument) ||
-        content.document.mozSyntheticDocument) {
-      return;
-    }
-    // Only send updates when there are articles; there's no point updating with
-    // |false| all the time.
-    if (ReaderMode.isProbablyReaderable(content.document)) {
-      sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: true });
-    }
-  },
-};
-AboutReaderListener.init();
-
 // An event listener for custom "WebChannelMessageToChrome" events on pages
 addEventListener("WebChannelMessageToChrome", function (e) {
   // if target is window then we want the document principal, otherwise fallback to target itself.
   let principal = e.target.nodePrincipal ? e.target.nodePrincipal : e.target.document.nodePrincipal;
 
   if (e.detail) {
     sendAsyncMessage("WebChannelMessageToChrome", e.detail, null, principal);
   } else  {
@@ -608,78 +286,16 @@ addMessageListener("WebChannelMessageToC
       }, content),
     }));
   } else {
     Cu.reportError("WebChannel message failed. No message data.");
   }
 });
 
 
-let ContentSearchMediator = {
-
-  whitelist: new Set([
-    "about:home",
-    "about:newtab",
-  ]),
-
-  init: function (chromeGlobal) {
-    chromeGlobal.addEventListener("ContentSearchClient", this, true, true);
-    addMessageListener("ContentSearch", this);
-  },
-
-  handleEvent: function (event) {
-    if (this._contentWhitelisted) {
-      this._sendMsg(event.detail.type, event.detail.data);
-    }
-  },
-
-  receiveMessage: function (msg) {
-    if (msg.data.type == "AddToWhitelist") {
-      for (let uri of msg.data.data) {
-        this.whitelist.add(uri);
-      }
-      this._sendMsg("AddToWhitelistAck");
-      return;
-    }
-    if (this._contentWhitelisted) {
-      this._fireEvent(msg.data.type, msg.data.data);
-    }
-  },
-
-  get _contentWhitelisted() {
-    return this.whitelist.has(content.document.documentURI);
-  },
-
-  _sendMsg: function (type, data=null) {
-    sendAsyncMessage("ContentSearch", {
-      type: type,
-      data: data,
-    });
-  },
-
-  _fireEvent: function (type, data=null) {
-    let event = Cu.cloneInto({
-      detail: {
-        type: type,
-        data: data,
-      },
-    }, content);
-    content.dispatchEvent(new content.CustomEvent("ContentSearchService",
-                                                  event));
-  },
-};
-ContentSearchMediator.init(this);
-
-// Lazily load the finder code
-addMessageListener("Finder:Initialize", function () {
-  let {RemoteFinderListener} = Cu.import("resource://gre/modules/RemoteFinder.jsm", {});
-  new RemoteFinderListener(global);
-});
-
-
 let ClickEventHandler = {
   init: function init() {
     Cc["@mozilla.org/eventlistenerservice;1"]
       .getService(Ci.nsIEventListenerService)
       .addSystemEventListener(global, "click", this, true);
   },
 
   handleEvent: function(event) {
@@ -833,221 +449,28 @@ ContentLinkHandler.init(this);
 
 // TODO: Load this lazily so the JSM is run only if a relevant event/message fires.
 let pluginContent = new PluginContent(global);
 
 addEventListener("DOMWebNotificationClicked", function(event) {
   sendAsyncMessage("DOMWebNotificationClicked", {});
 }, false);
 
-let PageStyleHandler = {
-  init: function() {
-    addMessageListener("PageStyle:Switch", this);
-    addMessageListener("PageStyle:Disable", this);
-
-    // Send a CPOW to the parent so that it can synchronously request
-    // the list of style sheets.
-    sendSyncMessage("PageStyle:SetSyncHandler", {}, {syncHandler: this});
-  },
-
-  get markupDocumentViewer() {
-    return docShell.contentViewer;
-  },
-
-  // Called synchronously via CPOW from the parent.
-  getStyleSheetInfo: function() {
-    let styleSheets = this._filterStyleSheets(this.getAllStyleSheets());
-    return {
-      styleSheets: styleSheets,
-      authorStyleDisabled: this.markupDocumentViewer.authorStyleDisabled,
-      preferredStyleSheetSet: content.document.preferredStyleSheetSet
-    };
-  },
-
-  // Called synchronously via CPOW from the parent.
-  getAllStyleSheets: function(frameset = content) {
-    let selfSheets = Array.slice(frameset.document.styleSheets);
-    let subSheets = Array.map(frameset.frames, frame => this.getAllStyleSheets(frame));
-    return selfSheets.concat(...subSheets);
-  },
-
-  receiveMessage: function(msg) {
-    switch (msg.name) {
-      case "PageStyle:Switch":
-        this.markupDocumentViewer.authorStyleDisabled = false;
-        this._stylesheetSwitchAll(content, msg.data.title);
-        break;
-
-      case "PageStyle:Disable":
-        this.markupDocumentViewer.authorStyleDisabled = true;
-        break;
-    }
-  },
-
-  _stylesheetSwitchAll: function (frameset, title) {
-    if (!title || this._stylesheetInFrame(frameset, title)) {
-      this._stylesheetSwitchFrame(frameset, title);
-    }
-
-    for (let i = 0; i < frameset.frames.length; i++) {
-      // Recurse into sub-frames.
-      this._stylesheetSwitchAll(frameset.frames[i], title);
-    }
-  },
-
-  _stylesheetSwitchFrame: function (frame, title) {
-    var docStyleSheets = frame.document.styleSheets;
-
-    for (let i = 0; i < docStyleSheets.length; ++i) {
-      let docStyleSheet = docStyleSheets[i];
-      if (docStyleSheet.title) {
-        docStyleSheet.disabled = (docStyleSheet.title != title);
-      } else if (docStyleSheet.disabled) {
-        docStyleSheet.disabled = false;
-      }
-    }
-  },
-
-  _stylesheetInFrame: function (frame, title) {
-    return Array.some(frame.document.styleSheets, (styleSheet) => styleSheet.title == title);
-  },
-
-  _filterStyleSheets: function(styleSheets) {
-    let result = [];
-
-    for (let currentStyleSheet of styleSheets) {
-      if (!currentStyleSheet.title)
-        continue;
-
-      // Skip any stylesheets that don't match the screen media type.
-      if (currentStyleSheet.media.length > 0) {
-        let mediaQueryList = currentStyleSheet.media.mediaText;
-        if (!content.matchMedia(mediaQueryList).matches) {
-          continue;
-        }
-      }
-
-      result.push({title: currentStyleSheet.title,
-                   disabled: currentStyleSheet.disabled});
-    }
-
-    return result;
-  },
-};
-PageStyleHandler.init();
-
-// Keep a reference to the translation content handler to avoid it it being GC'ed.
-let trHandler = null;
-if (Services.prefs.getBoolPref("browser.translation.detectLanguage")) {
-  Cu.import("resource:///modules/translation/TranslationContentHandler.jsm");
-  trHandler = new TranslationContentHandler(global, docShell);
-}
-
-let DOMFullscreenHandler = {
-  _fullscreenDoc: null,
-
-  init: function() {
-    addMessageListener("DOMFullscreen:Approved", this);
-    addMessageListener("DOMFullscreen:CleanUp", this);
-    addEventListener("MozEnteredDomFullscreen", this);
-  },
-
-  receiveMessage: function(aMessage) {
-    switch(aMessage.name) {
-      case "DOMFullscreen:Approved": {
-        if (this._fullscreenDoc) {
-          Services.obs.notifyObservers(this._fullscreenDoc,
-                                       "fullscreen-approved",
-                                       "");
-        }
-        break;
-      }
-      case "DOMFullscreen:CleanUp": {
-        this._fullscreenDoc = null;
-        break;
-      }
-    }
-  },
-
-  handleEvent: function(aEvent) {
-    if (aEvent.type == "MozEnteredDomFullscreen") {
-      this._fullscreenDoc = aEvent.target;
-      sendAsyncMessage("MozEnteredDomFullscreen", {
-        origin: this._fullscreenDoc.nodePrincipal.origin,
-      });
-    }
-  }
-};
-DOMFullscreenHandler.init();
-
 ContentWebRTC.init();
 addMessageListener("webrtc:Allow", ContentWebRTC);
 addMessageListener("webrtc:Deny", ContentWebRTC);
 addMessageListener("webrtc:StopSharing", ContentWebRTC);
 addMessageListener("webrtc:StartBrowserSharing", () => {
   let windowID = content.QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
   sendAsyncMessage("webrtc:response:StartBrowserSharing", {
     windowID: windowID
   });
 });
 
-function gKeywordURIFixup(fixupInfo) {
-  fixupInfo.QueryInterface(Ci.nsIURIFixupInfo);
-
-  // Ignore info from other docshells
-  let parent = fixupInfo.consumer.QueryInterface(Ci.nsIDocShellTreeItem).sameTypeRootTreeItem;
-  if (parent != docShell)
-    return;
-
-  let data = {};
-  for (let f of Object.keys(fixupInfo)) {
-    if (f == "consumer" || typeof fixupInfo[f] == "function")
-      continue;
-
-    if (fixupInfo[f] && fixupInfo[f] instanceof Ci.nsIURI) {
-      data[f] = fixupInfo[f].spec;
-    } else {
-      data[f] = fixupInfo[f];
-    }
-  }
-
-  sendAsyncMessage("Browser:URIFixup", data);
-}
-Services.obs.addObserver(gKeywordURIFixup, "keyword-uri-fixup", false);
-addEventListener("unload", () => {
-  Services.obs.removeObserver(gKeywordURIFixup, "keyword-uri-fixup");
-}, false);
-
-addMessageListener("Browser:AppTab", function(message) {
-  docShell.isAppTab = message.data.isAppTab;
-});
-
-let WebBrowserChrome = {
-  onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) {
-    return BrowserUtils.onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab);
-  },
-
-  // Check whether this URI should load in the current process
-  shouldLoadURI: function(aDocShell, aURI, aReferrer) {
-    if (!E10SUtils.shouldLoadURI(aDocShell, aURI, aReferrer)) {
-      E10SUtils.redirectLoad(aDocShell, aURI, aReferrer);
-      return false;
-    }
-
-    return true;
-  },
-};
-
-if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-  let tabchild = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsITabChild);
-  tabchild.webBrowserChrome = WebBrowserChrome;
-}
-
 addEventListener("pageshow", function(event) {
   if (event.target == content.document) {
     sendAsyncMessage("PageVisibility:Show", {
       persisted: event.persisted,
     });
   }
 });
 
@@ -1165,8 +588,14 @@ addMessageListener("ContextMenu:MediaCom
 addMessageListener("ContextMenu:Canvas:ToDataURL", (message) => {
   let dataURL = message.objects.target.toDataURL();
   sendAsyncMessage("ContextMenu:Canvas:ToDataURL:Result", { dataURL });
 });
 
 addMessageListener("ContextMenu:ReloadFrame", (message) => {
   message.objects.target.ownerDocument.location.reload();
 });
+
+addMessageListener("ContextMenu:ReloadImage", (message) => {
+  let image = message.objects.target;
+  if (image instanceof Ci.nsIImageLoadingContent)
+    image.forceReload();
+});
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -573,16 +573,17 @@ nsContextMenu.prototype = {
     this.onLink            = false;
     this.onMailtoLink      = false;
     this.onSaveableLink    = false;
     this.link              = null;
     this.linkURL           = "";
     this.linkURI           = null;
     this.linkText          = "";
     this.linkProtocol      = "";
+    this.linkDownload      = "";
     this.linkHasNoReferrer = false;
     this.onMathML          = false;
     this.inFrame           = false;
     this.inSrcdocFrame     = false;
     this.inSyntheticDoc    = false;
     this.hasBGImage        = false;
     this.bgImageURL        = "";
     this.onEditableArea    = false;
@@ -595,16 +596,17 @@ nsContextMenu.prototype = {
     // Remember the node that was clicked.
     this.target = aNode;
 
     let [elt, win] = BrowserUtils.getFocusSync(document);
     this.focusedWindow = win;
     this.focusedElement = elt;
 
     let ownerDoc = this.target.ownerDocument;
+    this.ownerDoc = ownerDoc;
 
     // If this is a remote context menu event, use the information from
     // gContextMenuContentData instead.
     if (this.isRemote) {
       this.browser = gContextMenuContentData.browser;
       this.principal = gContextMenuContentData.principal;
       this.frameOuterWindowID = gContextMenuContentData.frameOuterWindowID;
     } else {
@@ -745,16 +747,24 @@ nsContextMenu.prototype = {
           this.link = elem;
           this.linkURL = this.getLinkURL();
           this.linkURI = this.getLinkURI();
           this.linkText = this.getLinkText();
           this.linkProtocol = this.getLinkProtocol();
           this.onMailtoLink = (this.linkProtocol == "mailto");
           this.onSaveableLink = this.isLinkSaveable( this.link );
           this.linkHasNoReferrer = BrowserUtils.linkHasNoReferrer(elem);
+          try {
+            if (elem.download) {
+              // Ignore download attribute on cross-origin links
+              this.principal.checkMayLoad(this.linkURI, false, true);
+              this.linkDownload = elem.download;
+            }
+          }
+          catch (ex) {}
         }
 
         // Background image?  Don't bother if we've already found a
         // background image further down the hierarchy.  Otherwise,
         // we look for the computed background-image style.
         if (!this.hasBGImage &&
             !this._hasMultipleBGImages) {
           let bgImgUrl;
@@ -1022,23 +1032,23 @@ nsContextMenu.prototype = {
     openUILink(this.imageDescURL, e, { disallowInheritPrincipal: true,
                                        referrerURI: gContextMenuContentData.documentURIObject });
   },
 
   viewFrameInfo: function() {
     BrowserPageInfo(this.target.ownerDocument);
   },
 
-  reloadImage: function(e) {
+  reloadImage: function() {
     urlSecurityCheck(this.mediaURL,
                      this.browser.contentPrincipal,
                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
 
-    if (this.target instanceof Ci.nsIImageLoadingContent)
-      this.target.forceReload();
+    this.browser.messageManager.sendAsyncMessage("ContextMenu:ReloadImage",
+                                                 null, { target: this.target });
   },
 
   _canvasToDataURL: function(target) {
     let mm = this.browser.messageManager;
     return new Promise(function(resolve) {
       mm.sendAsyncMessage("ContextMenu:Canvas:ToDataURL", {}, { target });
 
       let onMessage = (message) => {
@@ -1165,17 +1175,18 @@ nsContextMenu.prototype = {
 
   // Save URL of clicked-on frame.
   saveFrame: function () {
     saveDocument(this.target.ownerDocument);
   },
 
   // Helper function to wait for appropriate MIME-type headers and
   // then prompt the user with a file picker
-  saveHelper: function(linkURL, linkText, dialogTitle, bypassCache, doc) {
+  saveHelper: function(linkURL, linkText, dialogTitle, bypassCache, doc,
+                       linkDownload) {
     // canonical def in nsURILoader.h
     const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020;
 
     // an object to proxy the data through to
     // nsIExternalHelperAppService.doContent, which will wait for the
     // appropriate MIME-type headers and then prompt the user with a
     // file picker
     function saveAsListener() {}
@@ -1271,16 +1282,18 @@ nsContextMenu.prototype = {
     var ioService = Cc["@mozilla.org/network/io-service;1"].
                     getService(Ci.nsIIOService);
     var channel = ioService.newChannelFromURI2(makeURI(linkURL),
                                                null, // aLoadingNode
                                                this.principal, // aLoadingPrincipal
                                                null, // aTriggeringPrincipal
                                                Ci.nsILoadInfo.SEC_NORMAL,
                                                Ci.nsIContentPolicy.TYPE_OTHER);
+    if (linkDownload)
+      channel.contentDispositionFilename = linkDownload;
     if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
       let docIsPrivate = PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser);
       channel.setPrivate(docIsPrivate);
     }
     channel.notificationCallbacks = new callbacks();
 
     let flags = Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS;
 
@@ -1306,19 +1319,19 @@ nsContextMenu.prototype = {
                            timer.TYPE_ONE_SHOT);
 
     // kick off the channel with our proxy object as the listener
     channel.asyncOpen(new saveAsListener(), null);
   },
 
   // Save URL of clicked-on link.
   saveLink: function() {
-    var doc =  this.target.ownerDocument;
     urlSecurityCheck(this.linkURL, this.principal);
-    this.saveHelper(this.linkURL, this.linkText, null, true, doc);
+    this.saveHelper(this.linkURL, this.linkText, null, true, this.ownerDoc,
+                    this.linkDownload);
   },
 
   // Backwards-compatibility wrapper
   saveImage : function() {
     if (this.onCanvas || this.onImage)
         this.saveMedia();
   },
 
@@ -1336,17 +1349,17 @@ nsContextMenu.prototype = {
       let uri = gContextMenuContentData.documentURIObject;
       saveImageURL(this.mediaURL, null, "SaveImageTitle", false,
                    false, uri, doc, gContextMenuContentData.contentType,
                    gContextMenuContentData.contentDisposition);
     }
     else if (this.onVideo || this.onAudio) {
       urlSecurityCheck(this.mediaURL, this.principal);
       var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
-      this.saveHelper(this.mediaURL, null, dialogTitle, false, doc);
+      this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, "");
     }
   },
 
   // Backwards-compatibility wrapper
   sendImage : function() {
     if (this.onCanvas || this.onImage)
         this.sendMedia();
   },
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tab-content.js
@@ -0,0 +1,619 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* This content script contains code that requires a tab browser. */
+
+let {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
+  "resource:///modules/E10SUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+  "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+  "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AboutReader",
+  "resource://gre/modules/AboutReader.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
+  "resource://gre/modules/ReaderMode.jsm");
+XPCOMUtils.defineLazyGetter(this, "SimpleServiceDiscovery", function() {
+  let ssdp = Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm", {}).SimpleServiceDiscovery;
+  // Register targets
+  ssdp.registerDevice({
+    id: "roku:ecp",
+    target: "roku:ecp",
+    factory: function(aService) {
+      Cu.import("resource://gre/modules/RokuApp.jsm");
+      return new RokuApp(aService);
+    },
+    mirror: true,
+    types: ["video/mp4"],
+    extensions: ["mp4"]
+  });
+  return ssdp;
+});
+
+// TabChildGlobal
+var global = this;
+
+
+addMessageListener("Browser:HideSessionRestoreButton", function (message) {
+  // Hide session restore button on about:home
+  let doc = content.document;
+  let container;
+  if (doc.documentURI.toLowerCase() == "about:home" &&
+      (container = doc.getElementById("sessionRestoreContainer"))) {
+    container.hidden = true;
+  }
+});
+
+
+addMessageListener("Browser:Reload", function(message) {
+  /* First, we'll try to use the session history object to reload so
+   * that framesets are handled properly. If we're in a special
+   * window (such as view-source) that has no session history, fall
+   * back on using the web navigation's reload method.
+   */
+
+  let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+  try {
+    let sh = webNav.sessionHistory;
+    if (sh)
+      webNav = sh.QueryInterface(Ci.nsIWebNavigation);
+  } catch (e) {
+  }
+
+  let reloadFlags = message.data.flags;
+  let handlingUserInput;
+  try {
+    handlingUserInput = content.QueryInterface(Ci.nsIInterfaceRequestor)
+                               .getInterface(Ci.nsIDOMWindowUtils)
+                               .setHandlingUserInput(message.data.handlingUserInput);
+    webNav.reload(reloadFlags);
+  } catch (e) {
+  } finally {
+    handlingUserInput.destruct();
+  }
+});
+
+addMessageListener("MixedContent:ReenableProtection", function() {
+  docShell.mixedContentChannel = null;
+});
+
+addMessageListener("SecondScreen:tab-mirror", function(message) {
+  if (!Services.prefs.getBoolPref("browser.casting.enabled")) {
+    return;
+  }
+  let app = SimpleServiceDiscovery.findAppForService(message.data.service);
+  if (app) {
+    let width = content.innerWidth;
+    let height = content.innerHeight;
+    let viewport = {cssWidth: width, cssHeight: height, width: width, height: height};
+    app.mirror(function() {}, content, viewport, function() {}, content);
+  }
+});
+
+let AboutHomeListener = {
+  init: function(chromeGlobal) {
+    chromeGlobal.addEventListener('AboutHomeLoad', this, false, true);
+  },
+
+  get isAboutHome() {
+    return content.document.documentURI.toLowerCase() == "about:home";
+  },
+
+  handleEvent: function(aEvent) {
+    if (!this.isAboutHome) {
+      return;
+    }
+    switch (aEvent.type) {
+      case "AboutHomeLoad":
+        this.onPageLoad();
+        break;
+      case "AboutHomeSearchEvent":
+        this.onSearch(aEvent);
+        break;
+      case "AboutHomeSearchPanel":
+        this.onOpenSearchPanel(aEvent);
+        break;
+      case "click":
+        this.onClick(aEvent);
+        break;
+      case "pagehide":
+        this.onPageHide(aEvent);
+        break;
+    }
+  },
+
+  receiveMessage: function(aMessage) {
+    if (!this.isAboutHome) {
+      return;
+    }
+    switch (aMessage.name) {
+      case "AboutHome:Update":
+        this.onUpdate(aMessage.data);
+        break;
+      case "AboutHome:FocusInput":
+        this.onFocusInput();
+        break;
+    }
+  },
+
+  onUpdate: function(aData) {
+    let doc = content.document;
+    if (aData.showRestoreLastSession && !PrivateBrowsingUtils.isContentWindowPrivate(content))
+      doc.getElementById("launcher").setAttribute("session", "true");
+
+    // Inject search engine and snippets URL.
+    let docElt = doc.documentElement;
+    // set the following attributes BEFORE searchEngineName, which triggers to
+    // show the snippets when it's set.
+    docElt.setAttribute("snippetsURL", aData.snippetsURL);
+    if (aData.showKnowYourRights)
+      docElt.setAttribute("showKnowYourRights", "true");
+    docElt.setAttribute("snippetsVersion", aData.snippetsVersion);
+    docElt.setAttribute("searchEngineName", aData.defaultEngineName);
+  },
+
+  onPageLoad: function() {
+    let doc = content.document;
+    if (doc.documentElement.hasAttribute("hasBrowserHandlers")) {
+      return;
+    }
+
+    doc.documentElement.setAttribute("hasBrowserHandlers", "true");
+    addMessageListener("AboutHome:Update", this);
+    addMessageListener("AboutHome:FocusInput", this);
+    addEventListener("click", this, true);
+    addEventListener("pagehide", this, true);
+
+    if (!Services.prefs.getBoolPref("browser.search.showOneOffButtons")) {
+      doc.documentElement.setAttribute("searchUIConfiguration", "oldsearchui");
+    }
+
+    sendAsyncMessage("AboutHome:RequestUpdate");
+    doc.addEventListener("AboutHomeSearchEvent", this, true, true);
+    doc.addEventListener("AboutHomeSearchPanel", this, true, true);
+  },
+
+  onClick: function(aEvent) {
+    if (!aEvent.isTrusted || // Don't trust synthetic events
+        aEvent.button == 2 || aEvent.target.localName != "button") {
+      return;
+    }
+
+    let originalTarget = aEvent.originalTarget;
+    let ownerDoc = originalTarget.ownerDocument;
+    if (ownerDoc.documentURI != "about:home") {
+      // This shouldn't happen, but we're being defensive.
+      return;
+    }
+
+    let elmId = originalTarget.getAttribute("id");
+
+    switch (elmId) {
+      case "restorePreviousSession":
+        sendAsyncMessage("AboutHome:RestorePreviousSession");
+        ownerDoc.getElementById("launcher").removeAttribute("session");
+        break;
+
+      case "downloads":
+        sendAsyncMessage("AboutHome:Downloads");
+        break;
+
+      case "bookmarks":
+        sendAsyncMessage("AboutHome:Bookmarks");
+        break;
+
+      case "history":
+        sendAsyncMessage("AboutHome:History");
+        break;
+
+      case "apps":
+        sendAsyncMessage("AboutHome:Apps");
+        break;
+
+      case "addons":
+        sendAsyncMessage("AboutHome:Addons");
+        break;
+
+      case "sync":
+        sendAsyncMessage("AboutHome:Sync");
+        break;
+
+      case "settings":
+        sendAsyncMessage("AboutHome:Settings");
+        break;
+
+      case "searchIcon":
+        sendAsyncMessage("AboutHome:OpenSearchPanel", null, { anchor: originalTarget });
+        break;
+    }
+  },
+
+  onPageHide: function(aEvent) {
+    if (aEvent.target.defaultView.frameElement) {
+      return;
+    }
+    removeMessageListener("AboutHome:Update", this);
+    removeEventListener("click", this, true);
+    removeEventListener("pagehide", this, true);
+    if (aEvent.target.documentElement) {
+      aEvent.target.documentElement.removeAttribute("hasBrowserHandlers");
+    }
+  },
+
+  onSearch: function(aEvent) {
+    sendAsyncMessage("AboutHome:Search", { searchData: aEvent.detail });
+  },
+
+  onOpenSearchPanel: function(aEvent) {
+    sendAsyncMessage("AboutHome:OpenSearchPanel");
+  },
+
+  onFocusInput: function () {
+    let searchInput = content.document.getElementById("searchText");
+    if (searchInput) {
+      searchInput.focus();
+    }
+  },
+};
+AboutHomeListener.init(this);
+
+let AboutPrivateBrowsingListener = {
+  init(chromeGlobal) {
+    chromeGlobal.addEventListener("AboutPrivateBrowsingOpenWindow", this,
+                                  false, true);
+  },
+
+  get isAboutPrivateBrowsing() {
+    return content.document.documentURI.toLowerCase() == "about:privatebrowsing";
+  },
+
+  handleEvent(aEvent) {
+    if (!this.isAboutPrivateBrowsing) {
+      return;
+    }
+    switch (aEvent.type) {
+      case "AboutPrivateBrowsingOpenWindow":
+        sendAsyncMessage("AboutPrivateBrowsing:OpenPrivateWindow");
+        break;
+    }
+  },
+};
+AboutPrivateBrowsingListener.init(this);
+
+let AboutReaderListener = {
+
+  _articlePromise: null,
+
+  init: function() {
+    addEventListener("AboutReaderContentLoaded", this, false, true);
+    addEventListener("DOMContentLoaded", this, false);
+    addEventListener("pageshow", this, false);
+    addEventListener("pagehide", this, false);
+    addMessageListener("Reader:ParseDocument", this);
+    addMessageListener("Reader:PushState", this);
+  },
+
+  receiveMessage: function(message) {
+    switch (message.name) {
+      case "Reader:ParseDocument":
+        this._articlePromise = ReaderMode.parseDocument(content.document).catch(Cu.reportError);
+        content.document.location = "about:reader?url=" + encodeURIComponent(message.data.url);
+        break;
+
+      case "Reader:PushState":
+        this.updateReaderButton();
+        break;
+    }
+  },
+
+  get isAboutReader() {
+    return content.document.documentURI.startsWith("about:reader");
+  },
+
+  handleEvent: function(aEvent) {
+    if (aEvent.originalTarget.defaultView != content) {
+      return;
+    }
+
+    switch (aEvent.type) {
+      case "AboutReaderContentLoaded":
+        if (!this.isAboutReader) {
+          return;
+        }
+
+        if (content.document.body) {
+          // Update the toolbar icon to show the "reader active" icon.
+          sendAsyncMessage("Reader:UpdateReaderButton");
+          new AboutReader(global, content, this._articlePromise);
+          this._articlePromise = null;
+        }
+        break;
+
+      case "pagehide":
+        sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false });
+        break;
+
+      case "pageshow":
+        // If a page is loaded from the bfcache, we won't get a "DOMContentLoaded"
+        // event, so we need to rely on "pageshow" in this case.
+        if (aEvent.persisted) {
+          this.updateReaderButton();
+        }
+        break;
+      case "DOMContentLoaded":
+        this.updateReaderButton();
+        break;
+
+    }
+  },
+  updateReaderButton: function() {
+    if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader ||
+        !(content.document instanceof content.HTMLDocument) ||
+        content.document.mozSyntheticDocument) {
+      return;
+    }
+    // Only send updates when there are articles; there's no point updating with
+    // |false| all the time.
+    if (ReaderMode.isProbablyReaderable(content.document)) {
+      sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: true });
+    }
+  },
+};
+AboutReaderListener.init();
+
+
+let ContentSearchMediator = {
+
+  whitelist: new Set([
+    "about:home",
+    "about:newtab",
+  ]),
+
+  init: function (chromeGlobal) {
+    chromeGlobal.addEventListener("ContentSearchClient", this, true, true);
+    addMessageListener("ContentSearch", this);
+  },
+
+  handleEvent: function (event) {
+    if (this._contentWhitelisted) {
+      this._sendMsg(event.detail.type, event.detail.data);
+    }
+  },
+
+  receiveMessage: function (msg) {
+    if (msg.data.type == "AddToWhitelist") {
+      for (let uri of msg.data.data) {
+        this.whitelist.add(uri);
+      }
+      this._sendMsg("AddToWhitelistAck");
+      return;
+    }
+    if (this._contentWhitelisted) {
+      this._fireEvent(msg.data.type, msg.data.data);
+    }
+  },
+
+  get _contentWhitelisted() {
+    return this.whitelist.has(content.document.documentURI);
+  },
+
+  _sendMsg: function (type, data=null) {
+    sendAsyncMessage("ContentSearch", {
+      type: type,
+      data: data,
+    });
+  },
+
+  _fireEvent: function (type, data=null) {
+    let event = Cu.cloneInto({
+      detail: {
+        type: type,
+        data: data,
+      },
+    }, content);
+    content.dispatchEvent(new content.CustomEvent("ContentSearchService",
+                                                  event));
+  },
+};
+ContentSearchMediator.init(this);
+
+let PageStyleHandler = {
+  init: function() {
+    addMessageListener("PageStyle:Switch", this);
+    addMessageListener("PageStyle:Disable", this);
+
+    // Send a CPOW to the parent so that it can synchronously request
+    // the list of style sheets.
+    sendSyncMessage("PageStyle:SetSyncHandler", {}, {syncHandler: this});
+  },
+
+  get markupDocumentViewer() {
+    return docShell.contentViewer;
+  },
+
+  // Called synchronously via CPOW from the parent.
+  getStyleSheetInfo: function() {
+    let styleSheets = this._filterStyleSheets(this.getAllStyleSheets());
+    return {
+      styleSheets: styleSheets,
+      authorStyleDisabled: this.markupDocumentViewer.authorStyleDisabled,
+      preferredStyleSheetSet: content.document.preferredStyleSheetSet
+    };
+  },
+
+  // Called synchronously via CPOW from the parent.
+  getAllStyleSheets: function(frameset = content) {
+    let selfSheets = Array.slice(frameset.document.styleSheets);
+    let subSheets = Array.map(frameset.frames, frame => this.getAllStyleSheets(frame));
+    return selfSheets.concat(...subSheets);
+  },
+
+  receiveMessage: function(msg) {
+    switch (msg.name) {
+      case "PageStyle:Switch":
+        this.markupDocumentViewer.authorStyleDisabled = false;
+        this._stylesheetSwitchAll(content, msg.data.title);
+        break;
+
+      case "PageStyle:Disable":
+        this.markupDocumentViewer.authorStyleDisabled = true;
+        break;
+    }
+  },
+
+  _stylesheetSwitchAll: function (frameset, title) {
+    if (!title || this._stylesheetInFrame(frameset, title)) {
+      this._stylesheetSwitchFrame(frameset, title);
+    }
+
+    for (let i = 0; i < frameset.frames.length; i++) {
+      // Recurse into sub-frames.
+      this._stylesheetSwitchAll(frameset.frames[i], title);
+    }
+  },
+
+  _stylesheetSwitchFrame: function (frame, title) {
+    var docStyleSheets = frame.document.styleSheets;
+
+    for (let i = 0; i < docStyleSheets.length; ++i) {
+      let docStyleSheet = docStyleSheets[i];
+      if (docStyleSheet.title) {
+        docStyleSheet.disabled = (docStyleSheet.title != title);
+      } else if (docStyleSheet.disabled) {
+        docStyleSheet.disabled = false;
+      }
+    }
+  },
+
+  _stylesheetInFrame: function (frame, title) {
+    return Array.some(frame.document.styleSheets, (styleSheet) => styleSheet.title == title);
+  },
+
+  _filterStyleSheets: function(styleSheets) {
+    let result = [];
+
+    for (let currentStyleSheet of styleSheets) {
+      if (!currentStyleSheet.title)
+        continue;
+
+      // Skip any stylesheets that don't match the screen media type.
+      if (currentStyleSheet.media.length > 0) {
+        let mediaQueryList = currentStyleSheet.media.mediaText;
+        if (!content.matchMedia(mediaQueryList).matches) {
+          continue;
+        }
+      }
+
+      result.push({title: currentStyleSheet.title,
+                   disabled: currentStyleSheet.disabled});
+    }
+
+    return result;
+  },
+};
+PageStyleHandler.init();
+
+// Keep a reference to the translation content handler to avoid it it being GC'ed.
+let trHandler = null;
+if (Services.prefs.getBoolPref("browser.translation.detectLanguage")) {
+  Cu.import("resource:///modules/translation/TranslationContentHandler.jsm");
+  trHandler = new TranslationContentHandler(global, docShell);
+}
+
+function gKeywordURIFixup(fixupInfo) {
+  fixupInfo.QueryInterface(Ci.nsIURIFixupInfo);
+
+  // Ignore info from other docshells
+  let parent = fixupInfo.consumer.QueryInterface(Ci.nsIDocShellTreeItem).sameTypeRootTreeItem;
+  if (parent != docShell)
+    return;
+
+  let data = {};
+  for (let f of Object.keys(fixupInfo)) {
+    if (f == "consumer" || typeof fixupInfo[f] == "function")
+      continue;
+
+    if (fixupInfo[f] && fixupInfo[f] instanceof Ci.nsIURI) {
+      data[f] = fixupInfo[f].spec;
+    } else {
+      data[f] = fixupInfo[f];
+    }
+  }
+
+  sendAsyncMessage("Browser:URIFixup", data);
+}
+Services.obs.addObserver(gKeywordURIFixup, "keyword-uri-fixup", false);
+addEventListener("unload", () => {
+  Services.obs.removeObserver(gKeywordURIFixup, "keyword-uri-fixup");
+}, false);
+
+addMessageListener("Browser:AppTab", function(message) {
+  docShell.isAppTab = message.data.isAppTab;
+});
+
+let WebBrowserChrome = {
+  onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) {
+    return BrowserUtils.onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab);
+  },
+
+  // Check whether this URI should load in the current process
+  shouldLoadURI: function(aDocShell, aURI, aReferrer) {
+    if (!E10SUtils.shouldLoadURI(aDocShell, aURI, aReferrer)) {
+      E10SUtils.redirectLoad(aDocShell, aURI, aReferrer);
+      return false;
+    }
+
+    return true;
+  },
+};
+
+if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+  let tabchild = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsITabChild);
+  tabchild.webBrowserChrome = WebBrowserChrome;
+}
+
+
+let DOMFullscreenHandler = {
+  _fullscreenDoc: null,
+
+  init: function() {
+    addMessageListener("DOMFullscreen:Approved", this);
+    addMessageListener("DOMFullscreen:CleanUp", this);
+    addEventListener("MozEnteredDomFullscreen", this);
+  },
+
+  receiveMessage: function(aMessage) {
+    switch(aMessage.name) {
+      case "DOMFullscreen:Approved": {
+        if (this._fullscreenDoc) {
+          Services.obs.notifyObservers(this._fullscreenDoc,
+                                       "fullscreen-approved",
+                                       "");
+        }
+        break;
+      }
+      case "DOMFullscreen:CleanUp": {
+        this._fullscreenDoc = null;
+        break;
+      }
+    }
+  },
+
+  handleEvent: function(aEvent) {
+    if (aEvent.type == "MozEnteredDomFullscreen") {
+      this._fullscreenDoc = aEvent.target;
+      sendAsyncMessage("MozEnteredDomFullscreen", {
+        origin: this._fullscreenDoc.nodePrincipal.origin,
+      });
+    }
+  }
+};
+DOMFullscreenHandler.init();
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -137,34 +137,31 @@ skip-if = e10s # Bug 1101993 - times out
 [browser_autocomplete_oldschool_wrap.js]
 [browser_autocomplete_tag_star_visibility.js]
 [browser_backButtonFitts.js]
 skip-if = os != "win" || e10s # The Fitts Law back button is only supported on Windows (bug 571454) / e10s - Bug 1099154: test touches content (attempts to add an event listener directly to the contentWindow)
 [browser_beforeunload_duplicate_dialogs.js]
 skip-if = e10s # bug 967873 means permitUnload doesn't work in e10s mode
 [browser_blob-channelname.js]
 [browser_bookmark_titles.js]
-skip-if = buildapp == 'mulet' || toolkit == "windows" || e10s # Disabled on Windows due to frequent failures (bugs 825739, 841341) / e10s - Bug 1094205 - places doesn't return the right thing in e10s mode, for some reason
+skip-if = buildapp == 'mulet' || toolkit == "windows" # Disabled on Windows due to frequent failures (bugs 825739, 841341)
 [browser_bug304198.js]
 skip-if = e10s
 [browser_bug321000.js]
 skip-if = true # browser_bug321000.js is disabled because newline handling is shaky (bug 592528)
 [browser_bug329212.js]
-skip-if = e10s
 [browser_bug331772_xul_tooltiptext_in_html.js]
-skip-if = e10s
 [browser_bug356571.js]
 [browser_bug380960.js]
 [browser_bug386835.js]
 skip-if = e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s
 [browser_bug405137.js]
 [browser_bug406216.js]
 [browser_bug409481.js]
 [browser_bug409624.js]
-skip-if = e10s
 [browser_bug413915.js]
 [browser_bug416661.js]
 skip-if = e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s
 [browser_bug417483.js]
 skip-if = e10s # Bug 1093155 - tries to use context menu from browser-chrome and gets in a mess when in e10s mode
 [browser_bug419612.js]
 skip-if = e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s
 [browser_bug422590.js]
@@ -176,28 +173,26 @@ skip-if = e10s # Bug 1093155 - tries to 
 [browser_bug431826.js]
 [browser_bug432599.js]
 [browser_bug435035.js]
 [browser_bug435325.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1099156 - test directly manipulates content
 [browser_bug441778.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s
 [browser_bug455852.js]
-skip-if = e10s
 [browser_bug460146.js]
 skip-if = e10s # Bug 866413 - PageInfo doesn't work in e10s
 [browser_bug462289.js]
 skip-if = toolkit == "cocoa" || e10s # Bug 1102017 - middle-button mousedown on selected tab2 does not activate tab - Didn't expect [object XULElement], but got it
 [browser_bug462673.js]
 [browser_bug477014.js]
 [browser_bug479408.js]
 skip-if = buildapp == 'mulet'
 [browser_bug481560.js]
 [browser_bug484315.js]
-skip-if = e10s
 [browser_bug491431.js]
 skip-if = buildapp == 'mulet'
 [browser_bug495058.js]
 [browser_bug517902.js]
 skip-if = e10s # Bug 866413 - PageInfo doesn't work in e10s
 [browser_bug519216.js]
 [browser_bug520538.js]
 [browser_bug521216.js]
@@ -211,17 +206,16 @@ skip-if = e10s # Bug 1102020 - test trie
 skip-if = buildapp == 'mulet' # Bug 1066070 - I don't think either popup notifications nor addon install stuff works on mulet?
 [browser_bug555224.js]
 skip-if = e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s
 [browser_bug555767.js]
 skip-if = e10s # Bug 1093373 - relies on browser.sessionHistory
 [browser_bug556061.js]
 [browser_bug559991.js]
 [browser_bug561623.js]
-skip-if = e10s
 [browser_bug561636.js]
 [browser_bug562649.js]
 [browser_bug563588.js]
 [browser_bug565575.js]
 skip-if = e10s
 [browser_bug565667.js]
 skip-if = toolkit != "cocoa"
 [browser_bug567306.js]
@@ -232,17 +226,16 @@ skip-if = e10s # Bug 1056146 - zoom test
 [browser_bug578534.js]
 [browser_bug579872.js]
 [browser_bug580638.js]
 [browser_bug580956.js]
 [browser_bug581242.js]
 [browser_bug581253.js]
 skip-if = e10s # Bug 1093756 - can't bookmark the data: url in e10s somehow
 [browser_bug581947.js]
-skip-if = e10s
 [browser_bug585558.js]
 [browser_bug585785.js]
 [browser_bug585830.js]
 [browser_bug590206.js]
 [browser_bug592338.js]
 skip-if = e10s # Bug 653065 - Make the lightweight theme web installer ready for e10s
 [browser_bug594131.js]
 [browser_bug595507.js]
@@ -267,17 +260,16 @@ skip-if = e10s # bug 1102331 - does focu
 [browser_bug719271.js]
 skip-if = e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s
 [browser_bug724239.js]
 [browser_bug734076.js]
 skip-if = e10s # Bug 1093155 - tries to use context menu from browser-chrome and gets in a mess when in e10s mode
 [browser_bug735471.js]
 [browser_bug749738.js]
 [browser_bug763468_perwindowpb.js]
-skip-if = e10s
 [browser_bug767836_perwindowpb.js]
 [browser_bug771331.js]
 [browser_bug783614.js]
 [browser_bug817947.js]
 [browser_bug822367.js]
 [browser_bug832435.js]
 [browser_bug839103.js]
 [browser_bug880101.js]
@@ -291,17 +283,16 @@ skip-if = buildapp == "mulet" || e10s # 
 skip-if = e10s # Bug 1093155 - tries to use context menu from browser-chrome and gets in a mess when in e10s mode
 [browser_bug1015721.js]
 skip-if = os == 'win' || e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s
 [browser_bug1064280_changeUrlInPinnedTab.js]
 [browser_bug1070778.js]
 [browser_canonizeURL.js]
 skip-if = e10s # Bug 1094510 - test hits the network in e10s mode only
 [browser_contentAreaClick.js]
-skip-if = e10s
 [browser_contextSearchTabPosition.js]
 skip-if = os == "mac" || e10s # bug 967013; e10s: bug 1094761 - test hits the network in e10s, causing next test to crash
 [browser_ctrlTab.js]
 [browser_datareporting_notification.js]
 skip-if = !datareporting
 [browser_devedition.js]
 [browser_devices_get_user_media.js]
 skip-if = buildapp == 'mulet' || (os == "linux" && debug) || e10s # linux: bug 976544; e10s: bug 1071623
@@ -311,17 +302,16 @@ skip-if = e10s # Bug 1071623
 skip-if = e10s # Bug 1071623
 [browser_discovery.js]
 [browser_double_close_tab.js]
 skip-if = e10s
 [browser_duplicateIDs.js]
 [browser_drag.js]
 skip-if = true # browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638.
 [browser_favicon_change.js]
-skip-if = e10s
 [browser_favicon_change_not_in_document.js]
 skip-if = e10s
 [browser_findbarClose.js]
 [browser_fullscreen-window-open.js]
 skip-if = buildapp == 'mulet' || e10s || os == "linux" # Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly. Linux: Intermittent failures - bug 941575.
 [browser_fxa_migrate.js]
 [browser_fxa_oauth.js]
 [browser_fxa_profile_channel.js]
@@ -363,32 +353,31 @@ skip-if = buildapp == 'mulet' || e10s # 
 skip-if = e10s
 [browser_parsable_script.js]
 skip-if = asan # Disabled because it takes a long time (see test for more information)
 
 [browser_pinnedTabs.js]
 [browser_plainTextLinks.js]
 skip-if = e10s # Bug 1093155 - tries to use context menu from browser-chrome and gets in a mess when in e10s mode
 [browser_popupUI.js]
-skip-if = buildapp == 'mulet' || e10s # Bug 1100707 - test fails in e10s because it can't get accel-w to close the popup (?)
+skip-if = buildapp == 'mulet'
 [browser_popup_blocker.js]
 skip-if = e10s && debug # Frequent bug 1125520 failures
 [browser_printpreview.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1101973 - breaks the next test in e10s, and may be responsible for later timeout after logging "Error: Channel closing: too late to send/recv, messages will be lost"
 [browser_private_browsing_window.js]
 skip-if = buildapp == 'mulet'
 [browser_private_no_prompt.js]
 skip-if = buildapp == 'mulet'
 [browser_relatedTabs.js]
 [browser_remoteTroubleshoot.js]
 support-files =
   test_remoteTroubleshoot.html
 [browser_removeTabsToTheEnd.js]
 [browser_removeUnsafeProtocolsFromURLBarPaste.js]
-skip-if = e10s
 [browser_restore_isAppTab.js]
 [browser_sanitize-passwordDisabledHosts.js]
 [browser_sanitize-sitepermissions.js]
 [browser_sanitize-timespans.js]
 skip-if = buildapp == 'mulet'
 [browser_sanitizeDialog.js]
 skip-if = buildapp == 'mulet'
 [browser_save_link-perwindowpb.js]
@@ -452,17 +441,16 @@ skip-if = e10s # Bug 1100700 - test reli
 [browser_urlbarAutoFillTrimURLs.js]
 skip-if = e10s # Bug 1093941 - Waits indefinitely for onSearchComplete
 [browser_urlbarCopying.js]
 [browser_urlbarEnter.js]
 skip-if = e10s # Bug 1093941 - used to cause obscure non-windows child process crashes on try
 [browser_urlbarEnterAfterMouseOver.js]
 skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
 [browser_urlbarRevert.js]
-skip-if = e10s # Bug 1093941 - ESC reverted the location bar value - Got foobar, expected example.com
 [browser_urlbarSearchSingleWordNotification.js]
 [browser_urlbarStop.js]
 [browser_urlbarTrimURLs.js]
 [browser_urlbar_search_healthreport.js]
 [browser_utilityOverlay.js]
 [browser_visibleFindSelection.js]
 [browser_visibleLabel.js]
 [browser_visibleTabs.js]
@@ -478,19 +466,17 @@ skip-if = buildapp == 'mulet'
 [browser_wyciwyg_urlbarCopying.js]
 skip-if = e10s # Bug 1100703 - test directly manipulates content (content.document.getElementById)
 [browser_zbug569342.js]
 skip-if = e10s # Bug 1094240 - has findbar-related failures
 [browser_registerProtocolHandler_notification.js]
 skip-if = e10s # Bug 940206 - nsIWebContentHandlerRegistrar::registerProtocolHandler doesn't work in e10s
 [browser_no_mcb_on_http_site.js]
 [browser_bug1003461-switchtab-override.js]
-skip-if = e10s
 [browser_bug1024133-switchtab-override-keynav.js]
-skip-if = e10s
 [browser_bug1025195_switchToTabHavingURI_aOpenParams.js]
 [browser_addCertException.js]
 skip-if = e10s # Bug 1100687 - test directly manipulates content (content.document.getElementById)
 [browser_bug1045809.js]
 [browser_e10s_switchbrowser.js]
 [browser_e10s_about_process.js]
 [browser_e10s_chrome_process.js]
 [browser_e10s_javascript.js]
--- a/browser/base/content/test/general/browser_bug963945.js
+++ b/browser/base/content/test/general/browser_bug963945.js
@@ -2,29 +2,22 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*
  * This test ensures the about:addons tab is only
  * opened one time when in private browsing.
  */
 
-function test() {
-  waitForExplicitFinish();
-	
-  var win = OpenBrowserWindow({private: true});
+add_task(function* test() {
+  let win = yield BrowserTestUtils.openNewBrowserWindow({private: true});
 
-  whenDelayedStartupFinished(win, function() {
-    win.gBrowser.loadURI("about:addons");
-
-    waitForFocus(function() {
-      EventUtils.synthesizeKey("a", { ctrlKey: true, shiftKey: true }, win);
+  let tab = win.gBrowser.selectedTab = win.gBrowser.addTab("about:addons");
+  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield promiseWaitForFocus(win);
 
-      is(win.gBrowser.tabs.length, 1, "about:addons tab was re-focused.");
-      is(win.gBrowser.currentURI.spec, "about:addons", "Addons tab was opened.");
+  EventUtils.synthesizeKey("a", { ctrlKey: true, shiftKey: true }, win);
 
-      win.close();
-      finish();    
-    });
-  });
-}
+  is(win.gBrowser.tabs.length, 2, "about:addons tab was re-focused.");
+  is(win.gBrowser.currentURI.spec, "about:addons", "Addons tab was opened.");
 
-
+  yield BrowserTestUtils.closeWindow(win);
+});
--- a/browser/base/content/test/general/browser_readerMode.js
+++ b/browser/base/content/test/general/browser_readerMode.js
@@ -12,17 +12,17 @@ const TEST_PREFS = [
   ["browser.readinglist.enabled", true],
   ["browser.readinglist.introShown", false],
 ];
 
 const TEST_PATH = "http://example.com/browser/browser/base/content/test/general/";
 
 let readerButton = document.getElementById("reader-mode-button");
 
-add_task(function* () {
+add_task(function* test_reader_button() {
   registerCleanupFunction(function() {
     // Reset test prefs.
     TEST_PREFS.forEach(([name, value]) => {
       Services.prefs.clearUserPref(name);
     });
     while (gBrowser.tabs.length > 1) {
       gBrowser.removeCurrentTab();
     }
@@ -85,8 +85,21 @@ add_task(function* () {
   yield promiseWaitForCondition(() => readerButton.hidden);
   is_element_hidden(readerButton, "Reader mode button is not present on a non-reader-able page");
 
   // Switch back to the original tab to make sure reader mode button is still visible.
   gBrowser.removeCurrentTab();
   yield promiseWaitForCondition(() => !readerButton.hidden);
   is_element_visible(readerButton, "Reader mode button is present on a reader-able page");
 });
+
+add_task(function* test_getOriginalUrl() {
+  let { ReaderMode } = Cu.import("resource://gre/modules/ReaderMode.jsm", {});
+  let url = "http://foo.com/article.html";
+
+  is(ReaderMode.getOriginalUrl("about:reader?url=" + encodeURIComponent(url)), url, "Found original URL from encoded URL");
+  is(ReaderMode.getOriginalUrl("about:reader?foobar"), null, "Did not find original URL from malformed reader URL");
+  is(ReaderMode.getOriginalUrl(url), null, "Did not find original URL from non-reader URL");
+
+  let badUrl = "http://foo.com/?;$%^^";
+  is(ReaderMode.getOriginalUrl("about:reader?url=" + encodeURIComponent(badUrl)), badUrl, "Found original URL from encoded malformed URL");
+  is(ReaderMode.getOriginalUrl("about:reader?url=" + badUrl), badUrl, "Found original URL from non-encoded malformed URL");
+});
--- a/browser/base/content/test/general/browser_ssl_error_reports.js
+++ b/browser/base/content/test/general/browser_ssl_error_reports.js
@@ -15,73 +15,42 @@ add_task(function* test_send_report_manu
 });
 
 add_task(function* test_send_report_manual_nocert() {
   yield testSendReportManual(noCertURL, "nocert");
 });
 
 // creates a promise of the message in an error page
 function createNetworkErrorMessagePromise(aBrowser) {
-  let progressListener;
   let promise = new Promise(function(resolve, reject) {
-    // Error pages do not fire "load" events, so use a progressListener.
     let originalDocumentURI = aBrowser.contentDocument.documentURI;
 
-    progressListener = {
-      onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
-        // Make sure nothing other than an error page is loaded.
-        if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE)) {
-          reject("location change was not to an error page");
-        }
-      },
+    let loadedListener = function() {
+      let doc = aBrowser.contentDocument;
 
-      onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
-        let doc = aBrowser.contentDocument;
-
-        if (doc && doc.getElementById("reportCertificateError")) {
-          // Wait until the documentURI changes (from about:blank) this should
-          // be the error page URI.
-          let documentURI = doc.documentURI;
-          if (documentURI == originalDocumentURI) {
-            return;
-          }
+      if (doc && doc.getElementById("reportCertificateError")) {
+        let documentURI = doc.documentURI;
 
-          aWebProgress.removeProgressListener(progressListener,
-            Ci.nsIWebProgress.NOTIFY_LOCATION |
-            Ci.nsIWebProgress.NOTIFY_STATE_REQUEST);
-          let matchArray = /about:neterror\?.*&d=([^&]*)/.exec(documentURI);
-          if (!matchArray) {
-            reject("no network error message found in URI")
+        aBrowser.removeEventListener("DOMContentLoaded", loadedListener, true);
+        let matchArray = /about:neterror\?.*&d=([^&]*)/.exec(documentURI);
+        if (!matchArray) {
+          reject("no network error message found in URI");
           return;
-          }
-
-          let errorMsg = matchArray[1];
-          resolve(decodeURIComponent(errorMsg));
         }
-      },
 
-      QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
-                          Ci.nsISupportsWeakReference])
+        let errorMsg = matchArray[1];
+        resolve(decodeURIComponent(errorMsg));
+      }
     };
-
-    aBrowser.addProgressListener(progressListener,
-            Ci.nsIWebProgress.NOTIFY_LOCATION |
-            Ci.nsIWebProgress.NOTIFY_STATE_REQUEST);
+    aBrowser.addEventListener("DOMContentLoaded", loadedListener, true);
   });
 
-  // Ensure the weak progress listener is kept alive as long as the promise.
-  createNetworkErrorMessagePromise.listeners.set(promise, progressListener);
-
   return promise;
 }
 
-// Keep a map of promises to their progress listeners so
-// the weak progress listeners aren't GCed too early.
-createNetworkErrorMessagePromise.listeners = new WeakMap();
-
 // check we can set the 'automatically send' pref
 add_task(function* test_set_automatic() {
   setup();
   let tab = gBrowser.addTab(badChainURL, {skipAnimation: true});
   let browser = tab.linkedBrowser;
   let mm = browser.messageManager;
   mm.loadFrameScript(ROOT + "browser_ssl_error_reports_content.js", true);
 
--- a/browser/base/content/test/general/browser_tabDrop.js
+++ b/browser/base/content/test/general/browser_tabDrop.js
@@ -47,20 +47,26 @@ function test() {
   function drop(text, valid) {
     triggeredDropCount++;
     if (valid)
       validDropCount++;
     executeSoon(function () {
       // A drop type of "link" onto an existing tab would normally trigger a
       // load in that same tab, but tabbrowser code in _getDragTargetTab treats
       // drops on the outer edges of a tab differently (loading a new tab
-      // instead). The events created by synthesizeDrop have all of their
+      // instead). Make events created by synthesizeDrop have all of their
       // coordinates set to 0 (screenX/screenY), so they're treated as drops
       // on the outer edge of the tab, thus they open new tabs.
-      ChromeUtils.synthesizeDrop(newTab, newTab, [[{type: "text/plain", data: text}]], "link", window);
+      var event = {
+        clientX: 0,
+        clientY: 0,
+        screenX: 0,
+        screenY: 0,
+      };
+      ChromeUtils.synthesizeDrop(newTab, newTab, [[{type: "text/plain", data: text}]], "link", window, undefined, event);
     });
   }
 
   // Begin and end with valid drops to make sure we wait for all drops before
   // ending the test
   drop("mochi.test/first", true);
   drop("javascript:'bad'");
   drop("jAvascript:'bad'");
--- a/browser/base/content/test/general/browser_tabReorder.js
+++ b/browser/base/content/test/general/browser_tabReorder.js
@@ -13,64 +13,31 @@ function test() {
     }
   });
 
   is(gBrowser.tabs.length, initialTabsLength + 3, "new tabs are opened");
   is(gBrowser.tabs[initialTabsLength], newTab1, "newTab1 position is correct");
   is(gBrowser.tabs[initialTabsLength + 1], newTab2, "newTab2 position is correct");
   is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 position is correct");
 
-  let dataTransfer;
-  let trapDrag = function(event) {
-    dataTransfer = event.dataTransfer;
-  };
-  window.addEventListener("dragstart", trapDrag, true);
-  registerCleanupFunction(function () {
-    window.removeEventListener("dragstart", trapDrag, true);
-  });
-
-  let windowUtil = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
-                          getInterface(Components.interfaces.nsIDOMWindowUtils);
-  let ds = Components.classes["@mozilla.org/widget/dragservice;1"].
-           getService(Components.interfaces.nsIDragService);
+  let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+                     getService(Ci.mozIJSSubScriptLoader);
+  let ChromeUtils = {};
+  scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js", ChromeUtils);
 
   function dragAndDrop(tab1, tab2, copy) {
-    let ctrlKey = copy;
-    let altKey = copy;
-
-    let rect = tab1.getBoundingClientRect();
-    let x = rect.width / 2;
-    let y = rect.height / 2;
-    let diffX = 10;
-
-    ds.startDragSession();
-    try {
-      EventUtils.synthesizeMouse(tab1, x, y, { type: "mousedown" }, window);
-      EventUtils.synthesizeMouse(tab1, x + diffX, y, { type: "mousemove" }, window);
-
-      dataTransfer.dropEffect = copy ? "copy" : "move";
+    let rect = tab2.getBoundingClientRect();
+    let event = {
+      ctrlKey: copy,
+      altKey: copy,
+      clientX: rect.left + rect.width / 2 + 10,
+      clientY: rect.top + rect.height / 2,
+    };
 
-      let event = window.document.createEvent("DragEvents");
-      event.initDragEvent("dragover", true, true, window, 0,
-                          tab2.boxObject.screenX + x + diffX,
-                          tab2.boxObject.screenY + y,
-                          x + diffX, y, ctrlKey, altKey, false, false, 0, null, dataTransfer);
-      windowUtil.dispatchDOMEventViaPresShell(tab2, event, true);
-
-      event = window.document.createEvent("DragEvents");
-      event.initDragEvent("drop", true, true, window, 0,
-                          tab2.boxObject.screenX + x + diffX,
-                          tab2.boxObject.screenY + y,
-                          x + diffX, y, ctrlKey, altKey, false, false, 0, null, dataTransfer);
-      windowUtil.dispatchDOMEventViaPresShell(tab2, event, true);
-
-      EventUtils.synthesizeMouse(tab2, x + diffX, y, { type: "mouseup" }, window);
-    } finally {
-      ds.endDragSession(true);
-    }
+    ChromeUtils.synthesizeDrop(tab1, tab2, null, copy ? "copy" : "move", window, window, event);
   }
 
   dragAndDrop(newTab1, newTab2, false);
   is(gBrowser.tabs.length, initialTabsLength + 3, "tabs are still there");
   is(gBrowser.tabs[initialTabsLength], newTab2, "newTab2 and newTab1 are swapped");
   is(gBrowser.tabs[initialTabsLength + 1], newTab1, "newTab1 and newTab2 are swapped");
   is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 stays same place");
 
--- a/browser/base/content/test/general/test_no_mcb_on_http_site_font.html
+++ b/browser/base/content/test/general/test_no_mcb_on_http_site_font.html
@@ -14,29 +14,29 @@
   function checkLoadStates() {
    var ui = SpecialPowers.wrap(window)
             .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
              .getInterface(SpecialPowers.Ci.nsIWebNavigation)
              .QueryInterface(SpecialPowers.Ci.nsIDocShell)
              .securityUI;
 
    var loadedMixedActive = ui &&
-     (ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
+     !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
    is(loadedMixedActive, false, "OK: Should not load mixed active content!");
 
    var blockedMixedActive = ui &&
-       (ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
+     !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
    is(blockedMixedActive, false, "OK: Should not block mixed active content!");
 
    var loadedMixedDisplay = ui &&
-     (ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
+     !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
    is(loadedMixedDisplay, false, "OK: Should not load mixed display content!");
 
    var blockedMixedDisplay = ui &&
-     (ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
+     !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
    is(blockedMixedDisplay, false, "OK: Should not block mixed display content!");
 
    var newValue = "Verifying MCB does not trigger warning/error for an http page with https css that includes http font";
    document.getElementById("testDiv").innerHTML = newValue;
   }
 </script>
 </head>
 <body onload="checkLoadStates()">
--- a/browser/base/content/test/general/test_no_mcb_on_http_site_font2.html
+++ b/browser/base/content/test/general/test_no_mcb_on_http_site_font2.html
@@ -14,29 +14,29 @@
   function checkLoadStates() {
    var ui = SpecialPowers.wrap(window)
         .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
              .getInterface(SpecialPowers.Ci.nsIWebNavigation)
              .QueryInterface(SpecialPowers.Ci.nsIDocShell)
              .securityUI;
 
    var loadedMixedActive = ui &&
-     (ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
+     !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
    is(loadedMixedActive, false, "OK: Should not load mixed active content!");
 
    var blockedMixedActive = ui &&
-       (ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
+     !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
    is(blockedMixedActive, false, "OK: Should not block mixed active content!");
 
    var loadedMixedDisplay = ui &&
-     (ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
+     !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
    is(loadedMixedDisplay, false, "OK: Should not load mixed display content!");
 
    var blockedMixedDisplay = ui &&
-     (ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
+     !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
    is(blockedMixedDisplay, false, "OK: Should not block mixed display content!");
 
    var newValue = "Verifying MCB does not trigger warning/error for an http page ";
     newValue += "with https css that imports another http css which includes http font";
    document.getElementById("testDiv").innerHTML = newValue;
   }
 </script>
 </head>
--- a/browser/base/content/test/general/test_no_mcb_on_http_site_img.html
+++ b/browser/base/content/test/general/test_no_mcb_on_http_site_img.html
@@ -14,29 +14,29 @@
   function checkLoadStates() {
    var ui = SpecialPowers.wrap(window)
             .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
              .getInterface(SpecialPowers.Ci.nsIWebNavigation)
              .QueryInterface(SpecialPowers.Ci.nsIDocShell)
              .securityUI;
 
    var loadedMixedActive = ui &&
-     (ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
+     !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
    is(loadedMixedActive, false, "OK: Should not load mixed active content!");
 
    var blockedMixedActive = ui &&
-       (ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
+     !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
    is(blockedMixedActive, false, "OK: Should not block mixed active content!");
 
    var loadedMixedDisplay = ui &&
-     (ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
+     !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
    is(loadedMixedDisplay, false, "OK: Should not load mixed display content!");
 
    var blockedMixedDisplay = ui &&
-     (ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
+     !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
    is(blockedMixedDisplay, false, "OK: Should not block mixed display content!");
 
    var newValue = "Verifying MCB does not trigger warning/error for an http page with https css that includes http image";
    document.getElementById("testDiv").innerHTML = newValue;
   }
 </script>
 </head>
 <body onload="checkLoadStates()">
--- a/browser/base/content/test/newtab/browser_newtab_block.js
+++ b/browser/base/content/test/newtab/browser_newtab_block.js
@@ -1,17 +1,31 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /*
  * These tests make sure that blocking/removing sites from the grid works
  * as expected. Pinned tabs should not be moved. Gaps will be re-filled
  * if more sites are available.
  */
+
+gDirectorySource = "data:application/json," + JSON.stringify({
+  "suggested": [{
+    url: "http://suggested.com/",
+    imageURI: "data:image/png;base64,helloWORLD3",
+    title: "title",
+    type: "affiliate",
+    frecent_sites: ["example0.com"]
+  }]
+});
+
 function runTests() {
+  let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
+  NewTabUtils.isTopPlacesSite = (site) => false;
+
   // we remove sites and expect the gaps to be filled as long as there still
   // are some sites available
   yield setLinks("0,1,2,3,4,5,6,7,8,9");
   setPinnedLinks("");
 
   yield addNewTabPageTab();
   checkGrid("0,1,2,3,4,5,6,7,8");
 
@@ -53,9 +67,21 @@ function runTests() {
   yield setLinks("0,1,2,3,4,5,6,7,8,9");
   setPinnedLinks(",,,,,,,,8");
 
   yield addNewTabPageTab();
   checkGrid("0,1,2,3,4,5,6,7,8p");
 
   yield blockCell(0);
   checkGrid("1,2,3,4,5,6,7,9,8p");
+
+  // Test that blocking the targeted site also removes its associated suggested tile
+  NewTabUtils.isTopPlacesSite = origIsTopPlacesSite;
+  yield restore();
+  yield setLinks("0,1,2,3,4,5,6,7,8,9");
+  yield addNewTabPageTab();
+  yield customizeNewTabPage("enhanced");
+  checkGrid("http://suggested.com/,0,1,2,3,4,5,6,7,8,9");
+
+  yield blockCell(1);
+  yield addNewTabPageTab();
+  checkGrid("1,2,3,4,5,6,7,8,9");
 }
--- a/browser/base/content/test/newtab/browser_newtab_enhanced.js
+++ b/browser/base/content/test/newtab/browser_newtab_enhanced.js
@@ -104,27 +104,28 @@ function runTests() {
 
   yield unpinCell(0);
 
 
 
   // Test that a suggested tile is not enhanced by a directory tile
   let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
   NewTabUtils.isTopPlacesSite = () => true;
-  yield setLinks("-1");
+  yield setLinks("-1,2,3,4,5,6,7,8");
 
   // Test with enhanced = false
   yield addNewTabPageTab();
   yield customizeNewTabPage("classic");
   ({type, enhanced, title} = getData(0));
   isnot(type, "enhanced", "history link is not enhanced");
   is(enhanced, "", "history link has no enhanced image");
   is(title, "site#-1");
 
-  is(getData(1), null, "there is only one link and it's a history link");
+  isnot(getData(7), null, "there are 8 history links");
+  is(getData(8), null, "there are 8 history links");
 
 
   // Test with enhanced = true
   yield addNewTabPageTab();
   yield customizeNewTabPage("enhanced");
 
   // Suggested link was not enhanced by directory link with same domain
   ({type, enhanced, title} = getData(0));
@@ -133,10 +134,10 @@ function runTests() {
   is(title, "title2");
 
   // Enhanced history link shows up second
   ({type, enhanced, title} = getData(1));
   is(type, "enhanced", "pinned history link is enhanced");
   isnot(enhanced, "", "pinned history link has enhanced image");
   is(title, "title");
 
-  is(getData(2), null, "there is only a suggested link followed by an enhanced history link");
+  is(getData(9), null, "there is a suggested link followed by an enhanced history link and the remaining history links");
 }
--- a/browser/base/content/test/newtab/head.js
+++ b/browser/base/content/test/newtab/head.js
@@ -509,31 +509,29 @@ function startAndCompleteDragOperation(a
     synthesizeNativeMouseLDown(aSource);
     synthesizeNativeMouseDrag(aDest);
     // In some tests, aSource and aDest are at the same position, so to ensure
     // a drag session is created (instead of it just turning into a click) we
     // move the mouse 10 pixels away and then back.
     synthesizeNativeMouseDrag(aDest, 10);
     synthesizeNativeMouseDrag(aDest);
     // Finally, release the drag and have it run the callback when done.
-    synthesizeNativeMouseLUp(aDest);
-    aCallback();
+    synthesizeNativeMouseLUp(aDest).then(aCallback, Cu.reportError);
   } else if (isWindows) {
     // on Windows once the drag is initiated, Windows doesn't spin our
     // message loop at all, so with async event synthesization the async
     // messages never get processed while a drag is in progress. So if
     // we did a mousedown followed by a mousemove, we would never be able
     // to successfully dispatch the mouseup. Instead, we just skip the move
     // entirely, so and just generate the up at the destination. This way
     // Windows does the drag and also terminates it right away. Note that
     // this only works for tests where aSource and aDest are sufficiently
     // far to trigger a drag, otherwise it may just end up doing a click.
     synthesizeNativeMouseLDown(aSource);
-    synthesizeNativeMouseLUp(aDest);
-    aCallback();
+    synthesizeNativeMouseLUp(aDest).then(aCallback, Cu.reportError);
   } else if (isLinux) {
     // Start by pressing the left mouse button.
     synthesizeNativeMouseLDown(aSource);
 
     // Move the mouse in 5px steps until the drag operation starts.
     // Note that we need to do this with pauses in between otherwise the
     // synthesized events get coalesced somewhere in the guts of GTK. In order
     // to successfully initiate a drag session in the case where aSource and
@@ -559,18 +557,17 @@ function startAndCompleteDragOperation(a
     // the mousemove synthesization is "more async" than the mouseup
     // synthesization - they use different gdk APIs. If we don't wait, the
     // up could get processed before the moves, dropping the item in the
     // wrong position.
     aDest.addEventListener("dragenter", function onDragEnter() {
       aDest.removeEventListener("dragenter", onDragEnter);
 
       // Finish the drop operation.
-      synthesizeNativeMouseLUp(aDest, null);
-      aCallback();
+      synthesizeNativeMouseLUp(aDest).then(aCallback, Cu.reportError);
     });
   } else {
     throw "Unsupported platform";
   }
 }
 
 /**
  * Helper function that creates a temporary iframe in the about:newtab
@@ -616,17 +613,17 @@ function synthesizeNativeMouseLDown(aEle
 }
 
 /**
  * Fires a synthetic 'mouseup' event on the current about:newtab page.
  * @param aElement The element used to determine the cursor position.
  */
 function synthesizeNativeMouseLUp(aElement) {
   let msg = isWindows ? 4 : (isMac ? 2 : 7);
-  synthesizeNativeMouseEvent(aElement, msg);
+  return synthesizeNativeMouseEvent(aElement, msg);
 }
 
 /**
  * Fires a synthetic mouse drag event on the current about:newtab page.
  * @param aElement The element used to determine the cursor position.
  * @param aOffsetX The left offset that is added to the position.
  */
 function synthesizeNativeMouseDrag(aElement, aOffsetX) {
@@ -645,26 +642,35 @@ function synthesizeNativeMouseMove(aElem
 
 /**
  * Fires a synthetic mouse event on the current about:newtab page.
  * @param aElement The element used to determine the cursor position.
  * @param aOffsetX The left offset that is added to the position (optional).
  * @param aOffsetY The top offset that is added to the position (optional).
  */
 function synthesizeNativeMouseEvent(aElement, aMsg, aOffsetX = 0, aOffsetY = 0) {
-  let rect = aElement.getBoundingClientRect();
-  let win = aElement.ownerDocument.defaultView;
-  let x = aOffsetX + win.mozInnerScreenX + rect.left + rect.width / 2;
-  let y = aOffsetY + win.mozInnerScreenY + rect.top + rect.height / 2;
+  return new Promise((resolve, reject) => {
+    let rect = aElement.getBoundingClientRect();
+    let win = aElement.ownerDocument.defaultView;
+    let x = aOffsetX + win.mozInnerScreenX + rect.left + rect.width / 2;
+    let y = aOffsetY + win.mozInnerScreenY + rect.top + rect.height / 2;
+
+    let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIDOMWindowUtils);
 
-  let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
-                 .getInterface(Ci.nsIDOMWindowUtils);
-
-  let scale = utils.screenPixelsPerCSSPixel;
-  utils.sendNativeMouseEvent(x * scale, y * scale, aMsg, 0, null);
+    let scale = utils.screenPixelsPerCSSPixel;
+    let observer = {
+      observe: function(aSubject, aTopic, aData) {
+        if (aTopic == "mouseevent") {
+          resolve();
+        }
+      }
+    };
+    utils.sendNativeMouseEvent(x * scale, y * scale, aMsg, 0, null, observer);
+  });
 }
 
 /**
  * Sends a custom drag event to a given DOM element.
  * @param aEventType The drag event's type.
  * @param aTarget The DOM element that the event is dispatched to.
  * @param aData The event's drag data (optional).
  */
--- a/browser/base/content/test/plugins/browser_pluginCrashReportNonDeterminism.js
+++ b/browser/base/content/test/plugins/browser_pluginCrashReportNonDeterminism.js
@@ -104,17 +104,17 @@ add_task(function* setup() {
 
     ok(pluginDumpFile.exists(), "Found minidump");
     ok(extraFile.exists(), "Found extra file");
 
     pluginDumpFile.remove(false);
     extraFile.remove(false);
   };
 
-  Services.obs.addObserver(crashObserver, "plugin-crashed");
+  Services.obs.addObserver(crashObserver, "plugin-crashed", false);
   // plugins.testmode will make BrowserPlugins:Test:ClearCrashData work.
   Services.prefs.setBoolPref("plugins.testmode", true);
   registerCleanupFunction(() => {
     Services.prefs.clearUserPref("plugins.testmode");
     Services.obs.removeObserver(crashObserver, "plugin-crashed");
   });
 });
 
--- a/browser/base/content/test/popupNotifications/browser.ini
+++ b/browser/base/content/test/popupNotifications/browser.ini
@@ -1,12 +1,12 @@
 [DEFAULT]
 support-files =
   head.js
 
 [browser_popupNotification.js]
-skip-if = (os == "linux" && debug) || e10s # e10s - Bug ?????? - popup notification test probably confused re content process notifications etc
+skip-if = (os == "linux" && (debug || asan)) || e10s # e10s - Bug ?????? - popup notification test probably confused re content process notifications etc
 [browser_popupNotification_2.js]
-skip-if = (os == "linux" && debug) || e10s # e10s - Bug ?????? - popup notification test probably confused re content process notifications etc
+skip-if = (os == "linux" && (debug || asan)) || e10s # e10s - Bug ?????? - popup notification test probably confused re content process notifications etc
 [browser_popupNotification_3.js]
-skip-if = (os == "linux" && debug) || e10s # e10s - Bug ?????? - popup notification test probably confused re content process notifications etc
+skip-if = (os == "linux" && (debug || asan)) || e10s # e10s - Bug ?????? - popup notification test probably confused re content process notifications etc
 [browser_popupNotification_4.js]
-skip-if = (os == "linux" && debug) || e10s # e10s - Bug ?????? - popup notification test probably confused re content process notifications etc
+skip-if = (os == "linux" && (debug || asan)) || e10s # e10s - Bug ?????? - popup notification test probably confused re content process notifications etc
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -163,17 +163,17 @@ file, You can obtain one at http://mozil
               }
               case "keyword": // Fall through.
               case "searchengine": {
                 returnValue = action.params.input;
                 break;
               }
             }
           } else {
-            let originalUrl = ReaderParent.parseReaderUrl(aValue);
+            let originalUrl = ReaderMode.getOriginalUrl(aValue);
             if (originalUrl) {
               returnValue = originalUrl;
             }
           }
 
           // Set the actiontype only if the user is not overriding actions.
           if (action && this._noActionsKeys.size == 0) {
             this.setAttribute("actiontype", action.type);
@@ -1039,16 +1039,24 @@ file, You can obtain one at http://mozil
                 class="search-panel-header search-panel-current-input">
         <xul:label anonid="searchbar-oneoffheader-search" value="&searchWithHeader.label;"/>
         <xul:hbox anonid="search-panel-searchforwith"
                   class="search-panel-current-input">
           <xul:label anonid="searchbar-oneoffheader-before" value="&searchFor.label;"/>
           <xul:label anonid="searchbar-oneoffheader-searchtext" flex="1" crop="end" class="search-panel-input-value"/>
           <xul:label anonid="searchbar-oneoffheader-after" flex="10000" value="&searchWith.label;"/>
         </xul:hbox>
+        <xul:hbox anonid="search-panel-searchonengine"
+                  class="search-panel-current-input">
+          <xul:label anonid="searchbar-oneoffheader-beforeengine" value="&search.label;"/>
+          <xul:label anonid="searchbar-oneoffheader-engine" flex="1" crop="end"
+                     class="search-panel-input-value"/>
+          <xul:label anonid="searchbar-oneoffheader-afterengine" flex="10000"
+                     value="&searchAfter.label;"/>
+        </xul:hbox>
       </xul:deck>
       <xul:description anonid="search-panel-one-offs"
                        role="group"
                        class="search-panel-one-offs"/>
       <xul:vbox anonid="add-engines"/>
       <xul:button anonid="search-settings"
                   oncommand="BrowserUITelemetry.countSearchSettingsEvent('searchbar');openPreferences('paneSearch')"
                   class="search-setting-button search-panel-header"
@@ -1135,29 +1143,34 @@ file, You can obtain one at http://mozil
                                                   "search-panel-one-offs-header");
         let list = document.getAnonymousElementByAttribute(this, "anonid",
                                                            "search-panel-one-offs");
         let textbox = searchbar.textbox;
         let self = this;
         let inputHandler = function() {
           headerSearchText.setAttribute("value", textbox.value);
           let groupText;
+          let isOneOffSelected =
+            this.selectedButton &&
+            this.selectedButton.classList.contains("searchbar-engine-one-off-item");
           if (textbox.value) {
             self.removeAttribute("showonlysettings");
             groupText = headerSearchText.previousSibling.value +
                         '"' + headerSearchText.value + '"' +
                         headerSearchText.nextSibling.value;
-            headerPanel.selectedIndex = 1;
+            if (!isOneOffSelected)
+              headerPanel.selectedIndex = 1;
           }
           else {
             let noSearchHeader =
               document.getAnonymousElementByAttribute(self, "anonid",
                                                       "searchbar-oneoffheader-search");
             groupText = noSearchHeader.value;
-            headerPanel.selectedIndex = 0;
+            if (!isOneOffSelected)
+              headerPanel.selectedIndex = 0;
           }
           list.setAttribute("aria-label", groupText);
         };
         textbox.addEventListener("input", inputHandler);
         this.addEventListener("popuphiding", function hiding() {
           textbox.removeEventListener("input", inputHandler);
           this.removeEventListener("popuphiding", hiding);
         });
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -72,16 +72,17 @@ browser.jar:
         content/browser/aboutTabCrashed.css           (content/aboutTabCrashed.css)
         content/browser/aboutTabCrashed.js            (content/aboutTabCrashed.js)
         content/browser/aboutTabCrashed.xhtml         (content/aboutTabCrashed.xhtml)
 *       content/browser/browser.css                   (content/browser.css)
 *       content/browser/browser.js                    (content/browser.js)
 *       content/browser/browser.xul                   (content/browser.xul)
 *       content/browser/browser-tabPreviews.xml       (content/browser-tabPreviews.xml)
 *       content/browser/chatWindow.xul                (content/chatWindow.xul)
+        content/browser/tab-content.js                (content/tab-content.js)
         content/browser/content.js                    (content/content.js)
         content/browser/defaultthemes/1.footer.jpg    (content/defaultthemes/1.footer.jpg)
         content/browser/defaultthemes/1.header.jpg    (content/defaultthemes/1.header.jpg)
         content/browser/defaultthemes/1.icon.jpg      (content/defaultthemes/1.icon.jpg)
         content/browser/defaultthemes/1.preview.jpg   (content/defaultthemes/1.preview.jpg)
         content/browser/defaultthemes/2.footer.jpg    (content/defaultthemes/2.footer.jpg)
         content/browser/defaultthemes/2.header.jpg    (content/defaultthemes/2.header.jpg)
         content/browser/defaultthemes/2.icon.jpg      (content/defaultthemes/2.icon.jpg)
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -26,17 +26,17 @@ struct RedirEntry {
 /*
   Entries which do not have URI_SAFE_FOR_UNTRUSTED_CONTENT will run with chrome
   privileges. This is potentially dangerous. Please use
   URI_SAFE_FOR_UNTRUSTED_CONTENT in the third argument to each map item below
   unless your about: page really needs chrome privileges. Security review is
   required before adding new map entries without
   URI_SAFE_FOR_UNTRUSTED_CONTENT.  Also note, however, that adding
   URI_SAFE_FOR_UNTRUSTED_CONTENT will allow random web sites to link to that
-  URI.  Perhaps we should separate the two concepts out...
+  URI.  If you want to prevent this, add MAKE_UNLINKABLE as well.
  */
 static RedirEntry kRedirMap[] = {
 #ifdef MOZ_SAFE_BROWSING
   { "blocked", "chrome://browser/content/blockedSite.xhtml",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::URI_CAN_LOAD_IN_CHILD |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT },
@@ -56,16 +56,17 @@ static RedirEntry kRedirMap[] = {
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT },
   { "feeds", "chrome://browser/content/feeds/subscribe.xhtml",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT },
   { "privatebrowsing", "chrome://browser/content/aboutPrivateBrowsing.xhtml",
+    nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
     nsIAboutModule::ALLOW_SCRIPT },
   { "rights",
 #ifdef MOZ_OFFICIAL_BRANDING
     "chrome://global/content/aboutRights.xhtml",
 #else
     "chrome://global/content/aboutRights-unbranded.xhtml",
 #endif
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
@@ -117,16 +118,17 @@ static RedirEntry kRedirMap[] = {
     nsIAboutModule::HIDE_FROM_ABOUTABOUT |
     nsIAboutModule::ENABLE_INDEXED_DB,
     // Shares an IndexedDB origin with about:loopconversation.
     "loopconversation" },
   { "reader", "chrome://global/content/reader/aboutReader.html",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
+    nsIAboutModule::MAKE_UNLINKABLE |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT },
 };
 static const int kRedirTotal = ArrayLength(kRedirMap);
 
 static nsAutoCString
 GetAboutModuleName(nsIURI *aURI)
 {
   nsAutoCString path;
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -235,18 +235,19 @@ let CustomizableUIInternal = {
 #if defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_QT)
         return true;
 #else
         // This is duplicated logic from /browser/base/jar.mn
         // for win6BrowserOverlay.xul.
         return Services.appinfo.OS == "WINNT" &&
                Services.sysinfo.getProperty("version") != "5.1";
 #endif
+#else
+        return false;
 #endif
-        return false;
       }
     }, true);
 #endif
     this.registerArea(CustomizableUI.AREA_TABSTRIP, {
       legacy: true,
       type: CustomizableUI.TYPE_TOOLBAR,
       defaultPlacements: [
         "tabbrowser-tabs",
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -555,17 +555,17 @@ loop.conversationViews = (function(mozL1
             ), 
             React.createElement("button", {className: retryClasses, 
                     onClick: this.retryCall}, 
               mozL10n.get("retry_call_button")
             ), 
             React.createElement("button", {className: emailClasses, 
                     onClick: this.emailLink, 
                     disabled: this.state.emailLinkButtonDisabled}, 
-              mozL10n.get("share_button2")
+              mozL10n.get("share_button3")
             )
           )
         )
       );
     }
   });
 
   var OngoingConversationView = React.createClass({displayName: "OngoingConversationView",
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -555,17 +555,17 @@ loop.conversationViews = (function(mozL1
             </button>
             <button className={retryClasses}
                     onClick={this.retryCall}>
               {mozL10n.get("retry_call_button")}
             </button>
             <button className={emailClasses}
                     onClick={this.emailLink}
                     disabled={this.state.emailLinkButtonDisabled}>
-              {mozL10n.get("share_button2")}
+              {mozL10n.get("share_button3")}
             </button>
           </div>
         </div>
       );
     }
   });
 
   var OngoingConversationView = React.createClass({
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -714,21 +714,24 @@ html, .fx-embedded, #main,
   }
 
   /* Nested video elements */
   .conversation .media.nested {
     position: relative;
     height: 100%;
   }
 
+  .standalone .media.nested {
+    margin-left: 10px;
+  }
+
   .standalone .remote_wrapper {
     position: relative;
-    width: calc(100% - 10px);
+    width: 100%;
     height: 100%;
-    margin-left: 10px;
   }
 
   .standalone {
     margin: 0 auto;
   }
 }
 
 @media screen and (max-width:640px) {
@@ -971,17 +974,17 @@ body[dir=rtl] .share-service-dropdown .s
   height: calc(100% - 50px - 60px);
 }
 
 .standalone .room-conversation-wrapper .room-inner-info-area {
   position: absolute;
   /* `top` is chosen to vertically position the area near the center
      of the media element. */
   top: calc(50% - 140px);
-  left: calc(25% + 62.5px + 10px);
+  left: 25%;
   z-index: 1000;
   /* `width` here is specified by the design spec. */
   width: 250px;
   color: #fff;
   box-sizing: content-box;
 }
 
 .standalone .prompt-media-message {
@@ -1024,37 +1027,63 @@ body[dir=rtl] .share-service-dropdown .s
 
 .standalone .room-conversation-wrapper .room-inner-info-area a.btn {
   padding: .5em 3em .3em 3em;
   border-radius: 3px;
   font-weight: normal;
   max-width: 400px;
 }
 
-.standalone .room-conversation h2.room-name,
-.standalone .room-conversation h2.room-info-failure {
+.standalone-room-info {
   position: absolute;
-  display: inline-block;
+  display: block;
   top: 0;
   right: 10px;
   /* 20px is 10px for left and right margins. */
   width: calc(25% - 20px);
-  color: #fff;
   z-index: 2000000;
   font-size: 1.2em;
   padding: .4em;
+  height: 100%;
+}
+
+.standalone-room-info > h2 {
+  color: #fff;
+}
+
+.standalone-context-url {
+  color: #fff;
+  /* Try and keep clear of local video */
+  height: 40%;
+}
+
+.standalone-context-url.screen-share-active {
+  /* Try and keep clear of remote video when screensharing */
+  height: 15%;
+}
+
+.standalone-context-url > img {
+  margin: 1em auto;
+  max-width: 50%;
+  /* allows 20% for the description wrapper plus the margins */
+  max-height: calc(80% - 2em);
+}
+
+.standalone-context-url-description-wrapper {
+  /* So that we can use max-height for the image */
+  height: 20%;
 }
 
 .standalone .room-conversation .media {
   background: #000;
 }
 
 .standalone .room-conversation .video_wrapper.remote_wrapper {
   background-color: #4e4e4e;
-  width: calc(75% - 10px); /* Take the left margin into account. */
+  width: 75%;
 }
 
 .standalone .room-conversation .conversation-toolbar {
   background: #000;
   border: none;
 }
 
 .standalone .room-conversation .conversation-toolbar .btn-hangup-entry {
@@ -1063,35 +1092,60 @@ body[dir=rtl] .share-service-dropdown .s
 
 .standalone .room-conversation-wrapper .ended-conversation {
   position: relative;
   height: auto;
 }
 
 
 @media screen and (max-width:640px) {
+  .standalone-room-info {
+    /* This isn't perfect, we just center the heading for now. Bug 1141493
+       should fix this. */
+    position: absolute;
+    width: 100%;
+    right: 0px;
+
+    /* Override the 100% specified in the .standalone-room-info selector
+       block so that this div doesn't take over the _whole_ screen and
+       transparently occlude UI widgetry (like the Join button), making
+       it unusable. */
+    height: auto;
+  }
+
+  .standalone-context-url {
+    /* XXX We haven't got UX for standalone yet, so temporarily not displaying
+       on narrow window widths. See bug 1153827. */
+    display: none;
+  }
+
   /* Rooms specific responsive styling */
   .standalone .room-conversation {
     background: #000;
   }
   .room-conversation-wrapper header {
     width: 100%;
   }
   .standalone .room-conversation-wrapper .room-inner-info-area {
     right: 0;
     margin: auto;
     width: 100%;
+    left: 0;
   }
   .standalone .room-conversation-wrapper .video-layout-wrapper {
     /* 50px: header's height; 25px: footer's height */
     height: calc(100% - 50px - 25px);
   }
   .standalone .room-conversation .video_wrapper.remote_wrapper {
     width: 100%;
   }
+  .standalone .room-conversation .video_wrapper.remote_wrapper.not-joined {
+    width: 100%;
+  }
+
   .standalone .conversation-toolbar {
     height: 38px;
     padding: 8px;
   }
   .standalone .focus-stream {
     /* Set at maximum height, minus height of conversation toolbar */
     height: 100%;
   }
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -195,16 +195,17 @@ loop.shared.actions = (function() {
     MediaConnected: Action.define("mediaConnected", {
     }),
 
     /**
      * Used for notifying that the dimensions of a stream just changed. Also
      * dispatched when a stream connects for the first time.
      */
     VideoDimensionsChanged: Action.define("videoDimensionsChanged", {
+      isLocal: Boolean,
       videoType: String,
       dimensions: Object
     }),
 
     /**
      * Used to mute or unmute a stream
      */
     SetMute: Action.define("setMute", {
@@ -414,16 +415,18 @@ loop.shared.actions = (function() {
      *
      * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
      */
     UpdateRoomInfo: Action.define("updateRoomInfo", {
       // context: Object - Optional.
       // roomName: String - Optional.
       roomOwner: String,
       roomUrl: String
+      // urls: Array - Optional.
+      // See https://wiki.mozilla.org/Loop/Architecture/Context#Format_of_context.value
     }),
 
     /**
      * Updates the Social API information when it is received.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     UpdateSocialShareInfo: Action.define("updateSocialShareInfo", {
       socialShareButtonAvailable: Boolean,
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -75,20 +75,24 @@ loop.store.ActiveRoomStore = (function()
         // session. 'Used' means at least one call has been placed
         // with it. Entering and leaving the room without seeing
         // anyone is not considered as 'used'
         used: false,
         localVideoDimensions: {},
         remoteVideoDimensions: {},
         screenSharingState: SCREEN_SHARE_STATES.INACTIVE,
         receivingScreenShare: false,
+        // Any urls (aka context) associated with the room.
+        roomContextUrls: null,
         // The roomCryptoKey to decode the context data if necessary.
         roomCryptoKey: null,
         // Room information failed to be obtained for a reason. See ROOM_INFO_FAILURES.
         roomInfoFailure: null,
+        // The name of the room.
+        roomName: null,
         // Social API state.
         socialShareButtonAvailable: false,
         socialShareProviders: null
       };
     },
 
     /**
      * Handles a room failure.
@@ -266,16 +270,17 @@ loop.store.ActiveRoomStore = (function()
         }
 
         var dispatcher = this.dispatcher;
 
         crypto.decryptBytes(roomCryptoKey, result.context.value)
               .then(function(decryptedResult) {
           var realResult = JSON.parse(decryptedResult);
 
+          roomInfoData.urls = realResult.urls;
           roomInfoData.roomName = realResult.roomName;
 
           dispatcher.dispatch(roomInfoData);
         }, function(err) {
           roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.DECRYPT_FAILED;
           dispatcher.dispatch(roomInfoData);
         });
       }.bind(this));
@@ -315,16 +320,17 @@ loop.store.ActiveRoomStore = (function()
 
     /**
      * Handles the updateRoomInfo action. Updates the room data.
      *
      * @param {sharedActions.UpdateRoomInfo} actionData
      */
     updateRoomInfo: function(actionData) {
       this.setStoreState({
+        roomContextUrls: actionData.urls,
         roomInfoFailure: actionData.roomInfoFailure,
         roomName: actionData.roomName,
         roomOwner: actionData.roomOwner,
         roomUrl: actionData.roomUrl
       });
     },
 
     /**
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -194,35 +194,79 @@ loop.standaloneRoomViews = (function(moz
         React.createElement("footer", null, 
           React.createElement("p", {dangerouslySetInnerHTML: {__html: this._getContent()}}), 
           React.createElement("div", {className: "footer-logo"})
         )
       );
     }
   });
 
+  var StandaloneRoomContextItem = React.createClass({displayName: "StandaloneRoomContextItem",
+    propTypes: {
+      receivingScreenShare: React.PropTypes.bool,
+      roomContextUrl: React.PropTypes.object
+    },
+
+    render: function() {
+      if (!this.props.roomContextUrl ||
+          !this.props.roomContextUrl.location) {
+        return null;
+      }
+
+      var location = this.props.roomContextUrl.location;
+
+      var cx = React.addons.classSet;
+
+      var classes = cx({
+        "standalone-context-url": true,
+        "screen-share-active": this.props.receivingScreenShare
+      });
+
+      return (
+        React.createElement("div", {className: classes}, 
+            React.createElement("img", {src: this.props.roomContextUrl.thumbnail}), 
+          React.createElement("div", {className: "standalone-context-url-description-wrapper"}, 
+            this.props.roomContextUrl.description, 
+            React.createElement("br", null), React.createElement("a", {href: location}, location)
+          )
+        )
+      );
+    }
+  });
+
   var StandaloneRoomContextView = React.createClass({displayName: "StandaloneRoomContextView",
     propTypes: {
+      receivingScreenShare: React.PropTypes.bool.isRequired,
+      roomContextUrls: React.PropTypes.array,
       roomName: React.PropTypes.string,
       roomInfoFailure: React.PropTypes.string
     },
 
     render: function() {
       if (this.props.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) {
         return (React.createElement("h2", {className: "room-info-failure"}, 
           mozL10n.get("room_information_failure_unsupported_browser")
         ));
       } else if (this.props.roomInfoFailure) {
         return (React.createElement("h2", {className: "room-info-failure"}, 
           mozL10n.get("room_information_failure_not_available")
         ));
       }
 
+      // We only support one item in the context Urls array for now.
+      var roomContextUrl = (this.props.roomContextUrls &&
+                            this.props.roomContextUrls.length > 0) ?
+                            this.props.roomContextUrls[0] : null;
       return (
-        React.createElement("h2", {className: "room-name"}, this.props.roomName)
+        React.createElement("div", {className: "standalone-room-info"}, 
+          React.createElement("h2", {className: "room-name"}, this.props.roomName), 
+          React.createElement(StandaloneRoomContextItem, {
+            receivingScreenShare: this.props.receivingScreenShare, 
+            roomContextUrl: roomContextUrl})
+        )
       );
     }
   });
 
   var StandaloneRoomView = React.createClass({displayName: "StandaloneRoomView",
     mixins: [
       Backbone.Events,
       sharedMixins.MediaSetupMixin,
@@ -477,18 +521,21 @@ loop.standaloneRoomViews = (function(moz
           React.createElement(StandaloneRoomInfoArea, {roomState: this.state.roomState, 
                                   failureReason: this.state.failureReason, 
                                   joinRoom: this.joinRoom, 
                                   isFirefox: this.props.isFirefox, 
                                   activeRoomStore: this.props.activeRoomStore, 
                                   roomUsed: this.state.used}), 
           React.createElement("div", {className: "video-layout-wrapper"}, 
             React.createElement("div", {className: "conversation room-conversation"}, 
-              React.createElement(StandaloneRoomContextView, {roomName: this.state.roomName, 
-                                         roomInfoFailure: this.state.roomInfoFailure}), 
+              React.createElement(StandaloneRoomContextView, {
+                receivingScreenShare: this.state.receivingScreenShare, 
+                roomContextUrls: this.state.roomContextUrls, 
+                roomName: this.state.roomName, 
+                roomInfoFailure: this.state.roomInfoFailure}), 
               React.createElement("div", {className: "media nested"}, 
                 React.createElement("span", {className: "self-view-hidden-message"}, 
                   mozL10n.get("self_view_hidden_message")
                 ), 
                 React.createElement("div", {className: "video_wrapper remote_wrapper"}, 
                   React.createElement("div", {className: remoteStreamClasses}), 
                   React.createElement("div", {className: screenShareStreamClasses})
                 ), 
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -194,35 +194,79 @@ loop.standaloneRoomViews = (function(moz
         <footer>
           <p dangerouslySetInnerHTML={{__html: this._getContent()}}></p>
           <div className="footer-logo" />
         </footer>
       );
     }
   });
 
+  var StandaloneRoomContextItem = React.createClass({
+    propTypes: {
+      receivingScreenShare: React.PropTypes.bool,
+      roomContextUrl: React.PropTypes.object
+    },
+
+    render: function() {
+      if (!this.props.roomContextUrl ||
+          !this.props.roomContextUrl.location) {
+        return null;
+      }
+
+      var location = this.props.roomContextUrl.location;
+
+      var cx = React.addons.classSet;
+
+      var classes = cx({
+        "standalone-context-url": true,
+        "screen-share-active": this.props.receivingScreenShare
+      });
+
+      return (
+        <div className={classes}>
+            <img src={this.props.roomContextUrl.thumbnail} />
+          <div className="standalone-context-url-description-wrapper">
+            {this.props.roomContextUrl.description}
+            <br /><a href={location}>{location}</a>
+          </div>
+        </div>
+      );
+    }
+  });
+
   var StandaloneRoomContextView = React.createClass({
     propTypes: {
+      receivingScreenShare: React.PropTypes.bool.isRequired,
+      roomContextUrls: React.PropTypes.array,
       roomName: React.PropTypes.string,
       roomInfoFailure: React.PropTypes.string
     },
 
     render: function() {
       if (this.props.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) {
         return (<h2 className="room-info-failure">
           {mozL10n.get("room_information_failure_unsupported_browser")}
         </h2>);
       } else if (this.props.roomInfoFailure) {
         return (<h2 className="room-info-failure">
           {mozL10n.get("room_information_failure_not_available")}
         </h2>);
       }
 
+      // We only support one item in the context Urls array for now.
+      var roomContextUrl = (this.props.roomContextUrls &&
+                            this.props.roomContextUrls.length > 0) ?
+                            this.props.roomContextUrls[0] : null;
       return (
-        <h2 className="room-name">{this.props.roomName}</h2>
+        <div className="standalone-room-info">
+          <h2 className="room-name">{this.props.roomName}</h2>
+          <StandaloneRoomContextItem
+            receivingScreenShare={this.props.receivingScreenShare}
+            roomContextUrl={roomContextUrl} />
+        </div>
       );
     }
   });
 
   var StandaloneRoomView = React.createClass({
     mixins: [
       Backbone.Events,
       sharedMixins.MediaSetupMixin,
@@ -477,18 +521,21 @@ loop.standaloneRoomViews = (function(moz
           <StandaloneRoomInfoArea roomState={this.state.roomState}
                                   failureReason={this.state.failureReason}
                                   joinRoom={this.joinRoom}
                                   isFirefox={this.props.isFirefox}
                                   activeRoomStore={this.props.activeRoomStore}
                                   roomUsed={this.state.used} />
           <div className="video-layout-wrapper">
             <div className="conversation room-conversation">
-              <StandaloneRoomContextView roomName={this.state.roomName}
-                                         roomInfoFailure={this.state.roomInfoFailure} />
+              <StandaloneRoomContextView
+                receivingScreenShare={this.state.receivingScreenShare}
+                roomContextUrls={this.state.roomContextUrls}
+                roomName={this.state.roomName}
+                roomInfoFailure={this.state.roomInfoFailure} />
               <div className="media nested">
                 <span className="self-view-hidden-message">
                   {mozL10n.get("self_view_hidden_message")}
                 </span>
                 <div className="video_wrapper remote_wrapper">
                   <div className={remoteStreamClasses}></div>
                   <div className={screenShareStreamClasses}></div>
                 </div>
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -452,36 +452,43 @@ describe("loop.store.ActiveRoomStore", f
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.UpdateRoomInfo(_.extend({
             roomInfoFailure: ROOM_INFO_FAILURES.DECRYPT_FAILED
           }, expectedDetails)));
       });
 
-      it("should dispatch UpdateRoomInfo message with the room name if decryption was successful", function() {
+      it("should dispatch UpdateRoomInfo message with the context if decryption was successful", function() {
         fetchServerAction.cryptoKey = "fakeKey";
 
+        var roomContext = {
+          roomName: "The wonderful Loopy room",
+          urls: [{
+            description: "An invalid page",
+            location: "http://invalid.com",
+            thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+          }]
+        };
+
         // This is a work around to turn promise into a sync action to make handling test failures
         // easier.
         sandbox.stub(loop.crypto, "decryptBytes", function() {
           return {
             then: function(resolve, reject) {
-              resolve(JSON.stringify({roomName: "The wonderful Loopy room"}));
+              resolve(JSON.stringify(roomContext));
             }
           };
         });
 
         store.fetchServerData(fetchServerAction);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.UpdateRoomInfo(_.extend({
-            roomName: "The wonderful Loopy room"
-          }, expectedDetails)));
+          new sharedActions.UpdateRoomInfo(_.extend(roomContext, expectedDetails)));
       });
     });
   });
 
   describe("#feedbackComplete", function() {
     it("should reset the room store state", function() {
       var initialState = store.getInitialStoreState();
       store.setStoreState({
@@ -557,27 +564,33 @@ describe("loop.store.ActiveRoomStore", f
 
   describe("#updateRoomInfo", function() {
     var fakeRoomInfo;
 
     beforeEach(function() {
       fakeRoomInfo = {
         roomName: "Its a room",
         roomOwner: "Me",
-        roomUrl: "http://invalid"
+        roomUrl: "http://invalid",
+        urls: [{
+          description: "fake site",
+          location: "http://invalid.com",
+          thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+        }]
       };
     });
 
     it("should save the room information", function() {
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo(fakeRoomInfo));
 
       var state = store.getStoreState();
       expect(state.roomName).eql(fakeRoomInfo.roomName);
       expect(state.roomOwner).eql(fakeRoomInfo.roomOwner);
       expect(state.roomUrl).eql(fakeRoomInfo.roomUrl);
+      expect(state.roomContextUrls).eql(fakeRoomInfo.urls);
     });
   });
 
   describe("#updateSocialShareInfo", function() {
     var fakeSocialShareInfo;
 
     beforeEach(function() {
       fakeSocialShareInfo = {
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -39,25 +39,27 @@ describe("loop.standaloneRoomViews", fun
     sandbox.restore();
   });
 
   describe("StandaloneRoomContextView", function() {
     beforeEach(function() {
       sandbox.stub(navigator.mozL10n, "get").returnsArg(0);
     });
 
-    function mountTestComponent(props) {
+    function mountTestComponent(extraProps) {
+      var props = _.extend({ receivingScreenShare: false }, extraProps);
       return TestUtils.renderIntoDocument(
         React.createElement(
           loop.standaloneRoomViews.StandaloneRoomContextView, props));
     }
 
     it("should display the room name if no failures are known", function() {
       var view = mountTestComponent({
-        roomName: "Mike's room"
+        roomName: "Mike's room",
+        receivingScreenShare: false
       });
 
       expect(view.getDOMNode().textContent).eql("Mike's room");
     });
 
     it("should display an unsupported browser message if crypto is unsupported", function() {
       var view = mountTestComponent({
         roomName: "Mark's room",
@@ -70,16 +72,37 @@ describe("loop.standaloneRoomViews", fun
     it("should display a general error message for any other failure", function() {
       var view = mountTestComponent({
         roomName: "Mark's room",
         roomInfoFailure: ROOM_INFO_FAILURES.NO_DATA
       });
 
       expect(view.getDOMNode().textContent).match(/not_available/);
     });
+
+    it("should display context information if a url is supplied", function() {
+      var view = mountTestComponent({
+        roomName: "Mike's room",
+        roomContextUrls: [{
+          description: "Mark's super page",
+          location: "http://invalid.com",
+          thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+        }]
+      });
+
+      expect(view.getDOMNode().querySelector(".standalone-context-url")).not.eql(null);
+    });
+
+    it("should not display context information if no urls are supplied", function() {
+      var view = mountTestComponent({
+        roomName: "Mike's room"
+      });
+
+      expect(view.getDOMNode().querySelector(".standalone-context-url")).eql(null);
+    });
   });
 
   describe("StandaloneRoomView", function() {
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(
           loop.standaloneRoomViews.StandaloneRoomView, {
             dispatcher: dispatcher,
--- a/browser/components/migration/IEProfileMigrator.js
+++ b/browser/components/migration/IEProfileMigrator.js
@@ -18,16 +18,18 @@ Cu.import("resource:///modules/Migration
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
                                   "resource://gre/modules/ctypes.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
                                   "resource://gre/modules/WindowsRegistry.jsm");
 
+Cu.importGlobalProperties(["File"]);
+
 ////////////////////////////////////////////////////////////////////////////////
 //// Helpers.
 
 let CtypesHelpers = {
   _structs: {},
   _functions: {},
   _libs: {},
 
@@ -78,41 +80,45 @@ let CtypesHelpers = {
       try {
         lib.close();
       } catch (ex) {}
     }
     this._libs = {};
   },
 
   /**
-   * Converts a FILETIME struct (2 DWORDS), to a SYSTEMTIME struct.
+   * Converts a FILETIME struct (2 DWORDS), to a SYSTEMTIME struct,
+   * and then deduces the number of seconds since the epoch (which
+   * is the data we want for the cookie expiry date).
    *
    * @param aTimeHi
    *        Least significant DWORD.
    * @param aTimeLo
    *        Most significant DWORD.
-   * @return a Date object representing the converted datetime.
+   * @return the number of seconds since the epoch
    */
-  fileTimeToDate: function CH_fileTimeToDate(aTimeHi, aTimeLo) {
+  fileTimeToSecondsSinceEpoch(aTimeHi, aTimeLo) {
     let fileTime = this._structs.FILETIME();
     fileTime.dwLowDateTime = aTimeLo;
     fileTime.dwHighDateTime = aTimeHi;
     let systemTime = this._structs.SYSTEMTIME();
     let result = this._functions.FileTimeToSystemTime(fileTime.address(),
                                                       systemTime.address());
     if (result == 0)
       throw new Error(ctypes.winLastError);
 
-    return new Date(systemTime.wYear,
-                    systemTime.wMonth - 1,
-                    systemTime.wDay,
-                    systemTime.wHour,
-                    systemTime.wMinute,
-                    systemTime.wSecond,
-                    systemTime.wMilliseconds);
+    // System time is in UTC, so we use Date.UTC to get milliseconds from epoch,
+    // then divide by 1000 to get seconds, and round down:
+    return Math.floor(Date.UTC(systemTime.wYear,
+                               systemTime.wMonth - 1,
+                               systemTime.wDay,
+                               systemTime.wHour,
+                               systemTime.wMinute,
+                               systemTime.wSecond,
+                               systemTime.wMilliseconds) / 1000);
   }
 };
 
 /**
  * Checks whether an host is an IP (v4 or v6) address.
  *
  * @param aHost
  *        The host to check.
@@ -405,17 +411,17 @@ Cookies.prototype = {
         this._parseCookieBuffer(fileReader.result);
       } catch (ex) {
         Components.utils.reportError("Unable to migrate cookie: " + ex);
         success = false;
       } finally {
         aCallback(success);
       }
     }).bind(this), false);
-    fileReader.readAsText(File(aFile));
+    fileReader.readAsText(new File(aFile));
   },
 
   /**
    * Parses a cookie file buffer and returns an array of the contained cookies.
    *
    * The cookie file format is a newline-separated-values with a "*" used as
    * delimeter between multiple records.
    * Each cookie has the following fields:
@@ -439,30 +445,30 @@ Cookies.prototype = {
            expireTimeLo, expireTimeHi] = record.split("\n");
 
       // IE stores deleted cookies with a zero-length value, skip them.
       if (value.length == 0)
         continue;
 
       let hostLen = hostpath.indexOf("/");
       let host = hostpath.substr(0, hostLen);
+      let path = hostpath.substr(hostLen);
 
       // For a non-null domain, assume it's what Mozilla considers
       // a domain cookie.  See bug 222343.
       if (host.length > 0) {
         // Fist delete any possible extant matching host cookie.
         Services.cookies.remove(host, name, path, false);
         // Now make it a domain cookie.
         if (host[0] != "." && !hostIsIPAddress(host))
           host = "." + host;
       }
 
-      let path = hostpath.substr(hostLen);
-      let expireTime = CtypesHelpers.fileTimeToDate(Number(expireTimeHi),
-                                                    Number(expireTimeLo));
+      let expireTime = CtypesHelpers.fileTimeToSecondsSinceEpoch(Number(expireTimeHi),
+                                                                 Number(expireTimeLo));
       Services.cookies.add(host,
                            path,
                            name,
                            value,
                            Number(flags) & 0x1, // secure
                            false, // httpOnly
                            false, // session
                            expireTime);
--- a/browser/components/migration/tests/unit/head_migration.js
+++ b/browser/components/migration/tests/unit/head_migration.js
@@ -1,64 +1,37 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
-const Cr = Components.results;
+Cu.importGlobalProperties([ "URL" ]);
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MigrationUtils",
                                   "resource:///modules/MigrationUtils.jsm");
+
 // Initialize profile.
 let gProfD = do_get_profile();
 
-// Create a fake XULAppInfo to satisfy the eventual needs of the migrators.
-let XULAppInfo = {
-  // nsIXUlAppInfo
-  get vendor() "Mozilla",
-  get name() "XPCShell",
-  get ID() "xpcshell@tests.mozilla.org",
-  get version() "1",
-  get appBuildID() "2007010101",
-  get platformVersion() "1.0",
-  get platformBuildID() "2007010101",
-
-  // nsIXUlRuntime (partial)
-  get inSafeMode() false,
-  logConsoleErrors: true,
-  get OS() "XPCShell",
-  get XPCOMABI() "noarch-spidermonkey",
-  invalidateCachesOnRestart: function () {},
-
-  // nsIWinAppHelper
-  get userCanElevate() false,
+Cu.import("resource://testing-common/AppInfo.jsm");
+updateAppInfo();
 
-  QueryInterface: function (aIID) {
-    let interfaces = [Ci.nsIXULAppInfo, Ci.nsIXULRuntime];
-    if ("nsIWinAppHelper" in Ci)
-      interfaces.push(Ci.nsIWinAppHelper);
-    if (!interfaces.some(function (v) aIID.equals(v)))
-      throw Cr.NS_ERROR_NO_INTERFACE;
-    return this;
-  }
-};
+/**
+ * Migrates the requested resource and waits for the migration to be complete.
+ */
+function promiseMigration(migrator, resourceType) {
+  // Ensure resource migration is available.
+  let availableSources = migrator.getMigrateData(null, false);
+  Assert.ok((availableSources & resourceType) > 0);
 
-const CONTRACT_ID = "@mozilla.org/xre/app-info;1";
-const CID = Components.ID("7685dac8-3637-4660-a544-928c5ec0e714}");
+  return new Promise (resolve => {
+    Services.obs.addObserver(function onMigrationEnded() {
+      Services.obs.removeObserver(onMigrationEnded, "Migration:Ended");
+      resolve();
+    }, "Migration:Ended", false);
 
-let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
-registrar.registerFactory(CID, "XULAppInfo", CONTRACT_ID, {
-  createInstance: function (aOuter, aIID) {
-    if (aOuter != null)
-      throw Cr.NS_ERROR_NO_AGGREGATION;
-    return XULAppInfo.QueryInterface(aIID);
-  },
-  QueryInterface: XPCOMUtils.generateQI(Ci.nsIFactory)
-});
+    migrator.migrate(resourceType, null, null);
+  });
+}
--- a/browser/components/migration/tests/unit/test_IE_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_IE_bookmarks.js
@@ -1,56 +1,36 @@
-/* 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/. */
-
-function run_test() {
-  do_test_pending();
-
+add_task(function* () {
   let migrator = MigrationUtils.getMigrator("ie");
-
   // Sanity check for the source.
-  do_check_true(migrator.sourceExists);
-
-  // Ensure bookmarks migration is available.
-  let availableSources = migrator.getMigrateData(null, false);
-  do_check_true((availableSources & MigrationUtils.resourceTypes.BOOKMARKS) > 0);
+  Assert.ok(migrator.sourceExists);
 
   // Wait for the imported bookmarks.  Check that "From Internet Explorer"
   // folders are created in the menu and on the toolbar.
   let source = MigrationUtils.getLocalizedString("sourceNameIE");
   let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
 
   let expectedParents = [ PlacesUtils.bookmarksMenuFolderId,
                           PlacesUtils.toolbarFolderId ];
 
   PlacesUtils.bookmarks.addObserver({
-    onItemAdded: function onItemAdded(aItemId, aParentId, aIndex, aItemType,
-                                      aURI, aTitle) {
+    onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) {
       if (aTitle == label) {
         let index = expectedParents.indexOf(aParentId);
-        do_check_neq(index, -1);
+        Assert.notEqual(index, -1);
         expectedParents.splice(index, 1);
         if (expectedParents.length == 0)
           PlacesUtils.bookmarks.removeObserver(this);
       }
     },
-    onBeginUpdateBatch: function () {},
-    onEndUpdateBatch: function () {},
-    onItemRemoved: function () {},
-    onItemChanged: function () {},
-    onItemVisited: function () {},
-    onItemMoved: function () {},
+    onBeginUpdateBatch() {},
+    onEndUpdateBatch() {},
+    onItemRemoved() {},
+    onItemChanged() {},
+    onItemVisited() {},
+    onItemMoved() {},
   }, false);
 
-  // Wait for migration.
-  Services.obs.addObserver(function onMigrationEnded() {
-    Services.obs.removeObserver(onMigrationEnded, "Migration:Ended");
-
-    // Check the bookmarks have been imported to all the expected parents.
-    do_check_eq(expectedParents.length, 0);
+  yield promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
 
-    do_test_finished();
-  }, "Migration:Ended", false);
-
-  migrator.migrate(MigrationUtils.resourceTypes.BOOKMARKS, null,
-                   null);
-}
+  // Check the bookmarks have been imported to all the expected parents.
+  Assert.equal(expectedParents.length, 0);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_IE_cookies.js
@@ -0,0 +1,67 @@
+XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
+                                  "resource://gre/modules/ctypes.jsm");
+
+add_task(function* () {
+  let migrator = MigrationUtils.getMigrator("ie");
+  // Sanity check for the source.
+  Assert.ok(migrator.sourceExists);
+
+  const BOOL = ctypes.bool;
+  const LPCTSTR = ctypes.char16_t.ptr;
+
+  let wininet = ctypes.open("Wininet");
+  do_register_cleanup(() => {
+    try {
+      wininet.close();
+    } catch (ex) {}
+  });
+
+  /*
+  BOOL InternetSetCookie(
+    _In_  LPCTSTR lpszUrl,
+    _In_  LPCTSTR lpszCookieName,
+    _In_  LPCTSTR lpszCookieData
+  );
+  */
+  let setIECookie = wininet.declare("InternetSetCookieW",
+                                    ctypes.default_abi,
+                                    BOOL,
+                                    LPCTSTR,
+                                    LPCTSTR,
+                                    LPCTSTR);
+
+  let expiry = new Date();
+  expiry.setDate(expiry.getDate() + 7);
+  const COOKIE = {
+    host: "mycookietest.com",
+    name: "testcookie",
+    value: "testvalue",
+    expiry
+  };
+
+  // Sanity check.
+  Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 0,
+               "There are no cookies initially");
+
+  // Create the persistent cookie in IE.
+  let value = COOKIE.name + " = " + COOKIE.value +"; expires = " +
+              COOKIE.expiry.toUTCString();
+  let rv = setIECookie(new URL("http://" + COOKIE.host).href, null, value);
+  Assert.ok(rv, "Added a persistent IE cookie");
+
+  // Migrate cookies.
+  yield promiseMigration(migrator, MigrationUtils.resourceTypes.COOKIES);
+
+  Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 1,
+               "Migrated the expected number of cookies");
+
+  // Now check the cookie details.
+  let enumerator = Services.cookies.getCookiesFromHost(COOKIE.host);
+  Assert.ok(enumerator.hasMoreElements());
+  let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
+
+  Assert.equal(foundCookie.name, COOKIE.name);
+  Assert.equal(foundCookie.value, COOKIE.value);
+  Assert.equal(foundCookie.host, "." + COOKIE.host);
+  Assert.equal(foundCookie.expiry, Math.floor(COOKIE.expiry / 1000));
+});
--- a/browser/components/migration/tests/unit/xpcshell.ini
+++ b/browser/components/migration/tests/unit/xpcshell.ini
@@ -1,10 +1,11 @@
 [DEFAULT]
 head = head_migration.js
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
+[test_fx_fhr.js]
 [test_IE_bookmarks.js]
 skip-if = os != "win"
-
-[test_fx_fhr.js]
+[test_IE_cookies.js]
+skip-if = os != "win"
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -132,17 +132,16 @@ this.PlacesUIUtils = {
 
   getString: function PUIU_getString(key) {
     return bundle.GetStringFromName(key);
   },
 
   get _copyableAnnotations() [
     this.DESCRIPTION_ANNO,
     this.LOAD_IN_SIDEBAR_ANNO,
-    PlacesUtils.POST_DATA_ANNO,
     PlacesUtils.READ_ONLY_ANNO,
   ],
 
   /**
    * Get a transaction for copying a uri item (either a bookmark or a history
    * entry) from one container to another.
    *
    * @param   aData
@@ -167,27 +166,27 @@ this.PlacesUIUtils = {
       );
     }
     if (aData.lastModified) {
       transactions.push(
         new PlacesEditItemLastModifiedTransaction(null, aData.lastModified)
       );
     }
 
-    let keyword = aData.keyword || null;
     let annos = [];
     if (aData.annos) {
       annos = aData.annos.filter(function (aAnno) {
         return this._copyableAnnotations.indexOf(aAnno.name) != -1;
       }, this);
     }
 
+    // There's no need to copy the keyword since it's bound to the bookmark url.
     return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(aData.uri),
                                                aContainer, aIndex, aData.title,
-                                               keyword, annos, transactions);
+                                               null, annos, transactions);
   },
 
   /**
    * Gets a transaction for copying (recursively nesting to include children)
    * a folder (or container) and its contents from one folder to another.
    *
    * @param   aData
    *          Unwrapped dropped folder data - Obj containing folder and children
--- a/browser/components/places/content/editBookmarkOverlay.xul
+++ b/browser/components/places/content/editBookmarkOverlay.xul
@@ -162,17 +162,18 @@
 
         <row id="editBMPanel_descriptionRow">
           <observes element="additionalInfoBroadcaster" attribute="hidden"/>
           <label value="&editBookmarkOverlay.description.label;"
                  class="editBMPanel_rowLabel"
                  accesskey="&editBookmarkOverlay.description.accesskey;"
                  control="editBMPanel_descriptionField"/>
           <textbox id="editBMPanel_descriptionField"
-                   multiline="true"/>
+                   multiline="true"
+                   rows="4"/>
         </row>
       </rows>
     </grid>
 
     <checkbox id="editBMPanel_loadInSidebarCheckbox"
               label="&editBookmarkOverlay.loadInSidebar.label;"
               accesskey="&editBookmarkOverlay.loadInSidebar.accesskey;"
               oncommand="gEditItemOverlay.onLoadInSidebarCheckboxCommand();">
--- a/browser/components/preferences/in-content/content.js
+++ b/browser/components/preferences/in-content/content.js
@@ -180,26 +180,26 @@ var gContentPane = {
   },
 
   /**
    * Displays the fonts dialog, where web page font names and sizes can be
    * configured.
    */  
   configureFonts: function ()
   {
-    gSubDialog.open("chrome://browser/content/preferences/fonts.xul");
+    gSubDialog.open("chrome://browser/content/preferences/fonts.xul", "resizable=no");
   },
 
   /**
    * Displays the colors dialog, where default web page/link/etc. colors can be
    * configured.
    */
   configureColors: function ()
   {
-    gSubDialog.open("chrome://browser/content/preferences/colors.xul");
+    gSubDialog.open("chrome://browser/content/preferences/colors.xul", "resizable=no");
   },
 
   // LANGUAGES
 
   /**
    * Shows a dialog in which the preferred language for web content may be set.
    */
   showLanguages: function ()
--- a/browser/components/preferences/in-content/privacy.js
+++ b/browser/components/preferences/in-content/privacy.js
@@ -506,17 +506,17 @@ var gPrivacyPane = {
    *   Clear Private Data settings, false otherwise
    */
 
   /**
    * Displays the Clear Private Data settings dialog.
    */
   showClearPrivateDataSettings: function ()
   {
-    gSubDialog.open("chrome://browser/content/preferences/sanitize.xul");
+    gSubDialog.open("chrome://browser/content/preferences/sanitize.xul", "resizable=no");
   },
 
 
   /**
    * Displays a dialog from which individual parts of private data may be
    * cleared.
    */
   clearPrivateDataNow: function (aClearEverything)
--- a/browser/components/preferences/in-content/search.js
+++ b/browser/components/preferences/in-content/search.js
@@ -1,14 +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/. */
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
 
 const ENGINE_FLAVOR = "text/x-moz-search-engine";
 
 var gEngineView = null;
 
 var gSearchPane = {
 
   init: function ()
@@ -186,29 +189,23 @@ var gSearchPane = {
     let index = gEngineView.selectedIndex;
     gEngineView.rowCountChanged(index, -1);
     gEngineView.invalidate();
     gEngineView.selection.select(Math.min(index, gEngineView.lastIndex));
     gEngineView.ensureRowIsVisible(gEngineView.currentIndex);
     document.getElementById("engineList").focus();
   },
 
-  editKeyword: function(aEngine, aNewKeyword) {
+  editKeyword: Task.async(function* (aEngine, aNewKeyword) {
     if (aNewKeyword) {
-      let bduplicate = false;
       let eduplicate = false;
       let dupName = "";
 
-      try {
-        let bmserv =
-          Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"]
-                    .getService(Components.interfaces.nsINavBookmarksService);
-        if (bmserv.getURIForKeyword(aNewKeyword))
-          bduplicate = true;
-      } catch(ex) {}
+      // Check for duplicates in Places keywords.
+      let bduplicate = !!(yield PlacesUtils.keywords.fetch(aNewKeyword));
 
       // Check for duplicates in changes we haven't committed yet
       let engines = gEngineView._engineStore.engines;
       for each (let engine in engines) {
         if (engine.alias == aNewKeyword &&
             engine.name != aEngine.name) {
           eduplicate = true;
           dupName = engine.name;
@@ -226,17 +223,17 @@ var gSearchPane = {
         Services.prompt.alert(window, dtitle, eduplicate ? emsg : bmsg);
         return false;
       }
     }
 
     gEngineView._engineStore.changeEngine(aEngine, "alias", aNewKeyword);
     gEngineView.invalidate();
     return true;
-  },
+  }),
 
   saveOneClickEnginesList: function () {
     let hiddenList = [];
     for (let engine of gEngineView._engineStore.engines) {
       if (!engine.shown)
         hiddenList.push(engine.name);
     }
     document.getElementById("browser.search.hiddenOneOffs").value =
@@ -514,19 +511,19 @@ EngineView.prototype = {
     if (column.id == "engineShown") {
       this._engineStore.engines[index].shown = value == "true";
       gEngineView.invalidate();
       gSearchPane.saveOneClickEnginesList();
     }
   },
   setCellText: function(index, column, value) {
     if (column.id == "engineKeyword") {
-      if (!gSearchPane.editKeyword(this._engineStore.engines[index], value)) {
-        setTimeout(() => {
+      gSearchPane.editKeyword(this._engineStore.engines[index], value)
+                 .then(valid => {
+        if (!valid)
           document.getElementById("engineList").startEditing(index, column);
-        }, 0);
-      }
+      });
     }
   },
   performAction: function(action) { },
   performActionOnRow: function(action, index) { },
   performActionOnCell: function(action, index, column) { }
 };
--- a/browser/components/preferences/in-content/security.js
+++ b/browser/components/preferences/in-content/security.js
@@ -212,17 +212,17 @@ var gSecurityPane = {
   },
 
   /**
    * Displays a dialog in which the master password may be changed.
    */
   changeMasterPassword: function ()
   {
     gSubDialog.open("chrome://mozapps/content/preferences/changemp.xul",
-                    null, null, this._initMasterPasswordUI.bind(this));
+                    "resizable=no", null, this._initMasterPasswordUI.bind(this));
   },
 
   /**
    * Shows the sites where the user has saved passwords and the associated login
    * information.
    */
   showPasswords: function ()
   {
--- a/browser/components/preferences/search.js
+++ b/browser/components/preferences/search.js
@@ -1,14 +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/. */
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
 
 const ENGINE_FLAVOR = "text/x-moz-search-engine";
 
 var gEngineView = null;
 
 var gSearchPane = {
 
   init: function ()
@@ -118,29 +121,23 @@ var gSearchPane = {
     let index = gEngineView.selectedIndex;
     gEngineView.rowCountChanged(index, -1);
     gEngineView.invalidate();
     gEngineView.selection.select(Math.min(index, gEngineView.lastIndex));
     gEngineView.ensureRowIsVisible(gEngineView.currentIndex);
     document.getElementById("engineList").focus();
   },
 
-  editKeyword: function(aEngine, aNewKeyword) {
+  editKeyword: Task.async(function* (aEngine, aNewKeyword) {
     if (aNewKeyword) {
-      let bduplicate = false;
       let eduplicate = false;
       let dupName = "";
 
-      try {
-        let bmserv =
-          Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"]
-                    .getService(Components.interfaces.nsINavBookmarksService);
-        if (bmserv.getURIForKeyword(aNewKeyword))
-          bduplicate = true;
-      } catch(ex) {}
+      // Check for duplicates in Places keywords.
+      let bduplicate = !!(yield PlacesUtils.keywords.fetch(aNewKeyword));
 
       // Check for duplicates in changes we haven't committed yet
       let engines = gEngineView._engineStore.engines;
       for each (let engine in engines) {
         if (engine.alias == aNewKeyword &&
             engine.name != aEngine.name) {
           eduplicate = true;
           dupName = engine.name;
@@ -158,17 +155,17 @@ var gSearchPane = {
         Services.prompt.alert(window, dtitle, eduplicate ? emsg : bmsg);
         return false;
       }
     }
 
     gEngineView._engineStore.changeEngine(aEngine, "alias", aNewKeyword);
     gEngineView.invalidate();
     return true;
-  },
+  }),
 
   saveOneClickEnginesList: function () {
     let hiddenList = [];
     for (let engine of gEngineView._engineStore.engines) {
       if (!engine.shown)
         hiddenList.push(engine.name);
     }
     document.getElementById("browser.search.hiddenOneOffs").value =
@@ -529,19 +526,19 @@ EngineView.prototype = {
     if (column.id == "engineShown") {
       this._engineStore.engines[index].shown = value == "true";
       gEngineView.invalidate();
       gSearchPane.saveOneClickEnginesList();
     }
   },
   setCellText: function(index, column, value) {
     if (column.id == "engineKeyword") {
-      if (!gSearchPane.editKeyword(this._engineStore.engines[index], value)) {
-        setTimeout(() => {
+      gSearchPane.editKeyword(this._engineStore.engines[index], value)
+                 .then(valid => {
+        if (!valid)
           document.getElementById("engineList").startEditing(index, column);
-        }, 0);
-      }
+      });
     }
   },
   performAction: function(action) { },
   performActionOnRow: function(action, index) { },
   performActionOnCell: function(action, index, column) { }
 };
--- a/browser/components/privatebrowsing/content/aboutPrivateBrowsing.js
+++ b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.js
@@ -12,23 +12,16 @@ var stringBundle = Cc["@mozilla.org/intl
 if (!PrivateBrowsingUtils.isContentWindowPrivate(window)) {
   document.title = stringBundle.GetStringFromName("title.normal");
   setFavIcon("chrome://global/skin/icons/question-16.png");
 } else {
   document.title = stringBundle.GetStringFromName("title");
   setFavIcon("chrome://browser/skin/Privacy-16.png");
 }
 
-var mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIWebNavigation)
-                       .QueryInterface(Ci.nsIDocShellTreeItem)
-                       .rootTreeItem
-                       .QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDOMWindow);
-
 function setFavIcon(url) {
   var icon = document.createElement("link");
   icon.setAttribute("rel", "icon");
   icon.setAttribute("type", "image/png");
   icon.setAttribute("href", url);
   var head = document.getElementsByTagName("head")[0];
   head.insertBefore(icon, head.firstChild);
 }
@@ -49,10 +42,12 @@ document.addEventListener("DOMContentLoa
   
   let startPrivateBrowsing = document.getElementById("startPrivateBrowsing");
   if (startPrivateBrowsing) {
     startPrivateBrowsing.addEventListener("command", openPrivateWindow);
   }
 }, false);
 
 function openPrivateWindow() {
-  mainWindow.OpenBrowserWindow({private: true});
+  // Ask chrome to open a private window
+  document.dispatchEvent(
+    new CustomEvent("AboutPrivateBrowsingOpenWindow", {bubbles:true}));
 }
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir.js
@@ -43,18 +43,16 @@ function test() {
       let gDownloadLastDir = new DownloadLastDir(win);
       aCallback(win, gDownloadLastDir);
       gDownloadLastDir.cleanupPrivateFile();
     });
   }
 
   function testDownloadDir(aWin, gDownloadLastDir, aFile, aDisplayDir, aLastDir,
                            aGlobalLastDir, aCallback) {
-    let context = aWin.gBrowser.selectedBrowser.contentWindow;
-
     // Check lastDir preference.
     is(prefs.getComplexValue("lastDir", Ci.nsIFile).path, aDisplayDir.path,
        "LastDir should be the expected display dir");
     // Check gDownloadLastDir value.
     is(gDownloadLastDir.file.path, aDisplayDir.path,
        "gDownloadLastDir should be the expected display dir");
 
     MockFilePicker.returnFiles = [aFile];
@@ -73,17 +71,17 @@ function test() {
       is(gDownloadLastDir.file.path, aGlobalLastDir.path,
          "gDownloadLastDir should be the expected global last dir");
 
       launcher.saveDestinationAvailable = null;
       aWin.close();
       aCallback();
     };
 
-    launcherDialog.promptForSaveToFileAsync(launcher, context, null, null, null);
+    launcherDialog.promptForSaveToFileAsync(launcher, aWin, null, null, null);
   }
 
   testOnWindow(false, function(win, downloadDir) {
     testDownloadDir(win, downloadDir, file1, tmpDir, dir1, dir1, function() {
       testOnWindow(true, function(win, downloadDir) {
         testDownloadDir(win, downloadDir, file2, dir1, dir1, dir2, function() {
           testOnWindow(false, function(win, downloadDir) {
             testDownloadDir(win, downloadDir, file3, dir1, dir3, dir3, finish);
--- a/browser/components/readinglist/ServerClient.jsm
+++ b/browser/components/readinglist/ServerClient.jsm
@@ -123,21 +123,32 @@ ServerClient.prototype = {
       if (headers) {
         for (let [headerName, headerValue] in Iterator(headers)) {
           log.trace("Caller specified header: ${headerName}=${headerValue}", {headerName, headerValue});
           request.setHeader(headerName, headerValue);
         }
       }
 
       request.onComplete = error => {
+        // Although the server API docs say the "Backoff" header is on
+        // successful responses while "Retry-After" is on error responses, we
+        // just look for them both in both cases (as the scheduler makes no
+        // distinction)
+        let response = request.response;
+        if (response && response.headers) {
+          let backoff = response.headers["backoff"] || response.headers["retry-after"];
+          if (backoff) {
+            log.info("Server requested backoff", backoff);
+            Services.obs.notifyObservers(null, "readinglist:backoff-requested", backoff);
+          }
+        }
         if (error) {
           return reject(this._convertRestError(error));
         }
 
-        let response = request.response;
         log.debug("received response status: ${status} ${statusText}", response);
         // Handle response status codes we know about
         let result = {
           status: response.status,
           headers: response.headers
         };
         try {
           if (response.body) {
--- 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/components/readinglist/sidebar.js
+++ b/browser/components/readinglist/sidebar.js
@@ -52,17 +52,19 @@ let RLSidebar = {
     log.debug("Initializing");
 
     addEventListener("unload", () => this.uninit());
 
     this.list = document.getElementById("list");
     this.emptyListInfo = document.getElementById("emptyListInfo");
     this.itemTemplate = document.getElementById("item-template");
 
-    this.list.addEventListener("click", event => this.onListClick(event));
+    // click events for middle-clicks are not sent to DOM nodes, only to the document.
+    document.addEventListener("click", event => this.onClick(event));
+
     this.list.addEventListener("mousemove", event => this.onListMouseMove(event));
     this.list.addEventListener("keydown", event => this.onListKeyDown(event), true);
 
     window.addEventListener("message", event => this.onMessage(event));
 
     this.listPromise = this.ensureListItems();
     ReadingList.addListener(this);
 
@@ -379,20 +381,20 @@ let RLSidebar = {
     if (node != this.list && node != document.documentElement) {
       return node;
     }
 
     return null;
   },
 
   /**
-   * Handle a click event on the list box.
+   * Handle a click event on the sidebar.
    * @param {Event} event - Triggering event.
    */
-  onListClick(event) {
+  onClick(event) {
     let itemNode = this.findParentItemNode(event.target);
     if (!itemNode)
       return;
 
     if (event.target.classList.contains("remove-button")) {
       ReadingList.deleteItem(this.getItemFromNode(itemNode));
       return;
     }
--- a/browser/components/readinglist/test/xpcshell/test_ServerClient.js
+++ b/browser/components/readinglist/test/xpcshell/test_ServerClient.js
@@ -76,16 +76,26 @@ function OAuthTokenServer() {
     },
   }
   server = new Server(handlers);
   server.numTokenFetches = 0;
   server.activeTokens = new Set();
   return server;
 }
 
+function promiseObserver(topic) {
+  return new Promise(resolve => {
+    function observe(subject, topic, data) {
+      Services.obs.removeObserver(observe, topic);
+      resolve(data);
+    }
+    Services.obs.addObserver(observe, topic, false);
+  });
+}
+
 // The tests.
 function run_test() {
   run_next_test();
 }
 
 // Arrange for the first token we hand out to be rejected - the client should
 // notice the 401 and silently get a new token and retry the request.
 add_task(function testAuthRetry() {
@@ -160,16 +170,82 @@ add_task(function testHeaders() {
       body: {foo: "bar"}});
     equal(response.status, 200, "got the 200 we expected");
     equal(response.headers["server-sent-header"], "hello", "got the server header");
   } finally {
     yield rlserver.stop();
   }
 });
 
+// Check that a "backoff" header causes the correct notification.
+add_task(function testBackoffHeader() {
+  let handlers = {
+    "/v1/batch": (request, response) => {
+      response.setHeader("Backoff", "123");
+      response.setStatusLine("1.1", 200, "OK");
+      response.write("{}");
+    }
+  };
+  let rlserver = new Server(handlers);
+  rlserver.start();
+
+  let observerPromise = promiseObserver("readinglist:backoff-requested");
+  try {
+    Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
+
+    let fxa = yield createMockFxA();
+    let sc = new ServerClient(fxa);
+    sc._getToken = () => Promise.resolve();
+
+    let response = yield sc.request({
+      path: "/batch",
+      method: "post",
+      headers: {"X-Foo": "bar"},
+      body: {foo: "bar"}});
+    equal(response.status, 200, "got the 200 we expected");
+    let data = yield observerPromise;
+    equal(data, "123", "got the expected header value.")
+  } finally {
+    yield rlserver.stop();
+  }
+});
+
+// Check that a "backoff" header causes the correct notification.
+add_task(function testRetryAfterHeader() {
+  let handlers = {
+    "/v1/batch": (request, response) => {
+      response.setHeader("Retry-After", "456");
+      response.setStatusLine("1.1", 500, "Not OK");
+      response.write("{}");
+    }
+  };
+  let rlserver = new Server(handlers);
+  rlserver.start();
+
+  let observerPromise = promiseObserver("readinglist:backoff-requested");
+  try {
+    Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
+
+    let fxa = yield createMockFxA();
+    let sc = new ServerClient(fxa);
+    sc._getToken = () => Promise.resolve();
+
+    let response = yield sc.request({
+      path: "/batch",
+      method: "post",
+      headers: {"X-Foo": "bar"},
+      body: {foo: "bar"}});
+    equal(response.status, 500, "got the 500 we expected");
+    let data = yield observerPromise;
+    equal(data, "456", "got the expected header value.")
+  } finally {
+    yield rlserver.stop();
+  }
+});
+
 // Check that unicode ends up as utf-8 in requests, and vice-versa in responses.
 // (Note the ServerClient assumes all strings in and out are UCS, and thus have
 // already been encoded/decoded (ie, it never expects to receive stuff already
 // utf-8 encoded, and never returns utf-8 encoded responses.)
 add_task(function testUTF8() {
   let handlers = {
     "/v1/hello": (request, response) => {
       // Get the body as bytes.
--- a/browser/components/search/content/engineManager.js
+++ b/browser/components/search/content/engineManager.js
@@ -1,13 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 
 const ENGINE_FLAVOR = "text/x-moz-search-engine";
 
 const BROWSER_SUGGEST_PREF = "browser.search.suggest.enabled";
 
@@ -100,38 +105,34 @@ var gEngineManagerDialog = {
 
     gEngineView.invalidate();
     gEngineView.selection.select(newIndex);
     gEngineView.ensureRowIsVisible(newIndex);
     this.showRestoreDefaults(true);
     document.getElementById("engineList").focus();
   },
 
-  editKeyword: function engineManager_editKeyword() {
+  editKeyword: Task.async(function* () {
     var selectedEngine = gEngineView.selectedEngine;
     if (!selectedEngine)
       return;
 
     var alias = { value: selectedEngine.alias };
     var strings = document.getElementById("engineManagerBundle");
     var title = strings.getString("editTitle");
     var msg = strings.getFormattedString("editMsg", [selectedEngine.name]);
 
     while (Services.prompt.prompt(window, title, msg, alias, null, {})) {
       var bduplicate = false;
       var eduplicate = false;
       var dupName = "";
 
       if (alias.value != "") {
-        try {
-          let bmserv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
-                       getService(Ci.nsINavBookmarksService);
-          if (bmserv.getURIForKeyword(alias.value))
-            bduplicate = true;
-        } catch(ex) {}
+        // Check for duplicates in Places keywords.
+        bduplicate = !!(yield PlacesUtils.keywords.fetch(alias.value));
 
         // Check for duplicates in changes we haven't committed yet
         let engines = gEngineView._engineStore.engines;
         for each (let engine in engines) {
           if (engine.alias == alias.value &&
               engine.name != selectedEngine.name) {
             eduplicate = true;
             dupName = engine.name;
@@ -149,17 +150,17 @@ var gEngineManagerDialog = {
         Services.prompt.alert(window, dtitle, eduplicate ? emsg : bmsg);
       } else {
         gEngineView._engineStore.changeEngine(selectedEngine, "alias",
                                               alias.value);
         gEngineView.invalidate();
         break;
       }
     }
-  },
+  }),
 
   onSelect: function engineManager_onSelect() {
     // Buttons only work if an engine is selected and it's not the last engine,
     // the latter is true when the selected is first and last at the same time.
     var lastSelected = (gEngineView.selectedIndex == gEngineView.lastIndex);
     var firstSelected = (gEngineView.selectedIndex == 0);
     var noSelection = (gEngineView.selectedIndex == -1);
 
--- a/browser/components/search/content/engineManager.xul
+++ b/browser/components/search/content/engineManager.xul
@@ -31,17 +31,17 @@
              disabled="true"/>
     <command id="cmd_moveup"
              oncommand="gEngineManagerDialog.bump(1);"
              disabled="true"/>
     <command id="cmd_movedown"
              oncommand="gEngineManagerDialog.bump(-1);"
              disabled="true"/>
     <command id="cmd_editkeyword"
-             oncommand="gEngineManagerDialog.editKeyword();"
+             oncommand="gEngineManagerDialog.editKeyword().catch(Components.utils.reportError);"
              disabled="true"/>
   </commandset>
 
   <keyset id="engineManagerKeyset">
     <key id="delete" keycode="VK_DELETE" command="cmd_remove"/>
   </keyset>
 
   <stringbundleset id="engineManagerBundleset">
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -1016,24 +1016,39 @@
       </method>
 
       <field name="_selectedButton"/>
       <property name="selectedButton" onget="return this._selectedButton;">
         <setter><![CDATA[
           if (this._selectedButton)
             this._selectedButton.removeAttribute("selected");
 
+          let textbox = document.getBindingParent(this).textbox;
+          let header =
+            document.getAnonymousElementByAttribute(this.popup, "anonid",
+                                                    "search-panel-one-offs-header");
           // Avoid selecting dummy buttons.
           if (val && !val.classList.contains("dummy")) {
             val.setAttribute("selected", "true");
             this._selectedButton = val;
+            if (val.classList.contains("searchbar-engine-one-off-item")) {
+              let headerEngineText =
+                document.getAnonymousElementByAttribute(this.popup, "anonid",
+                                                        "searchbar-oneoffheader-engine");
+              header.selectedIndex = 2;
+              headerEngineText.value = val.engine.name;
+            }
+            else {
+              header.selectedIndex = textbox.value ? 1 : 0;
+            }
             this.setAttribute("aria-activedescendant", val.id);
             return;
           }
 
+          header.selectedIndex = textbox.value ? 1 : 0;
           this.removeAttribute("aria-activedescendant");
           this._selectedButton = null;
         ]]></setter>
       </property>
 
       <method name="getSelectableButtons">
         <parameter name="aCycleEngines"/>
         <body><![CDATA[
--- a/browser/components/search/test/browser.ini
+++ b/browser/components/search/test/browser.ini
@@ -31,16 +31,18 @@ skip-if = e10s # Bug ?????? - Test touch
 [browser_eBay_behavior.js]
 skip-if = e10s # Bug ?????? - some issue with progress listeners [JavaScript Error: "req.originalURI is null" {file: "chrome://mochitests/content/browser/browser/components/search/test/browser_bing_behavior.js" line: 127}]
 [browser_google.js]
 [browser_google_behavior.js]
 skip-if = e10s # Bug ?????? - some issue with progress listeners [JavaScript Error: "req.originalURI is null" {file: "chrome://mochitests/content/browser/browser/components/search/test/browser_bing_behavior.js" line: 127}]
 [browser_healthreport.js]
 [browser_hiddenOneOffs_cleanup.js]
 [browser_hiddenOneOffs_diacritics.js]
+[browser_oneOffHeader.js]
+skip-if = e10s # bug ?????? - Test alters the searchbar textbox value which causes issues with other tests in e10s.
 [browser_private_search_perwindowpb.js]
 skip-if = e10s # Bug ?????? - Test uses load event and checks event.target.
 [browser_yahoo.js]
 [browser_yahoo_behavior.js]
 skip-if = e10s # Bug ?????? - some issue with progress listeners [JavaScript Error: "req.originalURI is null" {file: "chrome://mochitests/content/browser/browser/components/search/test/browser_bing_behavior.js" line: 127}]
 [browser_abouthome_behavior.js]
 skip-if = e10s || true # Bug ??????, Bug 1100301 - leaks windows until shutdown when --run-by-dir
 [browser_searchbar_openpopup.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/search/test/browser_oneOffHeader.js
@@ -0,0 +1,141 @@
+/* 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/. */
+// Tests that keyboard navigation in the search panel works as designed.
+
+const isMac = ("nsILocalFileMac" in Ci);
+
+const searchbar = document.getElementById("searchbar");
+const textbox = searchbar._textbox;
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const searchIcon = document.getAnonymousElementByAttribute(searchbar, "anonid",
+                                                           "searchbar-search-button");
+const searchSettings =
+  document.getAnonymousElementByAttribute(searchPopup, "anonid",
+                                          "search-settings");
+let header =
+  document.getAnonymousElementByAttribute(searchPopup, "anonid",
+                                          "search-panel-one-offs-header");
+function getHeaderText() {
+  let headerChild = header.selectedPanel;
+  while (headerChild.hasChildNodes()) {
+    headerChild = headerChild.firstChild;
+  }
+  let headerStrings = [];
+  for (let label = headerChild; label; label = label.nextSibling) {
+    headerStrings.push(label.value);
+  }
+  return headerStrings.join("");
+}
+
+// Get an array of the one-off buttons.
+function getOneOffs() {
+  let oneOffs = [];
+  let oneOff =
+    document.getAnonymousElementByAttribute(searchPopup, "anonid",
+                                            "search-panel-one-offs");
+  for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
+    if (oneOff.classList.contains("dummy"))
+      break;
+    oneOffs.push(oneOff);
+  }
+
+  return oneOffs;
+}
+
+const msg = isMac ? 5 : 1;
+const utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIDOMWindowUtils);
+const scale = utils.screenPixelsPerCSSPixel;
+function* synthesizeNativeMouseMove(aElement) {
+  let rect = aElement.getBoundingClientRect();
+  let win = aElement.ownerDocument.defaultView;
+  let x = win.mozInnerScreenX + (rect.left + rect.right) / 2;
+  let y = win.mozInnerScreenY + (rect.top + rect.bottom) / 2;
+
+  // Wait for the mouseup event to occur before continuing.
+  return new Promise((resolve, reject) => {
+    function eventOccurred(e)
+    {
+      aElement.removeEventListener("mouseover", eventOccurred, true);
+      resolve();
+    }
+
+    aElement.addEventListener("mouseover", eventOccurred, true);
+
+    utils.sendNativeMouseEvent(x * scale, y * scale, msg, 0, null);
+  });
+}
+
+
+add_task(function* init() {
+  yield promiseNewEngine("testEngine.xml");
+});
+
+add_task(function* test_notext() {
+  let promise = promiseEvent(searchPopup, "popupshown");
+  info("Opening search panel");
+  EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+  yield promise;
+
+  is(header.getAttribute("selectedIndex"), 0,
+     "Header has the correct index selected with no search terms.");
+
+  is(getHeaderText(), "Search with:",
+     "Search header string is correct when no search terms have been entered");
+
+  yield synthesizeNativeMouseMove(searchSettings);
+  is(header.getAttribute("selectedIndex"), 0,
+     "Header has the correct index when no search terms have been entered and the Change Search Settings button is selected.");
+  is(getHeaderText(), "Search with:",
+     "Header has the correct text when no search terms have been entered and the Change Search Settings button is selected.");
+
+  let buttons = getOneOffs();
+  yield synthesizeNativeMouseMove(buttons[0]);
+  is(header.getAttribute("selectedIndex"), 2,
+     "Header has the correct index selected when a search engine has been selected");
+  is(getHeaderText(), "Search " + buttons[0].engine.name,
+     "Is the header text correct when a search engine is selected and no terms have been entered.");
+
+  promise = promiseEvent(searchPopup, "popuphidden");
+  info("Closing search panel");
+  EventUtils.synthesizeKey("VK_ESCAPE", {});
+  yield promise;
+});
+
+add_task(function* test_text() {
+  textbox.value = "foo";
+  registerCleanupFunction(() => {
+    textbox.value = "";
+  });
+
+  let promise = promiseEvent(searchPopup, "popupshown");
+  info("Opening search panel");
+  SimpleTest.executeSoon(() => {
+    EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+  });
+  yield promise;
+
+  is(header.getAttribute("selectedIndex"), 1,
+     "Header has the correct index selected with a search term.");
+  is(getHeaderText(), "Search for foo with:",
+     "Search header string is correct when a search term has been entered");
+
+  let buttons = getOneOffs();
+  yield synthesizeNativeMouseMove(buttons[0]);
+  is(header.getAttribute("selectedIndex"), 2,
+     "Header has the correct index selected when a search engine has been selected");
+  is(getHeaderText(), "Search " + buttons[0].engine.name,
+     "Is the header text correct when search terms are entered after a search engine has been selected.");
+
+  yield synthesizeNativeMouseMove(searchSettings);
+  is(header.getAttribute("selectedIndex"), 1,
+     "Header has the correct index selected when search terms have been entered and the Change Search Settings button is selected.");
+  is(getHeaderText(), "Search for foo with:",
+     "Header has the correct text when search terms have been entered and the Change Search Settings button is selected.");
+
+  promise = promiseEvent(searchPopup, "popuphidden");
+  info("Closing search panel");
+  EventUtils.synthesizeKey("VK_ESCAPE", {});
+  yield promise;
+});
--- a/browser/components/sessionstore/ContentRestore.jsm
+++ b/browser/components/sessionstore/ContentRestore.jsm
@@ -118,31 +118,33 @@ ContentRestoreInternal.prototype = {
   },
 
   /**
    * Starts the process of restoring a tab. The tabData to be restored is passed
    * in here and used throughout the restoration. The epoch (which must be
    * non-zero) is passed through to all the callbacks. If a load in the tab
    * is started while it is pending, the appropriate callbacks are called.
    */
-  restoreHistory(epoch, tabData, callbacks) {
+  restoreHistory(epoch, tabData, loadArguments, callbacks) {
     this._tabData = tabData;
     this._epoch = epoch;
 
     // In case about:blank isn't done yet.
     let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
     webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL);
 
     // Make sure currentURI is set so that switch-to-tab works before the tab is
     // restored. We'll reset this to about:blank when we try to restore the tab
-    // to ensure that docshell doeesn't get confused.
+    // to ensure that docshell doeesn't get confused. Don't bother doing this if
+    // we're restoring immediately due to a process switch. It just causes the
+    // URL bar to be temporarily blank.
     let activeIndex = tabData.index - 1;
     let activePageData = tabData.entries[activeIndex] || {};
     let uri = activePageData.url || null;
-    if (uri) {
+    if (uri && !loadArguments) {
       webNavigation.setCurrentURI(Utils.makeURI(uri));
     }
 
     SessionHistory.restore(this.docShell, tabData);
 
     // Add a listener to watch for reloads.
     let listener = new HistoryListener(this.docShell, callbacks.onReload);
     webNavigation.sessionHistory.addSHistoryListener(listener);
@@ -186,18 +188,21 @@ ContentRestoreInternal.prototype = {
     let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
     let history = webNavigation.sessionHistory;
 
     // Listen for the tab to finish loading.
     this.restoreTabContentStarted(finishCallback);
 
     // Reset the current URI to about:blank. We changed it above for
     // switch-to-tab, but now it must go back to the correct value before the
-    // load happens.
-    webNavigation.setCurrentURI(Utils.makeURI("about:blank"));
+    // load happens. Don't bother doing this if we're restoring immediately
+    // due to a process switch.
+    if (!loadArguments) {
+      webNavigation.setCurrentURI(Utils.makeURI("about:blank"));
+    }
 
     try {
       if (loadArguments) {
         // A load has been redirected to a new process so get history into the
         // same state it was before the load started then trigger the load.
         let activeIndex = tabData.index - 1;
         if (activeIndex > 0) {
           // Go to the right history entry, but don't load anything yet.
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -2684,17 +2684,17 @@ let SessionStoreInternal = {
       scroll: tabData.scroll || null,
       storage: tabData.storage || null,
       formdata: tabData.formdata || null,
       disallow: tabData.disallow || null,
       pageStyle: tabData.pageStyle || null
     });
 
     browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory",
-                                            {tabData: tabData, epoch: epoch});
+                                            {tabData: tabData, epoch: epoch, loadArguments});
 
     // Restore tab attributes.
     if ("attributes" in tabData) {
       TabAttributes.set(tab, tabData.attributes);
     }
 
     // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
     // it ensures each window will have its selected tab loaded.
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -127,18 +127,18 @@ let MessageListener = {
         gContentRestore.resetRestore();
         break;
       default:
         debug("received unknown message '" + name + "'");
         break;
     }
   },
 
-  restoreHistory({epoch, tabData}) {
-    gContentRestore.restoreHistory(epoch, tabData, {
+  restoreHistory({epoch, tabData, loadArguments}) {
+    gContentRestore.restoreHistory(epoch, tabData, loadArguments, {
       onReload() {
         // Inform SessionStore.jsm about the reload. It will send
         // restoreTabContent in response.
         sendAsyncMessage("SessionStore:reloadPendingTab", {epoch});
       },
 
       // Note: The two callbacks passed here will only be used when a load
       // starts that was not initiated by sessionstore itself. This can happen
--- a/browser/components/uitour/UITour-lib.js
+++ b/browser/components/uitour/UITour-lib.js
@@ -276,14 +276,22 @@ if (typeof Mozilla == 'undefined') {
 	};
 
 	Mozilla.UITour.openSearchPanel = function(callback) {
 		_sendEvent('openSearchPanel', {
 			callbackID: _waitForCallback(callback)
 		});
 	};
 
+	Mozilla.UITour.forceShowReaderIcon = function() {
+		_sendEvent('forceShowReaderIcon');
+	};
+
+	Mozilla.UITour.toggleReaderMode = function(feature) {
+		_sendEvent('toggleReaderMode');
+	};
+
 })();
 
 // Make this library Require-able.
 if (typeof module !== 'undefined' && module.exports) {
-  module.exports = Mozilla.UITour;
+  module.exports = Mozilla.UITour;
 }
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -22,23 +22,29 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
   "resource://gre/modules/UITelemetry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
   "resource:///modules/BrowserUITelemetry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Metrics",
   "resource://gre/modules/Metrics.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
+  "resource://gre/modules/ReaderMode.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ReaderParent",
+  "resource:///modules/ReaderParent.jsm");
 
 // See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
 const PREF_LOG_LEVEL      = "browser.uitour.loglevel";
 const PREF_SEENPAGEIDS    = "browser.uitour.seenPageIDs";
+const PREF_READERVIEW_TRIGGER = "browser.uitour.readerViewTrigger";
 
 const BACKGROUND_PAGE_ACTIONS_ALLOWED = new Set([
   "endUrlbarCapture",
+  "forceShowReaderIcon",
   "getConfiguration",
   "getTreatmentTag",
   "hideHighlight",
   "hideInfo",
   "hideMenu",
   "ping",
   "registerPageID",
   "setConfiguration",
@@ -182,16 +188,17 @@ this.UITour = {
         if (!loopBrowser) {
           return null;
         }
         return loopBrowser.contentDocument.querySelector(".signin-link");
       },
     }],
     ["privateWindow",  {query: "#privatebrowsing-button"}],
     ["quit",        {query: "#PanelUI-quit"}],
+    ["readerMode-urlBar", {query: "#reader-mode-button"}],
     ["search",      {
       infoPanelOffsetX: 18,
       infoPanelPosition: "after_start",
       query: "#searchbar",
       widgetName: "search-container",
     }],
     ["searchProvider", {
       query: (aDocument) => {
@@ -336,16 +343,32 @@ this.UITour = {
       Services.prefs.clearUserPref(PREF_SEENPAGEIDS);
       return;
     }
 
     Services.prefs.setCharPref(PREF_SEENPAGEIDS,
                                JSON.stringify([...this.seenPageIDs]));
   },
 
+  get _readerViewTriggerRegEx() {
+    delete this._readerViewTriggerRegEx;
+    let readerViewUITourTrigger = Services.prefs.getCharPref(PREF_READERVIEW_TRIGGER);
+    return this._readerViewTriggerRegEx = new RegExp(readerViewUITourTrigger, "i");
+  },
+
+  onLocationChange: function(aLocation) {
+    // The ReadingList/ReaderView tour page is expected to run in Reader View,
+    // which disables JavaScript on the page. To get around that, we
+    // automatically start a pre-defined tour on page load.
+    let originalUrl = ReaderMode.getOriginalUrl(aLocation);
+    if (this._readerViewTriggerRegEx.test(originalUrl)) {
+      this.startSubTour("readinglist");
+    }
+  },
+
   onPageEvent: function(aMessage, aEvent) {
     let browser = aMessage.target;
     let window = browser.ownerDocument.defaultView;
 
     // Does the window have tabs? We need to make sure since windowless browsers do
     // not have tabs.
     if (!window.gBrowser) {
       // When using windowless browsers we don't have a valid |window|. If that's the case,
@@ -672,16 +695,28 @@ this.UITour = {
         break;
       }
 
       case "ping": {
         if (typeof data.callbackID == "string")
           this.sendPageCallback(messageManager, data.callbackID);
         break;
       }
+
+      case "forceShowReaderIcon": {
+        ReaderParent.forceShowReaderIcon(browser);
+        break;
+      }
+
+      case "toggleReaderMode": {
+        let targetPromise = this.getTarget(window, "readerMode-urlBar");
+        targetPromise.then(target => {
+          ReaderParent.toggleReaderMode({target: target.node});
+        });
+      }
     }
 
     if (!this.tourBrowsersByWindow.has(window)) {
       this.tourBrowsersByWindow.set(window, new Set());
     }
     this.tourBrowsersByWindow.get(window).add(browser);
 
     Services.obs.addObserver(this, "message-manager-close", false);
@@ -1707,16 +1742,30 @@ this.UITour = {
     }.bind(this)).catch(err => {
       log.error(err);
       this.sendPageCallback(aMessageManager, aCallbackID, {
         targets: [],
       });
     });
   },
 
+  startSubTour: function (aFeature) {
+    if (aFeature != "string") {
+      log.error("startSubTour: No feature option specified");
+      return;
+    }
+
+    if (aFeature == "readinglist") {
+      ReaderParent.showReaderModeInfoPanel(browser);
+    } else {
+      log.error("startSubTour: Unknown feature option specified");
+      return;
+    }
+  },
+
   addNavBarWidget: function (aTarget, aMessageManager, aCallbackID) {
     if (aTarget.node) {
       log.error("addNavBarWidget: can't add a widget already present:", aTarget);
       return;
     }
     if (!aTarget.allowAdd) {
       log.error("addNavBarWidget: not allowed to add this widget:", aTarget);
       return;
--- a/browser/components/uitour/test/browser.ini
+++ b/browser/components/uitour/test/browser.ini
@@ -4,33 +4,37 @@ support-files =
   image.png
   uitour.html
   ../UITour-lib.js
 
 [browser_no_tabs.js]
 [browser_UITour.js]
 skip-if = os == "linux" || e10s # Intermittent failures, bug 951965
 [browser_UITour2.js]
-skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
+skip-if = e10s # Bug 1073247 - UITour.jsm not e10s friendly
 # [browser_UITour3.js] Bug 1113038
-# skip-if = os == "linux" || e10s # Linux: Bug 986760, Bug 989101; e10s: Bug 941428 - UITour.jsm not e10s friendly
+# skip-if = os == "linux" || e10s # Linux: Bug 986760, Bug 989101; e10s: Bug 1073247 - UITour.jsm not e10s friendly
 [browser_UITour_availableTargets.js]
-skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
+skip-if = e10s # Bug 1073247 - UITour.jsm not e10s friendly
 [browser_UITour_detach_tab.js]
-skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
+skip-if = e10s # Bug 1073247 - UITour.jsm not e10s friendly
 [browser_UITour_annotation_size_attributes.js]
-skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly.
+skip-if = e10s # Bug 1073247 - UITour.jsm not e10s friendly.
+[browser_UITour_forceReaderMode.js]
+skip-if = e10s # Bug 1073247 - UITour.jsm not e10s friendly.
+[browser_UITour_toggleReaderMode.js]
+skip-if = e10s # Bug 1073247 - UITour.jsm not e10s friendly
 [browser_UITour_heartbeat.js]
-skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly.
+skip-if = e10s # Bug 1073247 - UITour.jsm not e10s friendly.
 [browser_UITour_loop.js]
-skip-if = os == "linux" || e10s # Bug 941428 - UITour.jsm not e10s friendly.
+skip-if = os == "linux" || e10s # Bug 1073247 - UITour.jsm not e10s friendly.
 [browser_UITour_modalDialog.js]
-skip-if = os != "mac" || e10s # modal dialog disabling only working on OS X.Bug 941428 - UITour.jsm not e10s friendly
+skip-if = os != "mac" || e10s # modal dialog disabling only working on OS X.Bug 1073247 - UITour.jsm not e10s friendly
 [browser_UITour_observe.js]
-skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly.
+skip-if = e10s # Bug 1073247 - UITour.jsm not e10s friendly.
 [browser_UITour_panel_close_annotation.js]
 skip-if = true # Disabled due to frequent failures, bugs 1026310 and 1032137
 [browser_UITour_registerPageID.js]
-skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
+skip-if = e10s # Bug 1073247 - UITour.jsm not e10s friendly
 [browser_UITour_sync.js]
-skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
+skip-if = e10s # Bug 1073247 - UITour.jsm not e10s friendly
 [browser_UITour_resetProfile.js]
-skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
+skip-if = e10s # Bug 1073247 - UITour.jsm not e10s friendly
--- a/browser/components/uitour/test/browser_UITour_availableTargets.js
+++ b/browser/components/uitour/test/browser_UITour_availableTargets.js
@@ -34,16 +34,17 @@ let tests = [
         "bookmarks",
         "customize",
         "help",
         "home",
         "loop",
         "devtools",
         "privateWindow",
         "quit",
+        "readerMode-urlBar",
         "search",
         "searchIcon",
         "urlbar",
         ...searchEngineTargets(),
         ...(hasWebIDE ? ["webide"] : [])
       ]);
 
       ok(UITour.availableTargetsCache.has(window),
@@ -64,16 +65,17 @@ let tests = [
         "backForward",
         "customize",
         "help",
         "loop",
         "devtools",
         "home",
         "privateWindow",
         "quit",
+        "readerMode-urlBar",
         "search",
         "searchIcon",
         "urlbar",
         ...searchEngineTargets(),
         ...(hasWebIDE ? ["webide"] : [])
       ]);
 
       ok(UITour.availableTargetsCache.has(window),
@@ -99,16 +101,17 @@ let tests = [
         "bookmarks",
         "customize",
         "help",
         "home",
         "loop",
         "devtools",
         "privateWindow",
         "quit",
+        "readerMode-urlBar",
         "urlbar",
         ...(hasWebIDE ? ["webide"] : [])
       ]);
 
       CustomizableUI.reset();
       done();
     });
   },
new file mode 100644
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_forceReaderMode.js
@@ -0,0 +1,23 @@
+"use strict";
+
+let gTestTab;
+let gContentAPI;
+let gContentWindow;
+
+Components.utils.import("resource:///modules/UITour.jsm");
+
+function test() {
+  UITourTest();
+}
+
+let tests = [
+  taskify(function*() {
+    ok(!gBrowser.selectedBrowser.isArticle, "Should not be an article when we start");
+    ok(document.getElementById("reader-mode-button").hidden, "Button should be hidden.");
+    gContentAPI.forceShowReaderIcon();
+    yield waitForConditionPromise(() => gBrowser.selectedBrowser.isArticle);
+    ok(gBrowser.selectedBrowser.isArticle, "Should suddenly be an article.");
+    ok(!document.getElementById("reader-mode-button").hidden, "Button should now be visible.");
+  })
+];
+
new file mode 100644
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_toggleReaderMode.js
@@ -0,0 +1,20 @@
+"use strict";
+
+let gTestTab;
+let gContentAPI;
+let gContentWindow;
+
+Components.utils.import("resource:///modules/UITour.jsm");
+
+function test() {
+  UITourTest();
+}
+
+let tests = [
+  taskify(function*() {
+    ok(!gBrowser.selectedBrowser.currentURI.spec.startsWith("about:reader"), "Should not be in reader mode at start of test.");
+    gContentAPI.toggleReaderMode();
+    yield waitForConditionPromise(() => gBrowser.selectedBrowser.currentURI.spec.startsWith("about:reader"));
+    ok(gBrowser.selectedBrowser.currentURI.spec.startsWith("about:reader"), "Should be in reader mode now.");
+  })
+];
new file mode 100644
--- /dev/null
+++ b/browser/config/mozconfigs/macosx64/nightly
@@ -0,0 +1,19 @@
+. $topsrcdir/build/macosx/mozconfig.common
+
+ac_add_options --enable-signmar
+ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
+
+# Needed to enable breakpad in application.ini
+export MOZILLA_OFFICIAL=1
+
+if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
+ac_add_options --with-macbundlename-prefix=Firefox
+fi
+
+# Treat warnings as errors in directories with FAIL_ON_WARNINGS.
+ac_add_options --enable-warnings-as-errors
+
+# Package js shell.
+export MOZ_PACKAGE_JSSHELL=1
+
+. "$topsrcdir/build/mozconfig.common.override"
--- a/browser/devtools/animationinspector/animation-controller.js
+++ b/browser/devtools/animationinspector/animation-controller.js
@@ -50,17 +50,17 @@ let startup = Task.async(function*(inspe
 /**
  * Shutdown the animationinspector controller and view, called by the sidebar
  * widget when loading/unloading the iframe into the tab.
  */
 let shutdown = Task.async(function*() {
   yield AnimationsController.destroy();
   // Don't assume that AnimationsPanel is defined here, it's in another file.
   if (typeof AnimationsPanel !== "undefined") {
-    yield AnimationsPanel.destroy()
+    yield AnimationsPanel.destroy();
   }
   gToolbox = gInspector = null;
 });
 
 // This is what makes the sidebar widget able to load/unload the panel.
 function setPanel(panel) {
   return startup(panel).catch(Cu.reportError);
 }
@@ -92,34 +92,38 @@ let AnimationsController = {
   PLAYERS_UPDATED_EVENT: "players-updated",
 
   initialize: Task.async(function*() {
     if (this.initialized) {
       return this.initialized.promise;
     }
     this.initialized = promise.defer();
 
+    this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
+    this.onNewNodeFront = this.onNewNodeFront.bind(this);
+    this.onAnimationMutations = this.onAnimationMutations.bind(this);
+
     let target = gToolbox.target;
-    this.animationsFront = new AnimationsFront(target.client, target.form);
 
     // Expose actor capabilities.
     this.hasToggleAll = yield target.actorHasMethod("animations", "toggleAll");
     this.hasSetCurrentTime = yield target.actorHasMethod("animationplayer",
                                                          "setCurrentTime");
     this.hasMutationEvents = yield target.actorHasMethod("animations",
                                                          "stopAnimationPlayerUpdates");
     this.hasSetPlaybackRate = yield target.actorHasMethod("animationplayer",
                                                           "setPlaybackRate");
 
-    this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
-    this.onNewNodeFront = this.onNewNodeFront.bind(this);
-    this.onAnimationMutations = this.onAnimationMutations.bind(this);
+    if (this.destroyed) {
+      console.warn("Could not fully initialize the AnimationsController");
+      return;
+    }
 
+    this.animationsFront = new AnimationsFront(target.client, target.form);
     this.startListeners();
-
     yield this.onNewNodeFront();
 
     this.initialized.resolve();
   }),
 
   destroy: Task.async(function*() {
     if (!this.initialized) {
       return;
--- a/browser/devtools/animationinspector/animation-panel.js
+++ b/browser/devtools/animationinspector/animation-panel.js
@@ -9,16 +9,20 @@
 /**
  * The main animations panel UI.
  */
 let AnimationsPanel = {
   UI_UPDATED_EVENT: "ui-updated",
   PANEL_INITIALIZED: "panel-initialized",
 
   initialize: Task.async(function*() {
+    if (AnimationsController.destroyed) {
+      console.warn("Could not initialize the animation-panel, controller was destroyed");
+      return;
+    }
     if (this.initialized) {
       return this.initialized.promise;
     }
     this.initialized = promise.defer();
 
     this.playersEl = document.querySelector("#players");
     this.errorMessageEl = document.querySelector("#error-message");
     this.pickerButtonEl = document.querySelector("#element-picker");
--- 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/app-manager/test/test_connection_store.html
+++ b/browser/devtools/app-manager/test/test_connection_store.html
@@ -53,17 +53,17 @@ Bug 901519 - [app manager] data store fo
         let port = root.querySelector("#port");
         let template = new Template(root, store, () => {});
         template.start();
 
         connection.host = "foobar";
         connection.port = 42;
 
         is(host.textContent, "foobar", "host updated");
-        is(port.textContent, 42, "port updated");
+        is(port.textContent, "42", "port updated");
 
         let been_through_connecting = false;
         let been_through_connected = false;
         let been_through_disconnected = false;
 
         is(status.textContent, "disconnected", "status updated (diconnected)");
 
         connection.once("connecting", (e) => {
--- 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/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -404,16 +404,20 @@ InspectorPanel.prototype = {
   _selectionCssSelector: null,
 
   /**
    * Set the currently selected node unique css selector.
    * Will store the current target url along with it to allow pre-selection at
    * reload
    */
   set selectionCssSelector(cssSelector = null) {
+    if (this._panelDestroyer) {
+      return;
+    }
+
     this._selectionCssSelector = {
       selector: cssSelector,
       url: this._target.url
     };
   },
 
   /**
    * Get the current selection unique css selector if any, that is, if a node
--- a/browser/devtools/inspector/selector-search.js
+++ b/browser/devtools/inspector/selector-search.js
@@ -420,48 +420,57 @@ SelectorSearch.prototype = {
         break;
     }
     this.emit("processing-done");
   },
 
   /**
    * Populates the suggestions list and show the suggestion popup.
    */
-  _showPopup: function(aList, aFirstPart) {
+  _showPopup: function(aList, aFirstPart, aState) {
     let total = 0;
     let query = this.searchBox.value;
-    let toLowerCase = false;
     let items = [];
-    // In case of tagNames, change the case to small.
-    if (query.match(/.*[\.#][^\.#]{0,}$/) == null) {
-      toLowerCase = true;
-    }
-    for (let [value, count] of aList) {
+
+    for (let [value, count, state] of aList) {
       // for cases like 'div ' or 'div >' or 'div+'
       if (query.match(/[\s>+]$/)) {
         value = query + value;
       }
       // for cases like 'div #a' or 'div .a' or 'div > d' and likewise
       else if (query.match(/[\s>+][\.#a-zA-Z][^\s>+\.#]*$/)) {
         let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+\.#]*$/)[0];
         value = query.slice(0, -1 * lastPart.length + 1) + value;
       }
       // for cases like 'div.class' or '#foo.bar' and likewise
       else if (query.match(/[a-zA-Z][#\.][^#\.\s+>]*$/)) {
         let lastPart = query.match(/[a-zA-Z][#\.][^#\.\s>+]*$/)[0];
         value = query.slice(0, -1 * lastPart.length + 1) + value;
       }
+
       let item = {
         preLabel: query,
         label: value,
         count: count
       };
-      if (toLowerCase) {
+
+      // In case of tagNames, change te case to small
+      if (value.match(/.*[\.#][^\.#]{0,}$/) == null) {
         item.label = value.toLowerCase();
       }
+
+      // In case the query's state is tag and the item's state is id or class
+      // adjust the preLabel
+      if (aState === this.States.TAG && state === this.States.CLASS) {
+        item.preLabel = "." + item.preLabel;
+      }
+      if (aState === this.States.TAG && state === this.States.ID) {
+        item.preLabel = "#" + item.preLabel;
+      }
+
       items.unshift(item);
       if (++total > MAX_SUGGESTIONS - 1) {
         break;
       }
     }
     if (total > 0) {
       this.searchPopup.setItems(items);
       this.searchPopup.openPopup(this.searchBox);
@@ -472,48 +481,53 @@ SelectorSearch.prototype = {
   },
 
   /**
    * Suggests classes,ids and tags based on the user input as user types in the
    * searchbox.
    */
   showSuggestions: function() {
     let query = this.searchBox.value;
+    let state = this.state;
     let firstPart = "";
-    if (this.state == this.States.TAG) {
+
+    if (state == this.States.TAG) {
       // gets the tag that is being completed. For ex. 'div.foo > s' returns 's',
       // 'di' returns 'di' and likewise.
       firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["", query])[1];
       query = query.slice(0, query.length - firstPart.length);
     }
-    else if (this.state == this.States.CLASS) {
+    else if (state == this.States.CLASS) {
       // gets the class that is being completed. For ex. '.foo.b' returns 'b'
       firstPart = query.match(/\.([^\.]*)$/)[1];
       query = query.slice(0, query.length - firstPart.length - 1);
     }
-    else if (this.state == this.States.ID) {
+    else if (state == this.States.ID) {
       // gets the id that is being completed. For ex. '.foo#b' returns 'b'
       firstPart = query.match(/#([^#]*)$/)[1];
       query = query.slice(0, query.length - firstPart.length - 1);
     }
     // TODO: implement some caching so that over the wire request is not made
     // everytime.
     if (/[\s+>~]$/.test(query)) {
       query += "*";
     }
+
     this._currentSuggesting = query;
-    return this.walker.getSuggestionsForQuery(query, firstPart, this.state).then(result => {
+    return this.walker.getSuggestionsForQuery(query, firstPart, state).then(result => {
       if (this._currentSuggesting != result.query) {
         // This means that this response is for a previous request and the user
         // as since typed something extra leading to a new request.
         return;
       }
       this._lastToLastValidSearch = this._lastValidSearch;
-      if (this.state == this.States.CLASS) {
+
+      if (state == this.States.CLASS) {
         firstPart = "." + firstPart;
       }
-      else if (this.state == this.States.ID) {
+      else if (state == this.States.ID) {
         firstPart = "#" + firstPart;
       }
-      this._showPopup(result.suggestions, firstPart);
+
+      this._showPopup(result.suggestions, firstPart, state);
     });
   }
 };
--- a/browser/devtools/inspector/test/browser_inspector_search-02.js
+++ b/browser/devtools/inspector/test/browser_inspector_search-02.js
@@ -10,17 +10,21 @@ const TEST_URL = TEST_URL_ROOT + "doc_in
 
 // An array of (key, suggestions) pairs where key is a key to press and
 // suggestions is an array of suggestions that should be shown in the popup.
 // Suggestion is an object with label of the entry and optional count
 // (defaults to 1)
 const TEST_DATA = [
   {
     key: "d",
-    suggestions: [{label: "div", count: 4}]
+    suggestions: [
+      {label: "div", count: 4},
+      {label: "#d1", count: 1},
+      {label: "#d2", count: 1}
+    ]
   },
   {
     key: "i",
     suggestions: [{label: "div", count: 4}]
   },
   {
     key: "v",
     suggestions: []
@@ -62,17 +66,21 @@ const TEST_DATA = [
     suggestions: []
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [{label: "div", count: 4}]
   },
   {
     key: "VK_BACK_SPACE",
-    suggestions: [{label: "div", count: 4}]
+    suggestions: [
+      {label: "div", count: 4},
+      {label: "#d1", count: 1},
+      {label: "#d2", count: 1}
+    ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: "p",
     suggestions: []
@@ -130,28 +138,29 @@ add_task(function* () {
 
     let command = once(searchBox, "command");
     EventUtils.synthesizeKey(key, {}, inspector.panelWin);
     yield command;
 
     info("Waiting for search query to complete");
     yield inspector.searchSuggestions._lastQuery;
 
-    info("Query completed. Performing checks for input '" + searchBox.value + "'");
+    info("Query completed. Performing checks for input '" + searchBox.value +
+      "' - key pressed: " + key);
     let actualSuggestions = popup.getItems().reverse();
 
     is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
        "There are expected number of suggestions.");
 
     for (let i = 0; i < suggestions.length; i++) {
-      is(suggestions[i].label, actualSuggestions[i].label,
+      is(actualSuggestions[i].label, suggestions[i].label,
          "The suggestion at " + i + "th index is correct.");
-      is(suggestions[i].count || 1, actualSuggestions[i].count,
+      is(actualSuggestions[i].count, suggestions[i].count || 1,
          "The count for suggestion at " + i + "th index is correct.");
     }
   }
 });
 
 function formatSuggestions(suggestions) {
   return "[" + suggestions
-                .map(s => "'" + s.label + "' (" + s.count || 1 + ")")
+                .map(s => "'" + s.label + "' (" + (s.count || 1) + ")")
                 .join(", ") + "]";
 }
--- a/browser/devtools/inspector/test/browser_inspector_search-03.js
+++ b/browser/devtools/inspector/test/browser_inspector_search-03.js
@@ -10,17 +10,21 @@ const TEST_URL = TEST_URL_ROOT + "doc_in
 
 // An array of (key, suggestions) pairs where key is a key to press and
 // suggestions is an array of suggestions that should be shown in the popup.
 // Suggestion is an object with label of the entry and optional count
 // (defaults to 1)
 let TEST_DATA = [
   {
     key: "d",
-    suggestions: [{label: "div", count: 2}]
+    suggestions: [
+      {label: "div", count: 2},
+      {label: "#d1", count: 1},
+      {label: "#d2", count: 1}
+    ]
   },
   {
     key: "i",
     suggestions: [{label: "div", count: 2}]
   },
   {
     key: "v",
     suggestions: []
@@ -45,17 +49,21 @@ let TEST_DATA = [
     suggestions: []
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [{label: "div", count: 2}]
   },
   {
     key: "VK_BACK_SPACE",
-    suggestions: [{label: "div", count: 2}]
+    suggestions: [
+      {label: "div", count: 2},
+      {label: "#d1", count: 1},
+      {label: "#d2", count: 1}
+    ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: ".",
     suggestions: [
@@ -174,21 +182,21 @@ add_task(function* () {
 
     info("Query completed. Performing checks for input '" + searchBox.value + "'");
     let actualSuggestions = popup.getItems().reverse();
 
     is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
        "There are expected number of suggestions.");
 
     for (let i = 0; i < suggestions.length; i++) {
-      is(suggestions[i].label, actualSuggestions[i].label,
+      is(actualSuggestions[i].label, suggestions[i].label,
          "The suggestion at " + i + "th index is correct.");
-      is(suggestions[i].count || 1, actualSuggestions[i].count,
+      is(actualSuggestions[i].count, suggestions[i].count || 1,
          "The count for suggestion at " + i + "th index is correct.");
     }
   }
 });
 
 function formatSuggestions(suggestions) {
   return "[" + suggestions
-                .map(s => "'" + s.label + "' (" + s.count || 1 + ")")
+                .map(s => "'" + s.label + "' (" + (s.count || 1) + ")")
                 .join(", ") + "]";
 }
--- a/browser/devtools/inspector/test/browser_inspector_search-04.js
+++ b/browser/devtools/inspector/test/browser_inspector_search-04.js
@@ -13,33 +13,41 @@ const TEST_URL = "data:text/html;charset
 
 // An array of (key, suggestions) pairs where key is a key to press and
 // suggestions is an array of suggestions that should be shown in the popup.
 // Suggestion is an object with label of the entry and optional count
 // (defaults to 1)
 let TEST_DATA = [
   {
     key: "d",
-    suggestions: [{label: "div", count: 5}]
+    suggestions: [
+      {label: "div", count: 5},
+      {label: "#d1", count: 2},
+      {label: "#d2", count: 2}
+    ]
   },
   {
     key: "i",
     suggestions: [{label: "div", count: 5}]
   },
   {
     key: "v",
     suggestions: []
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [{label: "div", count: 5}]
   },
   {
     key: "VK_BACK_SPACE",
-    suggestions: [{label: "div", count: 5}]
+    suggestions: [
+      {label: "div", count: 5},
+      {label: "#d1", count: 2},
+      {label: "#d2", count: 2}
+    ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: ".",
     suggestions: [
@@ -85,21 +93,21 @@ add_task(function* () {
 
     info("Query completed. Performing checks for input '" + searchBox.value + "'");
     let actualSuggestions = popup.getItems().reverse();
 
     is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
        "There are expected number of suggestions.");
 
     for (let i = 0; i < suggestions.length; i++) {
-      is(suggestions[i].label, actualSuggestions[i].label,
+      is(actualSuggestions[i].label, suggestions[i].label,
          "The suggestion at " + i + "th index is correct.");
-      is(suggestions[i].count || 1, actualSuggestions[i].count,
+      is(actualSuggestions[i].count, suggestions[i].count || 1,
          "The count for suggestion at " + i + "th index is correct.");
     }
   }
 });
 
 function formatSuggestions(suggestions) {
   return "[" + suggestions
-                .map(s => "'" + s.label + "' (" + s.count || 1 + ")")
+                .map(s => "'" + s.label + "' (" + (s.count || 1) + ")")
                 .join(", ") + "]";
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_search-suggests-ids-and-classes.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the selector-search input proposes ids and classes even when . and
+// # is missing, but that this only occurs when the query is one word (no
+// selector combination)
+
+function test()
+{
+  waitForExplicitFinish();
+
+  let inspector, searchBox, state, popup;
+
+  // The various states of the inspector: [key, suggestions array]
+  // [
+  //  what key to press,
+  //  suggestions array with count [
+  //    [suggestion1, count1], [suggestion2] ...
+  //  ] count can be left to represent 1
+  // ]
+  let keyStates = [
+    ["s", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["p", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["a", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["n", []],
+    [" ", [["span div", 1]]],
+    ["d", [["span div", 1]]], // mixed tag/class/id suggestions only work for the first word
+    ["VK_BACK_SPACE", [["span div", 1]]],
+    ["VK_BACK_SPACE", []],
+    ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["VK_BACK_SPACE", []],
+    // Test that mixed tags, classes and ids are grouped by types, sorted by
+    // count and alphabetical order
+    ["b", [
+      ["button", 3],
+      ["body", 1],
+      [".bc", 3],
+      [".ba", 1],
+      [".bb", 1],
+      ["#ba", 1],
+      ["#bb", 1],
+      ["#bc", 1]
+    ]],
+  ];
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    waitForFocus(setupTest, content);
+  }, true);
+
+  content.location = "data:text/html," +
+                     "<span class='span' id='span'>" +
+                     "  <div class='div' id='div'></div>" +
+                     "</span>" +
+                     "<button class='ba bc' id='bc'></button>" +
+                     "<button class='bb bc' id='bb'></button>" +
+                     "<button class='bc' id='ba'></button>";
+
+  function $(id) {
+    if (id == null) return null;
+    return content.document.getElementById(id);
+  }
+
+  function setupTest()
+  {
+    openInspector(startTest);
+  }
+
+  function startTest(aInspector)
+  {
+    inspector = aInspector;
+
+    searchBox =
+      inspector.panelWin.document.getElementById("inspector-searchbox");
+    popup = inspector.searchSuggestions.searchPopup;
+
+    focusSearchBoxUsingShortcut(inspector.panelWin, function() {
+      searchBox.addEventListener("command", checkState, true);
+      checkStateAndMoveOn(0);
+    });
+  }
+
+  function checkStateAndMoveOn(index) {
+    if (index == keyStates.length) {
+      finishUp();
+      return;
+    }
+
+    let [key, suggestions] = keyStates[index];
+    state = index;
+
+    info("pressing key " + key + " to get suggestions " +
+         JSON.stringify(suggestions));
+    EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+  }
+
+  function checkState(event) {
+    inspector.searchSuggestions._lastQuery.then(() => {
+      let [key, suggestions] = keyStates[state];
+      let actualSuggestions = popup.getItems();
+      is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
+         "There are expected number of suggestions at " + state + "th step.");
+      actualSuggestions.reverse();
+      for (let i = 0; i < suggestions.length; i++) {
+        is(suggestions[i][0], actualSuggestions[i].label,
+           "The suggestion at " + i + "th index for " + state +
+           "th step is correct.")
+        is(suggestions[i][1] || 1, actualSuggestions[i].count,
+           "The count for suggestion at " + i + "th index for " + state +
+           "th step is correct.")
+      }
+      checkStateAndMoveOn(state + 1);
+    });
+  }
+
+  function finishUp() {
+    searchBox = null;
+    popup = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
--- a/browser/devtools/layoutview/test/browser.ini
+++ b/browser/devtools/layoutview/test/browser.ini
@@ -10,12 +10,13 @@ support-files =
 [browser_layoutview.js]
 [browser_layoutview_editablemodel.js]
 # [browser_layoutview_editablemodel_allproperties.js]
 # Disabled for too many intermittent failures (bug 1009322)
 [browser_layoutview_editablemodel_border.js]
 [browser_layoutview_editablemodel_stylerules.js]
 [browser_layoutview_guides.js]
 [browser_layoutview_rotate-labels-on-sides.js]
+[browser_layoutview_tooltips.js]
 [browser_layoutview_update-after-navigation.js]
 [browser_layoutview_update-after-reload.js]
 # [browser_layoutview_update-in-iframes.js]
 # Bug 1020038 layout-view updates for iframe elements changes
new file mode 100644
--- /dev/null
+++ b/browser/devtools/layoutview/test/browser_layoutview_tooltips.js
@@ -0,0 +1,126 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the regions in the layout-view have tooltips, and that individual
+// values too. Also test that values that are set from a css rule have tooltips
+// referencing the rule.
+
+const TEST_URI = "<style>" +
+  "#div1 { color: red; margin: 3em; }\n" +
+  "#div2 { border-bottom: 1px solid black; background: red; }\n" +
+  "html, body, #div3 { box-sizing: border-box; padding: 0 2em; }" +
+  "</style>" +
+  "<div id='div1'></div><div id='div2'></div><div id='div3'></div>";
+
+// Test data for the tooltips over individual values.
+// Each entry should contain:
+// - selector: The selector for the node to be selected before starting to test
+// - values: An array containing objects for each of the values that are defined
+//   by css rules. Each entry should contain:
+//   - name: the name of the property that is set by the css rule
+//   - ruleSelector: the selector of the rule
+//   - styleSheetLocation: the fileName:lineNumber
+const VALUES_TEST_DATA = [{
+  selector: "#div1",
+  values: [{
+    name: "margin-top",
+    ruleSelector: "#div1",
+    styleSheetLocation: "null:1"
+  }, {
+    name: "margin-right",
+    ruleSelector: "#div1",
+    styleSheetLocation: "null:1"
+  }, {
+    name: "margin-bottom",
+    ruleSelector: "#div1",
+    styleSheetLocation: "null:1"
+  }, {
+    name: "margin-left",
+    ruleSelector: "#div1",
+    styleSheetLocation: "null:1"
+  }]
+},{
+  selector: "#div2",
+  values: [{
+    name: "border-bottom-width",
+    ruleSelector: "#div2",
+    styleSheetLocation: "null:2"
+  }]
+},{
+  selector: "#div3",
+  values: [{
+    name: "padding-top",
+    ruleSelector: "html, body, #div3",
+    styleSheetLocation: "null:3"
+  }, {
+    name: "padding-right",
+    ruleSelector: "html, body, #div3",
+    styleSheetLocation: "null:3"
+  }, {
+    name: "padding-bottom",
+    ruleSelector: "html, body, #div3",
+    styleSheetLocation: "null:3"
+  }, {
+    name: "padding-left",
+    ruleSelector: "html, body, #div3",
+    styleSheetLocation: "null:3"
+  }]
+}];
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {toolbox, inspector, view} = yield openLayoutView();
+
+  info("Checking the regions tooltips");
+
+  ok(view.doc.querySelector("#margins").hasAttribute("title"),
+    "The margin region has a tooltip");
+  is(view.doc.querySelector("#margins").getAttribute("title"), "margin",
+    "The margin region has the correct tooltip content");
+
+  ok(view.doc.querySelector("#borders").hasAttribute("title"),
+    "The border region has a tooltip");
+  is(view.doc.querySelector("#borders").getAttribute("title"), "border",
+    "The border region has the correct tooltip content");
+
+  ok(view.doc.querySelector("#padding").hasAttribute("title"),
+    "The padding region has a tooltip");
+  is(view.doc.querySelector("#padding").getAttribute("title"), "padding",
+    "The padding region has the correct tooltip content");
+
+  ok(view.doc.querySelector("#content").hasAttribute("title"),
+    "The content region has a tooltip");
+  is(view.doc.querySelector("#content").getAttribute("title"), "content",
+    "The content region has the correct tooltip content");
+
+  for (let {selector, values} of VALUES_TEST_DATA) {
+    info("Selecting " + selector + " and checking the values tooltips");
+    yield selectNode(selector, inspector);
+
+    info("Iterate over all values");
+    for (let key in view.map) {
+      if (key === "position") {
+        continue;
+      }
+
+      let name = view.map[key].property;
+      let expectedTooltipData = values.find(o => o.name === name);
+      let el = view.doc.querySelector(view.map[key].selector);
+
+      ok(el.hasAttribute("title"), "The " + name + " value has a tooltip");
+
+      if (expectedTooltipData) {
+        info("The " + name + " value comes from a css rule");
+        let expectedTooltip = name + "\n" + expectedTooltipData.ruleSelector +
+                              "\n" + expectedTooltipData.styleSheetLocation;
+        is(el.getAttribute("title"), expectedTooltip, "The tooltip is correct");
+      } else {
+        info("The " + name + " isn't set by a css rule");
+        is(el.getAttribute("title"), name, "The tooltip is correct");
+      }
+    }
+  }
+});
--- a/browser/devtools/layoutview/view.css
+++ b/browser/devtools/layoutview/view.css
@@ -55,16 +55,26 @@ body {
   border-style: solid;
   border-width: 25px;
 }
 
 #borders {
   padding: 25px;
 }
 
+.legend {
+  position: absolute;
+  margin: 5px 6px;
+  z-index: 1;
+}
+
+.legend[data-box="margin"] {
+  color: var(--theme-highlight-blue);
+}
+
 #main > p {
   position: absolute;
   pointer-events: none;
 }
 
 #main > p {
   margin: 0;
   text-align: center;
@@ -164,26 +174,19 @@ body {
 .rotate.left:not(.editing) {
   transform: rotate(-90deg);
 }
 
 .rotate.right:not(.editing) {
   transform: rotate(90deg);
 }
 
-.tooltip {
-  position: absolute;
-  bottom: 0;
-  right: 2px;
-  pointer-events: none;
-}
 
 body.dim > #header > #element-position,
-body.dim > #main > p,
-body.dim > #main > .tooltip {
+body.dim > #main > p {
   visibility: hidden;
 }
 
 @media (max-height: 228px) {
   #header {
     padding-top: 0;
     padding-bottom: 0;
     margin-top: 10px;
--- a/browser/devtools/layoutview/view.js
+++ b/browser/devtools/layoutview/view.js
@@ -436,16 +436,17 @@ LayoutView.prototype = {
       if ("top" in margins) this.map.marginTop.value = "auto";
       if ("right" in margins) this.map.marginRight.value = "auto";
       if ("bottom" in margins) this.map.marginBottom.value = "auto";
       if ("left" in margins) this.map.marginLeft.value = "auto";
 
       for (let i in this.map) {
         let selector = this.map[i].selector;
         let span = this.doc.querySelector(selector);
+        this.updateSourceRuleTooltip(span, this.map[i].property, styleEntries);
         if (span.textContent.length > 0 &&
             span.textContent == this.map[i].value) {
           continue;
         }
         span.textContent = this.map[i].value;
         this.manageOverflowingText(span);
       }
 
@@ -465,16 +466,50 @@ LayoutView.prototype = {
 
       this.inspector.emit("layoutview-updated");
     }).bind(this)).then(null, console.error);
 
     return this._lastRequest = lastRequest;
   },
 
   /**
+   * Update the text in the tooltip shown when hovering over a value to provide
+   * information about the source CSS rule that sets this value.
+   * @param {DOMNode} el The element that will receive the tooltip.
+   * @param {String} property The name of the CSS property for the tooltip.
+   * @param {Array} rules An array of applied rules retrieved by
+   * styleActor.getApplied.
+   */
+  updateSourceRuleTooltip: function(el, property, rules) {
+    // Dummy element used to parse the cssText of applied rules.
+    let dummyEl = this.doc.createElement("div");
+
+    // Rules are in order of priority so iterate until we find the first that
+    // defines a value for the property.
+    let sourceRule, value;
+    for (let {rule} of rules) {
+      dummyEl.style.cssText = rule.cssText;
+      value = dummyEl.style.getPropertyValue(property);
+      if (value !== "") {
+        sourceRule = rule;
+        break;
+      }
+    }
+
+    let title = property;
+    if (sourceRule && sourceRule.selectors) {
+      title += "\n" + sourceRule.selectors.join(", ");
+    }
+    if (sourceRule && sourceRule.parentStyleSheet) {
+      title += "\n" + sourceRule.parentStyleSheet.href + ":" + sourceRule.line;
+    }
+    el.setAttribute("title", title);
+  },
+
+  /**
    * Show the box-model highlighter on the currently selected element
    * @param {Object} options Options passed to the highlighter actor
    */
   showBoxModel: function(options={}) {
     let toolbox = this.inspector.toolbox;
     let nodeFront = this.inspector.selection.nodeFront;
 
     toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
@@ -495,40 +530,35 @@ LayoutView.prototype = {
     if (classList.contains("left") || classList.contains("right")) {
       let force = span.textContent.length > LONG_TEXT_ROTATE_LIMIT;
       classList.toggle("rotate", force);
     }
   }
 };
 
 let elts;
-let tooltip;
 
 let onmouseover = function(e) {
   let region = e.target.getAttribute("data-box");
-
-  tooltip.textContent = e.target.getAttribute("tooltip");
   this.layoutview.showBoxModel({region});
 
   return false;
 }.bind(window);
 
 let onmouseout = function(e) {
-  tooltip.textContent = "";
   this.layoutview.hideBoxModel();
 
   return false;
 }.bind(window);
 
 window.setPanel = function(panel) {
   this.layoutview = new LayoutView(panel, window);
 
-  // Tooltip mechanism
-  elts = document.querySelectorAll("*[tooltip]");
-  tooltip = document.querySelector(".tooltip");
+  // Box model highlighter mechanism
+  elts = document.querySelectorAll("*[title]");
   for (let i = 0; i < elts.length; i++) {
     let elt = elts[i];
     elt.addEventListener("mouseover", onmouseover, true);
     elt.addEventListener("mouseout", onmouseout, true);
   }
 
   // Mark document as RTL or LTR:
   let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
--- a/browser/devtools/layoutview/view.xhtml
+++ b/browser/devtools/layoutview/view.xhtml
@@ -25,43 +25,44 @@
   <body class="theme-sidebar devtools-monospace">
 
     <p id="header">
       <span id="element-size"></span><span id="element-position"></span>
     </p>
 
     <div id="main">
 
-      <div id="margins" data-box="margin" tooltip="&margin.tooltip;">
-        <div id="borders" data-box="border" tooltip="&border.tooltip;">
-          <div id="padding" data-box="padding" tooltip="&padding.tooltip;">
-            <div id="content" data-box="content" tooltip="&content.tooltip;">
+      <span class="legend" data-box="margin" title="&margin.tooltip;">&margin.tooltip;</span>
+      <div id="margins" data-box="margin" title="&margin.tooltip;">
+        <span class="legend" data-box="border" title="&border.tooltip;">&border.tooltip;</span>
+        <div id="borders" data-box="border" title="&border.tooltip;">
+          <span class="legend" data-box="padding" title="&padding.tooltip;">&padding.tooltip;</span>
+          <div id="padding" data-box="padding" title="&padding.tooltip;">
+            <div id="content" data-box="content" title="&content.tooltip;">
             </div>
           </div>
         </div>
       </div>
 
-      <p class="border top"><span data-box="border" class="editable" tooltip="border-top"></span></p>
-      <p class="border right"><span data-box="border" class="editable" tooltip="border-right"></span></p>
-      <p class="border bottom"><span data-box="border" class="editable" tooltip="border-bottom"></span></p>
-      <p class="border left"><span data-box="border" class="editable" tooltip="border-left"></span></p>
+      <p class="border top"><span data-box="border" class="editable" title="border-top"></span></p>
+      <p class="border right"><span data-box="border" class="editable" title="border-right"></span></p>
+      <p class="border bottom"><span data-box="border" class="editable" title="border-bottom"></span></p>
+      <p class="border left"><span data-box="border" class="editable" title="border-left"></span></p>
 
-      <p class="margin top"><span data-box="margin" class="editable" tooltip="margin-top"></span></p>
-      <p class="margin right"><span data-box="margin" class="editable" tooltip="margin-right"></span></p>
-      <p class="margin bottom"><span data-box="margin" class="editable" tooltip="margin-bottom"></span></p>
-      <p class="margin left"><span data-box="margin" class="editable" tooltip="margin-left"></span></p>
+      <p class="margin top"><span data-box="margin" class="editable" title="margin-top"></span></p>
+      <p class="margin right"><span data-box="margin" class="editable" title="margin-right"></span></p>
+      <p class="margin bottom"><span data-box="margin" class="editable" title="margin-bottom"></span></p>
+      <p class="margin left"><span data-box="margin" class="editable" title="margin-left"></span></p>
 
-      <p class="padding top"><span data-box="padding" class="editable" tooltip="padding-top"></span></p>
-      <p class="padding right"><span data-box="padding" class="editable" tooltip="padding-right"></span></p>
-      <p class="padding bottom"><span data-box="padding" class="editable" tooltip="padding-bottom"></span></p>
-      <p class="padding left"><span data-box="padding" class="editable" tooltip="padding-left"></span></p>
+      <p class="padding top"><span data-box="padding" class="editable" title="padding-top"></span></p>
+      <p class="padding right"><span data-box="padding" class="editable" title="padding-right"></span></p>
+      <p class="padding bottom"><span data-box="padding" class="editable" title="padding-bottom"></span></p>
+      <p class="padding left"><span data-box="padding" class="editable" title="padding-left"></span></p>
 
-      <p class="size"><span data-box="content" tooltip="&content.tooltip;"></span></p>
-
-      <span class="tooltip"></span>
+      <p class="size"><span data-box="content" title="&content.tooltip;"></span></p>
 
     </div>
 
     <div style="display: none">
       <p id="dummy"></p>
     </div>
   </body>
 </html>
--- 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]);
+