Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 05 May 2015 12:13:10 +0200
changeset 273714 50608a9c7b091eb8ba2b59105c7175954a572def
parent 273713 5e115ea3795cd43e288a7dda7c0f1c9685311469 (current diff)
parent 273703 754579ec0e68068d32be534d553d5b191d918d84 (diff)
child 273715 a91f9bcf83e83bda4e5bcd485c982859be34766a
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -1127,11 +1127,15 @@ pref("dom.mozSettings.SettingsService.ve
 pref("dom.mozSettings.allowForceReadOnly", false);
 
 // RequestSync API is enabled by default on B2G.
 pref("dom.requestSync.enabled", true);
 
 // Resample touch events on b2g
 pref("gfx.touch.resample", true);
 
+// Comma separated list of activity names that can only be provided by
+// the system app in dev mode.
+pref("dom.activities.developer_mode_only", "import-app");
+
 // mulet apparently loads firefox.js as well as b2g.js, so we have to explicitly
 // disable serviceworkers here to get them disabled in mulet.
 pref("dom.serviceWorkers.enabled", false);
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,17 +10,17 @@
   <!--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="2eda36a4795012a5d1275b77ebb20ac377c8cd26">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <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="0627790166dccd8dd370fa7d9f434ed9fc027fb4"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--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="70077825aab2c7a79611befb40a5fe7e610d5443"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="9a9797062c6001d6346504161c51187a2968466b"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="8c2d32bccc7061e9ca0165135457c3fd53e7107e"/>
   <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"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--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="70077825aab2c7a79611befb40a5fe7e610d5443"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0627790166dccd8dd370fa7d9f434ed9fc027fb4"/>
   <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"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--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="2eda36a4795012a5d1275b77ebb20ac377c8cd26">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <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="0627790166dccd8dd370fa7d9f434ed9fc027fb4"/>
--- a/b2g/config/emulator-l/sources.xml
+++ b/b2g/config/emulator-l/sources.xml
@@ -10,17 +10,17 @@
   <!--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="61e82f99bb8bc78d52b5717e9a2481ec7267fa33">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <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="0627790166dccd8dd370fa7d9f434ed9fc027fb4"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--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="70077825aab2c7a79611befb40a5fe7e610d5443"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="9a9797062c6001d6346504161c51187a2968466b"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="8c2d32bccc7061e9ca0165135457c3fd53e7107e"/>
   <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"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--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="2eda36a4795012a5d1275b77ebb20ac377c8cd26">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <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="0627790166dccd8dd370fa7d9f434ed9fc027fb4"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,17 +12,17 @@
   <!--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="70077825aab2c7a79611befb40a5fe7e610d5443"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0627790166dccd8dd370fa7d9f434ed9fc027fb4"/>
   <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"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "70077825aab2c7a79611befb40a5fe7e610d5443", 
+        "git_revision": "e1773d6d1014c997be4b5c4233bba3ee073b8d7b", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "536698903e2c4c12a3ff6c6d83f70278fab7311d", 
+    "revision": "172680469094791e563268fa7d074fee9e0105ab", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,17 +12,17 @@
   <!--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="70077825aab2c7a79611befb40a5fe7e610d5443"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="0627790166dccd8dd370fa7d9f434ed9fc027fb4"/>
   <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"/>
--- a/b2g/config/nexus-5-l/sources.xml
+++ b/b2g/config/nexus-5-l/sources.xml
@@ -10,17 +10,17 @@
   <!--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="61e82f99bb8bc78d52b5717e9a2481ec7267fa33">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="70077825aab2c7a79611befb40a5fe7e610d5443"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e1773d6d1014c997be4b5c4233bba3ee073b8d7b"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <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="0627790166dccd8dd370fa7d9f434ed9fc027fb4"/>
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -421,20 +421,18 @@ support-files =
   benignPage.html
 [browser_typeAheadFind.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 921935 - focusmanager issues with e10s (test calls waitForFocus)
 [browser_unknownContentType_title.js]
 [browser_unloaddialogs.js]
 skip-if = e10s # Bug 1100700 - test relies on unload event firing on closed tabs, which it doesn't
 [browser_urlHighlight.js]
 [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]
 [browser_urlbarSearchSingleWordNotification.js]
 [browser_urlbarStop.js]
 [browser_urlbarTrimURLs.js]
 [browser_urlbar_search_healthreport.js]
 [browser_utilityOverlay.js]
--- a/browser/base/content/test/general/browser_urlbarEnter.js
+++ b/browser/base/content/test/general/browser_urlbarEnter.js
@@ -20,20 +20,25 @@ add_task(function* () {
 
   // Cleanup.
   gBrowser.removeCurrentTab();
 });
 
 add_task(function* () {
   info("Alt+Return keypress");
   let tab = gBrowser.selectedTab = gBrowser.addTab(START_VALUE);
+  // due to bug 691608, we must wait for the load event, else isTabEmpty() will
+  // return true on e10s for this tab, so it will be reused even with altKey.
+  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
 
   gURLBar.focus();
   EventUtils.synthesizeKey("VK_RETURN", {altKey: true});
-  yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+  // wait for the new tab to appear.
+  yield BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
 
   // Check url bar and selected tab.
   is(gURLBar.textValue, TEST_VALUE, "Urlbar should preserve the value on return keypress");
   isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
 
   // Cleanup.
   gBrowser.removeTab(tab);
   gBrowser.removeCurrentTab();
--- a/browser/devtools/animationinspector/animation-controller.js
+++ b/browser/devtools/animationinspector/animation-controller.js
@@ -107,16 +107,18 @@ let AnimationsController = {
     // 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.hasTargetNode = yield target.actorHasMethod("domwalker",
+                                                     "getNodeFromActor");
 
     if (this.destroyed) {
       console.warn("Could not fully initialize the AnimationsController");
       return;
     }
 
     this.startListeners();
     yield this.onNewNodeFront();
--- a/browser/devtools/animationinspector/animation-panel.js
+++ b/browser/devtools/animationinspector/animation-panel.js
@@ -1,16 +1,23 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 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 {
+  PlayerMetaDataHeader,
+  PlaybackRateSelector,
+  AnimationTargetNode,
+  createNode
+} = require("devtools/animationinspector/components");
+
 /**
  * The main animations panel UI.
  */
 let AnimationsPanel = {
   UI_UPDATED_EVENT: "ui-updated",
   PANEL_INITIALIZED: "panel-initialized",
 
   initialize: Task.async(function*() {
@@ -194,16 +201,19 @@ function PlayerWidget(player, containerE
   this.onFastForwardBtnClick = this.onFastForwardBtnClick.bind(this);
   this.onCurrentTimeChanged = this.onCurrentTimeChanged.bind(this);
   this.onPlaybackRateChanged = this.onPlaybackRateChanged.bind(this);
 
   this.metaDataComponent = new PlayerMetaDataHeader();
   if (AnimationsController.hasSetPlaybackRate) {
     this.rateComponent = new PlaybackRateSelector();
   }
+  if (AnimationsController.hasTargetNode) {
+    this.targetNodeComponent = new AnimationTargetNode(gInspector);
+  }
 }
 
 PlayerWidget.prototype = {
   initialize: Task.async(function*() {
     if (this.initialized) {
       return;
     }
     this.initialized = true;
@@ -219,16 +229,19 @@ PlayerWidget.prototype = {
     this.destroyed = true;
 
     this.stopTimelineAnimation();
     this.stopListeners();
     this.metaDataComponent.destroy();
     if (this.rateComponent) {
       this.rateComponent.destroy();
     }
+    if (this.targetNodeComponent) {
+      this.targetNodeComponent.destroy();
+    }
 
     this.el.remove();
     this.playPauseBtnEl = this.rewindBtnEl = this.fastForwardBtnEl = null;
     this.currentTimeEl = this.timeDisplayEl = null;
     this.containerEl = this.el = this.player = null;
   }),
 
   startListeners: function() {
@@ -256,21 +269,27 @@ PlayerWidget.prototype = {
       this.rateComponent.off("rate-changed", this.onPlaybackRateChanged);
     }
   },
 
   createMarkup: function() {
     let state = this.player.state;
 
     this.el = createNode({
+      parent: this.containerEl,
       attributes: {
         "class": "player-widget " + state.playState
       }
     });
 
+    if (this.targetNodeComponent) {
+      this.targetNodeComponent.init(this.el);
+      this.targetNodeComponent.render(this.player);
+    }
+
     this.metaDataComponent.init(this.el);
     this.metaDataComponent.render(state);
 
     // Timeline widget.
     let timelineEl = createNode({
       parent: this.el,
       attributes: {
         "class": "timeline"
@@ -354,18 +373,16 @@ PlayerWidget.prototype = {
     // Time display
     this.timeDisplayEl = createNode({
       parent: timelineEl,
       attributes: {
         "class": "time-display"
       }
     });
 
-    this.containerEl.appendChild(this.el);
-
     // Show the initial time.
     this.displayTime(state.currentTime);
   },
 
   /**
    * Executed when the playPause button is clicked.
    * Note that tests may want to call this callback directly rather than
    * simulating a click on the button since it returns the promise returned by
@@ -566,266 +583,8 @@ PlayerWidget.prototype = {
    */
   stopTimelineAnimation: function() {
     if (this.rafID) {
       cancelAnimationFrame(this.rafID);
       this.rafID = null;
     }
   }
 };
-
-/**
- * UI component responsible for displaying and updating the player meta-data:
- * name, duration, iterations, delay.
- * The parent UI component for this should drive its updates by calling
- * render(state) whenever it wants the component to update.
- */
-function PlayerMetaDataHeader() {
-  // Store the various state pieces we need to only refresh the UI when things
-  // change.
-  this.state = {};
-}
-
-PlayerMetaDataHeader.prototype = {
-  init: function(containerEl) {
-    // The main title element.
-    this.el = createNode({
-      parent: containerEl,
-      attributes: {
-        "class": "animation-title"
-      }
-    });
-
-    // Animation name.
-    this.nameLabel = createNode({
-      parent: this.el,
-      nodeType: "span"
-    });
-
-    this.nameValue = createNode({
-      parent: this.el,
-      nodeType: "strong",
-      attributes: {
-        "style": "display:none;"
-      }
-    });
-
-    // Animation duration, delay and iteration container.
-    let metaData = createNode({
-      parent: this.el,
-      nodeType: "span",
-      attributes: {
-        "class": "meta-data"
-      }
-    });
-
-    // Animation duration.
-    this.durationLabel = createNode({
-      parent: metaData,
-      nodeType: "span"
-    });
-    this.durationLabel.textContent = L10N.getStr("player.animationDurationLabel");
-
-    this.durationValue = createNode({
-      parent: metaData,
-      nodeType: "strong"
-    });
-
-    // Animation delay (hidden by default since there may not be a delay).
-    this.delayLabel = createNode({
-      parent: metaData,
-      nodeType: "span",
-      attributes: {
-        "style": "display:none;"
-      }
-    });
-    this.delayLabel.textContent = L10N.getStr("player.animationDelayLabel");
-
-    this.delayValue = createNode({
-      parent: metaData,
-      nodeType: "strong"
-    });
-
-    // Animation iteration count (also hidden by default since we don't display
-    // single iterations).
-    this.iterationLabel = createNode({
-      parent: metaData,
-      nodeType: "span",
-      attributes: {
-        "style": "display:none;"
-      }
-    });
-    this.iterationLabel.textContent = L10N.getStr("player.animationIterationCountLabel");
-
-    this.iterationValue = createNode({
-      parent: metaData,
-      nodeType: "strong",
-      attributes: {
-        "style": "display:none;"
-      }
-    });
-  },
-
-  destroy: function() {
-    this.state = null;
-    this.el.remove();
-    this.el = null;
-    this.nameLabel = this.nameValue = null;
-    this.durationLabel = this.durationValue = null;
-    this.delayLabel = this.delayValue = null;
-    this.iterationLabel = this.iterationValue = null;
-  },
-
-  render: function(state) {
-    // Update the name if needed.
-    if (state.name !== this.state.name) {
-      if (state.name) {
-        // Animations (and transitions since bug 1122414) have names.
-        this.nameLabel.textContent = L10N.getStr("player.animationNameLabel");
-        this.nameValue.style.display = "inline";
-        this.nameValue.textContent = state.name;
-      } else {
-        // With older actors, Css transitions don't have names.
-        this.nameLabel.textContent = L10N.getStr("player.transitionNameLabel");
-        this.nameValue.style.display = "none";
-      }
-    }
-
-    // update the duration value if needed.
-    if (state.duration !== this.state.duration) {
-      this.durationValue.textContent = L10N.getFormatStr("player.timeLabel",
-        L10N.numberWithDecimals(state.duration / 1000, 2));
-    }
-
-    // Update the delay if needed.
-    if (state.delay !== this.state.delay) {
-      if (state.delay) {
-        this.delayLabel.style.display = "inline";
-        this.delayValue.style.display = "inline";
-        this.delayValue.textContent = L10N.getFormatStr("player.timeLabel",
-          L10N.numberWithDecimals(state.delay / 1000, 2));
-      } else {
-        // Hide the delay elements if there is no delay defined.
-        this.delayLabel.style.display = "none";
-        this.delayValue.style.display = "none";
-      }
-    }
-
-    // Update the iterationCount if needed.
-    if (state.iterationCount !== this.state.iterationCount) {
-      if (state.iterationCount !== 1) {
-        this.iterationLabel.style.display = "inline";
-        this.iterationValue.style.display = "inline";
-        let count = state.iterationCount ||
-                    L10N.getStr("player.infiniteIterationCount");
-        this.iterationValue.innerHTML = count;
-      } else {
-        // Hide the iteration elements if iteration is 1.
-        this.iterationLabel.style.display = "none";
-        this.iterationValue.style.display = "none";
-      }
-    }
-
-    this.state = state;
-  }
-};
-
-/**
- * UI component responsible for displaying the playback rate drop-down in each
- * player widget, updating it when the state changes, and emitting events when
- * the user selects a new value.
- * The parent UI component for this should drive its updates by calling
- * render(state) whenever it wants the component to update.
- */
-function PlaybackRateSelector() {
-  this.currentRate = null;
-  this.onSelectionChanged = this.onSelectionChanged.bind(this);
-  EventEmitter.decorate(this);
-}
-
-PlaybackRateSelector.prototype = {
-  PRESETS: [.1, .5, 1, 2, 5, 10],
-
-  init: function(containerEl) {
-    // This component is simple enough that we can re-create the markup every
-    // time it's rendered. So here we only store the parentEl.
-    this.parentEl = containerEl;
-  },
-
-  destroy: function() {
-    this.removeSelect();
-    this.parentEl = this.el = null;
-  },
-
-  removeSelect: function() {
-    if (this.el) {
-      this.el.removeEventListener("change", this.onSelectionChanged);
-      this.el.remove();
-    }
-  },
-
-  /**
-   * Get the ordered list of presets, including the current playbackRate if
-   * different from the existing presets.
-   */
-  getCurrentPresets: function({playbackRate}) {
-    return [...new Set([...this.PRESETS, playbackRate])].sort((a,b) => a > b);
-  },
-
-  render: function(state) {
-    if (state.playbackRate === this.currentRate) {
-      return;
-    }
-
-    this.removeSelect();
-
-    this.el = createNode({
-      parent: this.parentEl,
-      nodeType: "select",
-      attributes: {
-        "class": "rate devtools-button"
-      }
-    });
-
-    for (let preset of this.getCurrentPresets(state)) {
-      let option = createNode({
-        parent: this.el,
-        nodeType: "option",
-        attributes: {
-          value: preset,
-        }
-      });
-      option.textContent = L10N.getFormatStr("player.playbackRateLabel", preset);
-      if (preset === state.playbackRate) {
-        option.setAttribute("selected", "");
-      }
-    }
-
-    this.el.addEventListener("change", this.onSelectionChanged);
-
-    this.currentRate = state.playbackRate;
-  },
-
-  onSelectionChanged: function(e) {
-    this.emit("rate-changed", parseFloat(this.el.value));
-  }
-};
-
-/**
- * DOM node creation helper function.
- * @param {Object} Options to customize the node to be created.
- * @return {DOMNode} The newly created node.
- */
-function createNode(options) {
-  let type = options.nodeType || "div";
-  let node = document.createElement(type);
-
-  for (let name in options.attributes || {}) {
-    let value = options.attributes[name];
-    node.setAttribute(name, value);
-  }
-
-  if (options.parent) {
-    options.parent.appendChild(node);
-  }
-
-  return node;
-}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/components.js
@@ -0,0 +1,502 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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";
+
+// Set of reusable UI components for the animation-inspector UI.
+// All components in this module share a common API:
+// 1. construct the component:
+//    let c = new ComponentName();
+// 2. initialize the markup of the component in a given parent node:
+//    c.init(containerElement);
+// 3. render the component, passing in some sort of state:
+//    This may be called over and over again when the state changes, to update
+//    the component output.
+//    c.render(state);
+// 4. destroy the component:
+//    c.destroy();
+
+const {Cu} = require('chrome');
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+const STRINGS_URI = "chrome://browser/locale/devtools/animationinspector.properties";
+const L10N = new ViewHelpers.L10N(STRINGS_URI);
+
+/**
+ * UI component responsible for displaying and updating the player meta-data:
+ * name, duration, iterations, delay.
+ * The parent UI component for this should drive its updates by calling
+ * render(state) whenever it wants the component to update.
+ */
+function PlayerMetaDataHeader() {
+  // Store the various state pieces we need to only refresh the UI when things
+  // change.
+  this.state = {};
+}
+
+exports.PlayerMetaDataHeader = PlayerMetaDataHeader;
+
+PlayerMetaDataHeader.prototype = {
+  init: function(containerEl) {
+    // The main title element.
+    this.el = createNode({
+      parent: containerEl,
+      attributes: {
+        "class": "animation-title"
+      }
+    });
+
+    // Animation name.
+    this.nameLabel = createNode({
+      parent: this.el,
+      nodeType: "span"
+    });
+
+    this.nameValue = createNode({
+      parent: this.el,
+      nodeType: "strong",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+
+    // Animation duration, delay and iteration container.
+    let metaData = createNode({
+      parent: this.el,
+      nodeType: "span",
+      attributes: {
+        "class": "meta-data"
+      }
+    });
+
+    // Animation duration.
+    this.durationLabel = createNode({
+      parent: metaData,
+      nodeType: "span"
+    });
+    this.durationLabel.textContent = L10N.getStr("player.animationDurationLabel");
+
+    this.durationValue = createNode({
+      parent: metaData,
+      nodeType: "strong"
+    });
+
+    // Animation delay (hidden by default since there may not be a delay).
+    this.delayLabel = createNode({
+      parent: metaData,
+      nodeType: "span",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+    this.delayLabel.textContent = L10N.getStr("player.animationDelayLabel");
+
+    this.delayValue = createNode({
+      parent: metaData,
+      nodeType: "strong"
+    });
+
+    // Animation iteration count (also hidden by default since we don't display
+    // single iterations).
+    this.iterationLabel = createNode({
+      parent: metaData,
+      nodeType: "span",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+    this.iterationLabel.textContent = L10N.getStr("player.animationIterationCountLabel");
+
+    this.iterationValue = createNode({
+      parent: metaData,
+      nodeType: "strong",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+  },
+
+  destroy: function() {
+    this.state = null;
+    this.el.remove();
+    this.el = null;
+    this.nameLabel = this.nameValue = null;
+    this.durationLabel = this.durationValue = null;
+    this.delayLabel = this.delayValue = null;
+    this.iterationLabel = this.iterationValue = null;
+  },
+
+  render: function(state) {
+    // Update the name if needed.
+    if (state.name !== this.state.name) {
+      if (state.name) {
+        // Animations (and transitions since bug 1122414) have names.
+        this.nameLabel.textContent = L10N.getStr("player.animationNameLabel");
+        this.nameValue.style.display = "inline";
+        this.nameValue.textContent = state.name;
+      } else {
+        // With older actors, Css transitions don't have names.
+        this.nameLabel.textContent = L10N.getStr("player.transitionNameLabel");
+        this.nameValue.style.display = "none";
+      }
+    }
+
+    // update the duration value if needed.
+    if (state.duration !== this.state.duration) {
+      this.durationValue.textContent = L10N.getFormatStr("player.timeLabel",
+        L10N.numberWithDecimals(state.duration / 1000, 2));
+    }
+
+    // Update the delay if needed.
+    if (state.delay !== this.state.delay) {
+      if (state.delay) {
+        this.delayLabel.style.display = "inline";
+        this.delayValue.style.display = "inline";
+        this.delayValue.textContent = L10N.getFormatStr("player.timeLabel",
+          L10N.numberWithDecimals(state.delay / 1000, 2));
+      } else {
+        // Hide the delay elements if there is no delay defined.
+        this.delayLabel.style.display = "none";
+        this.delayValue.style.display = "none";
+      }
+    }
+
+    // Update the iterationCount if needed.
+    if (state.iterationCount !== this.state.iterationCount) {
+      if (state.iterationCount !== 1) {
+        this.iterationLabel.style.display = "inline";
+        this.iterationValue.style.display = "inline";
+        let count = state.iterationCount ||
+                    L10N.getStr("player.infiniteIterationCount");
+        this.iterationValue.innerHTML = count;
+      } else {
+        // Hide the iteration elements if iteration is 1.
+        this.iterationLabel.style.display = "none";
+        this.iterationValue.style.display = "none";
+      }
+    }
+
+    this.state = state;
+  }
+};
+
+/**
+ * UI component responsible for displaying the playback rate drop-down in each
+ * player widget, updating it when the state changes, and emitting events when
+ * the user selects a new value.
+ * The parent UI component for this should drive its updates by calling
+ * render(state) whenever it wants the component to update.
+ */
+function PlaybackRateSelector() {
+  this.currentRate = null;
+  this.onSelectionChanged = this.onSelectionChanged.bind(this);
+  EventEmitter.decorate(this);
+}
+
+exports.PlaybackRateSelector = PlaybackRateSelector;
+
+PlaybackRateSelector.prototype = {
+  PRESETS: [.1, .5, 1, 2, 5, 10],
+
+  init: function(containerEl) {
+    // This component is simple enough that we can re-create the markup every
+    // time it's rendered. So here we only store the parentEl.
+    this.parentEl = containerEl;
+  },
+
+  destroy: function() {
+    this.removeSelect();
+    this.parentEl = this.el = null;
+  },
+
+  removeSelect: function() {
+    if (this.el) {
+      this.el.removeEventListener("change", this.onSelectionChanged);
+      this.el.remove();
+    }
+  },
+
+  /**
+   * Get the ordered list of presets, including the current playbackRate if
+   * different from the existing presets.
+   */
+  getCurrentPresets: function({playbackRate}) {
+    return [...new Set([...this.PRESETS, playbackRate])].sort((a,b) => a > b);
+  },
+
+  render: function(state) {
+    if (state.playbackRate === this.currentRate) {
+      return;
+    }
+
+    this.removeSelect();
+
+    this.el = createNode({
+      parent: this.parentEl,
+      nodeType: "select",
+      attributes: {
+        "class": "rate devtools-button"
+      }
+    });
+
+    for (let preset of this.getCurrentPresets(state)) {
+      let option = createNode({
+        parent: this.el,
+        nodeType: "option",
+        attributes: {
+          value: preset,
+        }
+      });
+      option.textContent = L10N.getFormatStr("player.playbackRateLabel", preset);
+      if (preset === state.playbackRate) {
+        option.setAttribute("selected", "");
+      }
+    }
+
+    this.el.addEventListener("change", this.onSelectionChanged);
+
+    this.currentRate = state.playbackRate;
+  },
+
+  onSelectionChanged: function(e) {
+    this.emit("rate-changed", parseFloat(this.el.value));
+  }
+};
+
+/**
+ * UI component responsible for displaying a preview of the target dom node of
+ * a given animation.
+ * @param {InspectorPanel} inspector Requires a reference to the inspector-panel
+ * to highlight and select the node, as well as refresh it when there are
+ * mutations.
+ */
+function AnimationTargetNode(inspector) {
+  this.inspector = inspector;
+
+  this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this);
+  this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this);
+  this.onSelectNodeClick = this.onSelectNodeClick.bind(this);
+  this.onMarkupMutations = this.onMarkupMutations.bind(this);
+
+  EventEmitter.decorate(this);
+}
+
+exports.AnimationTargetNode = AnimationTargetNode;
+
+AnimationTargetNode.prototype = {
+  init: function(containerEl) {
+    let document = containerEl.ownerDocument;
+
+    // Init the markup for displaying the target node.
+    this.el = createNode({
+      parent: containerEl,
+      attributes: {
+        "class": "animation-target"
+      }
+    });
+
+    // Icon to select the node in the inspector.
+    this.selectNodeEl = createNode({
+      parent: this.el,
+      nodeType: "span",
+      attributes: {
+        "class": "node-selector"
+      }
+    });
+
+    // Wrapper used for mouseover/out event handling.
+    this.previewEl = createNode({
+      parent: this.el,
+      nodeType: "span"
+    });
+
+    this.previewEl.appendChild(document.createTextNode("<"));
+
+    // Tag name.
+    this.tagNameEl = createNode({
+      parent: this.previewEl,
+      nodeType: "span",
+      attributes: {
+        "class": "tag-name theme-fg-color3"
+      }
+    });
+
+    // Id attribute container.
+    this.idEl = createNode({
+      parent: this.previewEl,
+      nodeType: "span"
+    });
+
+    createNode({
+      parent: this.idEl,
+      nodeType: "span",
+      attributes: {
+        "class": "attribute-name theme-fg-color2"
+      }
+    }).textContent = "id";
+
+    this.idEl.appendChild(document.createTextNode("=\""));
+
+    createNode({
+      parent: this.idEl,
+      nodeType: "span",
+      attributes: {
+        "class": "attribute-value theme-fg-color6"
+      }
+    });
+
+    this.idEl.appendChild(document.createTextNode("\""));
+
+    // Class attribute container.
+    this.classEl = createNode({
+      parent: this.previewEl,
+      nodeType: "span"
+    });
+
+    createNode({
+      parent: this.classEl,
+      nodeType: "span",
+      attributes: {
+        "class": "attribute-name theme-fg-color2"
+      }
+    }).textContent = "class";
+
+    this.classEl.appendChild(document.createTextNode("=\""));
+
+    createNode({
+      parent: this.classEl,
+      nodeType: "span",
+      attributes: {
+        "class": "attribute-value theme-fg-color6"
+      }
+    });
+
+    this.classEl.appendChild(document.createTextNode("\""));
+
+    this.previewEl.appendChild(document.createTextNode(">"));
+
+    // Init events for highlighting and selecting the node.
+    this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver);
+    this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut);
+    this.selectNodeEl.addEventListener("click", this.onSelectNodeClick);
+
+    // Start to listen for markupmutation events.
+    this.inspector.on("markupmutation", this.onMarkupMutations);
+  },
+
+  destroy: function() {
+    this.inspector.off("markupmutation", this.onMarkupMutations);
+    this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver);
+    this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut);
+    this.selectNodeEl.removeEventListener("click", this.onSelectNodeClick);
+    this.el.remove();
+    this.el = this.tagNameEl = this.idEl = this.classEl = null;
+    this.selectNodeEl = this.previewEl = null;
+    this.nodeFront = this.inspector = this.playerFront = null;
+  },
+
+  onPreviewMouseOver: function() {
+    if (!this.nodeFront) {
+      return;
+    }
+    this.inspector.toolbox.highlighterUtils.highlightNodeFront(this.nodeFront);
+  },
+
+  onPreviewMouseOut: function() {
+    this.inspector.toolbox.highlighterUtils.unhighlight();
+  },
+
+  onSelectNodeClick: function() {
+    if (!this.nodeFront) {
+      return;
+    }
+    this.inspector.selection.setNodeFront(this.nodeFront, "animationinspector");
+  },
+
+  onMarkupMutations: function(e, mutations) {
+    if (!this.nodeFront || !this.playerFront) {
+      return;
+    }
+
+    for (let {target} of mutations) {
+      if (target === this.nodeFront) {
+        // Re-render with the same nodeFront to update the output.
+        this.render(this.playerFront);
+        break;
+      }
+    }
+  },
+
+  render: function(playerFront) {
+    this.playerFront = playerFront;
+    this.inspector.walker.getNodeFromActor(playerFront.actorID, ["node"]).then(nodeFront => {
+      // We might have been destroyed in the meantime, or the node might not be found.
+      if (!this.el || !nodeFront) {
+        return;
+      }
+
+      this.nodeFront = nodeFront;
+      let {tagName, attributes} = nodeFront;
+
+      this.tagNameEl.textContent = tagName.toLowerCase();
+
+      let idIndex = attributes.findIndex(({name}) => name === "id");
+      if (idIndex > -1 && attributes[idIndex].value) {
+        this.idEl.querySelector(".attribute-value").textContent =
+          attributes[idIndex].value;
+        this.idEl.style.display = "inline";
+      } else {
+        this.idEl.style.display = "none";
+      }
+
+      let classIndex = attributes.findIndex(({name}) => name === "class");
+      if (classIndex > -1 && attributes[classIndex].value) {
+        this.classEl.querySelector(".attribute-value").textContent =
+          attributes[classIndex].value;
+        this.classEl.style.display = "inline";
+      } else {
+        this.classEl.style.display = "none";
+      }
+
+      this.emit("target-retrieved");
+    }, e => {
+      this.nodeFront = null;
+      if (!this.el) {
+        console.warn("Cound't retrieve the animation target node, widget destroyed");
+      } else {
+        console.error(e);
+      }
+    });
+  }
+};
+
+/**
+ * DOM node creation helper function.
+ * @param {Object} Options to customize the node to be created.
+ * - nodeType {String} Optional, defaults to "div",
+ * - attributes {Object} Optional attributes object like
+ *   {attrName1:value1, attrName2: value2, ...}
+ * - parent {DOMNode} Mandatory node to append the newly created node to.
+ * @return {DOMNode} The newly created node.
+ */
+function createNode(options) {
+  if (!options.parent) {
+    throw new Error("Missing parent DOMNode to create new node");
+  }
+
+  let type = options.nodeType || "div";
+  let node = options.parent.ownerDocument.createElement(type);
+
+  for (let name in options.attributes || {}) {
+    let value = options.attributes[name];
+    node.setAttribute(name, value);
+  }
+
+  options.parent.appendChild(node);
+  return node;
+}
+
+exports.createNode = createNode;
--- a/browser/devtools/animationinspector/moz.build
+++ b/browser/devtools/animationinspector/moz.build
@@ -1,7 +1,11 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+EXTRA_JS_MODULES.devtools.animationinspector += [
+    'components.js',
+]
--- a/browser/devtools/animationinspector/test/browser.ini
+++ b/browser/devtools/animationinspector/test/browser.ini
@@ -15,24 +15,26 @@ support-files =
 [browser_animation_playerFronts_are_refreshed.js]
 [browser_animation_playerWidgets_appear_on_panel_init.js]
 [browser_animation_playerWidgets_destroy.js]
 [browser_animation_playerWidgets_disables_on_finished.js]
 [browser_animation_playerWidgets_dont_show_time_after_duration.js]
 [browser_animation_playerWidgets_have_control_buttons.js]
 [browser_animation_playerWidgets_meta_data.js]
 [browser_animation_playerWidgets_state_after_pause.js]
+[browser_animation_playerWidgets_target_nodes.js]
 [browser_animation_rate_select_shows_presets.js]
 [browser_animation_refresh_on_added_animation.js]
 [browser_animation_refresh_on_removed_animation.js]
 [browser_animation_refresh_when_active.js]
 [browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
 [browser_animation_setting_currentTime_works_and_pauses.js]
 [browser_animation_setting_playbackRate_works.js]
 [browser_animation_shows_player_on_valid_node.js]
+[browser_animation_target_highlight_select.js]
 [browser_animation_timeline_animates.js]
 [browser_animation_timeline_is_enabled.js]
 [browser_animation_timeline_waits_for_delay.js]
 [browser_animation_toggle_button_resets_on_navigate.js]
 [browser_animation_toggle_button_toggles_animations.js]
 [browser_animation_toggle_button_updates_playerWidgets.js]
 [browser_animation_toolbar_exists.js]
 [browser_animation_ui_updates_when_animation_changes.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_target_nodes.js
@@ -0,0 +1,31 @@
+/* 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 player widgets display information about target nodes
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel} = yield openAnimationInspector();
+
+  info("Select the simple animated node");
+  yield selectNode(".animated", inspector);
+
+  let widget = panel.playerWidgets[0];
+
+  // Make sure to wait for the target-retrieved event if the nodeFront hasn't
+  // yet been retrieved by the TargetNodeComponent.
+  if (!widget.targetNodeComponent.nodeFront) {
+    yield widget.targetNodeComponent.once("target-retrieved");
+  }
+
+  let targetEl = widget.el.querySelector(".animation-target");
+  ok(targetEl, "The player widget has a target element");
+  is(targetEl.textContent, "<divid=\"\"class=\"ball animated\">",
+    "The target element's content is correct");
+
+  let selectorEl = targetEl.querySelector(".node-selector");
+  ok(selectorEl, "The icon to select the target element in the inspector exists");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_target_highlight_select.js
@@ -0,0 +1,62 @@
+/* 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 DOM element targets displayed in animation player widgets can
+// be used to highlight elements in the DOM and select them in the inspector.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {toolbox, inspector, panel} = yield openAnimationInspector();
+
+  info("Select the simple animated node");
+  yield selectNode(".animated", inspector);
+
+  // Make sure to wait for the target-retrieved event if the nodeFront hasn't
+  // yet been retrieved by the TargetNodeComponent.
+  let targetNodeComponent = panel.playerWidgets[0].targetNodeComponent;
+  if (!targetNodeComponent.nodeFront) {
+    yield targetNodeComponent.once("target-retrieved");
+  }
+
+  info("Retrieve the part of the widget that highlights the node on hover");
+  let highlightingEl = targetNodeComponent.previewEl;
+
+  info("Listen to node-highlight event and mouse over the widget");
+  let onHighlight = toolbox.once("node-highlight");
+  EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseover"},
+                             highlightingEl.ownerDocument.defaultView);
+  let nodeFront = yield onHighlight;
+
+  ok(true, "The node-highlight event was fired");
+  is(targetNodeComponent.nodeFront, nodeFront,
+    "The highlighted node is the one stored on the animation widget");
+  is(nodeFront.tagName, "DIV", "The highlighted node has the correct tagName");
+  is(nodeFront.attributes[0].name, "class", "The highlighted node has the correct attributes");
+  is(nodeFront.attributes[0].value, "ball animated", "The highlighted node has the correct class");
+
+  info("Select the body node in order to have the list of all animations");
+  yield selectNode("body", inspector);
+
+  // Make sure to wait for the target-retrieved event if the nodeFront hasn't
+  // yet been retrieved by the TargetNodeComponent.
+  targetNodeComponent = panel.playerWidgets[0].targetNodeComponent;
+  if (!targetNodeComponent.nodeFront) {
+    yield targetNodeComponent.once("target-retrieved");
+  }
+
+  info("Click on the first animation widget's selector icon and wait for the selection to change");
+  let onSelection = inspector.selection.once("new-node-front");
+  let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
+  let selectIconEl = targetNodeComponent.selectNodeEl;
+  EventUtils.sendMouseEvent({type: "click"}, selectIconEl,
+                            selectIconEl.ownerDocument.defaultView);
+  yield onSelection;
+
+  is(inspector.selection.nodeFront, targetNodeComponent.nodeFront,
+    "The selected node is the one stored on the animation widget");
+
+  yield onPanelUpdated;
+});
--- a/browser/devtools/shared/moz.build
+++ b/browser/devtools/shared/moz.build
@@ -50,16 +50,17 @@ EXTRA_JS_MODULES.devtools.shared += [
     'devices.js',
     'doorhanger.js',
     'frame-script-utils.js',
     'getjson.js',
     'inplace-editor.js',
     'node-attribute-parser.js',
     'observable-object.js',
     'options-view.js',
+    'poller.js',
     'source-utils.js',
     'telemetry.js',
     'theme-switching.js',
     'theme.js',
     'undo.js',
 ]
 
 EXTRA_JS_MODULES.devtools.shared.widgets += [
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/poller.js
@@ -0,0 +1,115 @@
+/* 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";
+loader.lazyRequireGetter(this, "timers",
+  "resource://gre/modules/Timer.jsm");
+loader.lazyRequireGetter(this, "defer",
+  "sdk/core/promise", true);
+
+/**
+ * @constructor Poller
+ * Takes a function that is to be called on an interval,
+ * and can be turned on and off via methods to execute `fn` on the interval
+ * specified during `on`. If `fn` returns a promise, the polling waits for
+ * that promise to resolve before waiting the interval to call again.
+ *
+ * Specify the `wait` duration between polling here, and optionally
+ * an `immediate` boolean, indicating whether the function should be called
+ * immediately when toggling on.
+ *
+ * @param {function} fn
+ * @param {number} wait
+ * @param {boolean?} immediate
+ */
+function Poller (fn, wait, immediate) {
+  this._fn = fn;
+  this._wait = wait;
+  this._immediate = immediate;
+  this._poll = this._poll.bind(this);
+  this._preparePoll = this._preparePoll.bind(this);
+}
+exports.Poller = Poller;
+
+/**
+ * Returns a boolean indicating whether or not poller
+ * is polling.
+ *
+ * @return {boolean}
+ */
+Poller.prototype.isPolling = function pollerIsPolling () {
+  return !!this._timer;
+};
+
+/**
+ * Turns polling on.
+ *
+ * @return {Poller}
+ */
+Poller.prototype.on = function pollerOn () {
+  if (this._destroyed) {
+    throw Error("Poller cannot be turned on after destruction.");
+  }
+  if (this._timer) {
+    this.off();
+  }
+  this._immediate ? this._poll() : this._preparePoll();
+  return this;
+};
+
+/**
+ * Turns off polling. Returns a promise that resolves when
+ * the last outstanding `fn` call finishes if it's an async function.
+ *
+ * @return {Promise}
+ */
+Poller.prototype.off = function pollerOff () {
+  let { resolve, promise } = defer();
+  if (this._timer) {
+    timers.clearTimeout(this._timer);
+    this._timer = null;
+  }
+
+  // Settle an inflight poll call before resolving
+  // if using a promise-backed poll function
+  if (this._inflight) {
+    this._inflight.then(resolve);
+  } else {
+    resolve();
+  }
+  return promise;
+};
+
+/**
+ * Turns off polling and removes the reference to the poller function.
+ * Resolves when the last outstanding `fn` call finishes if it's an async function.
+ */
+Poller.prototype.destroy = function pollerDestroy () {
+  return this.off().then(() => {
+    this._destroyed = true;
+    this._fn = null
+  });
+};
+
+Poller.prototype._preparePoll = function pollerPrepare () {
+  this._timer = timers.setTimeout(this._poll, this._wait);
+};
+
+Poller.prototype._poll = function pollerPoll () {
+  let response = this._fn();
+  if (response && typeof response.then === "function") {
+    // Store the most recent in-flight polling
+    // call so we can clean it up when disabling
+    this._inflight = response;
+    response.then(() => {
+      // Only queue up the next call if poller was not turned off
+      // while this async poll call was in flight.
+      if (this._timer) {
+        this._preparePoll();
+      }
+    });
+  } else {
+    this._preparePoll();
+  }
+};
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -79,16 +79,17 @@ skip-if = e10s # Layouthelpers test shou
 skip-if = e10s # Layouthelpers test should not run in a content page.
 [browser_mdn-docs-01.js]
 [browser_mdn-docs-02.js]
 [browser_num-l10n.js]
 [browser_observableobject.js]
 [browser_options-view-01.js]
 [browser_outputparser.js]
 skip-if = e10s # Test intermittently fails with e10s. Bug 1124162.
+[browser_poller.js]
 [browser_prefs-01.js]
 [browser_prefs-02.js]
 [browser_require_basic.js]
 [browser_spectrum.js]
 [browser_theme.js]
 [browser_tableWidget_basic.js]
 [browser_tableWidget_keyboard_interaction.js]
 [browser_tableWidget_mouse_interaction.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_poller.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the Poller class.
+
+const { Poller } = devtools.require("devtools/shared/poller");
+
+add_task(function* () {
+  let count1 = 0, count2 = 0, count3 = 0;
+
+  let poller1 = new Poller(function () {
+    count1++;
+  }, 1000000000, true);
+  let poller2 = new Poller(function () {
+    count2++;
+  }, 10);
+  let poller3 = new Poller(function () {
+    count3++;
+  }, 1000000000);
+
+  poller2.on();
+
+  ok(!poller1.isPolling(), "isPolling() returns false for an off poller");
+  ok(poller2.isPolling(), "isPolling() returns true for an on poller");
+
+  yield waitUntil(() => count2 > 10);
+
+  ok(count2 > 10, "poller that was turned on polled several times");
+  ok(count1 === 0, "poller that was never turned on never polled");
+
+  yield poller2.off();
+  let currentCount2 = count2;
+
+  poller1.on(); // Really high poll time!
+  poller3.on(); // Really high poll time!
+
+  yield waitUntil(() => count1 === 1);
+  ok(true, "Poller calls fn immediately when `immediate` is true");
+  ok(count3 === 0, "Poller does not call fn immediately when `immediate` is not set");
+
+  ok(count2 === currentCount2, "a turned off poller does not continue to poll");
+  yield poller2.off();
+  yield poller2.off();
+  yield poller2.off();
+  ok(true, "Poller.prototype.off() is idempotent");
+
+  // This should still have not polled a second time
+  is(count1, 1, "wait time works");
+
+  ok(poller1.isPolling(), "isPolling() returns true for an on poller");
+  ok(!poller2.isPolling(), "isPolling() returns false for an off poller");
+});
+
+add_task(function *() {
+  let count = -1;
+  // Create a poller that returns a promise.
+  // The promise is resolved asynchronously after adding 9 to the count, ensuring
+  // that on every poll, we have a multiple of 10.
+  let asyncPoller = new Poller(function () {
+    count++;
+    ok(!(count%10), `Async poller called with a multiple of 10: ${count}`);
+    return new Promise(function (resolve, reject) {
+      let add9 = 9;
+      let interval = setInterval(() => {
+        if (add9--) {
+          count++;
+        } else {
+          clearInterval(interval);
+          resolve();
+        }
+      }, 10);
+    });
+  });
+
+  asyncPoller.on(1);
+  yield waitUntil(() => count > 50);
+  yield asyncPoller.off();
+});
+
+add_task(function *() {
+  // Create a poller that returns a promise. This poll call
+  // is called immediately, and then subsequently turned off.
+  // The call to `off` should not resolve until the inflight call
+  // finishes.
+  let inflightFinished = null;
+  let pollCalls = 0;
+  let asyncPoller = new Poller(function () {
+    pollCalls++;
+    return new Promise(function (resolve, reject) {
+      setTimeout(() => {
+        inflightFinished = true;
+        resolve();
+      }, 1000);
+    });
+  }, 1, true);
+  asyncPoller.on();
+
+  yield asyncPoller.off();
+  ok(inflightFinished, "off() method does not resolve until remaining inflight poll calls finish");
+  is(pollCalls, 1, "should only be one poll call to occur before turning off polling");
+});
+
+add_task(function *() {
+  // Create a poller that returns a promise. This poll call
+  // is called immediately, and then subsequently turned off.
+  // The call to `off` should not resolve until the inflight call
+  // finishes.
+  let inflightFinished = null;
+  let pollCalls = 0;
+  let asyncPoller = new Poller(function () {
+    pollCalls++;
+    return new Promise(function (resolve, reject) {
+      setTimeout(() => {
+        inflightFinished = true;
+        resolve();
+      }, 1000);
+    });
+  }, 1, true);
+  asyncPoller.on();
+
+  yield asyncPoller.destroy();
+  ok(inflightFinished, "destroy() method does not resolve until remaining inflight poll calls finish");
+  is(pollCalls, 1, "should only be one poll call to occur before destroying polling");
+  
+  try {
+    asyncPoller.on();
+    ok(false, "Calling on() after destruction should throw");
+  } catch (e) {
+    ok(true, "Calling on() after destruction should throw");
+  }
+});
--- a/browser/devtools/shared/test/head.js
+++ b/browser/devtools/shared/test/head.js
@@ -237,8 +237,29 @@ function* openAndCloseToolbox(nbOfTimes,
 
     // We use a timeout to check the toolbox's active time
     yield new Promise(resolve => setTimeout(resolve, usageTime));
 
     info("Closing toolbox " + (i + 1));
     yield gDevTools.closeToolbox(target);
   }
 }
+
+/**
+ * Waits until a predicate returns true.
+ *
+ * @param function predicate
+ *        Invoked once in a while until it returns true.
+ * @param number interval [optional]
+ *        How often the predicate is invoked, in milliseconds.
+ */
+function waitUntil(predicate, interval = 10) {
+  if (predicate()) {
+    return Promise.resolve(true);
+  }
+  return new Promise(resolve => {
+    setTimeout(function() {
+      waitUntil(predicate).then(() => resolve(true));
+    }, interval);
+  });
+}
+
+// EventUtils just doesn't work!
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -147,32 +147,34 @@ function CssHtmlTree(aStyleInspector, aP
   this.focusWindow = this.focusWindow.bind(this);
   this._onContextMenu = this._onContextMenu.bind(this);
   this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
   this._onSelectAll = this._onSelectAll.bind(this);
   this._onClick = this._onClick.bind(this);
   this._onCopy = this._onCopy.bind(this);
   this._onCopyColor = this._onCopyColor.bind(this);
   this._onFilterStyles = this._onFilterStyles.bind(this);
+  this._onFilterKeyPress = this._onFilterKeyPress.bind(this);
   this._onClearSearch = this._onClearSearch.bind(this);
   this._onIncludeBrowserStyles = this._onIncludeBrowserStyles.bind(this);
   this._onFilterTextboxContextMenu = this._onFilterTextboxContextMenu.bind(this);
 
   let doc = this.styleDocument;
   this.root = doc.getElementById("root");
   this.element = doc.getElementById("propertyContainer");
   this.searchField = doc.getElementById("computedview-searchbox");
   this.searchClearButton = doc.getElementById("computedview-searchinput-clear");
   this.includeBrowserStylesCheckbox = doc.getElementById("browser-style-checkbox");
 
   this.styleDocument.addEventListener("mousedown", this.focusWindow);
   this.element.addEventListener("click", this._onClick);
   this.element.addEventListener("copy", this._onCopy);
   this.element.addEventListener("contextmenu", this._onContextMenu);
   this.searchField.addEventListener("input", this._onFilterStyles);
+  this.searchField.addEventListener("keypress", this._onFilterKeyPress);
   this.searchField.addEventListener("contextmenu", this._onFilterTextboxContextMenu);
   this.searchClearButton.addEventListener("click", this._onClearSearch);
   this.includeBrowserStylesCheckbox.addEventListener("command",
     this._onIncludeBrowserStyles);
 
   this.searchClearButton.hidden = true;
 
   // No results text.
@@ -540,16 +542,56 @@ CssHtmlTree.prototype = {
       }
 
       this.refreshPanel();
       this._filterChangeTimeout = null;
     }, filterTimeout);
   },
 
   /**
+   * Handle the search box's keypress event. If the escape key is pressed,
+   * clear the search box field.
+   */
+  _onFilterKeyPress: function(aEvent) {
+    if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE &&
+        this._onClearSearch()) {
+      aEvent.preventDefault();
+      aEvent.stopPropagation();
+    }
+  },
+
+  /**
+   * Context menu handler for filter style search box.
+   */
+  _onFilterTextboxContextMenu: function(event) {
+    try {
+      this.styleDocument.defaultView.focus();
+      let contextmenu = this.inspector.toolbox.textboxContextMenuPopup;
+      contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
+    } catch(e) {
+      console.error(e);
+    }
+  },
+
+  /**
+   * Called when the user clicks on the clear button in the filter style search
+   * box. Returns true if the search box is cleared and false otherwise.
+   */
+  _onClearSearch: function() {
+    if (this.searchField.value) {
+      this.searchField.value = "";
+      this.searchField.focus();
+      this._onFilterStyles();
+      return true;
+    }
+
+    return false;
+  },
+
+  /**
    * The change event handler for the includeBrowserStyles checkbox.
    *
    * @param {Event} aEvent the DOM Event object.
    */
   _onIncludeBrowserStyles: function(aEvent)
   {
     this.refreshSourceFilter();
     this.refreshPanel();
@@ -826,39 +868,16 @@ CssHtmlTree.prototype = {
    *  Toggle the original sources pref.
    */
   _onToggleOrigSources: function()
   {
     let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
     Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
   },
 
-   /**
-   * Context menu handler for filter style search box.
-   */
-  _onFilterTextboxContextMenu: function(event) {
-    try {
-      this.styleDocument.defaultView.focus();
-      let contextmenu = this.inspector.toolbox.textboxContextMenuPopup;
-      contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
-    } catch(e) {
-      console.error(e);
-    }
-  },
-
-  /**
-   * Called when the user clicks on the clear button in the filter style search
-   * box.
-   */
-  _onClearSearch: function() {
-    this.searchField.value = "";
-    this.searchField.focus();
-    this._onFilterStyles();
-  },
-
   /**
    * Destructor for CssHtmlTree.
    */
   destroy: function CssHtmlTree_destroy()
   {
     this.viewedElement = null;
     this._outputParser = null;
 
@@ -901,16 +920,17 @@ CssHtmlTree.prototype = {
     this.highlighters.destroy();
 
     // Remove bound listeners
     this.styleDocument.removeEventListener("mousedown", this.focusWindow);
     this.element.removeEventListener("click", this._onClick);
     this.element.removeEventListener("copy", this._onCopy);
     this.element.removeEventListener("contextmenu", this._onContextMenu);
     this.searchField.removeEventListener("input", this._onFilterStyles);
+    this.searchField.removeEventListener("keypress", this._onFilterKeyPress);
     this.searchField.removeEventListener("contextmenu", this._onFilterTextboxContextMenu);
     this.searchClearButton.removeEventListener("click", this._onClearSearch);
     this.includeBrowserStylesCheckbox.removeEventListener("command",
       this.includeBrowserStylesChanged);
 
     // Nodes used in templating
     this.root = null;
     this.element = null;
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -1124,28 +1124,30 @@ function CssRuleView(aInspector, aDoc, a
   this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
   this._onAddRule = this._onAddRule.bind(this);
   this._onSelectAll = this._onSelectAll.bind(this);
   this._onCopy = this._onCopy.bind(this);
   this._onCopyColor = this._onCopyColor.bind(this);
   this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
   this._onShowMdnDocs = this._onShowMdnDocs.bind(this);
   this._onFilterStyles = this._onFilterStyles.bind(this);
+  this._onFilterKeyPress = this._onFilterKeyPress.bind(this);
   this._onClearSearch = this._onClearSearch.bind(this);
   this._onFilterTextboxContextMenu = this._onFilterTextboxContextMenu.bind(this);
 
   this.element = this.doc.getElementById("ruleview-container");
   this.searchField = this.doc.getElementById("ruleview-searchbox");
   this.searchClearButton = this.doc.getElementById("ruleview-searchinput-clear");
 
   this.searchClearButton.hidden = true;
 
   this.element.addEventListener("copy", this._onCopy);
   this.element.addEventListener("contextmenu", this._onContextMenu);
   this.searchField.addEventListener("input", this._onFilterStyles);
+  this.searchField.addEventListener("keypress", this._onFilterKeyPress);
   this.searchField.addEventListener("contextmenu", this._onFilterTextboxContextMenu);
   this.searchClearButton.addEventListener("click", this._onClearSearch);
 
   this._handlePrefChange = this._handlePrefChange.bind(this);
   this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this);
 
   this._prefObserver = new PrefObserver("devtools.");
   this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
@@ -1643,36 +1645,53 @@ CssRuleView.prototype = {
 
       this.inspector.emit("ruleview-filtered");
 
       this._filterChangeTimeout = null;
     }, filterTimeout);
   },
 
   /**
+   * Handle the search box's keypress event. If the escape key is pressed,
+   * clear the search box field.
+   */
+  _onFilterKeyPress: function(event) {
+    if (event.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE &&
+        this._onClearSearch()) {
+      event.preventDefault();
+      event.stopPropagation();
+    }
+  },
+
+  /**
    * Context menu handler for filter style search box.
    */
   _onFilterTextboxContextMenu: function(event) {
     try {
       this.doc.defaultView.focus();
       let contextmenu = this.inspector.toolbox.textboxContextMenuPopup;
       contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
     } catch(e) {
       console.error(e);
     }
   },
 
   /**
    * Called when the user clicks on the clear button in the filter style search
-   * box.
+   * box. Returns true if the search box is cleared and false otherwise.
    */
   _onClearSearch: function() {
-    this.searchField.value = "";
-    this.searchField.focus();
-    this._onFilterStyles();
+    if (this.searchField.value) {
+      this.searchField.value = "";
+      this.searchField.focus();
+      this._onFilterStyles();
+      return true;
+    }
+
+    return false;
   },
 
   destroy: function() {
     this.isDestroyed = true;
     this.clear();
 
     gDummyPromise = null;
 
@@ -1716,16 +1735,17 @@ CssRuleView.prototype = {
 
     this.tooltips.destroy();
     this.highlighters.destroy();
 
     // Remove bound listeners
     this.element.removeEventListener("copy", this._onCopy);
     this.element.removeEventListener("contextmenu", this._onContextMenu);
     this.searchField.removeEventListener("input", this._onFilterStyles);
+    this.searchField.removeEventListener("keypress", this._onFilterKeyPress);
     this.searchField.removeEventListener("contextmenu",
       this._onFilterTextboxContextMenu);
     this.searchClearButton.removeEventListener("click", this._onClearSearch);
     this.searchField = null;
     this.searchClearButton = null;
 
     if (this.element.parentNode) {
       this.element.parentNode.removeChild(this.element);
--- a/browser/devtools/styleinspector/test/browser.ini
+++ b/browser/devtools/styleinspector/test/browser.ini
@@ -37,16 +37,17 @@ support-files =
 [browser_computedview_media-queries.js]
 [browser_computedview_no-results-placeholder.js]
 [browser_computedview_original-source-link.js]
 [browser_computedview_pseudo-element_01.js]
 [browser_computedview_refresh-on-style-change_01.js]
 [browser_computedview_search-filter.js]
 [browser_computedview_search-filter_clear.js]
 [browser_computedview_search-filter_context-menu.js]
+[browser_computedview_search-filter_escape-keypress.js]
 [browser_computedview_select-and-copy-styles.js]
 [browser_computedview_style-editor-link.js]
 [browser_ruleview_add-property-and-reselect.js]
 [browser_ruleview_add-property-cancel_01.js]
 [browser_ruleview_add-property-cancel_02.js]
 [browser_ruleview_add-property-cancel_03.js]
 [browser_ruleview_add-property_01.js]
 [browser_ruleview_add-property_02.js]
@@ -118,16 +119,17 @@ skip-if = e10s # Bug 1090340
 [browser_ruleview_search-filter_05.js]
 [browser_ruleview_search-filter_06.js]
 [browser_ruleview_search-filter_07.js]
 [browser_ruleview_search-filter_08.js]
 [browser_ruleview_search-filter_09.js]
 [browser_ruleview_search-filter_10.js]
 [browser_ruleview_search-filter_clear.js]
 [browser_ruleview_search-filter_context-menu.js]
+[browser_ruleview_search-filter_escape-keypress.js]
 [browser_ruleview_select-and-copy-styles.js]
 [browser_ruleview_selector-highlighter_01.js]
 [browser_ruleview_selector-highlighter_02.js]
 [browser_ruleview_selector-highlighter_03.js]
 [browser_ruleview_style-editor-link.js]
 skip-if = e10s # bug 1040670 Cannot open inline styles in viewSourceUtils
 [browser_ruleview_urls-clickable.js]
 [browser_ruleview_user-agent-styles.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_computedview_search-filter_escape-keypress.js
@@ -0,0 +1,71 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that search filter escape keypress will clear the search field.
+
+let TEST_URI = [
+  '<style type="text/css">',
+  '  .matches {',
+  '    color: #F00;',
+  '  }',
+  '</style>',
+  '<span id="matches" class="matches">Some styled text</span>'
+].join("\n");
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {toolbox, inspector, view} = yield openComputedView();
+  yield selectNode("#matches", inspector);
+  yield testAddTextInFilter(inspector, view);
+  yield testEscapeKeypress(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, computedView) {
+  info("Setting filter text to \"background-color\"");
+
+  let win = computedView.styleWindow;
+  let propertyViews = computedView.propertyViews;
+  let searchField = computedView.searchField;
+  let checkbox = computedView.includeBrowserStylesCheckbox;
+
+  info("Include browser styles");
+  checkbox.click();
+  yield inspector.once("computed-view-refreshed");
+
+  searchField.focus();
+  synthesizeKeys("background-color", win);
+  yield inspector.once("computed-view-refreshed");
+
+  info("Check that the correct properties are visible");
+
+  propertyViews.forEach((propView) => {
+    let name = propView.name;
+    is(propView.visible, name.indexOf("background-color") > -1,
+      "span " + name + " property visibility check");
+  });
+}
+
+function* testEscapeKeypress(inspector, computedView) {
+  info("Pressing the escape key on search filter");
+
+  let win = computedView.styleWindow;
+  let propertyViews = computedView.propertyViews;
+  let searchField = computedView.searchField;
+  let onRefreshed = inspector.once("computed-view-refreshed");
+
+  searchField.focus();
+  EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+  yield onRefreshed;
+
+  info("Check that the correct properties are visible");
+
+  ok(!searchField.value, "Search filter is cleared");
+  propertyViews.forEach((propView) => {
+    let name = propView.name;
+    is(propView.visible, true,
+      "span " + name + " property is visible");
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_search-filter_escape-keypress.js
@@ -0,0 +1,66 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view search filter escape keypress will clear the search
+// field.
+
+let TEST_URI = [
+  '<style type="text/css">',
+  '  #testid {',
+  '    background-color: #00F;',
+  '  }',
+  '  .testclass {',
+  '    width: 100%;',
+  '  }',
+  '</style>',
+  '<div id="testid" class="testclass">Styled Node</div>'
+].join("\n");
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {toolbox, inspector, view} = yield openRuleView();
+  yield selectNode("#testid", inspector);
+  yield testAddTextInFilter(inspector, view);
+  yield testEscapeKeypress(inspector, view);
+});
+
+function* testAddTextInFilter(inspector, ruleView) {
+  info("Setting filter text to \"00F\"");
+
+  let win = ruleView.doc.defaultView;
+  let searchField = ruleView.searchField;
+  let onRuleViewFiltered = inspector.once("ruleview-filtered");
+
+  searchField.focus();
+  synthesizeKeys("00F", win);
+  yield onRuleViewFiltered;
+
+  info("Check that the correct rules are visible");
+  is(ruleView.element.children.length, 2, "Should have 2 rules.");
+  is(getRuleViewRuleEditor(ruleView, 0).rule.selectorText, "element", "First rule is inline element.");
+  is(getRuleViewRuleEditor(ruleView, 1).rule.selectorText, "#testid", "Second rule is #testid.");
+  ok(getRuleViewRuleEditor(ruleView, 1).rule.textProps[0].editor.element.classList.contains("ruleview-highlight"),
+    "background-color text property is correctly highlighted.");
+}
+
+function* testEscapeKeypress(inspector, ruleView) {
+  info("Pressing the escape key on search filter");
+
+  let doc = ruleView.doc;
+  let win = ruleView.doc.defaultView;
+  let searchField = ruleView.searchField;
+  let onRuleViewFiltered = inspector.once("ruleview-filtered");
+
+  searchField.focus();
+  EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+  yield onRuleViewFiltered;
+
+  info("Check the search filter is cleared and no rules are highlighted");
+  is(ruleView.element.children.length, 3, "Should have 3 rules.");
+  ok(!searchField.value, "Search filter is cleared");
+  ok(!doc.querySelectorAll(".ruleview-highlight").length &&
+     !ruleView._highlightedElements.length, "No rules are higlighted");
+}
--- a/browser/themes/shared/devtools/animationinspector.css
+++ b/browser/themes/shared/devtools/animationinspector.css
@@ -94,16 +94,46 @@ body {
     background-image: url("debugger-pause@2x.png");
   }
 
   #toggle-all.paused::before {
     background-image: url("debugger-play@2x.png");
   }
 }
 
+/* Animation target node gutter, contains a preview of the dom node */
+
+.animation-target {
+  background-color: var(--theme-toolbar-background);
+  padding: 1px 4px;
+  box-sizing: border-box;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.animation-target .attribute-name {
+  padding-left: 4px;
+}
+
+.animation-target .node-selector {
+  background: url("chrome://browser/skin/devtools/vview-open-inspector.png") no-repeat 0 0;
+  padding-left: 16px;
+  margin-right: 5px;
+  cursor: pointer;
+}
+
+.animation-target .node-selector:hover {
+  background-position: -32px 0;
+}
+
+.animation-target .node-selector:active {
+  background-position: -16px 0;
+}
+
 /* Animation title gutter, contains the name, duration, iteration */
 
 .animation-title {
   background-color: var(--theme-toolbar-background);
   border-bottom: 1px solid var(--theme-splitter-color);
   padding: 1px 4px;
   word-wrap: break-word;
   overflow: auto;
--- a/dom/activities/ActivitiesService.jsm
+++ b/dom/activities/ActivitiesService.jsm
@@ -350,16 +350,33 @@ let Activities = {
 
     let matchFunc = function matchFunc(aResult) {
       let calleeApp = DOMApplicationRegistry.getAppByManifestURL(aResult.manifest);
       // Only allow certified apps to handle this special activity
       if (aMsg.options.name === 'internal-system-engineering-mode' &&
           calleeApp.appStatus !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
         return false;
       }
+
+      // If the activity is in the developer mode activity list, only let the
+      // system app be a provider.
+      let isSystemApp = false;
+      let isDevModeActivity = false;
+      try {
+        isSystemApp =
+          aResult.manifest == Services.prefs.getCharPref("b2g.system_manifest_url");
+        isDevModeActivity =
+          Services.prefs.getCharPref("dom.activities.developer_mode_only")
+                        .split(",").indexOf(aMsg.options.name) !== -1;
+      } catch(e)  {}
+
+      if (isDevModeActivity && !isSystemApp) {
+        return false;
+      }
+
       return ActivitiesServiceFilter.match(aMsg.options.data,
                                            aResult.description.filters);
     };
 
     this.db.find(aMsg, successCb, errorCb, matchFunc);
   },
 
   trySendAndCleanup: function activities_trySendAndCleanup(aId, aName, aPayload) {
--- a/dom/activities/ActivityProxy.js
+++ b/dom/activities/ActivityProxy.js
@@ -64,16 +64,32 @@ ActivityProxy.prototype = {
     // Only let certified app to initiate this activitiy.
     if (aOptions.name === 'internal-system-engineering-mode' &&
         principal.appStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
       Services.DOMRequest.fireErrorAsync(this.activity, "SecurityError");
       Services.obs.notifyObservers(null, "Activity:Error", null);
       return;
     }
 
+    // Check the activities that are restricted to be used in dev mode.
+    let devMode = false;
+    let isDevModeActivity = false;
+    try {
+      devMode = Services.prefs.getBoolPref("dom.apps.developer_mode");
+      isDevModeActivity =
+        Services.prefs.getCharPref("dom.activities.developer_mode_only")
+                      .split(",").indexOf(aOptions.name) !== -1;
+
+    } catch(e) {}
+    if (isDevModeActivity && !devMode) {
+      Services.DOMRequest.fireErrorAsync(this.activity, "SecurityError");
+      Services.obs.notifyObservers(null, "Activity:Error", null);
+      return;
+    }
+
     cpmm.addMessageListener("Activity:FireSuccess", this);
     cpmm.addMessageListener("Activity:FireError", this);
 
     cpmm.sendAsyncMessage("Activity:Start",
       {
         id: this.id,
         options: {
           name: aOptions.name,
--- a/dom/activities/moz.build
+++ b/dom/activities/moz.build
@@ -3,16 +3,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/.
 
 DIRS += ['interfaces']
 
 XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
 
+MOCHITEST_CHROME_MANIFESTS += ['tests/mochi/mochitest.ini']
+
 EXPORTS.mozilla.dom += [
     'Activity.h',
 ]
 
 SOURCES += [
     'Activity.cpp',
 ]
 
new file mode 100644
--- /dev/null
+++ b/dom/activities/tests/mochi/manifest.webapp
@@ -0,0 +1,6 @@
+{
+  "name": "Random app",
+  "activities": {
+    "import-app": { "blob": { "required": true } }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/dom/activities/tests/mochi/manifest.webapp^headers^
@@ -0,0 +1,1 @@
+Content-Type: application/manifest+json
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/activities/tests/mochi/mochitest.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+skip-if = e10s
+support-files =
+  system.webapp
+  system.webapp^headers^
+  manifest.webapp
+  manifest.webapp^headers^
+
+[test_dev_mode_activity.html]
new file mode 100644
--- /dev/null
+++ b/dom/activities/tests/mochi/system.webapp
@@ -0,0 +1,6 @@
+{
+  "name": "System app",
+  "activities": {
+    "import-app": { "blob": { "required": true } }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/dom/activities/tests/mochi/system.webapp^headers^
@@ -0,0 +1,1 @@
+Content-Type: application/manifest+json
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/activities/tests/mochi/test_dev_mode_activity.html
@@ -0,0 +1,290 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id={1123846}
+-->
+<head>
+  <title>Test for Bug {1123846}</title>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/chrome-harness.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id={1123846}">Mozilla Bug {1123846}</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+
+/**
+  * Tests the developer mode activities that can only be provided by the
+  * system app.
+  *
+  * We test the following:
+  * 1) No dev mode, no system app installed (failure).
+  * 2) No dev mode, system app installed (failure).
+  * 3) No dev mode, system app and other app installed (failure).
+  * 4) Dev mode, system app and other app installed (success, only system app returned).
+  */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+var gRootUrl = "http://test/chrome/dom/activities/tests/mochi/";
+var gGenerator = runTest();
+
+function go() {
+  SpecialPowers.pushPermissions(
+    [{ "type": "webapps-manage", "allow": 1, "context": document },
+     { "type": "browser", "allow": 1, "context": document },
+     { "type": "embed-apps", "allow": 1, "context": document }],
+    function() {
+      SpecialPowers.pushPrefEnv(
+        {'set': [["dom.mozBrowserFramesEnabled", true],
+                 ["dom.sysmsg.enabled", true],
+                 ["dom.apps.developer_mode", false],
+                 ["dom.activities.developer_mode_only", "import-app"]]},
+        continueTest) });
+}
+
+function cbError(aEvent) {
+  ok(false, "Error callback invoked " +
+            aEvent.target.error.name + " " + aEvent.target.error.message);
+  finish();
+}
+
+function unexpectedSuccess(aMsg) {
+  return function() {
+    ok(false, "Should not have succeeded: " + aMsg);
+    finish();
+  }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+var systemAppUrl = gRootUrl + "system.webapp";
+var otherAppUrl = gRootUrl + "manifest.webapp";
+
+function installApp(aUrl) {
+  var request = navigator.mozApps.install(aUrl, { });
+  request.onerror = cbError;
+  request.onsuccess = continueTest;
+  return request;
+}
+
+function installSystemApp() {
+  return installApp(systemAppUrl);
+}
+
+function installOtherApp() {
+  return installApp(otherAppUrl);
+}
+
+function uninstall(aApp) {
+  info("Uninstalling " + (aApp ? aApp.manifestURL : "NO APP!!"));
+  var request = navigator.mozApps.mgmt.uninstall(aApp);
+  request.onerror = cbError;
+  request.onsuccess = continueTest;
+}
+
+function registerComponent(aObject, aDescription, aContract) {
+  var uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
+                        .getService(Ci.nsIUUIDGenerator);
+  var cid = uuidGenerator.generateUUID();
+
+  info("Registering " + cid);
+
+  var componentManager =
+    Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+  componentManager.registerFactory(cid, aDescription, aContract, aObject);
+
+  // Keep the id on the object so we can unregister later.
+  aObject.cid = cid;
+}
+
+function unregisterComponent(aObject) {
+  info("Unregistering " + aObject.cid);
+  var componentManager =
+    Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+  componentManager.unregisterFactory(aObject.cid, aObject);
+}
+
+var ActivityGlue = {
+  // nsISupports implementation.
+  QueryInterface: function(iid) {
+    if (iid.equals(Ci.nsISupports) ||
+        iid.equals(Ci.nsIFactory) ||
+        iid.equals(Ci.nsIActivityUIGlue)) {
+      return this;
+    }
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+  // nsIFactory implementation.
+  createInstance: function(outer, iid) {
+    return this.QueryInterface(iid);
+  },
+
+  // nsIActivityUIGlue implementation.
+  chooseActivity: function(aOptions, aActivities, aCallback) {
+    aCallback.handleEvent(Ci.nsIActivityUIGlueCallback.WEBAPPS_ACTIVITY,
+                          aActivities.length == 1 ? 0 : -1);
+  }
+};
+
+var SystemMessageGlue = {
+  // nsISupports implementation.
+  QueryInterface: function(iid) {
+    if (iid.equals(Ci.nsISupports) ||
+        iid.equals(Ci.nsIFactory) ||
+        iid.equals(Ci.nsISystemMessageGlue)) {
+      return this;
+    }
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+  // nsIFactory implementation.
+  createInstance: function(outer, iid) {
+    return this.QueryInterface(iid);
+  },
+
+  // nsISystemMessageGlue implementation.
+  openApp(pageURL, manifestURL, type, target, showApp, onlyShowApp, extra) {
+    // We should only try to open a page in the sytem app.
+    is(manifestURL, systemAppUrl, "Opening a page in the system app.");
+  }
+};
+
+registerComponent(ActivityGlue,
+                  "Activity Glue",
+                  "@mozilla.org/dom/activities/ui-glue;1");
+
+registerComponent(SystemMessageGlue,
+                  "System Message Glue",
+                  "@mozilla.org/dom/messages/system-message-glue;1");
+
+function finish() {
+  unregisterComponent(ActivityGlue);
+  unregisterComponent(SystemMessageGlue);
+  SimpleTest.finish();
+}
+
+function continueTest() {
+  try {
+    gGenerator.next();
+  } catch (e if e instanceof StopIteration) {
+    finish();
+  }
+}
+
+/**
+  * Test exporting and importing hosted and packaged apps.
+  */
+function runTest() {
+  SpecialPowers.setAllAppsLaunchable(true);
+
+  SpecialPowers.autoConfirmAppInstall(continueTest);
+  yield undefined;
+
+  SpecialPowers.autoConfirmAppUninstall(continueTest);
+  yield undefined;
+
+  // Check how many apps we are starting with.
+  var request = navigator.mozApps.mgmt.getAll();
+  request.onerror = cbError;
+  request.onsuccess = continueTest;
+  yield undefined;
+  var initialAppsCount = request.result.length;
+  info("Starting with " + initialAppsCount + " apps installed.");
+
+  // 1) No dev mode, no system app installed (failure).
+  var activity = new MozActivity({ name: "import-app" });
+  activity.onerror = function() {
+    ok(true, "1) No dev mode, no system app installed");
+    continueTest();
+  }
+  activity.onsuccess = unexpectedSuccess("1) No dev mode, no system app installed");
+  yield undefined;
+
+
+  // 2) No dev mode, system app installed (failure).
+  // Configure the system app manifest url.
+  SpecialPowers.pushPrefEnv(
+        {'set': [["b2g.system_manifest_url", systemAppUrl]]},
+        continueTest);
+  yield undefined;
+
+  // Install the system app.
+  request = installSystemApp();
+  yield undefined;
+  var systemApp = request.result;
+  ok(systemApp, "systemApp is non-null");
+
+  activity = new MozActivity({ name: "import-app" });
+  activity.onerror = function() {
+    ok(true, "2) No dev mode, system app installed");
+    continueTest();
+  }
+  activity.onsuccess = unexpectedSuccess("2) No dev mode, system app installed");
+  yield undefined;
+
+  // 3) No dev mode, system app and other app installed (failure).
+  request = installOtherApp();
+  yield undefined;
+  var otherApp = request.result;
+  ok(otherApp, "otherApp is non-null");
+
+  activity = new MozActivity({ name: "import-app" });
+  activity.onerror = function() {
+    ok(true, "3) No dev mode, system app and other app installed");
+    continueTest();
+  }
+  activity.onsuccess = unexpectedSuccess("3) No dev mode, system app and other app installed");
+  yield undefined;
+
+  // 4) Dev mode, no system app installed.
+  SpecialPowers.pushPrefEnv(
+        {'set': [["dom.apps.developer_mode", true]]},
+        continueTest);
+  yield undefined;
+
+  activity = new MozActivity({ name: "import-app" });
+  activity.onsuccess = function() {
+    ok(true, "4) Dev mode, system app and other app installed");
+    continueTest();
+  }
+  activity.onerror = function(aError) {
+    ok(false, "Got error: " + aError.name);
+    finish();
+  }
+  yield undefined;
+
+  // Cleanup
+  uninstall(systemApp);
+  yield undefined;
+
+  uninstall(otherApp);
+  yield undefined;
+
+  // Check that we restored the app registry.
+  request = navigator.mozApps.mgmt.getAll();
+  request.onerror = cbError;
+  request.onsuccess = continueTest;
+  yield undefined;
+
+  is(request.result.length, initialAppsCount, "All apps are uninstalled.");
+}
+
+addLoadEvent(go);
+
+</script>
+</pre>
+</body>
+</html>
--- a/mobile/android/base/GuestSession.java
+++ b/mobile/android/base/GuestSession.java
@@ -36,17 +36,17 @@ public class GuestSession {
             return false;
         }
 
         return profile.locked();
     }
 
     private static PendingIntent getNotificationIntent(Context context) {
         Intent intent = new Intent(NOTIFICATION_INTENT);
-        intent.setClass(context, BrowserApp.class);
+        intent.setClassName(context, AppConstants.BROWSER_INTENT_CLASS_NAME);
         return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
     }
 
     public static void showNotification(Context context) {
         final NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
         final Resources res = context.getResources();
         builder.setContentTitle(res.getString(R.string.guest_browsing_notification_title))
                .setContentText(res.getString(R.string.guest_browsing_notification_text))
--- a/mobile/android/base/tabqueue/TabQueueDispatcher.java
+++ b/mobile/android/base/tabqueue/TabQueueDispatcher.java
@@ -1,17 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.tabqueue;
 
 import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.sync.setup.activities.WebURLFinder;
 
 import android.content.Intent;
@@ -64,17 +63,17 @@ public class TabQueueDispatcher extends 
         startService(intent);
         finish();
     }
 
     /**
      * Start fennec with the supplied intent.
      */
     private void loadNormally(Intent intent) {
-        intent.setClass(getApplicationContext(), BrowserApp.class);
+        intent.setClassName(getApplicationContext(), AppConstants.BROWSER_INTENT_CLASS_NAME);
         startActivity(intent);
         finish();
     }
 
     /**
      * Abort as we were started with no URL.
      * @param dataString
      */
--- a/mobile/android/base/tabqueue/TabQueueHelper.java
+++ b/mobile/android/base/tabqueue/TabQueueHelper.java
@@ -1,16 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.tabqueue;
 
-import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.util.ThreadUtils;
 
@@ -149,17 +149,18 @@ public class TabQueueHelper {
      * will be replaced.
      *
      * @param context
      * @param tabsQueued
      */
     public static void showNotification(final Context context, final int tabsQueued) {
         ThreadUtils.assertNotOnUiThread();
 
-        Intent resultIntent = new Intent(context, BrowserApp.class);
+        Intent resultIntent = new Intent();
+        resultIntent.setClassName(context, AppConstants.BROWSER_INTENT_CLASS_NAME);
         resultIntent.setAction(TabQueueHelper.LOAD_URLS_ACTION);
 
         PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, resultIntent, PendingIntent.FLAG_CANCEL_CURRENT);
 
         final String text;
         final Resources resources = context.getResources();
         if (tabsQueued == 1) {
             text = resources.getString(R.string.tab_queue_notification_text_singular);
--- a/mobile/android/base/tabqueue/TabQueueService.java
+++ b/mobile/android/base/tabqueue/TabQueueService.java
@@ -1,16 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.tabqueue;
 
-import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 
 import android.app.Service;
 import android.content.Context;
@@ -185,17 +185,17 @@ public class TabQueueService extends Ser
 
         tabQueueHandler.postDelayed(stopServiceRunnable, TOAST_TIMEOUT);
 
         return START_REDELIVER_INTENT;
     }
 
     private void openNow(Intent intent) {
         Intent forwardIntent = new Intent(intent);
-        forwardIntent.setClass(getApplicationContext(), BrowserApp.class);
+        forwardIntent.setClassName(getApplicationContext(), AppConstants.BROWSER_INTENT_CLASS_NAME);
         forwardIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         startActivity(forwardIntent);
 
         GeckoSharedPrefs.forApp(getApplicationContext()).edit().remove(GeckoPreferences.PREFS_TAB_QUEUE_LAST_SITE)
                                                                .remove(GeckoPreferences.PREFS_TAB_QUEUE_LAST_TIME)
                                                                .apply();
     }
 
--- a/toolkit/components/telemetry/TelemetryController.jsm
+++ b/toolkit/components/telemetry/TelemetryController.jsm
@@ -191,29 +191,16 @@ this.TelemetryController = Object.freeze
   /**
    * Sets a server to send pings to.
    */
   setServer: function(aServer) {
     return Impl.setServer(aServer);
   },
 
   /**
-   * Adds a ping to the pending ping list by moving it to the saved pings directory
-   * and adding it to the pending ping list.
-   *
-   * @param {String} aPingPath The path of the ping to add to the pending ping list.
-   * @param {Boolean} [aRemoveOriginal] If true, deletes the ping at aPingPath after adding
-   *                  it to the saved pings directory.
-   * @return {Promise} Resolved when the ping is correctly moved to the saved pings directory.
-   */
-  addPendingPingFromFile: function(aPingPath, aRemoveOriginal) {
-    return Impl.addPendingPingFromFile(aPingPath, aRemoveOriginal);
-  },
-
-  /**
    * Submit ping payloads to Telemetry. This will assemble a complete ping, adding
    * environment data, client id and some general info.
    * Depending on configuration, the ping will be sent to the server (immediately or later)
    * and archived locally.
    *
    * @param {String} aType The type of the ping.
    * @param {Object} aPayload The actual data payload for the ping.
    * @param {Object} [aOptions] Options object.
@@ -280,16 +267,26 @@ this.TelemetryController = Object.freeze
     options.addClientId = aOptions.addClientId || false;
     options.addEnvironment = aOptions.addEnvironment || false;
     options.overwrite = aOptions.overwrite || false;
 
     return Impl.addPendingPing(aType, aPayload, options);
   },
 
   /**
+   * Save an aborted-session ping to the pending pings and archive it.
+   *
+   * @param {String} aFilePath The path to the aborted-session checkpoint ping.
+   * @return {Promise} Promise that is resolved when the ping is saved.
+   */
+  addAbortedSessionPing: function addAbortedSessionPing(aFilePath) {
+    return Impl.addAbortedSessionPing(aFilePath);
+  },
+
+  /**
    * Write a ping to a specified location on the disk. Does not add the ping to the
    * pending pings.
    *
    * @param {String} aType The type of the ping.
    * @param {Object} aPayload The actual data payload for the ping.
    * @param {String} aFilePath The path to save the ping to.
    * @param {Object} [aOptions] Options object.
    * @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
@@ -496,33 +493,16 @@ let Impl = {
    * Track any pending ping send and save tasks through the promise passed here.
    * This is needed to block shutdown on any outstanding ping activity.
    */
   _trackPendingPingTask: function (aPromise) {
     this._connectionsBarrier.client.addBlocker("Waiting for ping task", aPromise);
   },
 
   /**
-   * Adds a ping to the pending ping list by moving it to the saved pings directory
-   * and adding it to the pending ping list.
-   *
-   * @param {String} aPingPath The path of the ping to add to the pending ping list.
-   * @param {Boolean} [aRemoveOriginal] If true, deletes the ping at aPingPath after adding
-   *                  it to the saved pings directory.
-   * @return {Promise} Resolved when the ping is correctly moved to the saved pings directory.
-   */
-  addPendingPingFromFile: function(aPingPath, aRemoveOriginal) {
-    return TelemetryStorage.addPendingPingFromFile(aPingPath).then(() => {
-        if (aRemoveOriginal) {
-          return OS.File.remove(aPingPath);
-        }
-      }, error => this._log.error("addPendingPingFromFile - Unable to add the pending ping", error));
-  },
-
-  /**
    * This helper calculates the next time that we can send pings at.
    * Currently this mostly redistributes ping sends around midnight to avoid submission
    * spikes around local midnight for daily pings.
    *
    * @param now Date The current time.
    * @return Number The next time (ms from UNIX epoch) when we can send pings.
    */
   _getNextPingSendTime: function(now) {
@@ -715,16 +695,36 @@ let Impl = {
   savePing: function savePing(aType, aPayload, aFilePath, aOptions) {
     this._log.trace("savePing - Type " + aType + ", Server " + this._server +
                     ", File Path " + aFilePath + ", aOptions " + JSON.stringify(aOptions));
     let pingData = this.assemblePing(aType, aPayload, aOptions);
     return TelemetryStorage.savePingToFile(pingData, aFilePath, aOptions.overwrite)
                         .then(() => pingData.id);
   },
 
+  /**
+   * Save an aborted-session ping to the pending pings and archive it.
+   *
+   * @param {String} aFilePath The path to the aborted-session checkpoint ping.
+   * @return {Promise} Promise that is resolved when the ping is saved.
+   */
+  addAbortedSessionPing: Task.async(function* addAbortedSessionPing(aFilePath) {
+    this._log.trace("addAbortedSessionPing");
+
+    let ping = yield TelemetryStorage.loadPingFile(aFilePath);
+    try {
+      yield TelemetryStorage.addPendingPing(ping);
+      yield TelemetryArchive.promiseArchivePing(ping);
+    } catch (e) {
+      this._log.error("addAbortedSessionPing - Unable to add the pending ping", e);
+    } finally {
+      yield OS.File.remove(aFilePath);
+    }
+  }),
+
   onPingRequestFinished: function(success, startTime, ping, isPersisted) {
     this._log.trace("onPingRequestFinished - success: " + success + ", persisted: " + isPersisted);
 
     let hping = Telemetry.getHistogramById("TELEMETRY_PING");
     let hsuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
 
     hsuccess.add(success);
     hping.add(new Date() - startTime);
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -1527,22 +1527,21 @@ let Impl = {
     // Delay full telemetry initialization to give the browser time to
     // run various late initializers. Otherwise our gathered memory
     // footprint and other numbers would be too optimistic.
     this._delayedInitTaskDeferred = Promise.defer();
     this._delayedInitTask = new DeferredTask(function* () {
       try {
         this._initialized = true;
 
-        let hasLoaded = yield this._loadSessionData();
-        if (!hasLoaded) {
-          // We could not load a valid session data file. Create one.
-          yield this._saveSessionData(this._getSessionDataObject()).catch(() =>
-            this._log.error("setupChromeProcess - Could not write session data to disk."));
-        }
+        yield this._loadSessionData();
+        // Update the session data to keep track of new subsessions created before
+        // the initialization.
+        yield this._saveSessionData(this._getSessionDataObject());
+
         this.attachObservers();
         this.gatherMemory();
 
         Telemetry.asyncFetchTelemetryData(function () {});
 
         if (IS_UNIFIED_TELEMETRY) {
           // Check for a previously written aborted session ping.
           yield this._checkAbortedSessionPing();
@@ -1960,18 +1959,18 @@ let Impl = {
                                 SESSION_STATE_FILE_NAME);
 
     // Try to load the "profileSubsessionCounter" from the state file.
     try {
       let data = yield CommonUtils.readJSON(dataFile);
       if (data &&
           "profileSubsessionCounter" in data &&
           typeof(data.profileSubsessionCounter) == "number" &&
-          "previousSubsessionId" in data) {
-        this._previousSubsessionId = data.previousSubsessionId;
+          "subsessionId" in data) {
+        this._previousSubsessionId = data.subsessionId;
         // Add |_subsessionCounter| to the |_profileSubsessionCounter| to account for
         // new subsession while loading still takes place. This will always be exactly
         // 1 - the current subsessions.
         this._profileSubsessionCounter = data.profileSubsessionCounter +
                                          this._subsessionCounter;
         return true;
       }
     } catch (e) {
@@ -1980,17 +1979,17 @@ let Impl = {
     return false;
   }),
 
   /**
    * Get the session data object to serialise to disk.
    */
   _getSessionDataObject: function() {
     return {
-      previousSubsessionId: this._previousSubsessionId,
+      subsessionId: this._subsessionId,
       profileSubsessionCounter: this._profileSubsessionCounter,
     };
   },
 
   /**
    * Saves session data to disk.
    */
   _saveSessionData: Task.async(function* (sessionData) {
@@ -2004,18 +2003,17 @@ let Impl = {
       this._log.error("_saveSessionData - Failed to write session data to " + filePath, e);
     }
   }),
 
   _onEnvironmentChange: function(reason, oldEnvironment) {
     this._log.trace("_onEnvironmentChange", reason);
     let payload = this.getSessionPayload(REASON_ENVIRONMENT_CHANGE, true);
 
-    let clonedPayload = Cu.cloneInto(payload, myScope);
-    TelemetryScheduler.reschedulePings(REASON_ENVIRONMENT_CHANGE, clonedPayload);
+    TelemetryScheduler.reschedulePings(REASON_ENVIRONMENT_CHANGE, payload);
 
     let options = {
       retentionDays: RETENTION_DAYS,
       addClientId: true,
       addEnvironment: true,
       overrideEnvironment: oldEnvironment,
     };
     TelemetryController.submitExternalPing(getPingType(payload), payload, options);
@@ -2073,34 +2071,34 @@ let Impl = {
     yield OS.File.makeDir(ABORTED_SESSIONS_DIR, { ignoreExisting: true });
 
     const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
                                    ABORTED_SESSION_FILE_NAME);
     let abortedExists = yield OS.File.exists(FILE_PATH);
     if (abortedExists) {
       this._log.trace("_checkAbortedSessionPing - aborted session found: " + FILE_PATH);
       yield this._abortedSessionSerializer.enqueueTask(
-        () => TelemetryController.addPendingPingFromFile(FILE_PATH, true));
+        () => TelemetryController.addAbortedSessionPing(FILE_PATH));
     }
   }),
 
   /**
    * Saves the aborted session ping to disk.
    * @param {Object} [aProvidedPayload=null] A payload object to be used as an aborted
    *                 session ping. The reason of this payload is changed to aborted-session.
    *                 If not provided, a new payload is gathered.
    */
   _saveAbortedSessionPing: function(aProvidedPayload = null) {
     const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
                                    ABORTED_SESSION_FILE_NAME);
     this._log.trace("_saveAbortedSessionPing - ping path: " + FILE_PATH);
 
     let payload = null;
     if (aProvidedPayload) {
-      payload = aProvidedPayload;
+      payload = Cu.cloneInto(aProvidedPayload, myScope);
       // Overwrite the original reason.
       payload.info.reason = REASON_ABORTED_SESSION;
     } else {
       payload = this.getSessionPayload(REASON_ABORTED_SESSION, false);
     }
 
     let options = {
       retentionDays: RETENTION_DAYS,
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js
@@ -0,0 +1,227 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/TelemetryArchive.jsm", this);
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+const MS_IN_ONE_HOUR  = 60 * 60 * 1000;
+const MS_IN_ONE_DAY   = 24 * MS_IN_ONE_HOUR;
+
+const PREF_BRANCH = "toolkit.telemetry.";
+const PREF_ENABLED = PREF_BRANCH + "enabled";
+const PREF_ARCHIVE_ENABLED = PREF_BRANCH + "archive.enabled";
+
+const REASON_ABORTED_SESSION = "aborted-session";
+const REASON_DAILY = "daily";
+const REASON_ENVIRONMENT_CHANGE = "environment-change";
+const REASON_SHUTDOWN = "shutdown";
+
+XPCOMUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function() {
+  return OS.Path.join(OS.Constants.Path.profileDir, "datareporting");
+});
+
+let promiseValidateArchivedPings = Task.async(function*(aExpectedReasons) {
+  // The list of ping reasons which mark the session end (and must reset the subsession
+  // count).
+  const SESSION_END_PING_REASONS = new Set([ REASON_ABORTED_SESSION, REASON_SHUTDOWN ]);
+
+  let list = yield TelemetryArchive.promiseArchivedPingList();
+
+  // We're just interested in the "main" pings.
+  list = list.filter(p => p.type == "main");
+
+  Assert.equal(aExpectedReasons.length, list.length, "All the expected pings must be received.");
+
+  let previousPing = yield TelemetryArchive.promiseArchivedPingById(list[0].id);
+  Assert.equal(aExpectedReasons.shift(), previousPing.payload.info.reason,
+               "Telemetry should only get pings with expected reasons.");
+  Assert.equal(previousPing.payload.info.previousSubsessionId, null,
+               "The first subsession must report a null previous subsession id.");
+  Assert.equal(previousPing.payload.info.profileSubsessionCounter, 1,
+               "profileSubsessionCounter must be 1 the first time.");
+  Assert.equal(previousPing.payload.info.subsessionCounter, 1,
+               "subsessionCounter must be 1 the first time.");
+
+  let expectedSubsessionCounter = 1;
+
+  for (let i = 1; i < list.length; i++) {
+    let currentPing = yield TelemetryArchive.promiseArchivedPingById(list[i].id);
+    let currentInfo = currentPing.payload.info;
+    let previousInfo = previousPing.payload.info;
+    do_print("Archive entry " + i + " - id: " + currentPing.id + ", reason: " + currentInfo.reason);
+
+    Assert.equal(aExpectedReasons.shift(), currentInfo.reason,
+                 "Telemetry should only get pings with expected reasons.");
+    Assert.equal(currentInfo.previousSubsessionId, previousInfo.subsessionId,
+                 "Telemetry must correctly chain subsession identifiers.");
+    Assert.equal(currentInfo.profileSubsessionCounter, previousInfo.profileSubsessionCounter + 1,
+                 "Telemetry must correctly track the profile subsessions count.");
+    Assert.equal(currentInfo.subsessionCounter, expectedSubsessionCounter,
+                 "The subsession counter should be monotonically increasing.");
+
+    // Store the current ping as previous.
+    previousPing = currentPing;
+    // Reset the expected subsession counter, if required. Otherwise increment the expected
+    // subsession counter.
+    expectedSubsessionCounter =
+      SESSION_END_PING_REASONS.has(currentInfo.reason) ? 1 : (expectedSubsessionCounter + 1);
+  }
+});
+
+function run_test() {
+  do_test_pending();
+
+  // Addon manager needs a profile directory
+  do_get_profile();
+  loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+  Preferences.set(PREF_ENABLED, true);
+
+  run_next_test();
+}
+
+add_task(function* test_subsessionsChaining() {
+  if (gIsAndroid) {
+    // We don't support subsessions yet on Android, so skip the next checks.
+    return;
+  }
+
+  const PREF_TEST = PREF_BRANCH + "test.pref1";
+  const PREFS_TO_WATCH = new Map([
+    [PREF_TEST, TelemetryEnvironment.RECORD_PREF_VALUE],
+  ]);
+  Preferences.reset(PREF_TEST);
+
+  // Fake the clock data to manually trigger an aborted-session ping and a daily ping.
+  // This is also helpful to make sure we get the archived pings in an expected order.
+  let now = fakeNow(2009, 9, 18, 0, 0, 0);
+
+  let moveClockForward = (minutes) => {
+    now = futureDate(now, minutes * MILLISECONDS_PER_MINUTE);
+    fakeNow(now);
+  }
+
+  // Keep track of the ping reasons we're expecting in this test.
+  let expectedReasons = [];
+
+  // Start and shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 1,
+  // subsessionCounter: 1, subsessionId: A,  and previousSubsessionId: null to be archived.
+  yield TelemetrySession.reset();
+  yield TelemetrySession.shutdown();
+  expectedReasons.push(REASON_SHUTDOWN);
+
+  // Start Telemetry but don't wait for it to initialise before shutting down. We expect a
+  // shutdown ping with profileSubsessionCounter: 2, subsessionCounter: 1, subsessionId: B
+  // and previousSubsessionId: A to be archived.
+  moveClockForward(30);
+  TelemetrySession.reset();
+  yield TelemetrySession.shutdown();
+  expectedReasons.push(REASON_SHUTDOWN);
+
+  // Start Telemetry and simulate an aborted-session ping. We expect an aborted-session ping
+  // with profileSubsessionCounter: 3, subsessionCounter: 1, subsessionId: C and
+  // previousSubsessionId: B to be archived.
+  let schedulerTickCallback = null;
+  fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
+  yield TelemetrySession.reset();
+  moveClockForward(6);
+  // Trigger the an aborted session ping save. When testing,we are not saving the aborted-session
+  // ping as soon as Telemetry starts, otherwise we would end up with unexpected pings being
+  // sent when calling |TelemetrySession.reset()|, thus breaking some tests.
+  Assert.ok(!!schedulerTickCallback);
+  yield schedulerTickCallback();
+  expectedReasons.push(REASON_ABORTED_SESSION);
+
+  // Start Telemetry and trigger an environment change through a pref modification. We expect
+  // an environment-change ping with profileSubsessionCounter: 4, subsessionCounter: 1,
+  // subsessionId: D and previousSubsessionId: C to be archived.
+  moveClockForward(30);
+  yield TelemetryController.reset();
+  yield TelemetrySession.reset();
+  TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH);
+  moveClockForward(30);
+  Preferences.set(PREF_TEST, 1);
+  expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+  // Shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 5,
+  // subsessionCounter: 2, subsessionId: E and previousSubsessionId: D to be archived.
+  moveClockForward(30);
+  yield TelemetrySession.shutdown();
+  expectedReasons.push(REASON_SHUTDOWN);
+
+  // Start Telemetry and trigger a daily ping. We expect a daily ping with
+  // profileSubsessionCounter: 6, subsessionCounter: 1, subsessionId: F and
+  // previousSubsessionId: E to be archived.
+  moveClockForward(30);
+  yield TelemetrySession.reset();
+
+  // Delay the callback around midnight.
+  now = fakeNow(futureDate(now, MS_IN_ONE_DAY));
+  // Trigger the daily ping.
+  yield schedulerTickCallback();
+  expectedReasons.push(REASON_DAILY);
+
+  // Trigger an environment change ping. We expect an environment-changed ping with
+  // profileSubsessionCounter: 7, subsessionCounter: 2, subsessionId: G and
+  // previousSubsessionId: F to be archived.
+  moveClockForward(30);
+  Preferences.set(PREF_TEST, 0);
+  expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+  // Shut down Telemetry and trigger a shutdown ping.
+  moveClockForward(30);
+  yield TelemetrySession.shutdown();
+  expectedReasons.push(REASON_SHUTDOWN);
+
+  // Start Telemetry and trigger an environment change.
+  yield TelemetrySession.reset();
+  TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH);
+  moveClockForward(30);
+  Preferences.set(PREF_TEST, 1);
+  expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+  // Don't shut down, instead trigger an aborted-session ping.
+  moveClockForward(6);
+  // Trigger the an aborted session ping save.
+  yield schedulerTickCallback();
+  expectedReasons.push(REASON_ABORTED_SESSION);
+
+  // Start Telemetry and trigger a daily ping.
+  moveClockForward(30);
+  yield TelemetryController.reset();
+  yield TelemetrySession.reset();
+  // Delay the callback around midnight.
+  now = futureDate(now, MS_IN_ONE_DAY);
+  fakeNow(now);
+  // Trigger the daily ping.
+  yield schedulerTickCallback();
+  expectedReasons.push(REASON_DAILY);
+
+  // Trigger an environment change.
+  moveClockForward(30);
+  Preferences.set(PREF_TEST, 0);
+  expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+  // And an aborted-session ping again.
+  moveClockForward(6);
+  // Trigger the an aborted session ping save.
+  yield schedulerTickCallback();
+  expectedReasons.push(REASON_ABORTED_SESSION);
+
+  // Make sure the aborted-session ping gets archived.
+  yield TelemetryController.reset();
+  yield TelemetrySession.reset();
+
+  yield promiseValidateArchivedPings(expectedReasons);
+});
+
+add_task(function* () {
+  do_test_finished();
+});
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -1162,17 +1162,17 @@ add_task(function* test_savedPingsOnShut
 add_task(function* test_savedSessionData() {
   // Create the directory which will contain the data file, if it doesn't already
   // exist.
   yield OS.File.makeDir(DATAREPORTING_PATH);
 
   // Write test data to the session data file.
   const dataFilePath = OS.Path.join(DATAREPORTING_PATH, "session-state.json");
   const sessionState = {
-    previousSubsessionId: null,
+    subsessionId: null,
     profileSubsessionCounter: 3785,
   };
   yield CommonUtils.writeJSON(sessionState, dataFilePath);
 
   const PREF_TEST = "toolkit.telemetry.test.pref1";
   Preferences.reset(PREF_TEST);
   const PREFS_TO_WATCH = new Map([
     [PREF_TEST, TelemetryEnvironment.RECORD_PREF_VALUE],
@@ -1204,17 +1204,50 @@ add_task(function* test_savedSessionData
 
   let payload = TelemetrySession.getPayload();
   Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
   yield TelemetrySession.shutdown();
 
   // Load back the serialised session data.
   let data = yield CommonUtils.readJSON(dataFilePath);
   Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
-  Assert.equal(data.previousSubsessionId, expectedUUID);
+  Assert.equal(data.subsessionId, expectedUUID);
+});
+
+add_task(function* test_sessionData_ShortSession() {
+  if (gIsAndroid) {
+    // We don't support subsessions yet on Android, so skip the next checks.
+    return;
+  }
+
+  const SESSION_STATE_PATH = OS.Path.join(DATAREPORTING_PATH, "session-state.json");
+
+  // Shut down Telemetry and remove the session state file.
+  yield TelemetrySession.shutdown();
+  yield OS.File.remove(SESSION_STATE_PATH, { ignoreAbsent: true });
+
+  const expectedUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
+  fakeGenerateUUID(generateUUID, () => expectedUUID);
+
+  // We intentionally don't wait for the setup to complete and shut down to simulate
+  // short sessions. We expect the profile subsession counter to be 1.
+  TelemetrySession.reset();
+  yield TelemetrySession.shutdown();
+
+  // Restore the UUID generation functions.
+  fakeGenerateUUID(generateUUID, generateUUID);
+
+  // Start TelemetrySession so that it loads the session data file. We expect the profile
+  // subsession counter to be incremented by 1 again.
+  yield TelemetrySession.reset();
+
+  // We expect 2 profile subsession counter updates.
+  let payload = TelemetrySession.getPayload();
+  Assert.equal(payload.info.profileSubsessionCounter, 2);
+  Assert.equal(payload.info.previousSubsessionId, expectedUUID);
 });
 
 add_task(function* test_invalidSessionData() {
   // Create the directory which will contain the data file, if it doesn't already
   // exist.
   yield OS.File.makeDir(DATAREPORTING_PATH);
 
   // Write test data to the session data file.
@@ -1233,17 +1266,17 @@ add_task(function* test_invalidSessionDa
   yield TelemetrySession.reset();
   let payload = TelemetrySession.getPayload();
   Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
   yield TelemetrySession.shutdown();
 
   // Load back the serialised session data.
   let data = yield CommonUtils.readJSON(dataFilePath);
   Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
-  Assert.equal(data.previousSubsessionId, null);
+  Assert.equal(data.subsessionId, expectedUUID);
 });
 
 add_task(function* test_abortedSession() {
   if (gIsAndroid || gIsGonk) {
     // We don't have the aborted session ping here.
     return;
   }
 
--- a/toolkit/components/telemetry/tests/unit/xpcshell.ini
+++ b/toolkit/components/telemetry/tests/unit/xpcshell.ini
@@ -15,16 +15,17 @@ generated-files =
   dictionary.xpi
   experiment.xpi
   extension.xpi
   extension-2.xpi
   restartless.xpi
   theme.xpi
 
 [test_nsITelemetry.js]
+[test_SubsessionChaining.js]
 [test_TelemetryEnvironment.js]
 # Bug 1144395: crash on Android 4.3
 skip-if = android_version == "18"
 [test_PingAPI.js]
 [test_TelemetryFlagClear.js]
 [test_TelemetryLateWrites.js]
 [test_TelemetryLockCount.js]
 [test_TelemetryLog.js]
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -2880,37 +2880,85 @@ var WalkerActor = protocol.ActorClass({
       objectActorID: Arg(0, "string")
     },
     response: {
       nodeFront: RetVal("nullable:disconnectedNode")
     }
   }),
 
   /**
-   * Given an StyleSheetActor (identified by its ID), commonly used in the
+   * Given a StyleSheetActor (identified by its ID), commonly used in the
    * style-editor, get its ownerNode and return the corresponding walker's
-   * NodeActor
+   * NodeActor.
+   * Note that getNodeFromActor was added later and can now be used instead.
    */
   getStyleSheetOwnerNode: method(function(styleSheetActorID) {
-    let styleSheetActor = this.conn.getActor(styleSheetActorID);
-    let ownerNode = styleSheetActor.ownerNode;
-
-    if (!styleSheetActor || !ownerNode) {
-      return null;
-    }
-
-    return this.attachElement(ownerNode);
+    return this.getNodeFromActor(styleSheetActorID, ["ownerNode"]);
   }, {
     request: {
       styleSheetActorID: Arg(0, "string")
     },
     response: {
       ownerNode: RetVal("nullable:disconnectedNode")
     }
   }),
+
+  /**
+   * This method can be used to retrieve NodeActor for DOM nodes from other
+   * actors in a way that they can later be highlighted in the page, or
+   * selected in the inspector.
+   * If an actor has a reference to a DOM node, and the UI needs to know about
+   * this DOM node (and possibly select it in the inspector), the UI should
+   * first retrieve a reference to the walkerFront:
+   *
+   * // Make sure the inspector/walker have been initialized first.
+   * toolbox.initInspector().then(() => {
+   *  // Retrieve the walker.
+   *  let walker = toolbox.walker;
+   * });
+   *
+   * And then call this method:
+   *
+   * // Get the nodeFront from my actor, passing the ID and properties path.
+   * walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => {
+   *   // Use the nodeFront, e.g. select the node in the inspector.
+   *   toolbox.getPanel("inspector").selection.setNodeFront(nodeFront);
+   * });
+   *
+   * @param {String} actorID The ID for the actor that has a reference to the
+   * DOM node.
+   * @param {Array} path Where, on the actor, is the DOM node stored. If in the
+   * scope of the actor, the node is available as `this.data.node`, then this
+   * should be ["data", "node"].
+   * @return {NodeActor} The attached NodeActor, or null if it couldn't be found.
+   */
+  getNodeFromActor: method(function(actorID, path) {
+    let actor = this.conn.getActor(actorID);
+    if (!actor) {
+      return null;
+    }
+
+    let obj = actor;
+    for (let name of path) {
+      if (!(name in obj)) {
+        return null;
+      }
+      obj = obj[name];
+    }
+
+    return this.attachElement(obj);
+  }, {
+    request: {
+      actorID: Arg(0, "string"),
+      path: Arg(1, "array:string")
+    },
+    response: {
+      node: RetVal("nullable:disconnectedNode")
+    }
+  })
 });
 
 /**
  * Client side of the DOM walker.
  */
 var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
   // Set to true if cleanup should be requested after every mutation list.
   autoCleanup: true,
@@ -3058,16 +3106,24 @@ var WalkerFront = exports.WalkerFront = 
   getStyleSheetOwnerNode: protocol.custom(function(styleSheetActorID) {
     return this._getStyleSheetOwnerNode(styleSheetActorID).then(response => {
       return response ? response.node : null;
     });
   }, {
     impl: "_getStyleSheetOwnerNode"
   }),
 
+  getNodeFromActor: protocol.custom(function(actorID, path) {
+    return this._getNodeFromActor(actorID, path).then(response => {
+      return response ? response.node : null;
+    });
+  }, {
+    impl: "_getNodeFromActor"
+  }),
+
   _releaseFront: function(node, force) {
     if (node.retained && !force) {
       node.reparent(null);
       this._retainedOrphans.add(node);
       return;
     }
 
     if (node.retained) {
--- a/toolkit/devtools/server/actors/profiler.js
+++ b/toolkit/devtools/server/actors/profiler.js
@@ -54,16 +54,26 @@ ProfilerActor.prototype = {
    * Returns an array of feature strings, describing the profiler features
    * that are available on this platform. Can be called while the profiler
    * is stopped.
    */
   onGetFeatures: function() {
     return { features: nsIProfilerModule.GetFeatures([]) };
   },
 
+  onGetBufferInfo: function(request) {
+    let position = {}, totalSize = {}, generation = {};
+    nsIProfilerModule.GetBufferInfo(position, totalSize, generation);
+    return {
+      position: position.value,
+      totalSize: totalSize.value,
+      generation: generation.value
+    }
+  },
+
   /**
    * Returns the configuration used that was originally passed in to start up the
    * profiler. Used for tests, and does not account for others using nsIProfiler.
    */
   onGetStartOptions: function() {
     return this._profilerStartOptions || {};
   },
 
@@ -316,16 +326,17 @@ function checkProfilerConsumers() {
 
 /**
  * The request types this actor can handle.
  * At the moment there are two known users of the Profiler actor:
  * the devtools and the Gecko Profiler addon, which uses the debugger
  * protocol to get profiles from Fennec.
  */
 ProfilerActor.prototype.requestTypes = {
+  "getBufferInfo": ProfilerActor.prototype.onGetBufferInfo,
   "getFeatures": ProfilerActor.prototype.onGetFeatures,
   "startProfiler": ProfilerActor.prototype.onStartProfiler,
   "stopProfiler": ProfilerActor.prototype.onStopProfiler,
   "isActive": ProfilerActor.prototype.onIsActive,
   "getSharedLibraryInformation": ProfilerActor.prototype.onGetSharedLibraryInformation,
   "getProfile": ProfilerActor.prototype.onGetProfile,
   "registerEventNotifications": ProfilerActor.prototype.onRegisterEventNotifications,
   "unregisterEventNotifications": ProfilerActor.prototype.onUnregisterEventNotifications,
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -167,16 +167,19 @@ RootActor.prototype = {
     // no longer expose tab actors, but also that getProcess forbids
     // exposing actors for security reasons
     get allowChromeProcess() {
       return DebuggerServer.allowChromeProcess;
     },
     // Whether or not `getProfile()` supports specifying a `startTime`
     // and `endTime` to filter out samples. Fx40+
     profilerDataFilterable: true,
+    // Whether or not the profiler has a `getBufferInfo` method
+    // necessary as the profiler does not use the ActorFront class.
+    profilerBufferStatus: true,
   },
 
   /**
    * Return a 'hello' packet as specified by the Remote Debugging Protocol.
    */
   sayHello: function() {
     return {
       from: this.actorID,
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini
+++ b/toolkit/devtools/server/tests/mochitest/chrome.ini
@@ -48,16 +48,17 @@ skip-if = buildapp == 'mulet'
 [test_getProcess.html]
 skip-if = buildapp == 'mulet'
 [test_inspector-anonymous.html]
 [test_inspector-changeattrs.html]
 [test_inspector-changevalue.html]
 [test_inspector-dead-nodes.html]
 [test_inspector_getImageData.html]
 skip-if = buildapp == 'mulet'
+[test_inspector_getNodeFromActor.html]
 [test_inspector-hide.html]
 [test_inspector-insert.html]
 [test_inspector-mutations-attr.html]
 [test_inspector-mutations-childlist.html]
 [test_inspector-mutations-frameload.html]
 [test_inspector-mutations-value.html]
 [test_inspector-pseudoclass-lock.html]
 [test_inspector-release.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_inspector_getNodeFromActor.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1155653
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1155653</title>
+
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
+  <script type="application/javascript;version=1.8">
+Components.utils.import("resource://gre/modules/devtools/Loader.jsm");
+const inspector = devtools.require("devtools/server/actors/inspector");
+
+window.onload = function() {
+  SimpleTest.waitForExplicitFinish();
+  runNextTest();
+}
+
+var gWalker;
+
+addTest(function() {
+  let url = document.getElementById("inspectorContent").href;
+  attachURL(url, function(err, client, tab, doc) {
+    let {InspectorFront} = devtools.require("devtools/server/actors/inspector");
+    let inspector = InspectorFront(client, tab);
+
+    promiseDone(inspector.getWalker().then(walker => {
+      gWalker = walker;
+    }).then(runNextTest));
+  });
+});
+
+addTest(function() {
+  info("Try to get a NodeFront from an invalid actorID");
+  gWalker.getNodeFromActor("invalid", ["node"]).then(node => {
+    ok(!node, "The node returned is null");
+    runNextTest();
+  });
+});
+
+addTest(function() {
+  info("Try to get a NodeFront from a valid actorID but invalid path");
+  gWalker.getNodeFromActor(gWalker.actorID, ["invalid", "path"]).then(node => {
+    ok(!node, "The node returned is null");
+    runNextTest();
+  });
+});
+
+addTest(function() {
+  info("Try to get a NodeFront from a valid actorID and valid path");
+  gWalker.getNodeFromActor(gWalker.actorID, ["rootDoc"]).then(rootDocNode => {
+    ok(rootDocNode, "A node was returned");
+    is(rootDocNode, gWalker.rootNode, "The right node was returned");
+    runNextTest();
+  });
+});
+
+addTest(function() {
+  info("Try to get a NodeFront from a valid actorID and valid complex path");
+  gWalker.getNodeFromActor(gWalker.actorID,
+    ["tabActor", "window", "document", "body"]).then(bodyNode => {
+    ok(bodyNode, "A node was returned");
+    gWalker.querySelector(gWalker.rootNode, "body").then(node => {
+      is(bodyNode, node, "The body node was returned");
+      runNextTest();
+    });
+  });
+});
+
+addTest(function() {
+  gWalker = null;
+  runNextTest();
+});
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1155653">Mozilla Bug 1155653</a>
+<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_profiler_getbufferinfo.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the profiler actor returns its buffer status via getBufferInfo.
+ */
+
+const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
+const INITIAL_WAIT_TIME = 100; // ms
+const MAX_WAIT_TIME = 20000; // ms
+const MAX_PROFILER_ENTRIES = 10000000;
+
+function run_test()
+{
+  get_chrome_actors((client, form) => {
+    let actor = form.profilerActor;
+    activate_profiler(client, actor, startTime => {
+      wait_for_samples(client, actor, () => {
+        check_buffer(client, actor, () => {
+          deactivate_profiler(client, actor, () => {
+            client.close(do_test_finished);
+          });
+        });
+      });
+    });
+  })
+
+  do_test_pending();
+}
+
+function check_buffer(client, actor, callback)
+{
+  client.request({ to: actor, type: "getBufferInfo" }, response => {
+    do_check_true(typeof response.position === "number");
+    do_check_true(typeof response.totalSize === "number");
+    do_check_true(typeof response.generation === "number");
+    do_check_true(response.position > 0 && response.position < response.totalSize);
+    do_check_true(response.totalSize === MAX_PROFILER_ENTRIES);
+    // There's no way we'll fill the buffer in this test.
+    do_check_true(response.generation === 0);
+
+    callback();
+  });
+}
+
+function activate_profiler(client, actor, callback)
+{
+  client.request({ to: actor, type: "startProfiler", entries: MAX_PROFILER_ENTRIES }, response => {
+    do_check_true(response.started);
+    client.request({ to: actor, type: "isActive" }, response => {
+      do_check_true(response.isActive);
+      callback(response.currentTime);
+    });
+  });
+}
+
+function deactivate_profiler(client, actor, callback)
+{
+  client.request({ to: actor, type: "stopProfiler" }, response => {
+    do_check_false(response.started);
+    client.request({ to: actor, type: "isActive" }, response => {
+      do_check_false(response.isActive);
+      callback();
+    });
+  });
+}
+
+function wait_for_samples(client, actor, callback)
+{
+  function attempt(delay)
+  {
+    // No idea why, but Components.stack.sourceLine returns null.
+    let funcLine = Components.stack.lineNumber - 3;
+
+    // Spin for the requested time, then take a sample.
+    let start = Date.now();
+    let stack;
+    do_print("Attempt: delay = " + delay);
+    while (Date.now() - start < delay) { stack = Components.stack; }
+    do_print("Attempt: finished waiting.");
+
+    client.request({ to: actor, type: "getProfile" }, response => {
+      // At this point, we may or may not have samples, depending on
+      // whether the spin loop above has given the profiler enough time
+      // to get started.
+      if (response.profile.threads[0].samples.length == 0) {
+        if (delay < MAX_WAIT_TIME) {
+          // Double the spin-wait time and try again.
+          do_print("Attempt: no samples, going around again.");
+          return attempt(delay * 2);
+        } else {
+          // We've waited long enough, so just fail.
+          do_print("Attempt: waited a long time, but no samples were collected.");
+          do_print("Giving up.");
+          do_check_true(false);
+          return;
+        }
+      }
+      callback();
+    });
+  }
+
+  // Start off with a 100 millisecond delay.
+  attempt(INITIAL_WAIT_TIME);
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -211,16 +211,17 @@ skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_breakpoint-actor-map.js]
 [test_profiler_activation-01.js]
 [test_profiler_activation-02.js]
 [test_profiler_close.js]
 [test_profiler_data.js]
 [test_profiler_events-01.js]
 [test_profiler_events-02.js]
+[test_profiler_getbufferinfo.js]
 [test_profiler_getfeatures.js]
 [test_profiler_getsharedlibraryinformation.js]
 [test_unsafeDereference.js]
 [test_add_actors.js]
 [test_trace_actor-01.js]
 [test_trace_actor-02.js]
 [test_trace_actor-03.js]
 [test_trace_actor-04.js]
--- a/toolkit/themes/shared/extensions/extensions.inc.css
+++ b/toolkit/themes/shared/extensions/extensions.inc.css
@@ -960,43 +960,39 @@ setting[type="radio"] > radiogroup {
   display: block !important;
 }
 
 button.button-link {
   -moz-appearance: none;
   background: transparent;
   border: none;
   box-shadow: none;
-  text-decoration: underline;
   color: #0095dd;
   cursor: pointer;
   min-width: 0;
   height: 20px;
   margin: 0 6px;
 }
 
 button.button-link:not(:-moz-focusring) > .button-box {
   border-width: 0;
   margin: 1px;
 }
 
-.text-link {
-  color: #0095dd;
-  font-size: inherit;
-}
-
-button.button-link:hover,
-.text-link:hover {
-  color: #4cb1ff;
+button.button-link:hover {
   background-color: transparent;
+  color: #178ce5;
+  text-decoration: underline;
 }
 
 /* Needed to override normal button style from inContent.css */
 button.button-link:not([disabled="true"]):active:hover {
   background-color: transparent;
+  color: #ff9500;
+  text-decoration: none;
 }
 
 
 /*** telemetry experiments ***/
 
 #detail-experiment-container {
   font-size: 80%;
   margin-bottom: 1em;