Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Tue, 07 Apr 2015 16:20:54 -0400
changeset 267829 078128c2600a0c695f8a16bf561b171a523daedd
parent 267806 c13b6926e6eb6779c010a6119fc1c95b3f708ce7 (current diff)
parent 267828 ec70706deeec92c814353edb895258c91f20dcf0 (diff)
child 267837 105106bde9362e6087391f83cf7fbb90801fd957
child 267950 bfdc7e03426d51fe1816d8bf652a54455ccdd11b
child 267972 7bcd3974f95b3c86e322a603acb404801cf88b78
push id4830
push userjlund@mozilla.com
push dateMon, 29 Jun 2015 20:18:48 +0000
treeherdermozilla-beta@4c2175bb0420 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone40.0a1
first release with
nightly linux32
078128c2600a / 40.0a1 / 20150408030205 / files
nightly linux64
078128c2600a / 40.0a1 / 20150408030205 / files
nightly mac
078128c2600a / 40.0a1 / 20150408030205 / files
nightly win32
078128c2600a / 40.0a1 / 20150408030205 / files
nightly win64
078128c2600a / 40.0a1 / 20150408030205 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c. a=merge
browser/devtools/shared/widgets/FlameGraph.jsm
browser/themes/linux/theme-switcher-icon.png
browser/themes/osx/theme-switcher-icon.png
browser/themes/osx/theme-switcher-icon@2x.png
browser/themes/windows/theme-switcher-icon-aero.png
browser/themes/windows/theme-switcher-icon.png
services/datareporting/sessions.jsm
services/datareporting/tests/xpcshell/test_session_recorder.js
toolkit/modules/moz.build
toolkit/themes/shared/reader/RM-Close-hover-24x24.svg
--- a/browser/components/preferences/in-content/content.xul
+++ b/browser/components/preferences/in-content/content.xul
@@ -82,56 +82,70 @@
       </row>
     </rows>
   </grid>
 </groupbox>
 
 <!-- Fonts and Colors -->
 <groupbox id="fontsGroup" data-category="paneContent" hidden="true">
   <caption><label>&fontsAndColors.label;</label></caption>
-  <hbox align="center">
-    <label control="defaultFont" accesskey="&defaultFont.accesskey;">&defaultFont.label;</label>
-    <menulist id="defaultFont" flex="1"/>
-    <label id="defaultFontSizeLabel" control="defaultFontSize" accesskey="&defaultSize.accesskey;">&defaultSize.label;</label>
-    <menulist id="defaultFontSize">
-      <menupopup>
-        <menuitem value="9" label="9"/>
-        <menuitem value="10" label="10"/>
-        <menuitem value="11" label="11"/>
-        <menuitem value="12" label="12"/>
-        <menuitem value="13" label="13"/>
-        <menuitem value="14" label="14"/>
-        <menuitem value="15" label="15"/>
-        <menuitem value="16" label="16"/>
-        <menuitem value="17" label="17"/>
-        <menuitem value="18" label="18"/>
-        <menuitem value="20" label="20"/>
-        <menuitem value="22" label="22"/>
-        <menuitem value="24" label="24"/>
-        <menuitem value="26" label="26"/>
-        <menuitem value="28" label="28"/>
-        <menuitem value="30" label="30"/>
-        <menuitem value="32" label="32"/>
-        <menuitem value="34" label="34"/>
-        <menuitem value="36" label="36"/>
-        <menuitem value="40" label="40"/>
-        <menuitem value="44" label="44"/>
-        <menuitem value="48" label="48"/>
-        <menuitem value="56" label="56"/>
-        <menuitem value="64" label="64"/>
-        <menuitem value="72" label="72"/>
-      </menupopup>
-    </menulist>
-    <button id="advancedFonts" icon="select-font"
-            label="&advancedFonts.label;"
-            accesskey="&advancedFonts.accesskey;"/>
-    <button id="colors" icon="select-color"
-            label="&colors.label;"
-            accesskey="&colors.accesskey;"/>
-  </hbox>
+
+  <grid id="fontsGrid">
+    <columns>
+      <column flex="1"/>
+      <column/>
+    </columns>
+    <rows id="fontsRows">
+      <row id="fontRow">
+        <hbox align="center">
+          <label control="defaultFont" accesskey="&defaultFont.accesskey;">&defaultFont.label;</label>
+          <menulist id="defaultFont" />
+          <label id="defaultFontSizeLabel" control="defaultFontSize" accesskey="&defaultSize.accesskey;">&defaultSize.label;</label>
+          <menulist id="defaultFontSize">
+            <menupopup>
+              <menuitem value="9" label="9"/>
+              <menuitem value="10" label="10"/>
+              <menuitem value="11" label="11"/>
+              <menuitem value="12" label="12"/>
+              <menuitem value="13" label="13"/>
+              <menuitem value="14" label="14"/>
+              <menuitem value="15" label="15"/>
+              <menuitem value="16" label="16"/>
+              <menuitem value="17" label="17"/>
+              <menuitem value="18" label="18"/>
+              <menuitem value="20" label="20"/>
+              <menuitem value="22" label="22"/>
+              <menuitem value="24" label="24"/>
+              <menuitem value="26" label="26"/>
+              <menuitem value="28" label="28"/>
+              <menuitem value="30" label="30"/>
+              <menuitem value="32" label="32"/>
+              <menuitem value="34" label="34"/>
+              <menuitem value="36" label="36"/>
+              <menuitem value="40" label="40"/>
+              <menuitem value="44" label="44"/>
+              <menuitem value="48" label="48"/>
+              <menuitem value="56" label="56"/>
+              <menuitem value="64" label="64"/>
+              <menuitem value="72" label="72"/>
+            </menupopup>
+          </menulist>
+        </hbox>
+        <button id="advancedFonts" icon="select-font"
+                label="&advancedFonts.label;"
+                accesskey="&advancedFonts.accesskey;"/>
+      </row>
+      <row id="colorsRow">
+        <hbox/>
+        <button id="colors" icon="select-color"
+                label="&colors.label;"
+                accesskey="&colors.accesskey;"/>
+      </row>
+    </rows>
+  </grid>
 </groupbox>
 
 <!-- Languages -->
 <groupbox id="languagesGroup" data-category="paneContent" hidden="true">
   <caption><label>&languages.label;</label></caption>
 
   <hbox id="languagesBox" align="center">
     <description flex="1" control="chooseLanguage">&chooseLanguage.label;</description>
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -128,30 +128,30 @@
         <menuitem label="&historyHeader.custom.label;" value="custom"/>
       </menupopup>
     </menulist>
     <label>&historyHeader.post.label;</label>
   </hbox>
   <deck id="historyPane">
     <vbox id="historyRememberPane">
       <hbox align="center" flex="1">
-        <vbox>
+        <vbox flex="1">
           <description>&rememberDescription.label;</description>
           <separator class="thin"/>
           <description>&rememberActions.pre.label;<html:a
           class="inline-link" id="historyRememberClear" href="#"
           >&rememberActions.clearHistory.label;</html:a>&rememberActions.middle.label;<html:a
           class="inline-link" id="historyRememberCookies" href="#"
           >&rememberActions.removeCookies.label;</html:a>&rememberActions.post.label;</description>
         </vbox>
       </hbox>
     </vbox>
     <vbox id="historyDontRememberPane">
       <hbox align="center" flex="1">
-        <vbox>
+        <vbox flex="1">
           <description>&dontrememberDescription.label;</description>
           <separator class="thin"/>
           <description>&dontrememberActions.pre.label;<html:a
           class="inline-link" id="historyDontRememberClear" href="#"
           >&dontrememberActions.clearHistory.label;</html:a>&dontrememberActions.post.label;</description>
         </vbox>
       </hbox>
     </vbox>
--- a/browser/components/readinglist/ServerClient.jsm
+++ b/browser/components/readinglist/ServerClient.jsm
@@ -139,18 +139,18 @@ ServerClient.prototype = {
           status: response.status,
           headers: response.headers
         };
         try {
           if (response.body) {
             result.body = JSON.parse(response.body);
           }
         } catch (e) {
-          log.info("Failed to parse JSON body |${body}|: ${e}",
-                    {body: response.body, e});
+          log.debug("Response is not JSON. First 1024 chars: |${body}|",
+                    { body: response.body.substr(0, 1024) });
           // We don't reject due to this (and don't even make a huge amount of
           // log noise - eg, a 50X error from a load balancer etc may not write
           // JSON.
         }
 
         resolve(result);
       }
       // We are assuming the body has already been decoded and thus contains
--- a/browser/components/readinglist/sidebar.js
+++ b/browser/components/readinglist/sidebar.js
@@ -464,16 +464,19 @@ let RLSidebar = {
     if (msg.topic != "UpdateActiveItem") {
       return;
     }
 
     if (!msg.url) {
       this.activeItem = null;
     } else {
       ReadingList.itemForURL(msg.url).then(item => {
-        this.activeItem = this.itemNodesById.get(item.id);
+        let node;
+        if (item && (node = this.itemNodesById.get(item.id))) {
+          this.activeItem = node;
+        }
       });
     }
   }
 };
 
 
 addEventListener("DOMContentLoaded", () => RLSidebar.init());
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -42,23 +42,23 @@ devtools.lazyRequireGetter(this, "CallVi
 devtools.lazyRequireGetter(this, "ThreadNode",
   "devtools/shared/profiler/tree-model", true);
 devtools.lazyRequireGetter(this, "FrameNode",
   "devtools/shared/profiler/tree-model", true);
 devtools.lazyRequireGetter(this, "JITOptimizations",
   "devtools/shared/profiler/jit", true);
 devtools.lazyRequireGetter(this, "OptionsView",
   "devtools/shared/options-view", true);
+devtools.lazyRequireGetter(this, "FlameGraphUtils",
+  "devtools/shared/widgets/FlameGraph", true);
+devtools.lazyRequireGetter(this, "FlameGraph",
+  "devtools/shared/widgets/FlameGraph", true);
 
 devtools.lazyImporter(this, "CanvasGraphUtils",
   "resource:///modules/devtools/Graphs.jsm");
-devtools.lazyImporter(this, "FlameGraphUtils",
-  "resource:///modules/devtools/FlameGraph.jsm");
-devtools.lazyImporter(this, "FlameGraph",
-  "resource:///modules/devtools/FlameGraph.jsm");
 devtools.lazyImporter(this, "SideMenuWidget",
   "resource:///modules/devtools/SideMenuWidget.jsm");
 devtools.lazyImporter(this, "PluralForm",
   "resource://gre/modules/PluralForm.jsm");
 
 const BRANCH_NAME = "devtools.performance.ui.";
 
 // Events emitted by various objects in the panel.
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -37,16 +37,17 @@ support-files =
 [browser_perf-front-profiler-02.js]
 [browser_perf-front-profiler-03.js]
 [browser_perf-front-profiler-04.js]
 #[browser_perf-front-profiler-05.js] bug 1077464
 #[browser_perf-front-profiler-06.js]
 [browser_perf-front-01.js]
 [browser_perf-front-02.js]
 [browser_perf-jit-view-01.js]
+[browser_perf-jit-view-02.js]
 [browser_perf-jit-model-01.js]
 [browser_perf-jit-model-02.js]
 [browser_perf-jump-to-debugger-01.js]
 [browser_perf-jump-to-debugger-02.js]
 [browser_perf-options-01.js]
 [browser_perf-options-02.js]
 [browser_perf-options-invert-call-tree-01.js]
 [browser_perf-options-invert-call-tree-02.js]
--- a/browser/devtools/performance/test/browser_perf-jit-view-01.js
+++ b/browser/devtools/performance/test/browser_perf-jit-view-01.js
@@ -25,17 +25,17 @@ function spawnTest () {
   yield startRecording(panel);
   yield stopRecording(panel);
 
   yield DetailsView.selectView("js-calltree");
 
   yield injectAndRenderProfilerData();
 
   yield checkFrame(1, [0, 1]);
-  yield checkFrame(2, [1]);
+  yield checkFrame(2, [2]);
   yield checkFrame(3);
 
   let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
   let reset = once(JITOptimizationsView, EVENTS.OPTIMIZATIONS_RESET);
   RecordingsView.selectedIndex = 0;
   yield Promise.all([select, reset]);
   ok(true, "JITOptimizations view correctly reset when switching recordings.");
 
@@ -75,88 +75,111 @@ function spawnTest () {
     let isEmpty = $("#jit-optimizations-view").classList.contains("empty");
     if (expectedOptsIndex.length === 0) {
       ok(isEmpty, "JIT Optimizations view has an empty message when selecting a frame without opt data.");
       return;
     } else {
       ok(!isEmpty, "JIT Optimizations view has no empty message.");
     }
 
+    // Get the frame info for the first opt site, since all opt sites
+    // share the same frame info
+    let frameInfo = gOpts[expectedOptsIndex[0]]._testFrameInfo;
+
+    let { $headerName, $headerLine, $headerFile } = JITOptimizationsView;
+    ok(!$headerName.hidden, "header function name should be shown");
+    ok(!$headerLine.hidden, "header line should be shown");
+    ok(!$headerFile.hidden, "header file should be shown");
+    is($headerName.textContent, frameInfo.name, "correct header function name.");
+    is($headerLine.textContent, frameInfo.line, "correct header line");
+    is($headerFile.textContent, frameInfo.file, "correct header file");
+
     // Need the value of the optimizations in its array, as its
     // an index used internally by the view to uniquely ID the opt
     for (let i of expectedOptsIndex) {
       let opt = gOpts[i];
       let { types: ionTypes, attempts } = opt;
 
       // Check attempts
       is($$(`.tree-widget-container li[data-id='["${i}","${i}-attempts"]'] .tree-widget-children .tree-widget-item`).length, attempts.length,
         `found ${attempts.length} attempts`);
 
       for (let j = 0; j < ionTypes.length; j++) {
         ok($(`.tree-widget-container li[data-id='["${i}","${i}-types","${i}-types-${j}"]']`),
           "found an ion type row");
       }
 
-      // The second optimization should display optimization failures.
+      // The second and third optimization should display optimization failures.
       let warningIcon = $(`.tree-widget-container li[data-id='["${i}"]'] .opt-icon[severity=warning]`);
-      if (i === 1) {
+      if (i === 1 || i === 2) {
         ok(warningIcon, "did find a warning icon for all strategies failing.");
       } else {
         ok(!warningIcon, "did not find a warning icon for no successful strategies");
       }
     }
   }
 }
 
 let gSamples = [{
   time: 5,
   frames: [
     { location: "(root)" },
     { location: "A (http://foo/bar/baz:12)", optsIndex: 0 },
-    { location: "B (http://foo/bar/baz:34)", optsIndex: 1 },
+    { location: "B (http://foo/bar/boo:34)", optsIndex: 2 },
     { location: "C (http://foo/bar/baz:56)" }
   ]
 }, {
   time: 5 + 1,
   frames: [
     { location: "(root)" },
     { location: "A (http://foo/bar/baz:12)" },
-    { location: "B (http://foo/bar/baz:34)" },
+    { location: "B (http://foo/bar/boo:34)" },
   ]
 }, {
   time: 5 + 1 + 2,
   frames: [
     { location: "(root)" },
     { location: "A (http://foo/bar/baz:12)", optsIndex: 1 },
-    { location: "B (http://foo/bar/baz:34)" },
+    { location: "B (http://foo/bar/boo:34)" },
   ]
 }, {
   time: 5 + 1 + 2 + 7,
   frames: [
     { location: "(root)" },
     { location: "A (http://foo/bar/baz:12)", optsIndex: 0 },
     { location: "E (http://foo/bar/baz:90)" },
     { location: "F (http://foo/bar/baz:99)" }
   ]
 }];
 
 // Array of OptimizationSites
 let gOpts = [{
+  _testFrameInfo: { name: "A", line: "12", file: "@baz" },
   line: 12,
   column: 2,
   types: [{ mirType: "Object", site: "A (http://foo/bar/bar:12)", types: [
     { keyedBy: "constructor", name: "Foo", location: "A (http://foo/bar/baz:12)" },
     { keyedBy: "primitive", location: "self-hosted" }
   ]}],
   attempts: [
     { outcome: "Failure1", strategy: "SomeGetter1" },
     { outcome: "Failure2", strategy: "SomeGetter2" },
     { outcome: "Inlined", strategy: "SomeGetter3" },
   ]
 }, {
+  _testFrameInfo: { name: "A", line: "12", file: "@baz" },
+  line: 12,
+  types: [{ mirType: "Int32", site: "Receiver" }], // use no types
+  attempts: [
+    { outcome: "Failure1", strategy: "SomeGetter1" },
+    { outcome: "Failure2", strategy: "SomeGetter2" },
+    { outcome: "Failure3", strategy: "SomeGetter3" },
+  ]
+}, {
+  _testFrameInfo: { name: "B", line: "34", file: "@boo" },
   line: 34,
   types: [{ mirType: "Int32", site: "Receiver" }], // use no types
   attempts: [
     { outcome: "Failure1", strategy: "SomeGetter1" },
     { outcome: "Failure2", strategy: "SomeGetter2" },
     { outcome: "Failure3", strategy: "SomeGetter3" },
   ]
 }];
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-jit-view-02.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the JIT Optimizations view does not display information
+ * for meta nodes when viewing "content only".
+ */
+
+let { CATEGORY_MASK } = devtools.require("devtools/shared/profiler/global");
+Services.prefs.setBoolPref(INVERT_PREF, false);
+Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false);
+
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, $, $$, window, PerformanceController } = panel.panelWin;
+  let { OverviewView, DetailsView, JITOptimizationsView, JsCallTreeView, RecordingsView } = panel.panelWin;
+
+  let profilerData = { threads: [{samples: gSamples, optimizations: gOpts}] };
+
+  is(Services.prefs.getBoolPref(JIT_PREF), false, "show JIT Optimizations pref off by default");
+
+  // Make two recordings, so we have one to switch to later, as the
+  // second one will have fake sample data
+  yield startRecording(panel);
+  yield stopRecording(panel);
+
+  yield startRecording(panel);
+  yield stopRecording(panel);
+
+  yield DetailsView.selectView("js-calltree");
+
+  yield injectAndRenderProfilerData();
+
+  Services.prefs.setBoolPref(JIT_PREF, true);
+  // Click the frame
+  let rendered = once(JITOptimizationsView, EVENTS.OPTIMIZATIONS_RENDERED);
+  mousedown(window, $$(".call-tree-item")[2]);
+  yield rendered;
+
+  ok($("#jit-optimizations-view").classList.contains("empty"),
+    "platform meta frame shows as empty");
+
+  let { $headerName, $headerLine, $headerFile } = JITOptimizationsView;
+  ok(!$headerName.hidden, "header function name should be shown");
+  ok($headerLine.hidden, "header line should be hidden");
+  ok($headerFile.hidden, "header file should be hidden");
+  is($headerName.textContent, "JIT", "correct header function name.");
+  is($headerLine.textContent, "", "correct header line (empty string).");
+  is($headerFile.textContent, "", "correct header file (empty string).");
+
+  yield teardown(panel);
+  finish();
+
+  function *injectAndRenderProfilerData() {
+    // Get current recording and inject our mock data
+    info("Injecting mock profile data");
+    let recording = PerformanceController.getCurrentRecording();
+    recording._profile = profilerData;
+
+    // Force a rerender
+    let rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
+    JsCallTreeView.render();
+    yield rendered;
+
+    Services.prefs.setBoolPref(JIT_PREF, true);
+    ok($("#jit-optimizations-view").classList.contains("empty"),
+      "JIT Optimizations view has empty message when no frames selected.");
+
+     Services.prefs.setBoolPref(JIT_PREF, false);
+  }
+}
+
+let gSamples = [{
+  time: 5,
+  frames: [
+    { location: "(root)" },
+    { location: "A (http://foo/bar/baz:12)", optsIndex: 0 }
+  ]
+}, {
+  time: 5 + 1,
+  frames: [
+    { location: "(root)" },
+    { location: "A (http://foo/bar/baz:12)", optsIndex: 0 },
+    { location: "JS", optsIndex: 1, category: CATEGORY_MASK("js") },
+  ]
+}];
+
+// Array of OptimizationSites
+let gOpts = [{
+  line: 12,
+  column: 2,
+  types: [{ mirType: "Object", site: "A (http://foo/bar/bar:12)", types: [
+    { keyedBy: "constructor", name: "Foo", location: "A (http://foo/bar/baz:12)" },
+    { keyedBy: "primitive", location: "self-hosted" }
+  ]}],
+  attempts: [
+    { outcome: "Failure1", strategy: "SomeGetter1" },
+    { outcome: "Failure2", strategy: "SomeGetter2" },
+    { outcome: "Inlined", strategy: "SomeGetter3" },
+  ]
+}, {
+  line: 22,
+  types: [{ mirType: "Int32", site: "Receiver" }], // use no types
+  attempts: [
+    { outcome: "Failure1", strategy: "SomeGetter1" },
+    { outcome: "Failure2", strategy: "SomeGetter2" },
+    { outcome: "Failure3", strategy: "SomeGetter3" },
+  ]
+}];
--- a/browser/devtools/performance/views/details-js-flamegraph.js
+++ b/browser/devtools/performance/views/details-js-flamegraph.js
@@ -19,29 +19,33 @@ let JsFlameGraphView = Heritage.extend(D
   /**
    * Sets up the view with event binding.
    */
   initialize: Task.async(function* () {
     DetailsSubview.initialize.call(this);
 
     this.graph = new FlameGraph($("#js-flamegraph-view"));
     this.graph.timelineTickUnits = L10N.getStr("graphs.ms");
+    this.graph.setTheme(PerformanceController.getTheme());
     yield this.graph.ready();
 
     this._onRangeChangeInGraph = this._onRangeChangeInGraph.bind(this);
+    this._onThemeChanged = this._onThemeChanged.bind(this);
 
+    PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
     this.graph.on("selecting", this._onRangeChangeInGraph);
   }),
 
   /**
    * Unbinds events.
    */
   destroy: Task.async(function* () {
     DetailsSubview.destroy.call(this);
 
+    PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
     this.graph.off("selecting", this._onRangeChangeInGraph);
 
     yield this.graph.destroy();
   }),
 
   /**
    * Method for handling all the set up for rendering a new flamegraph.
    *
@@ -88,10 +92,18 @@ let JsFlameGraphView = Heritage.extend(D
    */
   _onRerenderPrefChanged: function() {
     let recording = PerformanceController.getCurrentRecording();
     let profile = recording.getProfile();
     let samples = profile.threads[0].samples;
     FlameGraphUtils.removeFromCache(samples);
   },
 
+  /**
+   * Called when `devtools.theme` changes.
+   */
+  _onThemeChanged: function (_, theme) {
+    this.graph.setTheme(theme);
+    this.graph.refresh({ force: true });
+  },
+
   toString: () => "[object JsFlameGraphView]"
 });
--- a/browser/devtools/performance/views/details-memory-flamegraph.js
+++ b/browser/devtools/performance/views/details-memory-flamegraph.js
@@ -18,29 +18,33 @@ let MemoryFlameGraphView = Heritage.exte
   /**
    * Sets up the view with event binding.
    */
   initialize: Task.async(function* () {
     DetailsSubview.initialize.call(this);
 
     this.graph = new FlameGraph($("#memory-flamegraph-view"));
     this.graph.timelineTickUnits = L10N.getStr("graphs.ms");
+    this.graph.setTheme(PerformanceController.getTheme());
     yield this.graph.ready();
 
     this._onRangeChangeInGraph = this._onRangeChangeInGraph.bind(this);
+    this._onThemeChanged = this._onThemeChanged.bind(this);
 
+    PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
     this.graph.on("selecting", this._onRangeChangeInGraph);
   }),
 
   /**
    * Unbinds events.
    */
   destroy: Task.async(function* () {
     DetailsSubview.destroy.call(this);
 
+    PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
     this.graph.off("selecting", this._onRangeChangeInGraph);
 
     yield this.graph.destroy();
   }),
 
   /**
    * Method for handling all the set up for rendering a new flamegraph.
    *
@@ -86,10 +90,18 @@ let MemoryFlameGraphView = Heritage.exte
    */
   _onRerenderPrefChanged: function() {
     let recording = PerformanceController.getCurrentRecording();
     let allocations = recording.getAllocations();
     let samples = RecordingUtils.getSamplesFromAllocations(allocations);
     FlameGraphUtils.removeFromCache(samples);
   },
 
+  /**
+   * Called when `devtools.theme` changes.
+   */
+  _onThemeChanged: function (_, theme) {
+    this.graph.setTheme(theme);
+    this.graph.refresh({ force: true });
+  },
+
   toString: () => "[object MemoryFlameGraphView]"
 });
--- a/browser/devtools/performance/views/jit-optimizations.js
+++ b/browser/devtools/performance/views/jit-optimizations.js
@@ -22,16 +22,19 @@ let JITOptimizationsView = {
    * Initialization function called when the tool starts up.
    */
   initialize: function () {
     this.reset = this.reset.bind(this);
     this._onFocusFrame = this._onFocusFrame.bind(this);
     this._toggleVisibility = this._toggleVisibility.bind(this);
 
     this.el = $("#jit-optimizations-view");
+    this.$headerName = $("#jit-optimizations-header .header-function-name");
+    this.$headerFile = $("#jit-optimizations-header .header-file");
+    this.$headerLine = $("#jit-optimizations-header .header-line");
 
     this.tree = new TreeWidget($("#jit-optimizations-raw-view"), {
       sorted: false,
       emptyText: JIT_EMPTY_TEXT
     });
 
     // Start the tree by resetting.
     this.reset();
@@ -43,16 +46,17 @@ let JITOptimizationsView = {
     JsCallTreeView.on("focus", this._onFocusFrame);
   },
 
   /**
    * Destruction function called when the tool cleans up.
    */
   destroy: function () {
     this.tree = null;
+    this.$headerName = this.$headerFile = this.$headerLine = this.el = null;
     PerformanceController.off(EVENTS.RECORDING_SELECTED, this.reset);
     PerformanceController.off(EVENTS.PREF_CHANGED, this._toggleVisibility);
     JsCallTreeView.off("focus", this._onFocusFrame);
   },
 
   /**
    * Takes a FrameNode, with corresponding optimization data to be displayed
    * in the view.
@@ -120,17 +124,19 @@ let JITOptimizationsView = {
     let view = this.tree;
 
     // Set header information, even if the frame node
     // does not have any optimization data
     let frameData = frameNode.getInfo();
     this._setHeaders(frameData);
     this.clear();
 
-    if (!frameNode.hasOptimizations()) {
+    // If this frame node does not have optimizations, or if its a meta node in the
+    // case of only showing content, reset the view.
+    if (!frameNode.hasOptimizations() || frameNode.isMetaCategory) {
       this.reset();
       return;
     }
     this.el.classList.remove("empty");
 
     // An array of sorted OptimizationSites.
     let sites = frameNode.getOptimizations().getOptimizationSites();
 
@@ -330,28 +336,37 @@ let JITOptimizationsView = {
     let fileName;
 
     if (this._isLinkableURL(url)) {
       fileName = url.slice(url.lastIndexOf("/") + 1);
       node.classList.add("debugger-link");
       node.setAttribute("tooltiptext", URL_LABEL_TOOLTIP + " → " + url);
       node.addEventListener("click", () => viewSourceInDebugger(url, line));
     }
-    node.textContent = `@${fileName || url}`;
+    fileName = fileName || url || "";
+    node.textContent = fileName ? `@${fileName}` : "";
     return node;
   },
 
   /**
    * Updates the headers with the current frame's data.
    */
 
   _setHeaders: function (frameData) {
-    $("#jit-optimizations-header .header-function-name").textContent = frameData.functionName;
-    this._createDebuggerLinkNode(frameData.url, frameData.line, $("#jit-optimizations-header .header-file"));
-    $("#jit-optimizations-header .header-line").textContent = frameData.line;
+    let isMeta = frameData.isMetaCategory;
+    let name = isMeta ? frameData.categoryData.label : frameData.functionName;
+    let url = isMeta ? "" : frameData.url;
+    let line = isMeta ? "" : frameData.line;
+
+    this.$headerName.textContent = name;
+    this.$headerLine.textContent = line;
+    this._createDebuggerLinkNode(url, line, this.$headerFile);
+
+    this.$headerLine.hidden = isMeta;
+    this.$headerFile.hidden = isMeta;
   },
 
   /**
    * Takes a string and returns a boolean indicating whether or not
    * this is a valid url for linking to the debugger.
    *
    * @param {String} url
    * @return {Boolean}
--- a/browser/devtools/shared/moz.build
+++ b/browser/devtools/shared/moz.build
@@ -16,17 +16,16 @@ EXTRA_JS_MODULES.devtools += [
     'Parser.jsm',
     'SplitView.jsm',
 ]
 
 EXTRA_JS_MODULES.devtools += [
     'widgets/AbstractTreeItem.jsm',
     'widgets/BreadcrumbsWidget.jsm',
     'widgets/Chart.jsm',
-    'widgets/FlameGraph.jsm',
     'widgets/Graphs.jsm',
     'widgets/GraphsWorker.js',
     'widgets/SideMenuWidget.jsm',
     'widgets/SimpleListWidget.jsm',
     'widgets/VariablesView.jsm',
     'widgets/VariablesViewController.jsm',
     'widgets/ViewHelpers.jsm',
 ]
@@ -60,13 +59,14 @@ EXTRA_JS_MODULES.devtools.shared += [
     'theme.js',
     'undo.js',
 ]
 
 EXTRA_JS_MODULES.devtools.shared.widgets += [
     'widgets/CubicBezierPresets.js',
     'widgets/CubicBezierWidget.js',
     'widgets/FastListWidget.js',
+    'widgets/FlameGraph.js',
     'widgets/Spectrum.js',
     'widgets/TableWidget.js',
     'widgets/Tooltip.js',
     'widgets/TreeWidget.js',
 ]
--- a/browser/devtools/shared/test/browser_flame-graph-01.js
+++ b/browser/devtools/shared/test/browser_flame-graph-01.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that flame graph widget works properly.
 
-let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {FlameGraph} = devtools.require("devtools/shared/widgets/FlameGraph");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_flame-graph-02.js
+++ b/browser/devtools/shared/test/browser_flame-graph-02.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that flame graph widgets may have a fixed width or height.
 
-let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {FlameGraph} = devtools.require("devtools/shared/widgets/FlameGraph");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_flame-graph-03a.js
+++ b/browser/devtools/shared/test/browser_flame-graph-03a.js
@@ -3,17 +3,17 @@
 
 // Tests that selections in the flame graph widget work properly.
 
 let TEST_DATA = [{ color: "#f00", blocks: [{ x: 0, y: 0, width: 50, height: 20, text: "FOO" }, { x: 50, y: 0, width: 100, height: 20, text: "BAR" }] }, { color: "#00f", blocks: [{ x: 0, y: 30, width: 30, height: 20, text: "BAZ" }] }];
 let TEST_BOUNDS = { startTime: 0, endTime: 150 };
 let TEST_WIDTH = 200;
 let TEST_HEIGHT = 100;
 
-let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {FlameGraph} = devtools.require("devtools/shared/widgets/FlameGraph");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_flame-graph-03b.js
+++ b/browser/devtools/shared/test/browser_flame-graph-03b.js
@@ -4,17 +4,17 @@
 // Tests that selections in the flame graph widget work properly on HiDPI.
 
 let TEST_DATA = [{ color: "#f00", blocks: [{ x: 0, y: 0, width: 50, height: 20, text: "FOO" }, { x: 50, y: 0, width: 100, height: 20, text: "BAR" }] }, { color: "#00f", blocks: [{ x: 0, y: 30, width: 30, height: 20, text: "BAZ" }] }];
 let TEST_BOUNDS = { startTime: 0, endTime: 150 };
 let TEST_WIDTH = 200;
 let TEST_HEIGHT = 100;
 let TEST_DPI_DENSITIY = 2;
 
-let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {FlameGraph} = devtools.require("devtools/shared/widgets/FlameGraph");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_flame-graph-03c.js
+++ b/browser/devtools/shared/test/browser_flame-graph-03c.js
@@ -4,17 +4,17 @@
 // Tests that vertical panning in the flame graph widget works properly.
 
 let TEST_DATA = [{ color: "#f00", blocks: [{ x: 0, y: 0, width: 50, height: 20, text: "FOO" }, { x: 50, y: 0, width: 100, height: 20, text: "BAR" }] }, { color: "#00f", blocks: [{ x: 0, y: 30, width: 30, height: 20, text: "BAZ" }] }];
 let TEST_BOUNDS = { startTime: 0, endTime: 150 };
 let TEST_WIDTH = 200;
 let TEST_HEIGHT = 100;
 let TEST_DPI_DENSITIY = 2;
 
-let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {FlameGraph} = devtools.require("devtools/shared/widgets/FlameGraph");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/devtools/shared/test/browser_flame-graph-04.js
+++ b/browser/devtools/shared/test/browser_flame-graph-04.js
@@ -1,18 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that text metrics in the flame graph widget work properly.
 
 let HTML_NS = "http://www.w3.org/1999/xhtml";
-let FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 8; // px
+let FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 9; // px
 let FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = "sans-serif";
 let {ViewHelpers} = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
-let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {FlameGraph} = devtools.require("devtools/shared/widgets/FlameGraph");
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
 let L10N = new ViewHelpers.L10N();
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
--- a/browser/devtools/shared/test/browser_flame-graph-utils-01.js
+++ b/browser/devtools/shared/test/browser_flame-graph-utils-01.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that text metrics and data conversion from profiler samples
 // widget work properly in the flame graph.
 
-let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {FlameGraphUtils, FLAME_GRAPH_BLOCK_HEIGHT} = devtools.require("devtools/shared/widgets/FlameGraph");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
 function* performTest() {
@@ -106,156 +106,156 @@ let EXPECTED_OUTPUT = [{
   blocks: [{
     srcData: {
       startTime: 50,
       rawLocation: "A"
     },
     x: 50,
     y: 0,
     width: 410,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "A"
   }]
 }, {
   blocks: [{
     srcData: {
       startTime: 50,
       rawLocation: "B"
     },
     x: 50,
-    y: 11,
+    y: FLAME_GRAPH_BLOCK_HEIGHT,
     width: 160,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "B"
   }, {
     srcData: {
       startTime: 330,
       rawLocation: "B"
     },
     x: 330,
-    y: 11,
+    y: FLAME_GRAPH_BLOCK_HEIGHT,
     width: 130,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "B"
   }]
 }, {
   blocks: [{
     srcData: {
       startTime: 0,
       rawLocation: "M"
     },
     x: 0,
     y: 0,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "M"
   }, {
     srcData: {
       startTime: 50,
       rawLocation: "C"
     },
     x: 50,
-    y: 22,
+    y: FLAME_GRAPH_BLOCK_HEIGHT * 2,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "C"
   }, {
     srcData: {
       startTime: 330,
       rawLocation: "C"
     },
     x: 330,
-    y: 22,
+    y: FLAME_GRAPH_BLOCK_HEIGHT * 2,
     width: 130,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "C"
   }]
 }, {
   blocks: [{
     srcData: {
       startTime: 0,
       rawLocation: "N"
     },
     x: 0,
-    y: 11,
+    y: FLAME_GRAPH_BLOCK_HEIGHT,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "N"
   }, {
     srcData: {
       startTime: 100,
       rawLocation: "D"
     },
     x: 100,
-    y: 22,
+    y: FLAME_GRAPH_BLOCK_HEIGHT * 2,
     width: 110,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "D"
   }, {
     srcData: {
       startTime: 460,
       rawLocation: "X"
     },
     x: 460,
     y: 0,
     width: 40,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "X"
   }]
 }, {
   blocks: [{
     srcData: {
       startTime: 210,
       rawLocation: "E"
     },
     x: 210,
-    y: 11,
+    y: FLAME_GRAPH_BLOCK_HEIGHT,
     width: 120,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "E"
   }, {
     srcData: {
       startTime: 460,
       rawLocation: "Y"
     },
     x: 460,
-    y: 11,
+    y: FLAME_GRAPH_BLOCK_HEIGHT,
     width: 40,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "Y"
   }]
 }, {
   blocks: [{
     srcData: {
       startTime: 0,
       rawLocation: "P"
     },
     x: 0,
-    y: 22,
+    y: FLAME_GRAPH_BLOCK_HEIGHT * 2,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "P"
   }, {
     srcData: {
       startTime: 210,
       rawLocation: "F"
     },
     x: 210,
-    y: 22,
+    y: FLAME_GRAPH_BLOCK_HEIGHT * 2,
     width: 120,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "F"
   }, {
     srcData: {
       startTime: 460,
       rawLocation: "Z"
     },
     x: 460,
-    y: 22,
+    y: FLAME_GRAPH_BLOCK_HEIGHT * 2,
     width: 40,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "Z"
   }]
 }, {
   blocks: []
 }, {
   blocks: []
 }];
--- a/browser/devtools/shared/test/browser_flame-graph-utils-02.js
+++ b/browser/devtools/shared/test/browser_flame-graph-utils-02.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests consecutive duplicate frames are removed from the flame graph data.
 
-let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {FlameGraphUtils, FLAME_GRAPH_BLOCK_HEIGHT} = devtools.require("devtools/shared/widgets/FlameGraph");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
 function* performTest() {
@@ -68,29 +68,29 @@ let EXPECTED_OUTPUT = [{
   blocks: [{
     srcData: {
       startTime: 0,
       rawLocation: "A"
     },
     x: 0,
     y: 0,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "A"
   }]
 }, {
   blocks: [{
     srcData: {
       startTime: 0,
       rawLocation: "B"
     },
     x: 0,
-    y: 11,
+    y: FLAME_GRAPH_BLOCK_HEIGHT,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "B"
   }]
 }, {
   blocks: []
 }, {
   blocks: []
 }, {
   blocks: []
--- a/browser/devtools/shared/test/browser_flame-graph-utils-03.js
+++ b/browser/devtools/shared/test/browser_flame-graph-utils-03.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests if platform frames are removed from the flame graph data.
 
-let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {FlameGraphUtils, FLAME_GRAPH_BLOCK_HEIGHT} = devtools.require("devtools/shared/widgets/FlameGraph");
 let {FrameNode} = devtools.require("devtools/shared/profiler/tree-model");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
@@ -67,27 +67,27 @@ let EXPECTED_OUTPUT = [{
   blocks: [{
     srcData: {
       startTime: 0,
       rawLocation: "http://A"
     },
     x: 0,
     y: 0,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "http://A"
   }, {
     srcData: {
       startTime: 0,
       rawLocation: "file://C"
     },
     x: 0,
-    y: 22,
+    y: FLAME_GRAPH_BLOCK_HEIGHT * 2,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "file://C"
   }]
 }, {
   blocks: []
 }, {
   blocks: []
 }, {
   blocks: []
@@ -97,16 +97,16 @@ let EXPECTED_OUTPUT = [{
   blocks: []
 }, {
   blocks: [{
     srcData: {
       startTime: 0,
       rawLocation: "https://B"
     },
     x: 0,
-    y: 11,
+    y: FLAME_GRAPH_BLOCK_HEIGHT,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "https://B"
   }]
 }, {
   blocks: []
 }];
--- a/browser/devtools/shared/test/browser_flame-graph-utils-04.js
+++ b/browser/devtools/shared/test/browser_flame-graph-utils-04.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests if (idle) nodes are added when necessary in the flame graph data.
 
-let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {FlameGraphUtils, FLAME_GRAPH_BLOCK_HEIGHT} = devtools.require("devtools/shared/widgets/FlameGraph");
 let {FrameNode} = devtools.require("devtools/shared/profiler/tree-model");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
@@ -91,49 +91,49 @@ let EXPECTED_OUTPUT = [{
   blocks: [{
     srcData: {
       startTime: 0,
       rawLocation: "http://A"
     },
     x: 0,
     y: 0,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "http://A"
   }, {
     srcData: {
       startTime: 0,
       rawLocation: "file://C"
     },
     x: 0,
-    y: 22,
+    y: FLAME_GRAPH_BLOCK_HEIGHT * 2,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "file://C"
   }, {
     srcData: {
       startTime: 100,
       rawLocation: "http://A"
     },
     x: 100,
     y: 0,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "http://A"
   }]
 }, {
   blocks: [{
     srcData: {
       startTime: 50,
       rawLocation: "\m/"
     },
     x: 50,
     y: 0,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "\m/"
   }]
 }, {
   blocks: []
 }, {
   blocks: []
 }, {
   blocks: []
@@ -141,26 +141,26 @@ let EXPECTED_OUTPUT = [{
   blocks: []
 }, {
   blocks: [{
     srcData: {
       startTime: 0,
       rawLocation: "https://B"
     },
     x: 0,
-    y: 11,
+    y: FLAME_GRAPH_BLOCK_HEIGHT,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "https://B"
   }, {
     srcData: {
       startTime: 100,
       rawLocation: "https://B"
     },
     x: 100,
-    y: 11,
+    y: FLAME_GRAPH_BLOCK_HEIGHT,
     width: 50,
-    height: 11,
+    height: FLAME_GRAPH_BLOCK_HEIGHT,
     text: "https://B"
   }]
 }, {
   blocks: []
 }];
--- a/browser/devtools/shared/test/browser_flame-graph-utils-05.js
+++ b/browser/devtools/shared/test/browser_flame-graph-utils-05.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that flame graph data is cached, and that the cache may be cleared.
 
-let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {FlameGraphUtils} = devtools.require("devtools/shared/widgets/FlameGraph");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
 function* performTest() {
--- a/browser/devtools/shared/test/browser_flame-graph-utils-hash.js
+++ b/browser/devtools/shared/test/browser_flame-graph-utils-hash.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests if (idle) nodes are added when necessary in the flame graph data.
 
-let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {FlameGraphUtils} = devtools.require("devtools/shared/widgets/FlameGraph");
 
 let test = Task.async(function*() {
   let hash1 = FlameGraphUtils._getStringHash("abc");
   let hash2 = FlameGraphUtils._getStringHash("acb");
   let hash3 = FlameGraphUtils._getStringHash(Array.from(Array(100000)).join("a"));
   let hash4 = FlameGraphUtils._getStringHash(Array.from(Array(100000)).join("b"));
 
   isnot(hash1, hash2, "The hashes should not be equal (1).");
rename from browser/devtools/shared/widgets/FlameGraph.jsm
rename to browser/devtools/shared/widgets/FlameGraph.js
--- a/browser/devtools/shared/widgets/FlameGraph.jsm
+++ b/browser/devtools/shared/widgets/FlameGraph.js
@@ -1,25 +1,19 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-const Cu = Components.utils;
-
-Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
-Cu.import("resource:///modules/devtools/Graphs.jsm");
-const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
-const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
-const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
-
-this.EXPORTED_SYMBOLS = [
-  "FlameGraph",
-  "FlameGraphUtils"
-];
+const { ViewHelpers } = require("resource:///modules/devtools/ViewHelpers.jsm");
+const { AbstractCanvasGraph, GraphArea, GraphAreaDragger } = require("resource:///modules/devtools/Graphs.jsm");
+const { Promise } = require("resource://gre/modules/Promise.jsm");
+const { Task } = require("resource://gre/modules/Task.jsm");
+const { getColor } = require("devtools/shared/theme");
+const EventEmitter = require("devtools/toolkit/event-emitter");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
 const L10N = new ViewHelpers.L10N();
 
 const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms
 
 const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00035;
@@ -29,27 +23,24 @@ const GRAPH_MIN_SELECTION_WIDTH = 0.001;
 const GRAPH_HORIZONTAL_PAN_THRESHOLD = 10; // px
 const GRAPH_VERTICAL_PAN_THRESHOLD = 30; // px
 
 const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
 const TIMELINE_TICKS_MULTIPLE = 5; // ms
 const TIMELINE_TICKS_SPACING_MIN = 75; // px
 
 const OVERVIEW_HEADER_HEIGHT = 16; // px
-const OVERVIEW_HEADER_BACKGROUND = "rgba(255,255,255,0.7)";
-const OVERVIEW_HEADER_TEXT_COLOR = "#18191a";
 const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
 const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
 const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6; // px
 const OVERVIEW_HEADER_TEXT_PADDING_TOP = 5; // px
-const OVERVIEW_TIMELINE_STROKES = "#ddd";
+const OVERVIEW_HEADER_TIMELINE_STROKE_COLOR = "rgba(128, 128, 128, 0.5)";
 
 const FLAME_GRAPH_BLOCK_BORDER = 1; // px
-const FLAME_GRAPH_BLOCK_TEXT_COLOR = "#000";
-const FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 8; // px
+const FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 9; // px
 const FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = "sans-serif";
 const FLAME_GRAPH_BLOCK_TEXT_PADDING_TOP = 0; // px
 const FLAME_GRAPH_BLOCK_TEXT_PADDING_LEFT = 3; // px
 const FLAME_GRAPH_BLOCK_TEXT_PADDING_RIGHT = 3; // px
 
 /**
  * A flamegraph visualization. This implementation is responsable only with
  * drawing the graph, using a data source consisting of rectangles and
@@ -96,17 +87,19 @@ const FLAME_GRAPH_BLOCK_TEXT_PADDING_RIG
  *        The parent node holding the graph.
  * @param number sharpness [optional]
  *        Defaults to the current device pixel ratio.
  */
 function FlameGraph(parent, sharpness) {
   EventEmitter.decorate(this);
 
   this._parent = parent;
-  this._ready = promise.defer();
+  this._ready = Promise.defer();
+
+  this.setTheme();
 
   AbstractCanvasGraph.createIframe(GRAPH_SRC, parent, iframe => {
     this._iframe = iframe;
     this._window = iframe.contentWindow;
     this._document = iframe.contentDocument;
     this._pixelRatio = sharpness || this._window.devicePixelRatio;
 
     let container = this._container = this._document.getElementById("graph-container");
@@ -211,24 +204,16 @@ FlameGraph.prototype = {
     this._textWidthsCache = null;
 
     this._data = null;
 
     this.emit("destroyed");
   }),
 
   /**
-   * Rendering options. Subclasses should override these.
-   */
-  overviewHeaderBackgroundColor: OVERVIEW_HEADER_BACKGROUND,
-  overviewHeaderTextColor: OVERVIEW_HEADER_TEXT_COLOR,
-  overviewTimelineStrokes: OVERVIEW_TIMELINE_STROKES,
-  blockTextColor: FLAME_GRAPH_BLOCK_TEXT_COLOR,
-
-  /**
    * Makes sure the canvas graph is of the specified width or height, and
    * doesn't flex to fit all the available space.
    */
   fixedWidth: null,
   fixedHeight: null,
 
   /**
    * How much preliminar drag is necessary to determine the panning direction.
@@ -325,24 +310,29 @@ FlameGraph.prototype = {
       startTime: this._selection.start / this._pixelRatio,
       endTime: this._selection.end / this._pixelRatio,
       verticalOffset: this._verticalOffset / this._pixelRatio
     };
   },
 
   /**
    * Updates this graph to reflect the new dimensions of the parent node.
+   *
+   * @param boolean options.force
+   *        Force redraw everything.
    */
-  refresh: function() {
+  refresh: function(options={}) {
     let bounds = this._parent.getBoundingClientRect();
     let newWidth = this.fixedWidth || bounds.width;
     let newHeight = this.fixedHeight || bounds.height;
 
-    // Prevent redrawing everything if the graph's width & height won't change.
-    if (this._width == newWidth * this._pixelRatio &&
+    // Prevent redrawing everything if the graph's width & height won't change,
+    // except if force=true.
+    if (!options.force &&
+        this._width == newWidth * this._pixelRatio &&
         this._height == newHeight * this._pixelRatio) {
       this.emit("refresh-cancelled");
       return;
     }
 
     bounds.width = newWidth;
     bounds.height = newHeight;
     this._iframe.setAttribute("width", bounds.width);
@@ -350,16 +340,29 @@ FlameGraph.prototype = {
     this._width = this._canvas.width = bounds.width * this._pixelRatio;
     this._height = this._canvas.height = bounds.height * this._pixelRatio;
 
     this._shouldRedraw = true;
     this.emit("refresh");
   },
 
   /**
+   * Sets the theme via `theme` to either "light" or "dark",
+   * and updates the internal styling to match. Requires a redraw
+   * to see the effects.
+   */
+  setTheme: function (theme) {
+    theme = theme || "light";
+    this.overviewHeaderBackgroundColor = getColor("body-background", theme);
+    this.overviewHeaderTextColor = getColor("body-color", theme);
+    // Hard to get a color that is readable across both themes for the text on the flames
+    this.blockTextColor = getColor(theme === "dark" ? "selection-color" : "body-color", theme);
+  },
+
+  /**
    * The contents of this graph are redrawn only when something changed,
    * like the data source, or the selection bounds etc. This flag tracks
    * if the rendering is "dirty" and needs to be refreshed.
    */
   _shouldRedraw: false,
 
   /**
    * Animation frame callback, invoked on each tick of the refresh driver.
@@ -380,18 +383,18 @@ FlameGraph.prototype = {
     let ctx = this._ctx;
     let canvasWidth = this._width;
     let canvasHeight = this._height;
     ctx.clearRect(0, 0, canvasWidth, canvasHeight);
 
     let selection = this._selection;
     let selectionWidth = selection.end - selection.start;
     let selectionScale = canvasWidth / selectionWidth;
+    this._drawTicks(selection.start, selectionScale);
     this._drawPyramid(this._data, this._verticalOffset, selection.start, selectionScale);
-    this._drawTicks(selection.start, selectionScale);
 
     this._shouldRedraw = false;
   },
 
   /**
    * Draws the overhead ticks in this graph.
    *
    * @param number dataOffset, dataScale
@@ -411,17 +414,17 @@ FlameGraph.prototype = {
     let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
     let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
     let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
     let tickInterval = this._findOptimalTickInterval(dataScale);
 
     ctx.textBaseline = "top";
     ctx.font = fontSize + "px " + fontFamily;
     ctx.fillStyle = this.overviewHeaderTextColor;
-    ctx.strokeStyle = this.overviewTimelineStrokes;
+    ctx.strokeStyle = OVERVIEW_HEADER_TIMELINE_STROKE_COLOR;
     ctx.beginPath();
 
     for (let x = -scaledOffset % tickInterval; x < canvasWidth; x += tickInterval) {
       let lineLeft = x;
       let textLeft = lineLeft + textPaddingLeft;
       let time = Math.round((x / dataScale + dataOffset) / this._pixelRatio);
       let label = time + " " + this.timelineTickUnits;
       ctx.fillText(label, textLeft, textPaddingTop);
@@ -921,24 +924,24 @@ FlameGraph.prototype = {
    */
   _onResize: function() {
     if (this.hasData()) {
       setNamedTimeout(this._uid, GRAPH_RESIZE_EVENTS_DRAIN, this.refresh);
     }
   }
 };
 
-const FLAME_GRAPH_BLOCK_HEIGHT = 11; // px
+const FLAME_GRAPH_BLOCK_HEIGHT = 12; // px
 
 const PALLETTE_SIZE = 10;
 const PALLETTE_HUE_OFFSET = Math.random() * 90;
 const PALLETTE_HUE_RANGE = 270;
-const PALLETTE_SATURATION = 60;
-const PALLETTE_BRIGHTNESS = 75;
-const PALLETTE_OPACITY = 0.7;
+const PALLETTE_SATURATION = 100;
+const PALLETTE_BRIGHTNESS = 65;
+const PALLETTE_OPACITY = 0.55;
 
 const COLOR_PALLETTE = Array.from(Array(PALLETTE_SIZE)).map((_, i) => "hsla" +
   "(" + ((PALLETTE_HUE_OFFSET + (i / PALLETTE_SIZE * PALLETTE_HUE_RANGE))|0 % 360) +
   "," + PALLETTE_SATURATION + "%" +
   "," + PALLETTE_BRIGHTNESS + "%" +
   "," + PALLETTE_OPACITY +
   ")"
 );
@@ -1109,8 +1112,12 @@ let FlameGraphUtils = {
       if (hash > Number.MAX_SAFE_INTEGER / STRING_HASH_PRIME2) {
         return hash;
       }
     }
 
     return hash;
   }
 };
+
+exports.FlameGraph = FlameGraph;
+exports.FlameGraphUtils = FlameGraphUtils;
+exports.FLAME_GRAPH_BLOCK_HEIGHT = FLAME_GRAPH_BLOCK_HEIGHT;
--- a/browser/devtools/styleeditor/StyleSheetEditor.jsm
+++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm
@@ -232,42 +232,34 @@ StyleSheetEditor.prototype = {
       this._fileModDate = info.lastModificationDate.getTime();
     }, this.markLinkedFileBroken);
 
     this.emit("linked-css-file");
   },
 
   /**
    * Start fetching the full text source for this editor's sheet.
+   *
+   * @return {Promise}
+   *         A promise that'll resolve with the source text once the source
+   *         has been loaded or reject on unexpected error.
    */
-  fetchSource: function(callback) {
-    return this.styleSheet.getText().then((longStr) => {
-      longStr.string().then((source) => {
-        let ruleCount = this.styleSheet.ruleCount;
-        if (!this.styleSheet.isOriginalSource) {
-          source = CssLogic.prettifyCSS(source, ruleCount);
-        }
-        this._state.text = source;
-        this.sourceLoaded = true;
+  fetchSource: function () {
+    return Task.spawn(function* () {
+      let longStr = yield this.styleSheet.getText();
+      let source = yield longStr.string();
+      let ruleCount = this.styleSheet.ruleCount;
+      if (!this.styleSheet.isOriginalSource) {
+        source = CssLogic.prettifyCSS(source, ruleCount);
+      }
+      this._state.text = source;
+      this.sourceLoaded = true;
 
-        if (callback) {
-          callback(source);
-        }
-        return source;
-      }, e => {
-        if (this._isDestroyed) {
-          console.warn("Could not fetch the source for " +
-                       this.styleSheet.href +
-                       ", the editor was destroyed");
-          Cu.reportError(e);
-        } else {
-          throw e;
-        }
-      });
-    }, e => {
+      return source;
+    }.bind(this)).then(null, e => {
       if (this._isDestroyed) {
         console.warn("Could not fetch the source for " +
                      this.styleSheet.href +
                      ", the editor was destroyed");
         Cu.reportError(e);
       } else {
         this.emit("error", { key: LOAD_ERROR, append: this.styleSheet.href });
         throw e;
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -905,16 +905,20 @@ PropertyInfo.prototype = {
 function createMenuItem(aMenu, aAttributes)
 {
   let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
 
   item.setAttribute("label", CssHtmlTree.l10n(aAttributes.label));
   item.setAttribute("accesskey", CssHtmlTree.l10n(aAttributes.accesskey));
   item.addEventListener("command", aAttributes.command);
 
+  if (aAttributes.type) {
+    item.setAttribute("type", aAttributes.type);
+  }
+
   aMenu.appendChild(item);
 
   return item;
 }
 
 /**
  * A container to give easy access to property data from the template engine.
  *
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -3058,16 +3058,20 @@ function createChild(aParent, aTag, aAtt
 
 function createMenuItem(aMenu, aAttributes) {
   let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
 
   item.setAttribute("label", _strings.GetStringFromName(aAttributes.label));
   item.setAttribute("accesskey", _strings.GetStringFromName(aAttributes.accesskey));
   item.addEventListener("command", aAttributes.command);
 
+  if (aAttributes.type) {
+    item.setAttribute("type", aAttributes.type);
+  }
+
   aMenu.appendChild(item);
 
   return item;
 }
 
 function setTimeout() {
   let window = Services.appShell.hiddenDOMWindow;
   return window.setTimeout.apply(window, arguments);
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -76,17 +76,17 @@ browser.jar:
   skin/classic/browser/search-engine-placeholder.png        (../shared/search/search-engine-placeholder.png)
   skin/classic/browser/badge-add-engine.png                 (../shared/search/badge-add-engine.png)
   skin/classic/browser/search-indicator-badge-add.png       (../shared/search/search-indicator-badge-add.png)
   skin/classic/browser/search-history-icon.svg              (../shared/search/history-icon.svg)
   skin/classic/browser/Secure.png
   skin/classic/browser/Security-broken.png
   skin/classic/browser/setDesktopBackground.css
   skin/classic/browser/slowStartup-16.png
-  skin/classic/browser/theme-switcher-icon.png
+  skin/classic/browser/theme-switcher-icon.png              (../shared/theme-switcher-icon.png)
   skin/classic/browser/Toolbar.png
   skin/classic/browser/Toolbar-inverted.png
   skin/classic/browser/Toolbar-small.png
   skin/classic/browser/undoCloseTab.png                        (../shared/undoCloseTab.png)
   skin/classic/browser/update-badge.svg                        (../shared/update-badge.svg)
   skin/classic/browser/urlbar-arrow.png
   skin/classic/browser/session-restore.svg                  (../shared/incontent-icons/session-restore.svg)
   skin/classic/browser/tab-crashed.svg                      (../shared/incontent-icons/tab-crashed.svg)
deleted file mode 100644
index f1e9a0271e957264ca34400bc436fd9bf3bf91ce..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -117,18 +117,18 @@ browser.jar:
   skin/classic/browser/badge-add-engine.png                    (../shared/search/badge-add-engine.png)
   skin/classic/browser/badge-add-engine@2x.png                 (../shared/search/badge-add-engine@2x.png)
   skin/classic/browser/search-indicator-badge-add.png          (../shared/search/search-indicator-badge-add.png)
   skin/classic/browser/search-indicator-badge-add@2x.png       (../shared/search/search-indicator-badge-add@2x.png)
   skin/classic/browser/search-history-icon.svg                 (../shared/search/history-icon.svg)
   skin/classic/browser/Secure-Glyph.png
   skin/classic/browser/Secure-Glyph@2x.png
   skin/classic/browser/slowStartup-16.png
-  skin/classic/browser/theme-switcher-icon.png
-  skin/classic/browser/theme-switcher-icon@2x.png
+  skin/classic/browser/theme-switcher-icon.png                 (../shared/theme-switcher-icon.png)
+  skin/classic/browser/theme-switcher-icon@2x.png              (../shared/theme-switcher-icon@2x.png)
   skin/classic/browser/Toolbar.png
   skin/classic/browser/Toolbar@2x.png
   skin/classic/browser/Toolbar-inverted.png
   skin/classic/browser/Toolbar-inverted@2x.png
   skin/classic/browser/toolbarbutton-dropmarker.png
   skin/classic/browser/undoCloseTab.png                        (../shared/undoCloseTab.png)
   skin/classic/browser/undoCloseTab@2x.png                     (../shared/undoCloseTab@2x.png)
   skin/classic/browser/update-badge.svg                        (../shared/update-badge.svg)
deleted file mode 100644
index 757d1e0c64d787bb55e8adab0f321b6555d7882c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 0d54510ec5f59b70f1962a1353d4575f1c5b989d..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/shared/readinglist/sidebar.inc.css
+++ b/browser/themes/shared/readinglist/sidebar.inc.css
@@ -1,14 +1,13 @@
 % This Source Code Form is subject to the terms of the Mozilla Public
 % License, v. 2.0. If a copy of the MPL was not distributed with this
 % file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 :root, body {
-  height: 100%;
   overflow-x: hidden;
 }
 
 body {
   margin: 0;
   font: message-box;
   color: #333333;
   -moz-user-select: none;
@@ -16,21 +15,16 @@ body {
 }
 
 #emptyListInfo {
   cursor: default;
   padding: 3em 1em;
   text-align: center;
 }
 
-#list {
-  height: 100%;
-  overflow-x: auto;
-}
-
 .item {
   display: flex;
   flex-flow: row;
   cursor: pointer;
   padding: 6px;
   opacity: 0;
   max-height: 0;
   transition: opacity 150ms ease-in-out, max-height 150ms ease-in-out 150ms;
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..891e7afb1055e484669e792432ac1df496aa14ca
GIT binary patch
literal 2084
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
O0000<MNUMnLSTY`B?P7b
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..286adfeaad64110999418d055e58ff097ef56c78
GIT binary patch
literal 5595
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
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -100,18 +100,17 @@ browser.jar:
         skin/classic/browser/search-engine-placeholder.png           (../shared/search/search-engine-placeholder.png)
         skin/classic/browser/badge-add-engine.png                    (../shared/search/badge-add-engine.png)
         skin/classic/browser/search-indicator-badge-add.png          (../shared/search/search-indicator-badge-add.png)
         skin/classic/browser/search-history-icon.svg                 (../shared/search/history-icon.svg)
         skin/classic/browser/Secure24.png
         skin/classic/browser/Secure24-aero.png
         skin/classic/browser/setDesktopBackground.css
         skin/classic/browser/slowStartup-16.png
-        skin/classic/browser/theme-switcher-icon.png
-        skin/classic/browser/theme-switcher-icon-aero.png
+        skin/classic/browser/theme-switcher-icon.png                 (../shared/theme-switcher-icon.png)
         skin/classic/browser/Toolbar.png
         skin/classic/browser/Toolbar-aero.png
         skin/classic/browser/Toolbar-inverted.png
         skin/classic/browser/Toolbar-lunaSilver.png
         skin/classic/browser/Toolbar-XP.png
         skin/classic/browser/toolbarbutton-dropdown-arrow.png
         skin/classic/browser/toolbarbutton-dropdown-arrow-XPVista7.png
         skin/classic/browser/toolbarbutton-dropdown-arrow-inverted.png
@@ -561,18 +560,16 @@ browser.jar:
 % override chrome://browser/skin/toolbarbutton-dropdown-arrow.png     chrome://browser/skin/toolbarbutton-dropdown-arrow-XPVista7.png   os=WINNT osversion<=6.1
 % override chrome://browser/skin/tabbrowser/newtab.png                chrome://browser/skin/tabbrowser/newtab-XPVista7.png              os=WINNT osversion<=6.1
 % override chrome://browser/skin/tabbrowser/tab-arrow-left.png        chrome://browser/skin/tabbrowser/tab-arrow-left-XPVista7.png      os=WINNT osversion<=6.1
 
 % override chrome://browser/skin/menuPanel.png                        chrome://browser/skin/menuPanel-aero.png                          os=WINNT osversion=6
 % override chrome://browser/skin/menuPanel.png                        chrome://browser/skin/menuPanel-aero.png                          os=WINNT osversion=6.1
 % override chrome://browser/skin/menuPanel-small.png                  chrome://browser/skin/menuPanel-small-aero.png                    os=WINNT osversion=6
 % override chrome://browser/skin/menuPanel-small.png                  chrome://browser/skin/menuPanel-small-aero.png                    os=WINNT osversion=6.1
-% override chrome://browser/skin/theme-switcher-icon.png              chrome://browser/skin/theme-switcher-icon-aero.png                os=WINNT osversion=6
-% override chrome://browser/skin/theme-switcher-icon.png              chrome://browser/skin/theme-switcher-icon-aero.png                os=WINNT osversion=6.1
 % override chrome://browser/skin/loop/menuPanel.png                   chrome://browser/skin/loop/menuPanel-aero.png                     os=WINNT osversion=6
 % override chrome://browser/skin/loop/menuPanel.png                   chrome://browser/skin/loop/menuPanel-aero.png                     os=WINNT osversion=6.1
 
 % override chrome://browser/skin/Toolbar.png                          chrome://browser/skin/Toolbar-aero.png                            os=WINNT osversion=6
 % override chrome://browser/skin/Toolbar.png                          chrome://browser/skin/Toolbar-aero.png                            os=WINNT osversion=6.1
 % override chrome://browser/skin/Toolbar.png                          chrome://browser/skin/Toolbar-XP.png                              os=WINNT osversion=5.2
 % override chrome://browser/skin/loop/toolbar.png                     chrome://browser/skin/loop/toolbar-aero.png                       os=WINNT osversion=6
 % override chrome://browser/skin/loop/toolbar.png                     chrome://browser/skin/loop/toolbar-aero.png                       os=WINNT osversion=6.1
deleted file mode 100644
index 57a49930e7310a23f9b7c84a88cb60d5f93ceb1b..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 0b356426ca50987a1c65fcd0f5f4d02d97a9f11e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -253,18 +253,16 @@
 #ifdef MOZ_ANDROID_TAB_QUEUE
         <!-- The main reason for the Tab Queue build flag is to not mess with the VIEW intent filter
              before the rest of the plumbing is in place -->
 
         <service android:name="org.mozilla.gecko.tabqueue.TabQueueService" />
 
         <activity android:name="org.mozilla.gecko.tabqueue.TabQueueDispatcher"
                   android:label="@MOZ_APP_DISPLAYNAME@"
-                  android:noHistory="true"
-                  android:excludeFromRecents="true"
                   android:launchMode="singleTask"
                   android:theme="@style/TabQueueActivity">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.BROWSABLE" />
                 <data android:scheme="http" />
                 <data android:scheme="https" />
--- a/services/datareporting/DataReportingService.js
+++ b/services/datareporting/DataReportingService.js
@@ -12,17 +12,16 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 
 
 const ROOT_BRANCH = "datareporting.";
 const POLICY_BRANCH = ROOT_BRANCH + "policy.";
-const SESSIONS_BRANCH = ROOT_BRANCH + "sessions.";
 const HEALTHREPORT_BRANCH = ROOT_BRANCH + "healthreport.";
 const HEALTHREPORT_LOGGING_BRANCH = HEALTHREPORT_BRANCH + "logging.";
 const DEFAULT_LOAD_DELAY_MSEC = 10 * 1000;
 const DEFAULT_LOAD_DELAY_FIRST_RUN_MSEC = 60 * 1000;
 
 /**
  * The Firefox Health Report XPCOM service.
  *
@@ -60,20 +59,16 @@ const DEFAULT_LOAD_DELAY_FIRST_RUN_MSEC 
  */
 this.DataReportingService = function () {
   this.wrappedJSObject = this;
 
   this._quitting = false;
 
   this._os = Cc["@mozilla.org/observer-service;1"]
                .getService(Ci.nsIObserverService);
-
-  // Used for testing only, when true results in getSessionRecorder() returning
-  // undefined. Controlled via simulate* methods.
-  this._simulateNoSessionRecorder = false;
 }
 
 DataReportingService.prototype = Object.freeze({
   classID: Components.ID("{41f6ae36-a79f-4613-9ac3-915e70f83789}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
 
@@ -115,26 +110,16 @@ DataReportingService.prototype = Object.
         break;
 
       case "profile-after-change":
         this._os.removeObserver(this, "profile-after-change");
 
         try {
           this._prefs = new Preferences(HEALTHREPORT_BRANCH);
 
-          // We don't initialize the sessions recorder unless Health Report is
-          // around to provide pruning of data.
-          //
-          // FUTURE consider having the SessionsRecorder always enabled and/or
-          // living in its own XPCOM service.
-          if (this._prefs.get("service.enabled", true)) {
-            this.sessionRecorder = new SessionRecorder(SESSIONS_BRANCH);
-            this.sessionRecorder.onStartup();
-          }
-
           // We can't interact with prefs until after the profile is present.
           let policyPrefs = new Preferences(POLICY_BRANCH);
           this.policy = new DataReportingPolicy(policyPrefs, this._prefs, this);
 
           this._os.addObserver(this, "sessionstore-windows-restored", true);
         } catch (ex) {
           Cu.reportError("Exception when initializing data reporting service: " +
                          CommonUtils.exceptionStr(ex));
@@ -277,19 +262,17 @@ DataReportingService.prototype = Object.
       appender.level = ns.Log.Level[level] || ns.Log.Level.Debug;
 
       for (let name of LOGGERS) {
         let logger = ns.Log.repository.getLogger(name);
         logger.addAppender(appender);
       }
     }
 
-    this._healthReporter = new ns.HealthReporter(HEALTHREPORT_BRANCH,
-                                                 this.policy,
-                                                 this.sessionRecorder);
+    this._healthReporter = new ns.HealthReporter(HEALTHREPORT_BRANCH, this.policy);
 
     // Wait for initialization to finish so if a shutdown occurs before init
     // has finished we don't adversely affect app startup on next run.
     this._healthReporter.init().then(function onInit() {
       this._prefs.set("service.firstRun", true);
     }.bind(this));
   },
 
@@ -307,40 +290,19 @@ DataReportingService.prototype = Object.
   /**
    * Reset the stable client id.
    *
    * @return Promise<string> The new client ID.
    */
   resetClientID: Task.async(function* () {
     return ClientID.resetClientID();
   }),
-
-  /**
-   * Returns the SessionRecorder instance associated with the data reporting service.
-   * Returns an actual object only if FHR is enabled and after initialization,
-   * else returns undefined.
-   */
-  getSessionRecorder: function() {
-    return this._simulateNoSessionRecorder ? undefined : this.sessionRecorder;
-  },
-
-  // These two simulate* methods below are only used for testings and control
-  // whether getSessionRecorder() behaves normally or forced to return undefined
-  simulateNoSessionRecorder() {
-    this._simulateNoSessionRecorder = true;
-  },
-
-  simulateRestoreSessionRecorder() {
-    this._simulateNoSessionRecorder = false;
-  },
 });
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataReportingService]);
 
 #define MERGED_COMPARTMENT
 
 #include ../common/observers.js
 ;
 #include policy.jsm
 ;
-#include sessions.jsm
-;
 
--- a/services/datareporting/moz.build
+++ b/services/datareporting/moz.build
@@ -11,14 +11,13 @@ EXTRA_COMPONENTS += [
 ]
 
 EXTRA_PP_COMPONENTS += [
     'DataReportingService.js',
 ]
 
 EXTRA_PP_JS_MODULES.services.datareporting += [
     'policy.jsm',
-    'sessions.jsm',
 ]
 
 TESTING_JS_MODULES.services.datareporting += [
     'modules-testing/mocks.jsm',
 ]
deleted file mode 100644
--- a/services/datareporting/sessions.jsm
+++ /dev/null
@@ -1,406 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-#ifndef MERGED_COMPARTMENT
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = [
-  "SessionRecorder",
-];
-
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-#endif
-
-Cu.import("resource://gre/modules/Preferences.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://services-common/utils.js");
-
-
-// We automatically prune sessions older than this.
-const MAX_SESSION_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days.
-const STARTUP_RETRY_INTERVAL_MS = 5000;
-
-// Wait up to 5 minutes for startup measurements before giving up.
-const MAX_STARTUP_TRIES = 300000 / STARTUP_RETRY_INTERVAL_MS;
-
-/**
- * Records information about browser sessions.
- *
- * This serves as an interface to both current session information as
- * well as a history of previous sessions.
- *
- * Typically only one instance of this will be installed in an
- * application. It is typically managed by an XPCOM service. The
- * instance is instantiated at application start; onStartup is called
- * once the profile is installed; onShutdown is called during shutdown.
- *
- * We currently record state in preferences. However, this should be
- * invisible to external consumers. We could easily swap in a different
- * storage mechanism if desired.
- *
- * Please note the different semantics for storing times and dates in
- * preferences. Full dates (notably the session start time) are stored
- * as strings because preferences have a 32-bit limit on integer values
- * and milliseconds since UNIX epoch would overflow. Many times are
- * stored as integer offsets from the session start time because they
- * should not overflow 32 bits.
- *
- * Since this records history of all sessions, there is a possibility
- * for unbounded data aggregation. This is curtailed through:
- *
- *   1) An "idle-daily" observer which delete sessions older than
- *      MAX_SESSION_AGE_MS.
- *   2) The creator of this instance explicitly calling
- *      `pruneOldSessions`.
- *
- * @param branch
- *        (string) Preferences branch on which to record state.
- */
-this.SessionRecorder = function (branch) {
-  if (!branch) {
-    throw new Error("branch argument must be defined.");
-  }
-
-  if (!branch.endsWith(".")) {
-    throw new Error("branch argument must end with '.': " + branch);
-  }
-
-  this._log = Log.repository.getLogger("Services.DataReporting.SessionRecorder");
-
-  this._prefs = new Preferences(branch);
-  this._lastActivityWasInactive = false;
-  this._activeTicks = 0;
-  this.fineTotalTime = 0;
-  this._started = false;
-  this._timer = null;
-  this._startupFieldTries = 0;
-
-  this._os = Cc["@mozilla.org/observer-service;1"]
-               .getService(Ci.nsIObserverService);
-
-};
-
-SessionRecorder.prototype = Object.freeze({
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
-
-  STARTUP_RETRY_INTERVAL_MS: STARTUP_RETRY_INTERVAL_MS,
-
-  get _currentIndex() {
-    return this._prefs.get("currentIndex", 0);
-  },
-
-  set _currentIndex(value) {
-    this._prefs.set("currentIndex", value);
-  },
-
-  get _prunedIndex() {
-    return this._prefs.get("prunedIndex", 0);
-  },
-
-  set _prunedIndex(value) {
-    this._prefs.set("prunedIndex", value);
-  },
-
-  get startDate() {
-    return CommonUtils.getDatePref(this._prefs, "current.startTime");
-  },
-
-  set _startDate(value) {
-    CommonUtils.setDatePref(this._prefs, "current.startTime", value);
-  },
-
-  get activeTicks() {
-    return this._prefs.get("current.activeTicks", 0);
-  },
-
-  incrementActiveTicks: function () {
-    this._prefs.set("current.activeTicks", ++this._activeTicks);
-  },
-
-  /**
-   * Total time of this session in integer seconds.
-   *
-   * See also fineTotalTime for the time in milliseconds.
-   */
-  get totalTime() {
-    return this._prefs.get("current.totalTime", 0);
-  },
-
-  updateTotalTime: function () {
-    // We store millisecond precision internally to prevent drift from
-    // repeated rounding.
-    this.fineTotalTime = Date.now() - this.startDate;
-    this._prefs.set("current.totalTime", Math.floor(this.fineTotalTime / 1000));
-  },
-
-  get main() {
-    return this._prefs.get("current.main", -1);
-  },
-
-  set _main(value) {
-    if (!Number.isInteger(value)) {
-      throw new Error("main time must be an integer.");
-    }
-
-    this._prefs.set("current.main", value);
-  },
-
-  get firstPaint() {
-    return this._prefs.get("current.firstPaint", -1);
-  },
-
-  set _firstPaint(value) {
-    if (!Number.isInteger(value)) {
-      throw new Error("firstPaint must be an integer.");
-    }
-
-    this._prefs.set("current.firstPaint", value);
-  },
-
-  get sessionRestored() {
-    return this._prefs.get("current.sessionRestored", -1);
-  },
-
-  set _sessionRestored(value) {
-    if (!Number.isInteger(value)) {
-      throw new Error("sessionRestored must be an integer.");
-    }
-
-    this._prefs.set("current.sessionRestored", value);
-  },
-
-  getPreviousSessions: function () {
-    let result = {};
-
-    for (let i = this._prunedIndex; i < this._currentIndex; i++) {
-      let s = this.getPreviousSession(i);
-      if (!s) {
-        continue;
-      }
-
-      result[i] = s;
-    }
-
-    return result;
-  },
-
-  getPreviousSession: function (index) {
-    return this._deserialize(this._prefs.get("previous." + index));
-  },
-
-  /**
-   * Prunes old, completed sessions that started earlier than the
-   * specified date.
-   */
-  pruneOldSessions: function (date) {
-    for (let i = this._prunedIndex; i < this._currentIndex; i++) {
-      let s = this.getPreviousSession(i);
-      if (!s) {
-        continue;
-      }
-
-      if (s.startDate >= date) {
-        continue;
-      }
-
-      this._log.debug("Pruning session #" + i + ".");
-      this._prefs.reset("previous." + i);
-      this._prunedIndex = i;
-    }
-  },
-
-  recordStartupFields: function () {
-    let si = this._getStartupInfo();
-
-    if (!si.process) {
-      throw new Error("Startup info not available.");
-    }
-
-    let missing = false;
-
-    for (let field of ["main", "firstPaint", "sessionRestored"]) {
-      if (!(field in si)) {
-        this._log.debug("Missing startup field: " + field);
-        missing = true;
-        continue;
-      }
-
-      this["_" + field] = si[field].getTime() - si.process.getTime();
-    }
-
-    if (!missing || this._startupFieldTries > MAX_STARTUP_TRIES) {
-      this._clearStartupTimer();
-      return;
-    }
-
-    // If we have missing fields, install a timer and keep waiting for
-    // data.
-    this._startupFieldTries++;
-
-    if (!this._timer) {
-      this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-      this._timer.initWithCallback({
-        notify: this.recordStartupFields.bind(this),
-      }, this.STARTUP_RETRY_INTERVAL_MS, this._timer.TYPE_REPEATING_SLACK);
-    }
-  },
-
-  _clearStartupTimer: function () {
-    if (this._timer) {
-      this._timer.cancel();
-      delete this._timer;
-    }
-  },
-
-  /**
-   * Perform functionality on application startup.
-   *
-   * This is typically called in a "profile-do-change" handler.
-   */
-  onStartup: function () {
-    if (this._started) {
-      throw new Error("onStartup has already been called.");
-    }
-
-    let si = this._getStartupInfo();
-    if (!si.process) {
-      throw new Error("Process information not available. Misconfigured app?");
-    }
-
-    this._started = true;
-
-    this._os.addObserver(this, "profile-before-change", false);
-    this._os.addObserver(this, "user-interaction-active", false);
-    this._os.addObserver(this, "user-interaction-inactive", false);
-    this._os.addObserver(this, "idle-daily", false);
-
-    // This has the side-effect of clearing current session state.
-    this._moveCurrentToPrevious();
-
-    this._startDate = si.process;
-    this._prefs.set("current.activeTicks", 0);
-    this.updateTotalTime();
-
-    this.recordStartupFields();
-  },
-
-  /**
-   * Record application activity.
-   */
-  onActivity: function (active) {
-    let updateActive = active && !this._lastActivityWasInactive;
-    this._lastActivityWasInactive = !active;
-
-    this.updateTotalTime();
-
-    if (updateActive) {
-      this.incrementActiveTicks();
-    }
-  },
-
-  onShutdown: function () {
-    this._log.info("Recording clean session shutdown.");
-    this._prefs.set("current.clean", true);
-    this.updateTotalTime();
-    this._clearStartupTimer();
-
-    this._os.removeObserver(this, "profile-before-change");
-    this._os.removeObserver(this, "user-interaction-active");
-    this._os.removeObserver(this, "user-interaction-inactive");
-    this._os.removeObserver(this, "idle-daily");
-  },
-
-  _CURRENT_PREFS: [
-    "current.startTime",
-    "current.activeTicks",
-    "current.totalTime",
-    "current.main",
-    "current.firstPaint",
-    "current.sessionRestored",
-    "current.clean",
-  ],
-
-  // This is meant to be called only during onStartup().
-  _moveCurrentToPrevious: function () {
-    try {
-      if (!this.startDate.getTime()) {
-        this._log.info("No previous session. Is this first app run?");
-        return;
-      }
-
-      let clean = this._prefs.get("current.clean", false);
-
-      let count = this._currentIndex++;
-      let obj = {
-        s: this.startDate.getTime(),
-        a: this.activeTicks,
-        t: this.totalTime,
-        c: clean,
-        m: this.main,
-        fp: this.firstPaint,
-        sr: this.sessionRestored,
-      };
-
-      this._log.debug("Recording last sessions as #" + count + ".");
-      this._prefs.set("previous." + count, JSON.stringify(obj));
-    } catch (ex) {
-      this._log.warn("Exception when migrating last session: " +
-                     CommonUtils.exceptionStr(ex));
-    } finally {
-      this._log.debug("Resetting prefs from last session.");
-      for (let pref of this._CURRENT_PREFS) {
-        this._prefs.reset(pref);
-      }
-    }
-  },
-
-  _deserialize: function (s) {
-    let o;
-    try {
-      o = JSON.parse(s);
-    } catch (ex) {
-      return null;
-    }
-
-    return {
-      startDate: new Date(o.s),
-      activeTicks: o.a,
-      totalTime: o.t,
-      clean: !!o.c,
-      main: o.m,
-      firstPaint: o.fp,
-      sessionRestored: o.sr,
-    };
-  },
-
-  // Implemented as a function to allow for monkeypatching in tests.
-  _getStartupInfo: function () {
-    return Cc["@mozilla.org/toolkit/app-startup;1"]
-             .getService(Ci.nsIAppStartup)
-             .getStartupInfo();
-  },
-
-  observe: function (subject, topic, data) {
-    switch (topic) {
-      case "profile-before-change":
-        this.onShutdown();
-        break;
-
-      case "user-interaction-active":
-        this.onActivity(true);
-        break;
-
-      case "user-interaction-inactive":
-        this.onActivity(false);
-        break;
-
-      case "idle-daily":
-        this.pruneOldSessions(new Date(Date.now() - MAX_SESSION_AGE_MS));
-        break;
-    }
-  },
-});
deleted file mode 100644
--- a/services/datareporting/tests/xpcshell/test_session_recorder.js
+++ /dev/null
@@ -1,306 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-const {utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/Promise.jsm");
-Cu.import("resource://gre/modules/services/datareporting/sessions.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://services-common/utils.js");
-
-
-function run_test() {
-  run_next_test();
-}
-
-function monkeypatchStartupInfo(recorder, start=new Date(), offset=500) {
-  Object.defineProperty(recorder, "_getStartupInfo", {
-    value: function _getStartupInfo() {
-      return {
-        process: start,
-        main: new Date(start.getTime() + offset),
-        firstPaint: new Date(start.getTime() + 2 * offset),
-        sessionRestored: new Date(start.getTime() + 3 * offset),
-      };
-    }
-  });
-}
-
-function sleep(wait) {
-  let deferred = Promise.defer();
-
-  let timer = CommonUtils.namedTimer(function onTimer() {
-    deferred.resolve();
-  }, wait, deferred.promise, "_sleepTimer");
-
-  return deferred.promise;
-}
-
-function getRecorder(name, start, offset) {
-  let recorder = new SessionRecorder("testing." + name + ".");
-  monkeypatchStartupInfo(recorder, start, offset);
-
-  return recorder;
-}
-
-add_test(function test_basic() {
-  let recorder = getRecorder("basic");
-  recorder.onStartup();
-  recorder.onShutdown();
-
-  run_next_test();
-});
-
-add_task(function test_current_properties() {
-  let now = new Date();
-  let recorder = getRecorder("current_properties", now);
-  yield sleep(25);
-  recorder.onStartup();
-
-  do_check_eq(recorder.startDate.getTime(), now.getTime());
-  do_check_eq(recorder.activeTicks, 0);
-  do_check_true(recorder.fineTotalTime > 0);
-  do_check_eq(recorder.main, 500);
-  do_check_eq(recorder.firstPaint, 1000);
-  do_check_eq(recorder.sessionRestored, 1500);
-
-  recorder.incrementActiveTicks();
-  do_check_eq(recorder.activeTicks, 1);
-
-  recorder._startDate = new Date(Date.now() - 1000);
-  recorder.updateTotalTime();
-  do_check_eq(recorder.totalTime, 1);
-
-  recorder.onShutdown();
-});
-
-// If startup info isn't present yet, we should install a timer and get
-// it eventually.
-add_task(function test_current_availability() {
-  let recorder = new SessionRecorder("testing.current_availability.");
-  let now = new Date();
-
-  Object.defineProperty(recorder, "_getStartupInfo", {
-    value: function _getStartupInfo() {
-      return {
-        process: now,
-        main: new Date(now.getTime() + 500),
-        firstPaint: new Date(now.getTime() + 1000),
-      };
-    },
-    writable: true,
-  });
-
-  Object.defineProperty(recorder, "STARTUP_RETRY_INTERVAL_MS", {
-    value: 100,
-  });
-
-  let oldRecord = recorder.recordStartupFields;
-  let recordCount = 0;
-
-  Object.defineProperty(recorder, "recordStartupFields", {
-    value: function () {
-      recordCount++;
-      return oldRecord.call(recorder);
-    }
-  });
-
-  do_check_null(recorder._timer);
-  recorder.onStartup();
-  do_check_eq(recordCount, 1);
-  do_check_eq(recorder.sessionRestored, -1);
-  do_check_neq(recorder._timer, null);
-
-  yield sleep(125);
-  do_check_eq(recordCount, 2);
-  yield sleep(100);
-  do_check_eq(recordCount, 3);
-  do_check_eq(recorder.sessionRestored, -1);
-
-  monkeypatchStartupInfo(recorder, now);
-  yield sleep(100);
-  do_check_eq(recordCount, 4);
-  do_check_eq(recorder.sessionRestored, 1500);
-
-  // The timer should be removed and we should not fire again.
-  do_check_null(recorder._timer);
-  yield sleep(100);
-  do_check_eq(recordCount, 4);
-
-  recorder.onShutdown();
-});
-
-add_test(function test_timer_clear_on_shutdown() {
-  let recorder = new SessionRecorder("testing.timer_clear_on_shutdown.");
-  let now = new Date();
-
-  Object.defineProperty(recorder, "_getStartupInfo", {
-    value: function _getStartupInfo() {
-      return {
-        process: now,
-        main: new Date(now.getTime() + 500),
-        firstPaint: new Date(now.getTime() + 1000),
-      };
-    },
-  });
-
-  do_check_null(recorder._timer);
-  recorder.onStartup();
-  do_check_neq(recorder._timer, null);
-
-  recorder.onShutdown();
-  do_check_null(recorder._timer);
-
-  run_next_test();
-});
-
-add_task(function test_previous_clean() {
-  let now = new Date();
-  let recorder = getRecorder("previous_clean", now);
-  yield sleep(25);
-  recorder.onStartup();
-
-  recorder.incrementActiveTicks();
-  recorder.incrementActiveTicks();
-
-  yield sleep(25);
-  recorder.onShutdown();
-
-  let total = recorder.totalTime;
-
-  yield sleep(25);
-  let now2 = new Date();
-  let recorder2 = getRecorder("previous_clean", now2, 100);
-  yield sleep(25);
-  recorder2.onStartup();
-
-  do_check_eq(recorder2.startDate.getTime(), now2.getTime());
-  do_check_eq(recorder2.main, 100);
-  do_check_eq(recorder2.firstPaint, 200);
-  do_check_eq(recorder2.sessionRestored, 300);
-
-  let sessions = recorder2.getPreviousSessions();
-  do_check_eq(Object.keys(sessions).length, 1);
-  do_check_true(0 in sessions);
-  let session = sessions[0];
-  do_check_true(session.clean);
-  do_check_eq(session.startDate.getTime(), now.getTime());
-  do_check_eq(session.main, 500);
-  do_check_eq(session.firstPaint, 1000);
-  do_check_eq(session.sessionRestored, 1500);
-  do_check_eq(session.totalTime, total);
-  do_check_eq(session.activeTicks, 2);
-
-  recorder2.onShutdown();
-});
-
-add_task(function test_previous_abort() {
-  let now = new Date();
-  let recorder = getRecorder("previous_abort", now);
-  yield sleep(25);
-  recorder.onStartup();
-  recorder.incrementActiveTicks();
-  yield sleep(25);
-  let total = recorder.totalTime;
-  yield sleep(25);
-
-  let now2 = new Date();
-  let recorder2 = getRecorder("previous_abort", now2);
-  yield sleep(25);
-  recorder2.onStartup();
-
-  let sessions = recorder2.getPreviousSessions();
-  do_check_eq(Object.keys(sessions).length, 1);
-  do_check_true(0 in sessions);
-  let session = sessions[0];
-  do_check_false(session.clean);
-  do_check_eq(session.totalTime, total);
-
-  recorder.onShutdown();
-  recorder2.onShutdown();
-});
-
-add_task(function test_multiple_sessions() {
-  for (let i = 0; i < 10; i++) {
-    let recorder = getRecorder("multiple_sessions");
-    yield sleep(25);
-    recorder.onStartup();
-    for (let j = 0; j < i; j++) {
-      recorder.incrementActiveTicks();
-    }
-    yield sleep(25);
-    recorder.onShutdown();
-    yield sleep(25);
-  }
-
-  let recorder = getRecorder("multiple_sessions");
-  recorder.onStartup();
-
-  let sessions = recorder.getPreviousSessions();
-  do_check_eq(Object.keys(sessions).length, 10);
-
-  for (let [i, session] in Iterator(sessions)) {
-    do_check_eq(session.activeTicks, i);
-
-    if (i > 0) {
-      do_check_true(session.startDate.getTime() > sessions[i-1].startDate.getTime());
-    }
-  }
-
-  // #6 is preserved since >=.
-  let threshold = sessions[6].startDate;
-  recorder.pruneOldSessions(threshold);
-
-  sessions = recorder.getPreviousSessions();
-  do_check_eq(Object.keys(sessions).length, 4);
-
-  recorder.pruneOldSessions(threshold);
-  sessions = recorder.getPreviousSessions();
-  do_check_eq(Object.keys(sessions).length, 4);
-  do_check_eq(recorder._prunedIndex, 5);
-
-  recorder.onShutdown();
-});
-
-add_task(function test_record_activity() {
-  let recorder = getRecorder("record_activity");
-  yield sleep(25);
-  recorder.onStartup();
-  let total = recorder.totalTime;
-  yield sleep(25);
-
-  for (let i = 0; i < 3; i++) {
-    Services.obs.notifyObservers(null, "user-interaction-active", null);
-    yield sleep(25);
-    do_check_true(recorder.fineTotalTime > total);
-    total = recorder.fineTotalTime;
-  }
-
-  do_check_eq(recorder.activeTicks, 3);
-
-  // Now send inactive. We should increment total time but not active.
-  Services.obs.notifyObservers(null, "user-interaction-inactive", null);
-  do_check_eq(recorder.activeTicks, 3);
-  do_check_true(recorder.fineTotalTime > total);
-  total = recorder.fineTotalTime;
-  yield sleep(25);
-
-  // If we send active again, this should be counted as inactive.
-  Services.obs.notifyObservers(null, "user-interaction-active", null);
-  do_check_eq(recorder.activeTicks, 3);
-  do_check_true(recorder.fineTotalTime > total);
-  total = recorder.fineTotalTime;
-  yield sleep(25);
-
-  // If we send active again, this should be counted as active.
-  Services.obs.notifyObservers(null, "user-interaction-active", null);
-  do_check_eq(recorder.activeTicks, 4);
-
-  Services.obs.notifyObservers(null, "user-interaction-active", null);
-  do_check_eq(recorder.activeTicks, 5);
-
-  recorder.onShutdown();
-});
-
--- a/services/datareporting/tests/xpcshell/xpcshell.ini
+++ b/services/datareporting/tests/xpcshell/xpcshell.ini
@@ -1,7 +1,6 @@
 [DEFAULT]
 head = head.js
 tail =
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_policy.js]
-[test_session_recorder.js]
--- a/services/healthreport/healthreporter.jsm
+++ b/services/healthreport/healthreporter.jsm
@@ -23,16 +23,18 @@ Cu.import("resource://services-common/ut
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryPing",
+                                  "resource://gre/modules/TelemetryPing.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
                                   "resource://gre/modules/UpdateChannel.jsm");
 
 // Oldest year to allow in date preferences. This module was implemented in
 // 2012 and no dates older than that should be encountered.
 const OLDEST_ALLOWED_YEAR = 2012;
 
 const DAYS_IN_PAYLOAD = 180;
@@ -1182,21 +1184,21 @@ AbstractHealthReporter.prototype = Objec
  *
  * @param branch
  *        (string) The preferences branch to use for state storage. The value
  *        must end with a period (.).
  *
  * @param policy
  *        (HealthReportPolicy) Policy driving execution of HealthReporter.
  */
-this.HealthReporter = function (branch, policy, sessionRecorder, stateLeaf=null) {
+this.HealthReporter = function (branch, policy, stateLeaf=null) {
   this._stateLeaf = stateLeaf;
   this._uploadInProgress = false;
 
-  AbstractHealthReporter.call(this, branch, policy, sessionRecorder);
+  AbstractHealthReporter.call(this, branch, policy, TelemetryPing.getSessionRecorder());
 
   if (!this.serverURI) {
     throw new Error("No server URI defined. Did you forget to define the pref?");
   }
 
   if (!this.serverNamespace) {
     throw new Error("No server namespace defined. Did you forget a pref?");
   }
--- a/services/healthreport/modules-testing/utils.jsm
+++ b/services/healthreport/modules-testing/utils.jsm
@@ -125,18 +125,18 @@ this.createFakeCrash = function (submitt
 };
 
 
 /**
  * A HealthReporter that is probed with various callbacks and counters.
  *
  * The purpose of this type is to aid testing of startup and shutdown.
  */
-this.InspectedHealthReporter = function (branch, policy, recorder, stateLeaf) {
-  HealthReporter.call(this, branch, policy, recorder, stateLeaf);
+this.InspectedHealthReporter = function (branch, policy, stateLeaf) {
+  HealthReporter.call(this, branch, policy, stateLeaf);
 
   this.onStorageCreated = null;
   this.onProviderManagerInitialized = null;
   this.providerManagerShutdownCount = 0;
   this.storageCloseCount = 0;
 }
 
 InspectedHealthReporter.prototype = {
@@ -207,13 +207,13 @@ this.getHealthReporter = function (name,
   }
   listener.onRequestRemoteDelete = function (request) {
     let promise = reporter.deleteRemoteData(request);
     MockPolicyListener.prototype.onRequestRemoteDelete.call(this, request);
     return promise;
   }
   let policy = new DataReportingPolicy(policyPrefs, prefs, listener);
   let type = inspected ? InspectedHealthReporter : HealthReporter;
-  reporter = new type(branch + "healthreport.", policy, null,
+  reporter = new type(branch + "healthreport.", policy,
                       "state-" + name + ".json");
 
   return reporter;
 };
--- a/services/healthreport/tests/xpcshell/test_provider_sessions.js
+++ b/services/healthreport/tests/xpcshell/test_provider_sessions.js
@@ -5,17 +5,17 @@
 
 const {utils: Cu} = Components;
 
 
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/services-common/utils.js");
-Cu.import("resource://gre/modules/services/datareporting/sessions.jsm");
+Cu.import("resource://gre/modules/SessionRecorder.jsm");
 Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
 
 
 function run_test() {
   run_next_test();
 }
 
 add_test(function test_constructor() {
--- a/toolkit/components/telemetry/TelemetryPing.jsm
+++ b/toolkit/components/telemetry/TelemetryPing.jsm
@@ -26,18 +26,20 @@ const LOGGER_NAME = "Toolkit.Telemetry";
 const LOGGER_PREFIX = "TelemetryPing::";
 
 const PREF_BRANCH = "toolkit.telemetry.";
 const PREF_BRANCH_LOG = PREF_BRANCH + "log.";
 const PREF_SERVER = PREF_BRANCH + "server";
 const PREF_ENABLED = PREF_BRANCH + "enabled";
 const PREF_LOG_LEVEL = PREF_BRANCH_LOG + "level";
 const PREF_LOG_DUMP = PREF_BRANCH_LOG + "dump";
-const PREF_CACHED_CLIENTID = PREF_BRANCH + "cachedClientID"
+const PREF_CACHED_CLIENTID = PREF_BRANCH + "cachedClientID";
+const PREF_FHR_ENABLED = "datareporting.healthreport.service.enabled";
 const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
+const PREF_SESSIONS_BRANCH = "datareporting.sessions.";
 
 const PING_FORMAT_VERSION = 4;
 
 // Delay before intializing telemetry (ms)
 const TELEMETRY_DELAY = 60000;
 // Delay before initializing telemetry if we're testing (ms)
 const TELEMETRY_TEST_DELAY = 100;
 // The number of days to keep pings serialised on the disk in case of failures.
@@ -55,16 +57,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryFile",
                                   "resource://gre/modules/TelemetryFile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryLog",
                                   "resource://gre/modules/TelemetryLog.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ThirdPartyCookieProbe",
                                   "resource://gre/modules/ThirdPartyCookieProbe.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment",
                                   "resource://gre/modules/TelemetryEnvironment.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionRecorder",
+                                  "resource://gre/modules/SessionRecorder.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
                                   "resource://gre/modules/UpdateChannel.jsm");
 
 /**
  * Setup Telemetry logging. This function also gets called when loggin related
  * preferences change.
  */
 let gLogger = null;
@@ -255,32 +259,41 @@ this.TelemetryPing = Object.freeze({
    },
 
    /**
     * The AsyncShutdown.Barrier to synchronize with TelemetryPing shutdown.
     */
    get shutdown() {
     return Impl._shutdownBarrier.client;
    },
+
+   /**
+    * The session recorder instance managed by Telemetry.
+    * @return {Object} The active SessionRecorder instance or null if not available.
+    */
+   getSessionRecorder: function() {
+    return Impl._sessionRecorder;
+   },
 });
 
 let Impl = {
   _initialized: false,
   _initStarted: false, // Whether we started setting up TelemetryPing.
   _log: null,
   _prevValues: {},
   // The previous build ID, if this is the first run with a new build.
   // Undefined if this is not the first run, or the previous build ID is unknown.
   _previousBuildID: undefined,
   _clientID: null,
   // A task performing delayed initialization
   _delayedInitTask: null,
   // The deferred promise resolved when the initialization task completes.
   _delayedInitTaskDeferred: null,
-
+  // The session recorder, shared with FHR and the Data Reporting Service.
+  _sessionRecorder: null,
   // This is a public barrier Telemetry clients can use to add blockers to the shutdown
   // of TelemetryPing.
   // After this barrier, clients can not submit Telemetry pings anymore.
   _shutdownBarrier: new AsyncShutdown.Barrier("TelemetryPing: Waiting for clients."),
   // This is a private barrier blocked by pending async ping activity (sending & saving).
   _connectionsBarrier: new AsyncShutdown.Barrier("TelemetryPing: Waiting for pending ping activity"),
 
   // This tracks all pending ping requests to the server.
@@ -744,16 +757,24 @@ let Impl = {
       return this._delayedInitTaskDeferred.promise;
     }
 
     if (this._initialized && !testing) {
       this._log.error("setupTelemetry - already initialized");
       return Promise.resolve();
     }
 
+    // Only initialize the session recorder if FHR is enabled.
+    // TODO: move this after the |enableTelemetryRecording| block and drop the
+    // PREF_FHR_ENABLED check after bug 1137252 lands.
+    if (!this._sessionRecorder && Preferences.get(PREF_FHR_ENABLED, true)) {
+      this._sessionRecorder = new SessionRecorder(PREF_SESSIONS_BRANCH);
+      this._sessionRecorder.onStartup();
+    }
+
     // Initialize some probes that are kept in their own modules
     this._thirdPartyCookies = new ThirdPartyCookieProbe();
     this._thirdPartyCookies.init();
 
     if (!this.enableTelemetryRecording(testing)) {
       this._log.config("setupTelemetry - Telemetry recording is disabled, skipping Telemetry setup.");
       return Promise.resolve();
     }
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -967,34 +967,28 @@ let Impl = {
       hasPingBeenSent = Telemetry.getHistogramById("TELEMETRY_SUCCESS").snapshot().sum > 0;
     } catch(e) {
     }
     if (!forSavedSession || hasPingBeenSent) {
       ret.savedPings = TelemetryFile.pingsLoaded;
     }
 
     ret.activeTicks = -1;
-    if ("@mozilla.org/datareporting/service;1" in Cc) {
-      let drs = Cc["@mozilla.org/datareporting/service;1"]
-                  .getService(Ci.nsISupports)
-                  .wrappedJSObject;
+    let sr = TelemetryPing.getSessionRecorder();
+    if (sr) {
+      let activeTicks = sr.activeTicks;
+      if (isSubsession) {
+        activeTicks = sr.activeTicks - this._subsessionStartActiveTicks;
+      }
 
-      let sr = drs.getSessionRecorder();
-      if (sr) {
-        let activeTicks = sr.activeTicks;
-        if (isSubsession) {
-          activeTicks = sr.activeTicks - this._subsessionStartActiveTicks;
-        }
+      if (clearSubsession) {
+        this._subsessionStartActiveTicks = activeTicks;
+      }
 
-        if (clearSubsession) {
-          this._subsessionStartActiveTicks = activeTicks;
-        }
-
-        ret.activeTicks = activeTicks;
-      }
+      ret.activeTicks = activeTicks;
     }
 
     ret.pingsOverdue = TelemetryFile.pingsOverdue;
     ret.pingsDiscarded = TelemetryFile.pingsDiscarded;
 
     return ret;
   },
 
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -66,39 +66,31 @@ 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_SERVER = PREF_BRANCH + "server";
 const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
 const PREF_FHR_SERVICE_ENABLED = "datareporting.healthreport.service.enabled";
 
-const SESSION_RECORDER_EXPECTED = HAS_DATAREPORTINGSERVICE &&
-                                  Preferences.get(PREF_FHR_SERVICE_ENABLED, true);
-
 const DATAREPORTING_DIR = "datareporting";
 const ABORTED_PING_FILE_NAME = "aborted-session-ping";
 const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
 
 const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
 
 XPCOMUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function() {
   return OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR);
 });
 
 let gHttpServer = new HttpServer();
 let gServerStarted = false;
 let gRequestIterator = null;
 let gClientID = null;
 
-XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
-  () => Cc["@mozilla.org/datareporting/service;1"]
-          .getService(Ci.nsISupports)
-          .wrappedJSObject);
-
 function generateUUID() {
   let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString();
   // strip {}
   return str.substring(1, str.length - 1);
 }
 
 function truncateDateToDays(date) {
   return new Date(date.getFullYear(),
@@ -316,17 +308,17 @@ function checkPayload(payload, reason, s
   Assert.ok(payload.simpleMeasurements.uptime >= 0);
   Assert.equal(payload.simpleMeasurements.startupInterrupted, 1);
   Assert.equal(payload.simpleMeasurements.shutdownDuration, SHUTDOWN_TIME);
   Assert.equal(payload.simpleMeasurements.savedPings, 1);
   Assert.ok("maximalNumberOfConcurrentThreads" in payload.simpleMeasurements);
   Assert.ok(payload.simpleMeasurements.maximalNumberOfConcurrentThreads >= gNumberOfThreadsLaunched);
 
   let activeTicks = payload.simpleMeasurements.activeTicks;
-  Assert.ok(SESSION_RECORDER_EXPECTED ? activeTicks >= 0 : activeTicks == -1);
+  Assert.ok(activeTicks >= 0);
 
   Assert.equal(payload.simpleMeasurements.failedProfileLockCount,
               FAILED_PROFILE_LOCK_ATTEMPTS);
   let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
   let failedProfileLocksFile = profileDirectory.clone();
   failedProfileLocksFile.append("Telemetry.FailedProfileLocks.txt");
   Assert.ok(!failedProfileLocksFile.exists());
 
@@ -486,23 +478,16 @@ function run_test() {
 
   // Addon manager needs a profile directory
   do_get_profile();
   loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
 
   Services.prefs.setBoolPref(PREF_ENABLED, true);
   Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
 
-  // Send the needed startup notifications to the datareporting service
-  // to ensure that it has been initialized.
-  if (HAS_DATAREPORTINGSERVICE) {
-    gDatareportingService.observe(null, "app-startup", null);
-    gDatareportingService.observe(null, "profile-after-change", null);
-  }
-
   // Make it look like we've previously failed to lock a profile a couple times.
   write_fake_failedprofilelocks_file();
 
   // Make it look like we've shutdown before.
   write_fake_shutdown_file();
 
   let currentMaxNumberOfThreads = Telemetry.maximalNumberOfConcurrentThreads;
   do_check_true(currentMaxNumberOfThreads > 0);
@@ -527,30 +512,16 @@ function run_test() {
   });
 
   Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(run_next_test));
 }
 
 add_task(function* asyncSetup() {
   yield TelemetrySession.setup();
   yield TelemetryPing.setup();
-
-  if (HAS_DATAREPORTINGSERVICE) {
-    // force getSessionRecorder()==undefined to check the payload's activeTicks
-    gDatareportingService.simulateNoSessionRecorder();
-  }
-
-  // When no DRS or no DRS.getSessionRecorder(), activeTicks should be -1.
-  do_check_eq(TelemetrySession.getPayload().simpleMeasurements.activeTicks, -1);
-
-  if (HAS_DATAREPORTINGSERVICE) {
-    // Restore normal behavior for getSessionRecorder()
-    gDatareportingService.simulateRestoreSessionRecorder();
-  }
-
   // Load the client ID from the client ID provider to check for pings sanity.
   gClientID = yield ClientID.getClientID();
 });
 
 // Ensures that expired histograms are not part of the payload.
 add_task(function* test_expiredHistogram() {
   let histogram_id = "FOOBAR";
   let dummy = Telemetry.newHistogram(histogram_id, "30", Telemetry.HISTOGRAM_EXPONENTIAL, 1, 2, 3);
@@ -846,24 +817,23 @@ add_task(function* test_checkSubsessionH
   Assert.ok(KEYED_ID in subsession.keyedHistograms);
   Assert.equal(classic.keyedHistograms[KEYED_ID]["a"].sum, 2);
   Assert.equal(classic.keyedHistograms[KEYED_ID]["b"].sum, 2);
   Assert.equal(subsession.keyedHistograms[KEYED_ID]["a"].sum, 1);
   Assert.equal(subsession.keyedHistograms[KEYED_ID]["b"].sum, 1);
 });
 
 add_task(function* test_checkSubsessionData() {
-  if (gIsAndroid || !SESSION_RECORDER_EXPECTED) {
-    // We don't support subsessions yet on Android. Also bail out if we
-    // can't use the session recorder.
+  if (gIsAndroid) {
+    // We don't support subsessions yet on Android.
     return;
   }
 
   // Keep track of the active ticks count if the session recorder is available.
-  let sessionRecorder = gDatareportingService.getSessionRecorder();
+  let sessionRecorder = TelemetryPing.getSessionRecorder();
   let activeTicksAtSubsessionStart = sessionRecorder.activeTicks;
   let expectedActiveTicks = activeTicksAtSubsessionStart;
 
   incrementActiveTicks = () => {
     sessionRecorder.incrementActiveTicks();
     ++expectedActiveTicks;
   }
 
--- a/toolkit/content/widgets/preferences.xml
+++ b/toolkit/content/widgets/preferences.xml
@@ -616,19 +616,21 @@
       <xul:hbox>
         <children/>
       </xul:hbox>
     </content>
     <implementation implements="nsITimerCallback">
       <constructor>
       <![CDATA[
         if (this.type != "child") {
-          var psvc = Components.classes["@mozilla.org/preferences-service;1"]
-                               .getService(Components.interfaces.nsIPrefBranch);
-          this.instantApply = psvc.getBoolPref("browser.preferences.instantApply");
+          if (!this._instantApplyInitialized) {
+            let psvc = Components.classes["@mozilla.org/preferences-service;1"]
+                                 .getService(Components.interfaces.nsIPrefBranch);
+            this.instantApply = psvc.getBoolPref("browser.preferences.instantApply");
+          }
           if (this.instantApply) {
             var docElt = document.documentElement;
             var acceptButton = docElt.getButton("accept");
             acceptButton.hidden = true;
             var cancelButton  = docElt.getButton("cancel");
 #ifdef XP_MACOSX
             // no buttons on Mac except Help
             cancelButton.hidden = true;
@@ -688,18 +690,23 @@
         }
         if (this._fadeTimer) {
           this._fadeTimer.cancel();
           this._fadeTimer = null;
         }
       ]]>
       </destructor>
 
+      <!-- Derived bindings can set this to true to cause us to skip
+           reading the browser.preferences.instantApply pref in the constructor.
+           Then they can set instantApply to their wished value. -->
+      <field name="_instantApplyInitialized">false</field>
+      <!-- Controls whether changed pref values take effect immediately. -->
       <field name="instantApply">false</field>
-      
+
       <property name="preferencePanes"
                 onget="return this.getElementsByTagName('prefpane');"/>
 
       <property name="type" onget="return this.getAttribute('type');"/>
       <property name="_paneDeck"
                 onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'paneDeck');"/>
       <property name="_paneDeckContainer"
                 onget="return document.getAnonymousElementByAttribute(this, 'class', 'paneDeckContainer');"/>
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -319,19 +319,25 @@ var NodeActor = exports.NodeActor = prot
   get computedStyle() {
     return CssLogic.getComputedStyle(this.rawNode);
   },
 
   /**
    * Is the node's display computed style value other than "none"
    */
   get isDisplayed() {
+    // Consider all non-element nodes as displayed.
+    if (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE ||
+        this.isAfterPseudoElement ||
+        this.isBeforePseudoElement) {
+      return true;
+    }
+
     let style = this.computedStyle;
     if (!style) {
-      // Consider all non-element nodes as displayed
       return true;
     } else {
       return style.display !== "none";
     }
   },
 
   /**
    * Are there event listeners that are listening on this node? This method
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -27,18 +27,18 @@ XPCOMUtils.defineLazyGetter(this, "Conso
   return require("devtools/toolkit/webconsole/network-monitor")
          .ConsoleProgressListener;
 });
 XPCOMUtils.defineLazyGetter(this, "events", () => {
   return require("sdk/event/core");
 });
 
 for (let name of ["WebConsoleUtils", "ConsoleServiceListener",
-                  "ConsoleAPIListener", "JSTermHelpers", "JSPropertyProvider",
-                  "ConsoleReflowListener"]) {
+    "ConsoleAPIListener", "addWebConsoleCommands", "JSPropertyProvider",
+    "ConsoleReflowListener"]) {
   Object.defineProperty(this, name, {
     get: function(prop) {
       if (prop == "WebConsoleUtils") {
         prop = "Utils";
       }
       return require("devtools/toolkit/webconsole/utils")[prop];
     }.bind(null, name),
     configurable: true,
@@ -290,21 +290,21 @@ WebConsoleActor.prototype =
   consoleProgressListener: null,
 
   /**
    * The ConsoleReflowListener instance.
    */
   consoleReflowListener: null,
 
   /**
-   * The JSTerm Helpers names cache.
+   * The Web Console Commands names cache.
    * @private
    * @type array
    */
-  _jstermHelpersCache: null,
+  _webConsoleCommandsCache: null,
 
   actorPrefix: "console",
 
   grip: function WCA_grip()
   {
     return { actor: this.actorID };
   },
 
@@ -353,17 +353,17 @@ WebConsoleActor.prototype =
     events.off(this.parentActor, "changed-toplevel-document", this._onChangedToplevelDocument);
     this.conn.removeActorPool(this._actorPool);
     if (this.parentActor.isRootActor) {
       Services.obs.removeObserver(this._onObserverNotification,
                                   "last-pb-context-exited");
     }
     this._actorPool = null;
 
-    this._jstermHelpersCache = null;
+    this._webConsoleCommandsCache = null;
     this._lastConsoleInputEvaluation = null;
     this._evalWindow = null;
     this._netEvents.clear();
     this.dbg.enabled = false;
     this.dbg = null;
     this.conn = null;
   },
 
@@ -877,24 +877,26 @@ WebConsoleActor.prototype =
                                     aRequest.cursor, frameActorId) || {};
     let matches = result.matches || [];
     let reqText = aRequest.text.substr(0, aRequest.cursor);
 
     // We consider '$' as alphanumerc because it is used in the names of some
     // helper functions.
     let lastNonAlphaIsDot = /[.][a-zA-Z0-9$]*$/.test(reqText);
     if (!lastNonAlphaIsDot) {
-      if (!this._jstermHelpersCache) {
+      if (!this._webConsoleCommandsCache) {
         let helpers = {
           sandbox: Object.create(null)
         };
-        JSTermHelpers(helpers);
-        this._jstermHelpersCache = Object.getOwnPropertyNames(helpers.sandbox);
+        addWebConsoleCommands(helpers);
+        this._webConsoleCommandsCache =
+          Object.getOwnPropertyNames(helpers.sandbox);
       }
-      matches = matches.concat(this._jstermHelpersCache.filter(n => n.startsWith(result.matchProp)));
+      matches = matches.concat(this._webConsoleCommandsCache
+          .filter(n => n.startsWith(result.matchProp)));
     }
 
     return {
       from: this.actorID,
       matches: matches.sort(),
       matchProp: result.matchProp,
     };
   },
@@ -961,34 +963,34 @@ WebConsoleActor.prototype =
   /**
    * Create an object with the API we expose to the Web Console during
    * JavaScript evaluation.
    * This object inherits properties and methods from the Web Console actor.
    *
    * @private
    * @param object aDebuggerGlobal
    *        A Debugger.Object that wraps a content global. This is used for the
-   *        JSTerm helpers.
+   *        Web Console Commands.
    * @return object
    *         The same object as |this|, but with an added |sandbox| property.
    *         The sandbox holds methods and properties that can be used as
    *         bindings during JS evaluation.
    */
-  _getJSTermHelpers: function WCA__getJSTermHelpers(aDebuggerGlobal)
+  _getWebConsoleCommands: function(aDebuggerGlobal)
   {
     let helpers = {
       window: this.evalWindow,
       chromeWindow: this.chromeWindow.bind(this),
       makeDebuggeeValue: aDebuggerGlobal.makeDebuggeeValue.bind(aDebuggerGlobal),
       createValueGrip: this.createValueGrip.bind(this),
       sandbox: Object.create(null),
       helperResult: null,
       consoleActor: this,
     };
-    JSTermHelpers(helpers);
+    addWebConsoleCommands(helpers);
 
     let evalWindow = this.evalWindow;
     function maybeExport(obj, name) {
       if (typeof obj[name] != "function") {
         return;
       }
 
       // By default, chrome-implemented functions that are exposed to content
@@ -1032,19 +1034,19 @@ WebConsoleActor.prototype =
    * user-selected stack frame.
    *
    * For the above to work we need the debugger and the Web Console to share
    * a connection, otherwise the Web Console actor will not find the frame
    * actor.
    *
    * The Debugger.Frame comes from the jsdebugger's Debugger instance, which
    * is different from the Web Console's Debugger instance. This means that
-   * for evaluation to work, we need to create a new instance for the jsterm
-   * helpers - they need to be Debugger.Objects coming from the jsdebugger's
-   * Debugger instance.
+   * for evaluation to work, we need to create a new instance for the Web
+   * Console Commands helpers - they need to be Debugger.Objects coming from the
+   * jsdebugger's Debugger instance.
    *
    * When |bindObjectActor| is used objects can come from different iframes,
    * from different domains. To avoid permission-related errors when objects
    * come from a different window, we also determine the object's own global,
    * such that evaluation happens in the context of that global. This means that
    * evaluation will happen in the object's iframe, rather than the top level
    * window.
    *
@@ -1064,17 +1066,18 @@ WebConsoleActor.prototype =
    *        node, like $0.
    * @return object
    *         An object that holds the following properties:
    *         - dbg: the debugger where the string was evaluated.
    *         - frame: (optional) the frame where the string was evaluated.
    *         - window: the Debugger.Object for the global where the string was
    *         evaluated.
    *         - result: the result of the evaluation.
-   *         - helperResult: any result coming from a JSTerm helper function.
+   *         - helperResult: any result coming from a Web Console commands
+   *         function.
    *         - url: the url to evaluate the script as. Defaults to
    *         "debugger eval code".
    */
   evalWithDebugger: function WCA_evalWithDebugger(aString, aOptions = {})
   {
     let trimmedString = aString.trim();
     // The help function needs to be easy to guess, so we make the () optional.
     if (trimmedString == "help" || trimmedString == "?") {
@@ -1120,32 +1123,33 @@ WebConsoleActor.prototype =
         // that is, without wrappers. The evalWithBindings call will then wrap
         // jsObj appropriately for the evaluation compartment.
         let global = Cu.getGlobalForObject(jsObj);
         dbgWindow = dbg.makeGlobalObjectReference(global);
         bindSelf = dbgWindow.makeDebuggeeValue(jsObj);
       }
     }
 
-    // Get the JSTerm helpers for the given debugger window.
-    let helpers = this._getJSTermHelpers(dbgWindow);
+    // Get the Web Console commands for the given debugger window.
+    let helpers = this._getWebConsoleCommands(dbgWindow);
     let bindings = helpers.sandbox;
     if (bindSelf) {
       bindings._self = bindSelf;
     }
 
     if (aOptions.selectedNodeActor) {
       let actor = this.conn.getActor(aOptions.selectedNodeActor);
       if (actor) {
         helpers.selectedNode = actor.rawNode;
       }
     }
 
     // Check if the Debugger.Frame or Debugger.Object for the global include
-    // $ or $$. We will not overwrite these functions with the jsterm helpers.
+    // $ or $$. We will not overwrite these functions with the Web Console
+    // commands.
     let found$ = false, found$$ = false;
     if (frame) {
       let env = frame.environment;
       if (env) {
         found$ = !!env.find("$");
         found$$ = !!env.find("$$");
       }
     }
--- a/toolkit/devtools/server/tests/browser/browser_animation_actors_11.js
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_11.js
@@ -22,28 +22,34 @@ add_task(function*() {
   info("Retrieve an animated node");
   let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
 
   info("Retrieve the animation player for the node");
   let [player] = yield animations.getAnimationPlayersForNode(node);
 
   ok(player.setCurrentTime, "Player has the setCurrentTime method");
 
-  info("Set the current time to currentTime + 5s");
-  yield player.setCurrentTime(player.initialState.currentTime + 5000);
+  info("Check that the setCurrentTime method can be called");
+  // Note that we don't check that it sets the animation to the right time here,
+  // this is too prone to intermittent failures, we'll do this later after
+  // pausing the animation. Here we merely test that the method doesn't fail.
+  yield player.setCurrentTime(player.initialState.currentTime + 1000);
 
-  // We're not really interested in making sure the currentTime is set precisely
-  // and doing this could lead to intermittents, so only check that the new time
-  // is now above the initial time.
-  let state = yield player.getCurrentState();
-  ok(state.currentTime > player.initialState.currentTime,
+  info("Pause the animation so we can really test if setCurrentTime works");
+  yield player.pause();
+  let pausedState = yield player.getCurrentState();
+
+  info("Set the current time to currentTime + 5s");
+  yield player.setCurrentTime(pausedState.currentTime + 5000);
+
+  let updatedState1 = yield player.getCurrentState();
+  is(Math.round(updatedState1.currentTime - pausedState.currentTime), 5000,
     "The currentTime was updated to +5s");
 
   info("Set the current time to currentTime - 2s");
-  yield player.setCurrentTime(state.currentTime - 2000);
-
-  let newState = yield player.getCurrentState();
-  ok(newState.currentTime < state.currentTime,
+  yield player.setCurrentTime(updatedState1.currentTime - 2000);
+  let updatedState2 = yield player.getCurrentState();
+  is(Math.round(updatedState2.currentTime - updatedState1.currentTime), -2000,
     "The currentTime was updated to -2s");
 
   yield closeDebuggerClient(client);
   gBrowser.removeCurrentTab();
 });
--- a/toolkit/devtools/webconsole/test/chrome.ini
+++ b/toolkit/devtools/webconsole/test/chrome.ini
@@ -6,16 +6,17 @@ support-files =
   data.json
   data.json^headers^
   network_requests_iframe.html
   sandboxed_iframe.html
 
 [test_basics.html]
 [test_bug819670_getter_throws.html]
 [test_cached_messages.html]
+[test_commands_registration.html]
 [test_consoleapi.html]
 [test_consoleapi_innerID.html]
 [test_file_uri.html]
 [test_reflow.html]
 [test_jsterm.html]
 [test_jsterm_cd_iframe.html]
 [test_jsterm_last_result.html]
 [test_network_get.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/test_commands_registration.html
@@ -0,0 +1,159 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+  <meta charset="utf8">
+  <title>Test for Web Console commands registration.</title>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript;version=1.8" src="common.js"></script>
+  <!-- Any copyright is dedicated to the Public Domain.
+     - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for Web Console commands registration.</p>
+<p id="quack"></p>
+
+<script class="testbody" type="text/javascript;version=1.8">
+SimpleTest.waitForExplicitFinish();
+
+let gState;
+let tests;
+
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let {WebConsoleCommands} = devtools.require("devtools/toolkit/webconsole/utils");
+
+function evaluateJS(input) {
+  return new Promise((resolve) => gState.client.evaluateJS(input, resolve));
+}
+
+function startTest()
+{
+  removeEventListener("load", startTest);
+
+  attachConsole(["PageError"], onAttach, true);
+}
+
+function onAttach(aState, aResponse)
+{
+  gState = aState;
+
+  runTests(tests, testEnd);
+}
+
+tests = [
+  Task.async(function* registerNewCommand() {
+    let win;
+    WebConsoleCommands.register("setFoo", (owner, value) => {
+      owner.window.foo = value;
+      return "ok";
+    });
+
+    ok(WebConsoleCommands.hasCommand("setFoo"),
+        "The command should be registered");
+
+    let command = "setFoo('bar')";
+    let response = yield evaluateJS(command);
+
+    checkObject(response, {
+      from: gState.actor,
+      input: command,
+      result: "ok"
+    });
+    is(top.foo, "bar", "top.foo should equal to 'bar'");
+    nextTest();
+  }),
+
+  Task.async(function* wrapCommand() {
+    let origKeys = WebConsoleCommands.getCommand("keys");
+
+    let newKeys = (...args) => {
+      let [owner, arg0] = args;
+      if (arg0 === ">o_/") {
+        return "bang!";
+      }
+      else {
+        return origKeys(...args);
+      }
+    };
+
+    WebConsoleCommands.register("keys", newKeys);
+    is(WebConsoleCommands.getCommand("keys"), newKeys,
+        "the keys() command should have been replaced");
+
+    let response = yield evaluateJS("keys('>o_/')");
+    checkObject(response, {
+      from: gState.actor,
+      result: "bang!"
+    });
+
+    response = yield evaluateJS("keys({foo: 'bar'})");
+    checkObject(response, {
+      from: gState.actor,
+      result: {
+        class: "Array",
+        preview: {
+          items: ["foo"]
+        }
+      }
+    });
+
+    WebConsoleCommands.register("keys", origKeys);
+    is(WebConsoleCommands.getCommand("keys"), origKeys,
+      "the keys() command should be restored");
+    nextTest();
+  }),
+
+  Task.async(function* unregisterCommand() {
+    WebConsoleCommands.unregister("setFoo");
+
+    let response = yield evaluateJS("setFoo");
+
+    checkObject(response, {
+      from: gState.actor,
+      input: "setFoo",
+      result: {
+        type: "undefined"
+      },
+      exceptionMessage: /setFoo is not defined/
+    });
+    nextTest();
+  }),
+
+  Task.async(function* registerAccessor() {
+    WebConsoleCommands.register("$foo", {
+      get(owner) {
+        let foo = owner.window.frames[0].window.document.getElementById("quack");
+        return owner.makeDebuggeeValue(foo);
+      }
+    });
+    let command = "$foo.textContent = '>o_/'";
+    let response = yield evaluateJS(command);
+
+    checkObject(response, {
+      from: gState.actor,
+      input: command,
+      result: ">o_/"
+    });
+    is(document.getElementById("quack").textContent, ">o_/",
+        "#foo textContent should equal to \">o_/\"");
+    WebConsoleCommands.unregister("$foo");
+    ok(!WebConsoleCommands.hasCommand("$foo"), "$foo should be unregistered");
+    nextTest();
+  })
+];
+
+function testEnd()
+{
+  // If this is the first run, reload the page and do it again.
+  // Otherwise, end the test.
+  delete top.foo;
+  closeDebugger(gState, function() {
+    gState = null;
+    SimpleTest.finish();
+  });
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
+
--- a/toolkit/devtools/webconsole/utils.js
+++ b/toolkit/devtools/webconsole/utils.js
@@ -9,17 +9,17 @@
 const {Cc, Ci, Cu, components} = require("chrome");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
 loader.lazyImporter(this, "LayoutHelpers", "resource://gre/modules/devtools/LayoutHelpers.jsm");
 
 // TODO: Bug 842672 - toolkit/ imports modules from browser/.
-// Note that these are only used in JSTermHelpers, see $0 and pprint().
+// Note that these are only used in WebConsoleCommands, see $0 and pprint().
 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
 loader.lazyImporter(this, "devtools", "resource://gre/modules/devtools/Loader.jsm");
 loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm");
 loader.lazyImporter(this, "DevToolsUtils", "resource://gre/modules/devtools/DevToolsUtils.jsm");
 
 // Match the function name from the result of toString() or toSource().
 //
 // Examples:
@@ -27,17 +27,17 @@ loader.lazyImporter(this, "DevToolsUtils
 // function foobar2(a) { ...
 // function() { ...
 const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/;
 
 // Match the function arguments from the result of toString() or toSource().
 const REGEX_MATCH_FUNCTION_ARGS = /^\(?function\s*[^\s(]*\s*\((.+?)\)/;
 
 // Number of terminal entries for the self-xss prevention to go away
-const CONSOLE_ENTRY_THRESHOLD = 5
+const CONSOLE_ENTRY_THRESHOLD = 5;
 
 // Provide an easy way to bail out of even attempting an autocompletion
 // if an object has way too many properties. Protects against large objects
 // with numeric values that wouldn't be tallied towards MAX_AUTOCOMPLETIONS.
 const MAX_AUTOCOMPLETE_ATTEMPTS = exports.MAX_AUTOCOMPLETE_ATTEMPTS = 100000;
 
 // Prevent iterating over too many properties during autocomplete suggestions.
 const MAX_AUTOCOMPLETIONS = exports.MAX_AUTOCOMPLETIONS = 1500;
@@ -545,17 +545,17 @@ let WebConsoleUtils = {
    * Value of devtools.selfxss.count preference
    *
    * @type number
    * @private
    */
   _usageCount: 0,
   get usageCount() {
     if (WebConsoleUtils._usageCount < CONSOLE_ENTRY_THRESHOLD) {
-      WebConsoleUtils._usageCount = Services.prefs.getIntPref("devtools.selfxss.count")
+      WebConsoleUtils._usageCount = Services.prefs.getIntPref("devtools.selfxss.count");
       if (Services.prefs.getBoolPref("devtools.chrome.enabled")) {
         WebConsoleUtils.usageCount = CONSOLE_ENTRY_THRESHOLD;
       }
     }
     return WebConsoleUtils._usageCount;
   },
   set usageCount(newUC) {
     if (newUC <= CONSOLE_ENTRY_THRESHOLD) {
@@ -907,17 +907,17 @@ function JSPropertyProvider(aDbgObject, 
   // We get the rest of the properties recursively starting from the Debugger.Object
   // that wraps the first property
   for (let prop of properties) {
     prop = prop.trim();
     if (!prop) {
       return null;
     }
 
-    if (/\[\d+\]$/.test(prop)) {
+    if (/\[\d+\]$/.test(prop)) {
       // The property to autocomplete is a member of array. For example
       // list[i][j]..[n]. Traverse the array to get the actual element.
       obj = getArrayMemberProperty(obj, prop);
     }
     else {
       obj = DevToolsUtils.getProperty(obj, prop);
     }
 
@@ -1502,317 +1502,416 @@ ConsoleAPIListener.prototype =
    */
   destroy: function CAL_destroy()
   {
     Services.obs.removeObserver(this, "console-api-log-event");
     this.window = this.owner = null;
   },
 };
 
+/**
+ * WebConsole commands manager.
+ *
+ * Defines a set of functions /variables ("commands") that are available from
+ * the Web Console but not from the web page.
+ *
+ */
+let WebConsoleCommands = {
+  _registeredCommands: new Map(),
+
+  /**
+   * Register a new command.
+   * @param {string} name The command name (exemple: "$")
+   * @param {(function|object)} command The command to register.
+   *  It can be a function so the command is a function (like "$()"),
+   *  or it can also be a property descriptor to describe a getter / value (like
+   *  "$0").
+   *
+   *  The command function or the command getter are passed a owner object as
+   *  their first parameter (see the example below).
+   *
+   *  Note that setters don't work currently and "enumerable" and "configurable"
+   *  are forced to true.
+   *
+   * @example
+   *
+   *   WebConsoleCommands.register("$", function JSTH_$(aOwner, aSelector)
+   *   {
+   *     return aOwner.window.document.querySelector(aSelector);
+   *   });
+   *
+   *   WebConsoleCommands.register("$0", {
+   *     get: function(aOwner) {
+   *       return aOwner.makeDebuggeeValue(aOwner.selectedNode);
+   *     }
+   *   });
+   */
+  register: function(name, command) {
+    this._registeredCommands.set(name, command);
+  },
+
+  /**
+   * Unregister a command.
+   *
+   * @param {string} name The name of the command
+   */
+  unregister: function(name) {
+    this._registeredCommands.delete(name);
+  },
+
+  /**
+   * Returns a command by its name.
+   *
+   * @param {string} name The name of the command.
+   *
+   * @return {(function|object)} The command.
+   */
+  getCommand: function(name) {
+    return this._registeredCommands.get(name);
+  },
+
+  /**
+   * Returns true if a command is registered with the given name.
+   *
+   * @param {string} name The name of the command.
+   *
+   * @return {boolean} True if the command is registered.
+   */
+  hasCommand: function(name) {
+    return this._registeredCommands.has(name);
+  },
+};
+
+exports.WebConsoleCommands = WebConsoleCommands;
+
+
+/*
+ * Built-in commands.
+  *
+  * A list of helper functions used by Firebug can be found here:
+  *   http://getfirebug.com/wiki/index.php/Command_Line_API
+ */
+
+/**
+ * Find a node by ID.
+ *
+ * @param string aId
+ *        The ID of the element you want.
+ * @return nsIDOMNode or null
+ *         The result of calling document.querySelector(aSelector).
+ */
+WebConsoleCommands.register("$", function JSTH_$(aOwner, aSelector)
+{
+  return aOwner.window.document.querySelector(aSelector);
+});
+
+/**
+ * Find the nodes matching a CSS selector.
+ *
+ * @param string aSelector
+ *        A string that is passed to window.document.querySelectorAll.
+ * @return nsIDOMNodeList
+ *         Returns the result of document.querySelectorAll(aSelector).
+ */
+WebConsoleCommands.register("$$", function JSTH_$$(aOwner, aSelector)
+{
+  return aOwner.window.document.querySelectorAll(aSelector);
+});
+
+/**
+ * Returns the result of the last console input evaluation
+ *
+ * @return object|undefined
+ * Returns last console evaluation or undefined
+ */
+WebConsoleCommands.register("$_", {
+  get: function(aOwner) {
+    return aOwner.consoleActor.getLastConsoleInputEvaluation();
+  }
+});
 
 
 /**
- * JSTerm helper functions.
- *
- * Defines a set of functions ("helper functions") that are available from the
- * Web Console but not from the web page.
+ * Runs an xPath query and returns all matched nodes.
  *
- * A list of helper functions used by Firebug can be found here:
- *   http://getfirebug.com/wiki/index.php/Command_Line_API
- *
- * @param object aOwner
- *        The owning object.
+ * @param string aXPath
+ *        xPath search query to execute.
+ * @param [optional] nsIDOMNode aContext
+ *        Context to run the xPath query on. Uses window.document if not set.
+ * @return array of nsIDOMNode
  */
-function JSTermHelpers(aOwner)
+WebConsoleCommands.register("$x", function JSTH_$x(aOwner, aXPath, aContext)
 {
-  /**
-   * Find a node by ID.
-   *
-   * @param string aId
-   *        The ID of the element you want.
-   * @return nsIDOMNode or null
-   *         The result of calling document.querySelector(aSelector).
-   */
-  aOwner.sandbox.$ = function JSTH_$(aSelector)
-  {
-    return aOwner.window.document.querySelector(aSelector);
-  };
+  let nodes = new aOwner.window.wrappedJSObject.Array();
+  let doc = aOwner.window.document;
+  aContext = aContext || doc;
+
+  let results = doc.evaluate(aXPath, aContext, null,
+                             Ci.nsIDOMXPathResult.ANY_TYPE, null);
+  let node;
+  while ((node = results.iterateNext())) {
+    nodes.push(node);
+  }
+
+  return nodes;
+});
 
-  /**
-   * Find the nodes matching a CSS selector.
-   *
-   * @param string aSelector
-   *        A string that is passed to window.document.querySelectorAll.
-   * @return nsIDOMNodeList
-   *         Returns the result of document.querySelectorAll(aSelector).
-   */
-  aOwner.sandbox.$$ = function JSTH_$$(aSelector)
-  {
-    return aOwner.window.document.querySelectorAll(aSelector);
-  };
+/**
+ * Returns the currently selected object in the highlighter.
+ *
+ * @return Object representing the current selection in the
+ *         Inspector, or null if no selection exists.
+ */
+WebConsoleCommands.register("$0", {
+  get: function(aOwner) {
+    return aOwner.makeDebuggeeValue(aOwner.selectedNode);
+  }
+});
 
-  /**
-   * Returns the result of the last console input evaluation
-   *
-   * @return object|undefined
-   * Returns last console evaluation or undefined
-   */
-  Object.defineProperty(aOwner.sandbox, "$_", {
-    get: function() {
-      return aOwner.consoleActor.getLastConsoleInputEvaluation();
-    },
-    enumerable: true,
-    configurable: true
-  });
+/**
+ * Clears the output of the WebConsole.
+ */
+WebConsoleCommands.register("clear", function JSTH_clear(aOwner)
+{
+  aOwner.helperResult = {
+    type: "clearOutput",
+  };
+});
 
-  /**
-   * Runs an xPath query and returns all matched nodes.
-   *
-   * @param string aXPath
-   *        xPath search query to execute.
-   * @param [optional] nsIDOMNode aContext
-   *        Context to run the xPath query on. Uses window.document if not set.
-   * @return array of nsIDOMNode
-   */
-  aOwner.sandbox.$x = function JSTH_$x(aXPath, aContext)
-  {
-    let nodes = new aOwner.window.wrappedJSObject.Array();
-    let doc = aOwner.window.document;
-    aContext = aContext || doc;
+/**
+ * Clears the input history of the WebConsole.
+ */
+WebConsoleCommands.register("clearHistory", function JSTH_clearHistory(aOwner)
+{
+  aOwner.helperResult = {
+    type: "clearHistory",
+  };
+});
 
-    let results = doc.evaluate(aXPath, aContext, null,
-                               Ci.nsIDOMXPathResult.ANY_TYPE, null);
-    let node;
-    while ((node = results.iterateNext())) {
-      nodes.push(node);
-    }
-
-    return nodes;
-  };
+/**
+ * Returns the result of Object.keys(aObject).
+ *
+ * @param object aObject
+ *        Object to return the property names from.
+ * @return array of strings
+ */
+WebConsoleCommands.register("keys", function JSTH_keys(aOwner, aObject)
+{
+  return aOwner.window.wrappedJSObject.Object.keys(WebConsoleUtils.unwrap(aObject));
+});
 
-  /**
-   * Returns the currently selected object in the highlighter.
-   *
-   * @return Object representing the current selection in the
-   *         Inspector, or null if no selection exists.
-   */
-  Object.defineProperty(aOwner.sandbox, "$0", {
-    get: function() {
-      return aOwner.makeDebuggeeValue(aOwner.selectedNode)
-    },
-    enumerable: true,
-    configurable: true
-  });
+/**
+ * Returns the values of all properties on aObject.
+ *
+ * @param object aObject
+ *        Object to display the values from.
+ * @return array of string
+ */
+WebConsoleCommands.register("values", function JSTH_values(aOwner, aObject)
+{
+  let arrValues = new aOwner.window.wrappedJSObject.Array();
+  let obj = WebConsoleUtils.unwrap(aObject);
 
-  /**
-   * Clears the output of the JSTerm.
-   */
-  aOwner.sandbox.clear = function JSTH_clear()
-  {
-    aOwner.helperResult = {
-      type: "clearOutput",
-    };
-  };
+  for (let prop in obj) {
+    arrValues.push(obj[prop]);
+  }
+
+  return arrValues;
+});
 
-  /**
-   * Clears the input history of the JSTerm.
-   */
-  aOwner.sandbox.clearHistory = function JSTH_clearHistory()
-  {
-    aOwner.helperResult = {
-      type: "clearHistory",
-    };
-  };
+/**
+ * Opens a help window in MDN.
+ */
+WebConsoleCommands.register("help", function JSTH_help(aOwner)
+{
+  aOwner.helperResult = { type: "help" };
+});
 
-  /**
-   * Returns the result of Object.keys(aObject).
-   *
-   * @param object aObject
-   *        Object to return the property names from.
-   * @return array of strings
-   */
-  aOwner.sandbox.keys = function JSTH_keys(aObject)
-  {
-    return aOwner.window.wrappedJSObject.Object.keys(WebConsoleUtils.unwrap(aObject));
-  };
+/**
+ * Change the JS evaluation scope.
+ *
+ * @param DOMElement|string|window aWindow
+ *        The window object to use for eval scope. This can be a string that
+ *        is used to perform document.querySelector(), to find the iframe that
+ *        you want to cd() to. A DOMElement can be given as well, the
+ *        .contentWindow property is used. Lastly, you can directly pass
+ *        a window object. If you call cd() with no arguments, the current
+ *        eval scope is cleared back to its default (the top window).
+ */
+WebConsoleCommands.register("cd", function JSTH_cd(aOwner, aWindow)
+{
+  if (!aWindow) {
+    aOwner.consoleActor.evalWindow = null;
+    aOwner.helperResult = { type: "cd" };
+    return;
+  }
 
-  /**
-   * Returns the values of all properties on aObject.
-   *
-   * @param object aObject
-   *        Object to display the values from.
-   * @return array of string
-   */
-  aOwner.sandbox.values = function JSTH_values(aObject)
-  {
-    let arrValues = new aOwner.window.wrappedJSObject.Array();
-    let obj = WebConsoleUtils.unwrap(aObject);
+  if (typeof aWindow == "string") {
+    aWindow = aOwner.window.document.querySelector(aWindow);
+  }
+  if (aWindow instanceof Ci.nsIDOMElement && aWindow.contentWindow) {
+    aWindow = aWindow.contentWindow;
+  }
+  if (!(aWindow instanceof Ci.nsIDOMWindow)) {
+    aOwner.helperResult = { type: "error", message: "cdFunctionInvalidArgument" };
+    return;
+  }
 
-    for (let prop in obj) {
-      arrValues.push(obj[prop]);
-    }
-
-    return arrValues;
-  };
+  aOwner.consoleActor.evalWindow = aWindow;
+  aOwner.helperResult = { type: "cd" };
+});
 
-  /**
-   * Opens a help window in MDN.
-   */
-  aOwner.sandbox.help = function JSTH_help()
-  {
-    aOwner.helperResult = { type: "help" };
+/**
+ * Inspects the passed aObject. This is done by opening the PropertyPanel.
+ *
+ * @param object aObject
+ *        Object to inspect.
+ */
+WebConsoleCommands.register("inspect", function JSTH_inspect(aOwner, aObject)
+{
+  let dbgObj = aOwner.makeDebuggeeValue(aObject);
+  let grip = aOwner.createValueGrip(dbgObj);
+  aOwner.helperResult = {
+    type: "inspectObject",
+    input: aOwner.evalInput,
+    object: grip,
   };
-
-  /**
-   * Change the JS evaluation scope.
-   *
-   * @param DOMElement|string|window aWindow
-   *        The window object to use for eval scope. This can be a string that
-   *        is used to perform document.querySelector(), to find the iframe that
-   *        you want to cd() to. A DOMElement can be given as well, the
-   *        .contentWindow property is used. Lastly, you can directly pass
-   *        a window object. If you call cd() with no arguments, the current
-   *        eval scope is cleared back to its default (the top window).
-   */
-  aOwner.sandbox.cd = function JSTH_cd(aWindow)
-  {
-    if (!aWindow) {
-      aOwner.consoleActor.evalWindow = null;
-      aOwner.helperResult = { type: "cd" };
-      return;
-    }
-
-    if (typeof aWindow == "string") {
-      aWindow = aOwner.window.document.querySelector(aWindow);
-    }
-    if (aWindow instanceof Ci.nsIDOMElement && aWindow.contentWindow) {
-      aWindow = aWindow.contentWindow;
-    }
-    if (!(aWindow instanceof Ci.nsIDOMWindow)) {
-      aOwner.helperResult = { type: "error", message: "cdFunctionInvalidArgument" };
-      return;
-    }
+});
 
-    aOwner.consoleActor.evalWindow = aWindow;
-    aOwner.helperResult = { type: "cd" };
-  };
-
-  /**
-   * Inspects the passed aObject. This is done by opening the PropertyPanel.
-   *
-   * @param object aObject
-   *        Object to inspect.
-   */
-  aOwner.sandbox.inspect = function JSTH_inspect(aObject)
-  {
-    let dbgObj = aOwner.makeDebuggeeValue(aObject);
-    let grip = aOwner.createValueGrip(dbgObj);
+/**
+ * Prints aObject to the output.
+ *
+ * @param object aObject
+ *        Object to print to the output.
+ * @return string
+ */
+WebConsoleCommands.register("pprint", function JSTH_pprint(aOwner, aObject)
+{
+  if (aObject === null || aObject === undefined || aObject === true ||
+      aObject === false) {
     aOwner.helperResult = {
-      type: "inspectObject",
-      input: aOwner.evalInput,
-      object: grip,
+      type: "error",
+      message: "helperFuncUnsupportedTypeError",
     };
-  };
+    return null;
+  }
+
+  aOwner.helperResult = { rawOutput: true };
+
+  if (typeof aObject == "function") {
+    return aObject + "\n";
+  }
+
+  let output = [];
 
-  /**
-   * Prints aObject to the output.
-   *
-   * @param object aObject
-   *        Object to print to the output.
-   * @return string
-   */
-  aOwner.sandbox.pprint = function JSTH_pprint(aObject)
-  {
-    if (aObject === null || aObject === undefined || aObject === true ||
-        aObject === false) {
-      aOwner.helperResult = {
-        type: "error",
-        message: "helperFuncUnsupportedTypeError",
-      };
-      return null;
+  let obj = WebConsoleUtils.unwrap(aObject);
+  for (let name in obj) {
+    let desc = WebConsoleUtils.getPropertyDescriptor(obj, name) || {};
+    if (desc.get || desc.set) {
+      // TODO: Bug 842672 - toolkit/ imports modules from browser/.
+      let getGrip = VariablesView.getGrip(desc.get);
+      let setGrip = VariablesView.getGrip(desc.set);
+      let getString = VariablesView.getString(getGrip);
+      let setString = VariablesView.getString(setGrip);
+      output.push(name + ":", "  get: " + getString, "  set: " + setString);
     }
+    else {
+      let valueGrip = VariablesView.getGrip(obj[name]);
+      let valueString = VariablesView.getString(valueGrip);
+      output.push(name + ": " + valueString);
+    }
+  }
+
+  return "  " + output.join("\n  ");
+});
 
-    aOwner.helperResult = { rawOutput: true };
-
-    if (typeof aObject == "function") {
-      return aObject + "\n";
-    }
-
-    let output = [];
-
-    let obj = WebConsoleUtils.unwrap(aObject);
-    for (let name in obj) {
-      let desc = WebConsoleUtils.getPropertyDescriptor(obj, name) || {};
-      if (desc.get || desc.set) {
-        // TODO: Bug 842672 - toolkit/ imports modules from browser/.
-        let getGrip = VariablesView.getGrip(desc.get);
-        let setGrip = VariablesView.getGrip(desc.set);
-        let getString = VariablesView.getString(getGrip);
-        let setString = VariablesView.getString(setGrip);
-        output.push(name + ":", "  get: " + getString, "  set: " + setString);
-      }
-      else {
-        let valueGrip = VariablesView.getGrip(obj[name]);
-        let valueString = VariablesView.getString(valueGrip);
-        output.push(name + ": " + valueString);
-      }
-    }
-
-    return "  " + output.join("\n  ");
-  };
+/**
+ * Print the String representation of a value to the output, as-is.
+ *
+ * @param any aValue
+ *        A value you want to output as a string.
+ * @return void
+ */
+WebConsoleCommands.register("print", function JSTH_print(aOwner, aValue)
+{
+  aOwner.helperResult = { rawOutput: true };
+  if (typeof aValue === "symbol") {
+    return Symbol.prototype.toString.call(aValue);
+  }
+  // Waiving Xrays here allows us to see a closer representation of the
+  // underlying object. This may execute arbitrary content code, but that
+  // code will run with content privileges, and the result will be rendered
+  // inert by coercing it to a String.
+  return String(Cu.waiveXrays(aValue));
+});
 
-  /**
-   * Print the String representation of a value to the output, as-is.
-   *
-   * @param any aValue
-   *        A value you want to output as a string.
-   * @return void
-   */
-  aOwner.sandbox.print = function JSTH_print(aValue)
-  {
-    aOwner.helperResult = { rawOutput: true };
-    if (typeof aValue === "symbol") {
-      return Symbol.prototype.toString.call(aValue);
+/**
+ * Copy the String representation of a value to the clipboard.
+ *
+ * @param any aValue
+ *        A value you want to copy as a string.
+ * @return void
+ */
+WebConsoleCommands.register("copy", function JSTH_copy(aOwner, aValue)
+{
+  let payload;
+  try {
+    if (aValue instanceof Ci.nsIDOMElement) {
+      payload = aValue.outerHTML;
+    } else if (typeof aValue == "string") {
+      payload = aValue;
+    } else {
+      payload = JSON.stringify(aValue, null, "  ");
     }
-    // Waiving Xrays here allows us to see a closer representation of the
-    // underlying object. This may execute arbitrary content code, but that
-    // code will run with content privileges, and the result will be rendered
-    // inert by coercing it to a String.
-    return String(Cu.waiveXrays(aValue));
+  } catch (ex) {
+    payload = "/* " + ex  + " */";
+  }
+  aOwner.helperResult = {
+    type: "copyValueToClipboard",
+    value: payload,
   };
+});
+
 
-  /**
-   * Copy the String representation of a value to the clipboard.
-   *
-   * @param any aValue
-   *        A value you want to copy as a string.
-   * @return void
-   */
-  aOwner.sandbox.copy = function JSTH_copy(aValue)
-  {
-    let payload;
-    try {
-      if (aValue instanceof Ci.nsIDOMElement) {
-        payload = aValue.outerHTML;
-      } else if (typeof aValue == "string") {
-        payload = aValue;
-      } else {
-        payload = JSON.stringify(aValue, null, "  ");
+/**
+ * (Internal only) Add the bindings to |owner.sandbox|.
+ * This is intended to be used by the WebConsole actor only.
+  *
+  * @param object aOwner
+  *        The owning object.
+  */
+function addWebConsoleCommands(owner) {
+  if (!owner) {
+    throw new Error("The owner is required");
+  }
+  for (let [name, command] of WebConsoleCommands._registeredCommands) {
+    if (typeof command === "function") {
+      owner.sandbox[name] = command.bind(undefined, owner);
+    }
+    else if (typeof command === "object") {
+      let clone = Object.assign({}, command, {
+        // We force the enumerability and the configurability (so the
+        // WebConsoleActor can reconfigure the property).
+        enumerable: true,
+        configurable: true
+      });
+
+      if (typeof command.get === "function") {
+        clone.get = command.get.bind(undefined, owner);
       }
-    } catch (ex) {
-      payload = "/* " + ex  + " */";
+      if (typeof command.set === "function") {
+        clone.set = command.set.bind(undefined, owner);
+      }
+
+      Object.defineProperty(owner.sandbox, name, clone);
     }
-    aOwner.helperResult = {
-      type: "copyValueToClipboard",
-      value: payload,
-    };
-  };
+  }
 }
-exports.JSTermHelpers = JSTermHelpers;
 
+exports.addWebConsoleCommands = addWebConsoleCommands;
 
 /**
  * A ReflowObserver that listens for reflow events from the page.
  * Implements nsIReflowObserver.
  *
  * @constructor
  * @param object aWindow
  *        The window for which we need to track reflow.
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/SessionRecorder.jsm
@@ -0,0 +1,406 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MERGED_COMPARTMENT
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "SessionRecorder",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+#endif
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://services-common/utils.js");
+
+
+// We automatically prune sessions older than this.
+const MAX_SESSION_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days.
+const STARTUP_RETRY_INTERVAL_MS = 5000;
+
+// Wait up to 5 minutes for startup measurements before giving up.
+const MAX_STARTUP_TRIES = 300000 / STARTUP_RETRY_INTERVAL_MS;
+
+/**
+ * Records information about browser sessions.
+ *
+ * This serves as an interface to both current session information as
+ * well as a history of previous sessions.
+ *
+ * Typically only one instance of this will be installed in an
+ * application. It is typically managed by an XPCOM service. The
+ * instance is instantiated at application start; onStartup is called
+ * once the profile is installed; onShutdown is called during shutdown.
+ *
+ * We currently record state in preferences. However, this should be
+ * invisible to external consumers. We could easily swap in a different
+ * storage mechanism if desired.
+ *
+ * Please note the different semantics for storing times and dates in
+ * preferences. Full dates (notably the session start time) are stored
+ * as strings because preferences have a 32-bit limit on integer values
+ * and milliseconds since UNIX epoch would overflow. Many times are
+ * stored as integer offsets from the session start time because they
+ * should not overflow 32 bits.
+ *
+ * Since this records history of all sessions, there is a possibility
+ * for unbounded data aggregation. This is curtailed through:
+ *
+ *   1) An "idle-daily" observer which delete sessions older than
+ *      MAX_SESSION_AGE_MS.
+ *   2) The creator of this instance explicitly calling
+ *      `pruneOldSessions`.
+ *
+ * @param branch
+ *        (string) Preferences branch on which to record state.
+ */
+this.SessionRecorder = function (branch) {
+  if (!branch) {
+    throw new Error("branch argument must be defined.");
+  }
+
+  if (!branch.endsWith(".")) {
+    throw new Error("branch argument must end with '.': " + branch);
+  }
+
+  this._log = Log.repository.getLogger("Services.DataReporting.SessionRecorder");
+
+  this._prefs = new Preferences(branch);
+  this._lastActivityWasInactive = false;
+  this._activeTicks = 0;
+  this.fineTotalTime = 0;
+  this._started = false;
+  this._timer = null;
+  this._startupFieldTries = 0;
+
+  this._os = Cc["@mozilla.org/observer-service;1"]
+               .getService(Ci.nsIObserverService);
+
+};
+
+SessionRecorder.prototype = Object.freeze({
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+  STARTUP_RETRY_INTERVAL_MS: STARTUP_RETRY_INTERVAL_MS,
+
+  get _currentIndex() {
+    return this._prefs.get("currentIndex", 0);
+  },
+
+  set _currentIndex(value) {
+    this._prefs.set("currentIndex", value);
+  },
+
+  get _prunedIndex() {
+    return this._prefs.get("prunedIndex", 0);
+  },
+
+  set _prunedIndex(value) {
+    this._prefs.set("prunedIndex", value);
+  },
+
+  get startDate() {
+    return CommonUtils.getDatePref(this._prefs, "current.startTime");
+  },
+
+  set _startDate(value) {
+    CommonUtils.setDatePref(this._prefs, "current.startTime", value);
+  },
+
+  get activeTicks() {
+    return this._prefs.get("current.activeTicks", 0);
+  },
+
+  incrementActiveTicks: function () {
+    this._prefs.set("current.activeTicks", ++this._activeTicks);
+  },
+
+  /**
+   * Total time of this session in integer seconds.
+   *
+   * See also fineTotalTime for the time in milliseconds.
+   */
+  get totalTime() {
+    return this._prefs.get("current.totalTime", 0);
+  },
+
+  updateTotalTime: function () {
+    // We store millisecond precision internally to prevent drift from
+    // repeated rounding.
+    this.fineTotalTime = Date.now() - this.startDate;
+    this._prefs.set("current.totalTime", Math.floor(this.fineTotalTime / 1000));
+  },
+
+  get main() {
+    return this._prefs.get("current.main", -1);
+  },
+
+  set _main(value) {
+    if (!Number.isInteger(value)) {
+      throw new Error("main time must be an integer.");
+    }
+
+    this._prefs.set("current.main", value);
+  },
+
+  get firstPaint() {
+    return this._prefs.get("current.firstPaint", -1);
+  },
+
+  set _firstPaint(value) {
+    if (!Number.isInteger(value)) {
+      throw new Error("firstPaint must be an integer.");
+    }
+
+    this._prefs.set("current.firstPaint", value);
+  },
+
+  get sessionRestored() {
+    return this._prefs.get("current.sessionRestored", -1);
+  },
+
+  set _sessionRestored(value) {
+    if (!Number.isInteger(value)) {
+      throw new Error("sessionRestored must be an integer.");
+    }
+
+    this._prefs.set("current.sessionRestored", value);
+  },
+
+  getPreviousSessions: function () {
+    let result = {};
+
+    for (let i = this._prunedIndex; i < this._currentIndex; i++) {
+      let s = this.getPreviousSession(i);
+      if (!s) {
+        continue;
+      }
+
+      result[i] = s;
+    }
+
+    return result;
+  },
+
+  getPreviousSession: function (index) {
+    return this._deserialize(this._prefs.get("previous." + index));
+  },
+
+  /**
+   * Prunes old, completed sessions that started earlier than the
+   * specified date.
+   */
+  pruneOldSessions: function (date) {
+    for (let i = this._prunedIndex; i < this._currentIndex; i++) {
+      let s = this.getPreviousSession(i);
+      if (!s) {
+        continue;
+      }
+
+      if (s.startDate >= date) {
+        continue;
+      }
+
+      this._log.debug("Pruning session #" + i + ".");
+      this._prefs.reset("previous." + i);
+      this._prunedIndex = i;
+    }
+  },
+
+  recordStartupFields: function () {
+    let si = this._getStartupInfo();
+
+    if (!si.process) {
+      throw new Error("Startup info not available.");
+    }
+
+    let missing = false;
+
+    for (let field of ["main", "firstPaint", "sessionRestored"]) {
+      if (!(field in si)) {
+        this._log.debug("Missing startup field: " + field);
+        missing = true;
+        continue;
+      }
+
+      this["_" + field] = si[field].getTime() - si.process.getTime();
+    }
+
+    if (!missing || this._startupFieldTries > MAX_STARTUP_TRIES) {
+      this._clearStartupTimer();
+      return;
+    }
+
+    // If we have missing fields, install a timer and keep waiting for
+    // data.
+    this._startupFieldTries++;
+
+    if (!this._timer) {
+      this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+      this._timer.initWithCallback({
+        notify: this.recordStartupFields.bind(this),
+      }, this.STARTUP_RETRY_INTERVAL_MS, this._timer.TYPE_REPEATING_SLACK);
+    }
+  },
+
+  _clearStartupTimer: function () {
+    if (this._timer) {
+      this._timer.cancel();
+      delete this._timer;
+    }
+  },
+
+  /**
+   * Perform functionality on application startup.
+   *
+   * This is typically called in a "profile-do-change" handler.
+   */
+  onStartup: function () {
+    if (this._started) {
+      throw new Error("onStartup has already been called.");
+    }
+
+    let si = this._getStartupInfo();
+    if (!si.process) {
+      throw new Error("Process information not available. Misconfigured app?");
+    }
+
+    this._started = true;
+
+    this._os.addObserver(this, "profile-before-change", false);
+    this._os.addObserver(this, "user-interaction-active", false);
+    this._os.addObserver(this, "user-interaction-inactive", false);
+    this._os.addObserver(this, "idle-daily", false);
+
+    // This has the side-effect of clearing current session state.
+    this._moveCurrentToPrevious();
+
+    this._startDate = si.process;
+    this._prefs.set("current.activeTicks", 0);
+    this.updateTotalTime();
+
+    this.recordStartupFields();
+  },
+
+  /**
+   * Record application activity.
+   */
+  onActivity: function (active) {
+    let updateActive = active && !this._lastActivityWasInactive;
+    this._lastActivityWasInactive = !active;
+
+    this.updateTotalTime();
+
+    if (updateActive) {
+      this.incrementActiveTicks();
+    }
+  },
+
+  onShutdown: function () {
+    this._log.info("Recording clean session shutdown.");
+    this._prefs.set("current.clean", true);
+    this.updateTotalTime();
+    this._clearStartupTimer();
+
+    this._os.removeObserver(this, "profile-before-change");
+    this._os.removeObserver(this, "user-interaction-active");
+    this._os.removeObserver(this, "user-interaction-inactive");
+    this._os.removeObserver(this, "idle-daily");
+  },
+
+  _CURRENT_PREFS: [
+    "current.startTime",
+    "current.activeTicks",
+    "current.totalTime",
+    "current.main",
+    "current.firstPaint",
+    "current.sessionRestored",
+    "current.clean",
+  ],
+
+  // This is meant to be called only during onStartup().
+  _moveCurrentToPrevious: function () {
+    try {
+      if (!this.startDate.getTime()) {
+        this._log.info("No previous session. Is this first app run?");
+        return;
+      }
+
+      let clean = this._prefs.get("current.clean", false);
+
+      let count = this._currentIndex++;
+      let obj = {
+        s: this.startDate.getTime(),
+        a: this.activeTicks,
+        t: this.totalTime,
+        c: clean,
+        m: this.main,
+        fp: this.firstPaint,
+        sr: this.sessionRestored,
+      };
+
+      this._log.debug("Recording last sessions as #" + count + ".");
+      this._prefs.set("previous." + count, JSON.stringify(obj));
+    } catch (ex) {
+      this._log.warn("Exception when migrating last session: " +
+                     CommonUtils.exceptionStr(ex));
+    } finally {
+      this._log.debug("Resetting prefs from last session.");
+      for (let pref of this._CURRENT_PREFS) {
+        this._prefs.reset(pref);
+      }
+    }
+  },
+
+  _deserialize: function (s) {
+    let o;
+    try {
+      o = JSON.parse(s);
+    } catch (ex) {
+      return null;
+    }
+
+    return {
+      startDate: new Date(o.s),
+      activeTicks: o.a,
+      totalTime: o.t,
+      clean: !!o.c,
+      main: o.m,
+      firstPaint: o.fp,
+      sessionRestored: o.sr,
+    };
+  },
+
+  // Implemented as a function to allow for monkeypatching in tests.
+  _getStartupInfo: function () {
+    return Cc["@mozilla.org/toolkit/app-startup;1"]
+             .getService(Ci.nsIAppStartup)
+             .getStartupInfo();
+  },
+
+  observe: function (subject, topic, data) {
+    switch (topic) {
+      case "profile-before-change":
+        this.onShutdown();
+        break;
+
+      case "user-interaction-active":
+        this.onActivity(true);
+        break;
+
+      case "user-interaction-inactive":
+        this.onActivity(false);
+        break;
+
+      case "idle-daily":
+        this.pruneOldSessions(new Date(Date.now() - MAX_SESSION_AGE_MS));
+        break;
+    }
+  },
+});
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -72,16 +72,17 @@ EXTRA_JS_MODULES += [
     'WebChannel.jsm',
     'WindowDraggingUtils.jsm',
     'ZipUtils.jsm',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'AppConstants.jsm',
     'Services.jsm',
+    'SessionRecorder.jsm',
     'UpdateChannel.jsm',
     'WindowsPrefSync.jsm',
 ]
 
 if 'Android' != CONFIG['OS_TARGET']:
     EXTRA_JS_MODULES += [
         'LightweightThemeConsumer.jsm',
     ]
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_session_recorder.js
@@ -0,0 +1,306 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/SessionRecorder.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-common/utils.js");
+
+
+function run_test() {
+  run_next_test();
+}
+
+function monkeypatchStartupInfo(recorder, start=new Date(), offset=500) {
+  Object.defineProperty(recorder, "_getStartupInfo", {
+    value: function _getStartupInfo() {
+      return {
+        process: start,
+        main: new Date(start.getTime() + offset),
+        firstPaint: new Date(start.getTime() + 2 * offset),
+        sessionRestored: new Date(start.getTime() + 3 * offset),
+      };
+    }
+  });
+}
+
+function sleep(wait) {
+  let deferred = Promise.defer();
+
+  let timer = CommonUtils.namedTimer(function onTimer() {
+    deferred.resolve();
+  }, wait, deferred.promise, "_sleepTimer");
+
+  return deferred.promise;
+}
+
+function getRecorder(name, start, offset) {
+  let recorder = new SessionRecorder("testing." + name + ".");
+  monkeypatchStartupInfo(recorder, start, offset);
+
+  return recorder;
+}
+
+add_test(function test_basic() {
+  let recorder = getRecorder("basic");
+  recorder.onStartup();
+  recorder.onShutdown();
+
+  run_next_test();
+});
+
+add_task(function test_current_properties() {
+  let now = new Date();
+  let recorder = getRecorder("current_properties", now);
+  yield sleep(25);
+  recorder.onStartup();
+
+  do_check_eq(recorder.startDate.getTime(), now.getTime());
+  do_check_eq(recorder.activeTicks, 0);
+  do_check_true(recorder.fineTotalTime > 0);
+  do_check_eq(recorder.main, 500);
+  do_check_eq(recorder.firstPaint, 1000);
+  do_check_eq(recorder.sessionRestored, 1500);
+
+  recorder.incrementActiveTicks();
+  do_check_eq(recorder.activeTicks, 1);
+
+  recorder._startDate = new Date(Date.now() - 1000);
+  recorder.updateTotalTime();
+  do_check_eq(recorder.totalTime, 1);
+
+  recorder.onShutdown();
+});
+
+// If startup info isn't present yet, we should install a timer and get
+// it eventually.
+add_task(function test_current_availability() {
+  let recorder = new SessionRecorder("testing.current_availability.");
+  let now = new Date();
+
+  Object.defineProperty(recorder, "_getStartupInfo", {
+    value: function _getStartupInfo() {
+      return {
+        process: now,
+        main: new Date(now.getTime() + 500),
+        firstPaint: new Date(now.getTime() + 1000),
+      };
+    },
+    writable: true,
+  });
+
+  Object.defineProperty(recorder, "STARTUP_RETRY_INTERVAL_MS", {
+    value: 100,
+  });
+
+  let oldRecord = recorder.recordStartupFields;
+  let recordCount = 0;
+
+  Object.defineProperty(recorder, "recordStartupFields", {
+    value: function () {
+      recordCount++;
+      return oldRecord.call(recorder);
+    }
+  });
+
+  do_check_null(recorder._timer);
+  recorder.onStartup();
+  do_check_eq(recordCount, 1);
+  do_check_eq(recorder.sessionRestored, -1);
+  do_check_neq(recorder._timer, null);
+
+  yield sleep(125);
+  do_check_eq(recordCount, 2);
+  yield sleep(100);
+  do_check_eq(recordCount, 3);
+  do_check_eq(recorder.sessionRestored, -1);
+
+  monkeypatchStartupInfo(recorder, now);
+  yield sleep(100);
+  do_check_eq(recordCount, 4);
+  do_check_eq(recorder.sessionRestored, 1500);
+
+  // The timer should be removed and we should not fire again.
+  do_check_null(recorder._timer);
+  yield sleep(100);
+  do_check_eq(recordCount, 4);
+
+  recorder.onShutdown();
+});
+
+add_test(function test_timer_clear_on_shutdown() {
+  let recorder = new SessionRecorder("testing.timer_clear_on_shutdown.");
+  let now = new Date();
+
+  Object.defineProperty(recorder, "_getStartupInfo", {
+    value: function _getStartupInfo() {
+      return {
+        process: now,
+        main: new Date(now.getTime() + 500),
+        firstPaint: new Date(now.getTime() + 1000),
+      };
+    },
+  });
+
+  do_check_null(recorder._timer);
+  recorder.onStartup();
+  do_check_neq(recorder._timer, null);
+
+  recorder.onShutdown();
+  do_check_null(recorder._timer);
+
+  run_next_test();
+});
+
+add_task(function test_previous_clean() {
+  let now = new Date();
+  let recorder = getRecorder("previous_clean", now);
+  yield sleep(25);
+  recorder.onStartup();
+
+  recorder.incrementActiveTicks();
+  recorder.incrementActiveTicks();
+
+  yield sleep(25);
+  recorder.onShutdown();
+
+  let total = recorder.totalTime;
+
+  yield sleep(25);
+  let now2 = new Date();
+  let recorder2 = getRecorder("previous_clean", now2, 100);
+  yield sleep(25);
+  recorder2.onStartup();
+
+  do_check_eq(recorder2.startDate.getTime(), now2.getTime());
+  do_check_eq(recorder2.main, 100);
+  do_check_eq(recorder2.firstPaint, 200);
+  do_check_eq(recorder2.sessionRestored, 300);
+
+  let sessions = recorder2.getPreviousSessions();
+  do_check_eq(Object.keys(sessions).length, 1);
+  do_check_true(0 in sessions);
+  let session = sessions[0];
+  do_check_true(session.clean);
+  do_check_eq(session.startDate.getTime(), now.getTime());
+  do_check_eq(session.main, 500);
+  do_check_eq(session.firstPaint, 1000);
+  do_check_eq(session.sessionRestored, 1500);
+  do_check_eq(session.totalTime, total);
+  do_check_eq(session.activeTicks, 2);
+
+  recorder2.onShutdown();
+});
+
+add_task(function test_previous_abort() {
+  let now = new Date();
+  let recorder = getRecorder("previous_abort", now);
+  yield sleep(25);
+  recorder.onStartup();
+  recorder.incrementActiveTicks();
+  yield sleep(25);
+  let total = recorder.totalTime;
+  yield sleep(25);
+
+  let now2 = new Date();
+  let recorder2 = getRecorder("previous_abort", now2);
+  yield sleep(25);
+  recorder2.onStartup();
+
+  let sessions = recorder2.getPreviousSessions();
+  do_check_eq(Object.keys(sessions).length, 1);
+  do_check_true(0 in sessions);
+  let session = sessions[0];
+  do_check_false(session.clean);
+  do_check_eq(session.totalTime, total);
+
+  recorder.onShutdown();
+  recorder2.onShutdown();
+});
+
+add_task(function test_multiple_sessions() {
+  for (let i = 0; i < 10; i++) {
+    let recorder = getRecorder("multiple_sessions");
+    yield sleep(25);
+    recorder.onStartup();
+    for (let j = 0; j < i; j++) {
+      recorder.incrementActiveTicks();
+    }
+    yield sleep(25);
+    recorder.onShutdown();
+    yield sleep(25);
+  }
+
+  let recorder = getRecorder("multiple_sessions");
+  recorder.onStartup();
+
+  let sessions = recorder.getPreviousSessions();
+  do_check_eq(Object.keys(sessions).length, 10);
+
+  for (let [i, session] in Iterator(sessions)) {
+    do_check_eq(session.activeTicks, i);
+
+    if (i > 0) {
+      do_check_true(session.startDate.getTime() > sessions[i-1].startDate.getTime());
+    }
+  }
+
+  // #6 is preserved since >=.
+  let threshold = sessions[6].startDate;
+  recorder.pruneOldSessions(threshold);
+
+  sessions = recorder.getPreviousSessions();
+  do_check_eq(Object.keys(sessions).length, 4);
+
+  recorder.pruneOldSessions(threshold);
+  sessions = recorder.getPreviousSessions();
+  do_check_eq(Object.keys(sessions).length, 4);
+  do_check_eq(recorder._prunedIndex, 5);
+
+  recorder.onShutdown();
+});
+
+add_task(function test_record_activity() {
+  let recorder = getRecorder("record_activity");
+  yield sleep(25);
+  recorder.onStartup();
+  let total = recorder.totalTime;
+  yield sleep(25);
+
+  for (let i = 0; i < 3; i++) {
+    Services.obs.notifyObservers(null, "user-interaction-active", null);
+    yield sleep(25);
+    do_check_true(recorder.fineTotalTime > total);
+    total = recorder.fineTotalTime;
+  }
+
+  do_check_eq(recorder.activeTicks, 3);
+
+  // Now send inactive. We should increment total time but not active.
+  Services.obs.notifyObservers(null, "user-interaction-inactive", null);
+  do_check_eq(recorder.activeTicks, 3);
+  do_check_true(recorder.fineTotalTime > total);
+  total = recorder.fineTotalTime;
+  yield sleep(25);
+
+  // If we send active again, this should be counted as inactive.
+  Services.obs.notifyObservers(null, "user-interaction-active", null);
+  do_check_eq(recorder.activeTicks, 3);
+  do_check_true(recorder.fineTotalTime > total);
+  total = recorder.fineTotalTime;
+  yield sleep(25);
+
+  // If we send active again, this should be counted as active.
+  Services.obs.notifyObservers(null, "user-interaction-active", null);
+  do_check_eq(recorder.activeTicks, 4);
+
+  Services.obs.notifyObservers(null, "user-interaction-active", null);
+  do_check_eq(recorder.activeTicks, 5);
+
+  recorder.onShutdown();
+});
+
--- a/toolkit/modules/tests/xpcshell/xpcshell.ini
+++ b/toolkit/modules/tests/xpcshell/xpcshell.ini
@@ -20,16 +20,17 @@ support-files =
 [test_ObjectUtils.js]
 [test_PermissionsUtils.js]
 [test_Preferences.js]
 [test_Promise.js]
 [test_PromiseUtils.js]
 [test_propertyListsUtils.js]
 [test_readCertPrefs.js]
 [test_Services.js]
+[test_session_recorder.js]
 [test_sqlite.js]
 [test_sqlite_shutdown.js]
 [test_task.js]
 [test_TelemetryTimestamps.js]
 [test_timer.js]
 [test_web_channel.js]
 [test_web_channel_broker.js]
 [test_ZipUtils.js]
--- a/toolkit/mozapps/extensions/internal/GMPProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/GMPProvider.jsm
@@ -55,17 +55,17 @@ const GMP_PLUGINS = [
     name:            "eme-adobe_name",
     description:     "eme-adobe_description",
     // The following learnMoreURL is another hack to be able to support a SUMO page for this
     // feature.
     get learnMoreURL() {
       return Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content";
     },
     licenseURL:      "http://help.adobe.com/en_US/primetime/drm/HTML5_CDM_EULA/index.html",
-    homepageURL:     "http://help.adobe.com/en_US/primetime/drm/index.html",
+    homepageURL:     "https://www.adobe.com/marketing-cloud/primetime-tv-platform.html",
     optionsURL:      "chrome://mozapps/content/extensions/gmpPrefs.xul",
     isEME:           true
   }];
 
 XPCOMUtils.defineLazyGetter(this, "pluginsBundle",
   () => Services.strings.createBundle("chrome://global/locale/plugins.properties"));
 XPCOMUtils.defineLazyGetter(this, "gmpService",
   () => Cc["@mozilla.org/gecko-media-plugin-service;1"].getService(Ci.mozIGeckoMediaPluginChromeService));
--- a/toolkit/themes/osx/global/jar.mn
+++ b/toolkit/themes/osx/global/jar.mn
@@ -190,17 +190,16 @@ toolkit.jar:
 * skin/classic/global/in-content/info-pages.css                      (in-content/info-pages.css)
   skin/classic/global/in-content/check.svg                           (../../shared/in-content/check.svg)
   skin/classic/global/in-content/check-partial.svg                   (../../shared/in-content/check-partial.svg)
   skin/classic/global/in-content/dropdown.svg                        (../../shared/in-content/dropdown.svg)
   skin/classic/global/in-content/help-glyph.svg                      (../../shared/in-content/help-glyph.svg)
   skin/classic/global/in-content/radio.svg                           (../../shared/in-content/radio.svg)
   skin/classic/global/reader/RM-Add-24x24.svg                        (../../shared/reader/RM-Add-24x24.svg)
   skin/classic/global/reader/RM-Close-24x24.svg                      (../../shared/reader/RM-Close-24x24.svg)
-  skin/classic/global/reader/RM-Close-hover-24x24.svg                (../../shared/reader/RM-Close-hover-24x24.svg)
   skin/classic/global/reader/RM-Delete-24x24.svg                     (../../shared/reader/RM-Delete-24x24.svg)
   skin/classic/global/reader/RM-Minus-24x24.svg                      (../../shared/reader/RM-Minus-24x24.svg)
   skin/classic/global/reader/RM-Plus-24x24.svg                       (../../shared/reader/RM-Plus-24x24.svg)
   skin/classic/global/reader/RM-Reading-List-24x24.svg               (../../shared/reader/RM-Reading-List-24x24.svg)
   skin/classic/global/reader/RM-Type-Controls-24x24.svg              (../../shared/reader/RM-Type-Controls-24x24.svg)
   skin/classic/global/reader/RM-Type-Controls-Arrow.svg              (../../shared/reader/RM-Type-Controls-Arrow.svg)
   skin/classic/global/scale/scale-tray-horiz.gif                     (scale/scale-tray-horiz.gif)
   skin/classic/global/scale/scale-tray-vert.gif                      (scale/scale-tray-vert.gif)
--- a/toolkit/themes/shared/reader/RM-Close-24x24.svg
+++ b/toolkit/themes/shared/reader/RM-Close-24x24.svg
@@ -1,8 +1,31 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg version="1.1" id="Icons" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
-      viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
-<g>
-    <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#808080" points="20,6.748 17.338,4.079 12.038,9.391 6.661,4 4,6.669
-        9.377,12.059 4.157,17.292 6.819,19.961 12.039,14.728 17.298,20 19.959,17.331 14.701,12.06 	"/>
-</g>
-</svg>
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg viewBox="0 0 24 24"
+  xmlns="http://www.w3.org/2000/svg"
+  xmlns:xlink="http://www.w3.org/1999/xlink">
+  <defs>
+    <style type="text/css">
+      use:not(:target) {
+        display: none;
+      }
+
+      #close {
+        fill: #808080;
+      }
+      #close-hover {
+        fill: #FFFFFF;
+      }
+    </style>
+
+    <g id="close-shape">
+      <polygon points="20.477,6.551 20.477,17.449 11.992,17.449 11.992,20 23,20 23,4 11.992,4 11.992,6.551"/>
+      <polygon points="1,11.981 9.698,19.95 9.698,15.13 18.184,15.13 18.184,8.87 9.698,8.87 9.698,4.011"/>
+    </g>
+  </defs>
+
+  <use id="close"       xlink:href="#close-shape"/>
+  <use id="close-hover" xlink:href="#close-shape"/>
+
+</svg>
\ No newline at end of file
deleted file mode 100644
--- a/toolkit/themes/shared/reader/RM-Close-hover-24x24.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg version="1.1" id="Icons" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
-     viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
-<g>
-    <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" points="20,6.748 17.338,4.079 12.038,9.391 6.661,4 4,6.669
-        9.377,12.059 4.157,17.292 6.819,19.961 12.039,14.728 17.298,20 19.959,17.331 14.701,12.06 	"/>
-</g>
-</svg>
--- a/toolkit/themes/windows/global/aboutReader.css
+++ b/toolkit/themes/windows/global/aboutReader.css
@@ -272,28 +272,29 @@ body.loaded {
   -moz-user-select: none;
   border-right: 1px solid #b5b5b5;
 }
 
 .button {
   display: block;
   background-size: 24px 24px;
   background-repeat: no-repeat;
+  color: #333;
+  background-color: #fbfbfb;
   height: 40px;
   padding: 0;
 }
 
 .toolbar .button {
-  color: white;
   width: 40px;
   background-position: center;
-  background-color: #fbfbfb;
+  margin-right: -1px;
   border-top: 0;
   border-left: 0;
-  border-right: 0;
+  border-right: 1px solid #b5b5b5;
   border-bottom: 1px solid #c1c1c1;
 }
 
 .button[hidden] {
   display: none;
 }
 
 .dropdown {
@@ -461,27 +462,23 @@ body.loaded {
   height: 64px;
   background-color: #ebebeb;
   position: absolute;
   left: 0;
   width: 100%;
   text-align: center;
   padding: 12px 0;
   box-sizing: border-box;
-  box-shadow: 0 9px 9px -9px #c1c1c1 inset;
+  box-shadow: 0 3px 3px -3px rgba(0, 0, 0, 0.35) inset;
 }
 
 .sepia .footer {
   background-color: #dedad4;
 }
 
-.dark .footer {
-  background-color: #777;
-}
-
 .remove-button {
   background-image: url("chrome://global/skin/reader/RM-Delete-24x24.svg");
   margin: 0 auto;
   border: 1px solid #c1c1c1;
   background-position: 10px 7px;
   padding-left: 42px;
   padding-right: 10px;
   border-radius: 2px;
@@ -493,25 +490,33 @@ body.loaded {
 /*======= Toolbar icons =======*/
 
 /* Android-only controls */
 .share-button {
   display: none;
 }
 
 .close-button {
-  background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg");
+  background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close");
   height: 68px;
   background-position: center 8px;
 }
 
-.close-button:active,
 .close-button:hover {
-  background-image: url("chrome://global/skin/reader/RM-Close-hover-24x24.svg");
+  background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close-hover");
   background-color: #d94141;
+  border-bottom: 1px solid #d94141;
+  border-right: 1px solid #d94141;
+}
+
+.close-button:hover:active {
+  background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg#close-hover");
+  background-color: #AE2325;
+  border-bottom: 1px solid #AE2325;
+  border-right: 1px solid #AE2325;
 }
 
 .style-button {
   background-image: url("chrome://global/skin/reader/RM-Type-Controls-24x24.svg");
 }
 
 .toggle-button.on {
   background-image: url("chrome://global/skin/reader/RM-Delete-24x24.svg");
--- a/toolkit/themes/windows/global/jar.mn
+++ b/toolkit/themes/windows/global/jar.mn
@@ -177,17 +177,16 @@ toolkit.jar:
         skin/classic/global/printpreview/arrow-left.png          (printpreview/arrow-left.png)
         skin/classic/global/printpreview/arrow-left-end.png      (printpreview/arrow-left-end.png)
         skin/classic/global/printpreview/arrow-right.png         (printpreview/arrow-right.png)
         skin/classic/global/printpreview/arrow-right-end.png     (printpreview/arrow-right-end.png)
         skin/classic/global/radio/radio-check.gif                (radio/radio-check.gif)
         skin/classic/global/radio/radio-check-dis.gif            (radio/radio-check-dis.gif)
         skin/classic/global/reader/RM-Add-24x24.svg              (../../shared/reader/RM-Add-24x24.svg)
         skin/classic/global/reader/RM-Close-24x24.svg            (../../shared/reader/RM-Close-24x24.svg)
-        skin/classic/global/reader/RM-Close-hover-24x24.svg      (../../shared/reader/RM-Close-hover-24x24.svg)
         skin/classic/global/reader/RM-Delete-24x24.svg           (../../shared/reader/RM-Delete-24x24.svg)
         skin/classic/global/reader/RM-Minus-24x24.svg            (../../shared/reader/RM-Minus-24x24.svg)
         skin/classic/global/reader/RM-Plus-24x24.svg             (../../shared/reader/RM-Plus-24x24.svg)
         skin/classic/global/reader/RM-Reading-List-24x24.svg     (../../shared/reader/RM-Reading-List-24x24.svg)
         skin/classic/global/reader/RM-Type-Controls-24x24.svg    (../../shared/reader/RM-Type-Controls-24x24.svg)
         skin/classic/global/reader/RM-Type-Controls-Arrow.svg    (../../shared/reader/RM-Type-Controls-Arrow.svg)
         skin/classic/global/scrollbar/slider.gif                 (scrollbar/slider.gif)
         skin/classic/global/splitter/grip-bottom.gif             (splitter/grip-bottom.gif)