merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 26 Feb 2015 12:00:41 +0100
changeset 230921 df3daecd381fa30f28edd16de11a97dd0bd2c2ed
parent 230859 599b84826bf3bfafbe382bee89ee4548c2407fbb (current diff)
parent 230920 6b70def56608ed8465ec27f7631626c5638a112f (diff)
child 230922 dcc86f78d75eca856d39a364ba48f3e6fd2d8596
child 230944 38382572cf6f2acbaef097f90a56bd3bfbf51637
child 231026 109362e9b13474c63344358542de1dec75aa32c1
child 231059 4749ddb0935ae54e686fa6f1850e80c899a99cf6
push id28338
push usercbook@mozilla.com
push dateThu, 26 Feb 2015 11:01:28 +0000
treeherdermozilla-central@df3daecd381f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone39.0a1
first release with
nightly linux32
df3daecd381f / 39.0a1 / 20150226030225 / files
nightly linux64
df3daecd381f / 39.0a1 / 20150226030225 / files
nightly mac
df3daecd381f / 39.0a1 / 20150226030225 / files
nightly win32
df3daecd381f / 39.0a1 / 20150226030225 / files
nightly win64
df3daecd381f / 39.0a1 / 20150226030225 / 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 mozilla-central a=merge
browser/devtools/canvasdebugger/test/browser_canvas-frontend-snapshot-select.js
browser/devtools/timeline/test/browser_timeline_filters.js
--- a/addon-sdk/source/.travis.yml
+++ b/addon-sdk/source/.travis.yml
@@ -1,11 +1,12 @@
 sudo: false
 language: node_js
 node_js:
+  - "iojs"
   - "0.10"
 
 notifications:
   irc: "irc.mozilla.org#jetpack"
 
 before_install:
   - "export DISPLAY=:99.0"
   - "sh -e /etc/init.d/xvfb start"
--- a/addon-sdk/source/CONTRIBUTING.md
+++ b/addon-sdk/source/CONTRIBUTING.md
@@ -12,22 +12,24 @@ If you have questions, ask in [#jetpack 
 
 If you have code that you'd like to contribute the Jetpack project, follow these steps:
 
 1. Look for your issue in the [bugs already filed][open bugs]
 2. If no bug exists, [submit one][submit bug]
 3. Make your changes, per the Overview
 4. Write a test ([intro][test intro], [API][test API])
 5. Submit pull request with changes and a title in a form of `Bug XXX - description`
-6. Copy the pull request link from GitHub and paste it in as an attachment to the bug
-7. Each pull request should idealy contain only one commit, so squash the commits if necessary.
-8. Flag the attachment for code review from one of the Jetpack reviewers listed below.
+6. Make sure that [Travis CI](https://travis-ci.org/mozilla/addon-sdk/branches) tests are passing for your branch.
+7. Copy the pull request link from GitHub and paste it in as an attachment to the bug
+8. Each pull request should idealy contain only one commit, so squash the commits if necessary.
+9. Flag the attachment for code review from one of the Jetpack reviewers listed below.
    This step is optional, but could speed things up.
-9. Address any nits (ie style changes), or other issues mentioned in the review.
-10. Finally, once review is approved, a team member will do the merging
+10. Address any nits (ie style changes), or other issues mentioned in the review.
+
+Finally, once review is approved, a team member will do the merging
 
 ## Good First Bugs
 
 There is a list of [good first bugs here](https://bugzilla.mozilla.org/buglist.cgi?list_id=7345714&columnlist=bug_severity%2Cpriority%2Cassigned_to%2Cbug_status%2Ctarget_milestone%2Cresolution%2Cshort_desc%2Cchangeddate&query_based_on=jetpack-good-1st-bugs&status_whiteboard_type=allwordssubstr&query_format=advanced&status_whiteboard=[good%20first%20bug]&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&bug_status=VERIFIED&product=Add-on%20SDK&known_name=jetpack-good-1st-bugs).
 
 ## Reviewers
 
 All changes must be reviewed by someone on the Jetpack review crew:
--- a/addon-sdk/source/lib/sdk/addon/runner.js
+++ b/addon-sdk/source/lib/sdk/addon/runner.js
@@ -76,32 +76,30 @@ function startup(reason, options) Startu
     then(null, function failure(error) {
       if (!isNative)
         console.info("Error while loading localization: " + error.message);
     }).
     then(function onLocalizationReady(data) {
       // Exports data to a pseudo module so that api-utils/l10n/core
       // can get access to it
       definePseudo(options.loader, '@l10n/data', data ? data : null);
-      return ready;
-    }).then(function() {
-      run(options);
+      return ready.then(() => run(options, !!data));
     }).then(null, console.exception);
     return void 0; // otherwise we raise a warning, see bug 910304
 });
 
-function run(options) {
+function run(options, hasL10n) {
   try {
     // Try initializing HTML localization before running main module. Just print
     // an exception in case of error, instead of preventing addon to be run.
     try {
       // Do not enable HTML localization while running test as it is hard to
       // disable. Because unit tests are evaluated in a another Loader who
       // doesn't have access to this current loader.
-      if (options.main !== 'sdk/test/runner') {
+      if (hasL10n && options.main !== 'sdk/test/runner') {
         require('../l10n/html').enable();
       }
     }
     catch(error) {
       console.exception(error);
     }
 
     // native-options does stuff directly with preferences key from package.json
--- a/addon-sdk/source/lib/sdk/test/harness.js
+++ b/addon-sdk/source/lib/sdk/test/harness.js
@@ -335,17 +335,17 @@ function getPotentialLeaks() {
       if (!details) {
         console.error("Unable to parse compartment detail " + matches[1]);
         return;
       }
 
       let item = {
         path: matches[1],
         principal: details[1],
-        location: details[2] ? details[2].replace("\\", "/", "g") : undefined,
+        location: details[2] ? details[2].replace(/\\/g, "/") : undefined,
         source: details[3] ? details[3].split(" -> ").reverse() : undefined,
         toString: function() this.location
       };
 
       if (!isPossibleLeak(item))
         return;
 
       compartments[matches[1]] = item;
@@ -359,18 +359,18 @@ function getPotentialLeaks() {
       let details = windowDetails.exec(matches[1]);
       if (!details) {
         console.error("Unable to parse window detail " + matches[1]);
         return;
       }
 
       let item = {
         path: matches[1],
-        location: details[1].replace("\\", "/", "g"),
-        source: [details[1].replace("\\", "/", "g")],
+        location: details[1].replace(/\\/g, "/"),
+        source: [details[1].replace(/\\/g, "/")],
         toString: function() this.location
       };
 
       if (!isPossibleLeak(item))
         return;
 
       windows[matches[1]] = item;
     }
--- a/addon-sdk/source/lib/sdk/windows/observer.js
+++ b/addon-sdk/source/lib/sdk/windows/observer.js
@@ -7,16 +7,17 @@ module.metadata = {
   "stability": "unstable"
 };
 
 const { EventTarget } = require("../event/target");
 const { emit } = require("../event/core");
 const { WindowTracker, windowIterator } = require("../deprecated/window-utils");
 const { DOMEventAssembler } = require("../deprecated/events/assembler");
 const { Class } = require("../core/heritage");
+const { Cu } = require("chrome");
 
 // Event emitter objects used to register listeners and emit events on them
 // when they occur.
 const Observer = Class({
   initialize() {
     // Using `WindowTracker` to track window events.
     WindowTracker({
       onTrack: chromeWindow => {
@@ -37,13 +38,16 @@ const Observer = Class({
   /**
    * Function handles all the supported events on all the windows that are
    * observed. Method is used to proxy events to the listeners registered on
    * this event emitter.
    * @param {Event} event
    *    Keyboard event being emitted.
    */
   handleEvent(event) {
+    // Ignore events from windows in the child process as they can't be top-level
+    if (Cu.isCrossProcessWrapper(event.target))
+      return;
     emit(this, event.type, event.target, event);
   }
 });
 
 exports.observer = new Observer();
--- a/addon-sdk/source/lib/toolkit/loader.js
+++ b/addon-sdk/source/lib/toolkit/loader.js
@@ -698,17 +698,18 @@ const main = iced(function main(loader, 
 });
 Loader.main = main;
 
 // Makes module object that is made available to CommonJS modules when they
 // are evaluated, along with `exports` and `require`.
 const Module = iced(function Module(id, uri) {
   return create(null, {
     id: { enumerable: true, value: id },
-    exports: { enumerable: true, writable: true, value: create(null) },
+    exports: { enumerable: true, writable: true, value: create(null),
+               configurable: true },
     uri: { value: uri }
   });
 });
 Loader.Module = Module;
 
 // Takes `loader`, and unload `reason` string and notifies all observers that
 // they should cleanup after them-self.
 const unload = iced(function unload(loader, reason) {
@@ -779,25 +780,35 @@ function Loader(options) {
     'chrome': { Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm,
                 CC: bind(CC, Components), components: Components,
                 // `ChromeWorker` has to be inject in loader global scope.
                 // It is done by bootstrap.js:loadSandbox for the SDK.
                 ChromeWorker: ChromeWorker
     }
   }, modules);
 
+  const builtinModuleExports = modules;
   modules = keys(modules).reduce(function(result, id) {
     // We resolve `uri` from `id` since modules are cached by `uri`.
     let uri = resolveURI(id, mapping);
     // In native loader, the mapping will not contain values for
     // pseudomodules -- store them as their ID rather than the URI
     if (isNative && !uri)
       uri = id;
     let module = Module(id, uri);
-    module.exports = freeze(modules[id]);
+
+    // Lazily expose built-in modules in order to
+    // allow them to be loaded lazily.
+    Object.defineProperty(module, "exports", {
+      enumerable: true,
+      get: function() {
+        return builtinModuleExports[id];
+      }
+    });
+
     result[uri] = freeze(module);
     return result;
   }, {});
 
   let sharedGlobalSandbox;
   if (sharedGlobal) {
     // Create the unique sandbox we will be using for all modules,
     // so that we prevent creating a new comportment per module.
--- a/addon-sdk/source/python-lib/cuddlefish/prefs.py
+++ b/addon-sdk/source/python-lib/cuddlefish/prefs.py
@@ -55,16 +55,17 @@ DEFAULT_NO_CONNECTIONS_PREFS = {
     'media.gmp-manager.url.override': 'http://localhost/dummy-gmp-manager.xml',
     'browser.newtab.url' : 'about:blank',
     'browser.search.update': False,
     'browser.safebrowsing.enabled' : False,
     'browser.safebrowsing.updateURL': 'http://localhost/safebrowsing-dummy/update',
     'browser.safebrowsing.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
     'browser.safebrowsing.reportURL': 'http://localhost/safebrowsing-dummy/report',
     'browser.safebrowsing.malware.reportURL': 'http://localhost/safebrowsing-dummy/malwarereport',
+    'browser.selfsupport.url': 'http://localhost/repair-dummy',
     'browser.trackingprotection.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
     'browser.trackingprotection.updateURL': 'http://localhost/safebrowsing-dummy/update',
 
     # Disable app update
     'app.update.enabled' : False,
     'app.update.staging.enabled': False,
 
     # Disable about:newtab content fetch and ping
--- a/addon-sdk/source/test/addons/private-browsing-supported/sidebar/utils.js
+++ b/addon-sdk/source/test/addons/private-browsing-supported/sidebar/utils.js
@@ -5,17 +5,18 @@
 
 const { Cu } = require('chrome');
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { fromIterator } = require('sdk/util/array');
 
 const BUILTIN_SIDEBAR_MENUITEMS = exports.BUILTIN_SIDEBAR_MENUITEMS = [
   'menu_socialSidebar',
   'menu_historySidebar',
-  'menu_bookmarksSidebar'
+  'menu_bookmarksSidebar',
+  'menu_readingListSidebar'
 ];
 
 function isSidebarShowing(window) {
   window = window || getMostRecentBrowserWindow();
   let sidebar = window.document.getElementById('sidebar-box');
   return !sidebar.hidden;
 }
 exports.isSidebarShowing = isSidebarShowing;
--- a/addon-sdk/source/test/addons/private-browsing-supported/test-sidebar.js
+++ b/addon-sdk/source/test/addons/private-browsing-supported/test-sidebar.js
@@ -124,18 +124,17 @@ exports.testDestroyEdgeCaseBugWithPrivat
       let sidebar = loader.require('sdk/ui/sidebar').Sidebar({
         id: testName,
         title: testName,
         url:  'data:text/html;charset=utf-8,'+ testName,
         onShow: function() {
           assert.pass('onShow works for Sidebar');
           loader.unload();
 
-          let sidebarMI = getSidebarMenuitems();
-          for (let mi of sidebarMI) {
+          for (let mi of getSidebarMenuitems()) {
             assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
             assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
           }
           assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
           assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
 
           done();
         }
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/loader/lazy/main.js
@@ -0,0 +1,9 @@
+/* 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";
+
+exports.useFoo= function () {
+  return require('foo');
+}
--- a/addon-sdk/source/test/preferences/no-connections.json
+++ b/addon-sdk/source/test/preferences/no-connections.json
@@ -12,16 +12,17 @@
   "media.gmp-manager.url.override": "http://localhost/dummy-gmp-manager.xml",
   "browser.newtab.url": "about:blank",
   "browser.search.update": false,
   "browser.safebrowsing.enabled": false,
   "browser.safebrowsing.updateURL": "http://localhost/safebrowsing-dummy/update",
   "browser.safebrowsing.gethashURL": "http://localhost/safebrowsing-dummy/gethash",
   "browser.safebrowsing.reportURL": "http://localhost/safebrowsing-dummy/report",
   "browser.safebrowsing.malware.reportURL": "http://localhost/safebrowsing-dummy/malwarereport",
+  "browser.selfsupport.url": "http://localhost/repair-dummy",
   "browser.trackingprotection.gethashURL": "http://localhost/safebrowsing-dummy/gethash",
   "browser.trackingprotection.updateURL": "http://localhost/safebrowsing-dummy/update",
   "browser.newtabpage.directory.source": "data:application/json,{'jetpack':1}",
   "browser.newtabpage.directory.ping": "",
   "extensions.update.url": "http://localhost/extensions-dummy/updateURL",
   "extensions.update.background.url": "http://localhost/extensions-dummy/updateBackgroundURL",
   "extensions.blocklist.url": "http://localhost/extensions-dummy/blocklistURL",
   "extensions.webservice.discoverURL": "http://localhost/extensions-dummy/discoveryURL",
--- a/addon-sdk/source/test/sidebar/utils.js
+++ b/addon-sdk/source/test/sidebar/utils.js
@@ -11,17 +11,18 @@ module.metadata = {
 
 const { Cu } = require('chrome');
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { fromIterator } = require('sdk/util/array');
 
 const BUILTIN_SIDEBAR_MENUITEMS = exports.BUILTIN_SIDEBAR_MENUITEMS = [
   'menu_socialSidebar',
   'menu_historySidebar',
-  'menu_bookmarksSidebar'
+  'menu_bookmarksSidebar',
+  'menu_readingListSidebar'
 ];
 
 function isSidebarShowing(window) {
   window = window || getMostRecentBrowserWindow();
   let sidebar = window.document.getElementById('sidebar-box');
   return !sidebar.hidden;
 }
 exports.isSidebarShowing = isSidebarShowing;
--- a/addon-sdk/source/test/test-loader.js
+++ b/addon-sdk/source/test/test-loader.js
@@ -501,9 +501,27 @@ exports["test Cu.import in b2g style"] =
   assert.equal(typeof(exported.Loader),
                "function",
                "loader is a function");
   assert.equal(typeof(exported.Loader.Loader),
                "function",
                "Loader.Loader is a funciton");
 };
 
+exports['test lazy globals'] = function (assert) {
+  let uri = root + '/fixtures/loader/lazy/';
+  let gotFoo = false;
+  let foo = {};
+  let modules = {
+    get foo() {
+      gotFoo = true;
+      return foo;
+    }
+  };
+  let loader = Loader({ paths: { '': uri }, modules: modules});
+  assert.ok(!gotFoo, "foo hasn't been accessed during loader instanciation");
+  let program = main(loader, 'main');
+  assert.ok(!gotFoo, "foo hasn't been accessed during module loading");
+  assert.equal(program.useFoo(), foo, "foo mock works");
+  assert.ok(gotFoo, "foo has been accessed only when we first try to use it");
+};
+
 require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-preferences-service.js
+++ b/addon-sdk/source/test/test-preferences-service.js
@@ -24,16 +24,27 @@ exports.testGetAndSet = function(assert)
   let svc = Cc["@mozilla.org/preferences-service;1"].
             getService(Ci.nsIPrefService).
             getBranch(null);
   svc.setCharPref("test_set_get_pref", "a normal string");
   assert.equal(prefs.get("test_set_get_pref"), "a normal string",
                    "preferences-service should read from " +
                    "application-wide preferences service");
 
+  // test getting a pref that does not exist,
+  // and where we provide no default
+  assert.equal(
+      prefs.get("test_dne_get_pref", "default"),
+      "default",
+      "default was used for a pref that does not exist");
+  assert.equal(
+      prefs.get("test_dne_get_pref"),
+      undefined,
+      "undefined was returned for a pref that does not exist with no default");
+
   prefs.set("test_set_get_pref.integer", 1);
   assert.equal(prefs.get("test_set_get_pref.integer"), 1,
                    "set/get integer preference should work");
 
   assert.equal(
       prefs.keys("test_set_get_pref").sort().toString(),
       ["test_set_get_pref.integer","test_set_get_pref"].sort().toString(),
       "the key list is correct");
--- a/addon-sdk/source/test/test-ui-sidebar.js
+++ b/addon-sdk/source/test/test-ui-sidebar.js
@@ -91,19 +91,22 @@ exports.testSidebarBasicLifeCycle = func
     sidebar.hide();
     assert.pass('hiding sidebar..');
   });
 
   // calling destroy twice should not matter
   sidebar.destroy();
   sidebar.destroy();
 
-  let sidebarMI = getSidebarMenuitems();
-  for (let mi of sidebarMI) {
-    assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
+  for (let mi of getSidebarMenuitems()) {
+    let id = mi.getAttribute('id');
+
+    if (BUILTIN_SIDEBAR_MENUITEMS.indexOf(id) < 0) {
+      assert.fail('the menuitem "' + id + '" is not a built-in sidebar');
+    }
     assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
   }
 
   assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
   assert.pass('calling destroy worked without error');
 }
 
 exports.testSideBarIsInNewWindows = function*(assert) {
@@ -355,18 +358,17 @@ exports.testSidebarUnload = function*(as
     url:  'data:text/html;charset=utf-8,'+ testName
   });
 
   yield sidebar.show();
   assert.pass('showing the sidebar');
 
   loader.unload();
 
-  let sidebarMI = getSidebarMenuitems();
-  for (let mi of sidebarMI) {
+  for (let mi of getSidebarMenuitems()) {
     assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
     assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
   }
 
   assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
   assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
 }
 
@@ -593,19 +595,22 @@ exports.testDestroyEdgeCaseBug = functio
     onShow: function() {
     }
   })
 
   assert.pass('showing the sidebar');
   yield sidebar.show();
   loader.unload();
 
-  let sidebarMI = getSidebarMenuitems();
-  for (let mi of sidebarMI) {
-    assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
+  for (let mi of getSidebarMenuitems()) {
+    let id = mi.getAttribute('id');
+
+    if (BUILTIN_SIDEBAR_MENUITEMS.indexOf(id) < 0) {
+      assert.fail('the menuitem "' + id + '" is not a built-in sidebar');
+    }
     assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
   }
   assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
   assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
 }
 
 exports.testClickingACheckedMenuitem = function*(assert) {
   const { Sidebar } = require('sdk/ui/sidebar');
@@ -757,16 +762,19 @@ exports.testURLSetterToSameValueReloadsS
   assert.ok(!isChecked(document.getElementById(makeID(sidebar1.id))),
                'the menuitem is not checked');
   assert.equal(isSidebarShowing(window), false, 'the new window sidebar is not showing');
 
   window = yield windowPromise(window.OpenBrowserWindow(), 'load');
   document = window.document;
   assert.pass('new window was opened');
 
+  yield focus(window);
+  assert.pass('new window was focused');
+
   yield sidebar1.show();
 
   assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
   assert.ok(isChecked(document.getElementById(makeID(sidebar1.id))),
                'the menuitem is checked');
   assert.ok(isSidebarShowing(window), 'the new window sidebar is showing');
 
   let shown = defer();
@@ -1485,33 +1493,39 @@ exports.testShowHideRawWindowArg = funct
   let testName = 'testShowHideRawWindowArg';
   let sidebar = Sidebar({
     id: testName,
     title: testName,
     url: 'data:text/html;charset=utf-8,' + testName
   });
 
   let mainWindow = getMostRecentBrowserWindow();
-  let newWindow = yield open().then(focus);
+  let newWindow = yield windowPromise(mainWindow.OpenBrowserWindow(), 'load');
+  assert.pass("Created the new window");
+
+  yield focus(newWindow);
+  assert.pass("Focused the new window");
 
   yield focus(mainWindow);
+  assert.pass("Focused the old window");
 
   yield sidebar.show(newWindow);
 
   assert.pass('the sidebar was shown');
-  assert.ok(!isSidebarShowing(mainWindow), 'sidebar is not showing in main window');
-  assert.ok(isSidebarShowing(newWindow), 'sidebar is showing in new window');
+  assert.equal(isSidebarShowing(mainWindow), false, 'sidebar is not showing in main window');
+  assert.equal(isSidebarShowing(newWindow), true, 'sidebar is showing in new window');
 
   assert.ok(isFocused(mainWindow), 'main window is still focused');
 
   yield sidebar.hide(newWindow);
 
-  assert.ok(isFocused(mainWindow), 'main window is still focused');
-  assert.ok(!isSidebarShowing(mainWindow), 'sidebar is not showing in main window');
-  assert.ok(!isSidebarShowing(newWindow), 'sidebar is not showing in new window');
+  assert.equal(isFocused(mainWindow), true, 'main window is still focused');
+  assert.equal(isSidebarShowing(mainWindow), false, 'sidebar is not showing in main window');
+  assert.equal(isSidebarShowing(newWindow), false, 'sidebar is not showing in new window');
+
   sidebar.destroy();
 }
 
 exports.testShowHideSDKWindowArg = function*(assert) {
   const { Sidebar } = require('sdk/ui/sidebar');
 
   let testName = 'testShowHideSDKWindowArg';
   let sidebar = Sidebar({
@@ -1542,13 +1556,14 @@ exports.testShowHideSDKWindowArg = funct
   sidebar.destroy();
 }
 
 before(exports, (name, assert) => {
   assert.equal(isSidebarShowing(), false, 'no sidebar is showing');
 });
 
 after(exports, function*(name, assert) {
+  assert.pass("Cleaning new windows and tabs");
   yield cleanUI();
   assert.equal(isSidebarShowing(), false, 'no sidebar is showing');
 });
 
 require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-weak-set.js
+++ b/addon-sdk/source/test/test-weak-set.js
@@ -1,19 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 'use strict';
 
 const { Cu } = require('chrome');
 const { Loader } = require('sdk/test/loader');
-
-function gc() {
-  return new Promise(resolve => Cu.schedulePreciseGC(resolve));
-};
+const { gc } = require("sdk/test/memory");
 
 exports['test adding item'] = function*(assert) {
   let loader = Loader(module);
   let { add, remove, has, clear, iterator } = loader.require('sdk/lang/weak-set');
 
   let items = {};
   let item = {};
 
@@ -116,74 +113,74 @@ exports['test adding non object or null 
   let loader = Loader(module);
   let { add, remove, has, clear, iterator } = loader.require('sdk/lang/weak-set');
 
   let items = {};
 
   assert.throws(() => {
     add(items, 'foo');
   },
-  /^value is not a non-null object/,
+  /^\w+ is not a non-null object/,
   'only non-null object are allowed');
 
   assert.throws(() => {
     add(items, 0);
   },
-  /^value is not a non-null object/,
+  /^\w+ is not a non-null object/,
   'only non-null object are allowed');
 
   assert.throws(() => {
     add(items, undefined);
   },
-  /^value is not a non-null object/,
+  /^\w+ is not a non-null object/,
   'only non-null object are allowed');
 
   assert.throws(() => {
     add(items, null);
   },
-  /^value is not a non-null object/,
+  /^\w+ is not a non-null object/,
   'only non-null object are allowed');
 
   assert.throws(() => {
     add(items, true);
   },
-  /^value is not a non-null object/,
+  /^\w+ is not a non-null object/,
   'only non-null object are allowed');
 };
 
 exports['test adding to non object or null item'] = function(assert) {
   let loader = Loader(module);
   let { add, remove, has, clear, iterator } = loader.require('sdk/lang/weak-set');
 
   let item = {};
 
   assert.throws(() => {
     add('foo', item);
   },
-  /^value is not a non-null object/,
+  /^\w+ is not a non-null object/,
   'only non-null object are allowed');
 
   assert.throws(() => {
     add(0, item);
   },
-  /^value is not a non-null object/,
+  /^\w+ is not a non-null object/,
   'only non-null object are allowed');
 
   assert.throws(() => {
     add(undefined, item);
   },
-  /^value is not a non-null object/,
+  /^\w+ is not a non-null object/,
   'only non-null object are allowed');
 
   assert.throws(() => {
     add(null, item);
   },
-  /^value is not a non-null object/,
+  /^\w+ is not a non-null object/,
   'only non-null object are allowed');
 
   assert.throws(() => {
     add(true, item);
   },
-  /^value is not a non-null object/,
+  /^\w+ is not a non-null object/,
   'only non-null object are allowed');
 };
 
 require('sdk/test').run(exports);
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1331,17 +1331,17 @@ pref("browser.devedition.theme.enabled",
 pref("browser.devedition.theme.showCustomizeButton", true);
 #else
 pref("browser.devedition.theme.enabled", false);
 pref("browser.devedition.theme.showCustomizeButton", false);
 #endif
 
 // Developer edition promo preferences
 pref("devtools.devedition.promo.shown", false);
-pref("devtools.devedition.promo.url", "https://mozilla.org/firefox/developer");
+pref("devtools.devedition.promo.url", "https://www.mozilla.org/firefox/developer/?utm_source=firefox-dev-tools&utm_medium=firefox-browser&utm_content=betadoorhanger");
 
 // Only potentially show in beta release
 #if MOZ_UPDATE_CHANNEL == beta
   pref("devtools.devedition.promo.enabled", true);
 #else
   pref("devtools.devedition.promo.enabled", false);
 #endif
 
@@ -1433,16 +1433,19 @@ pref("devtools.debugger.ui.variables-sea
 pref("devtools.profiler.enabled", true);
 
 // Timeline panel settings
 #ifdef NIGHTLY_BUILD
 pref("devtools.timeline.enabled", true);
 #else
 pref("devtools.timeline.enabled", false);
 #endif
+
+// TODO remove `devtools.timeline.hiddenMarkers.` branches when performance
+// tool lands (bug 1075567)
 pref("devtools.timeline.hiddenMarkers", "[]");
 
 // Enable perftools via build command
 #ifdef MOZ_DEVTOOLS_PERFTOOLS
   pref("devtools.performance_dev.enabled", true);
 #else
   pref("devtools.performance_dev.enabled", false);
 #endif
@@ -1450,16 +1453,17 @@ pref("devtools.timeline.hiddenMarkers", 
 // The default Profiler UI settings
 // TODO remove `devtools.profiler.ui.` branches when performance
 // tool lands (bug 1075567)
 pref("devtools.profiler.ui.flatten-tree-recursion", true);
 pref("devtools.profiler.ui.show-platform-data", false);
 pref("devtools.profiler.ui.show-idle-blocks", true);
 
 // The default Performance UI settings
+pref("devtools.performance.timeline.hidden-markers", "[]");
 pref("devtools.performance.ui.invert-call-tree", true);
 pref("devtools.performance.ui.invert-flame-graph", false);
 pref("devtools.performance.ui.flatten-tree-recursion", true);
 pref("devtools.performance.ui.show-platform-data", false);
 pref("devtools.performance.ui.show-idle-blocks", true);
 pref("devtools.performance.ui.enable-memory", false);
 pref("devtools.performance.ui.enable-framerate", true);
 
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -683,17 +683,16 @@ let SessionStoreInternal = {
       case "SessionStore:restoreTabContentComplete":
         if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
           // This callback is used exclusively by tests that want to
           // monitor the progress of network loads.
           if (gDebuggingEnabled) {
             Services.obs.notifyObservers(browser, NOTIFY_TAB_RESTORED, null);
           }
 
-          delete browser.__SS_restore_data;
           delete browser.__SS_data;
 
           SessionStoreInternal._resetLocalTabRestoringState(tab);
           SessionStoreInternal.restoreNextTab();
 
           this._sendTabRestoredNotification(tab);
         }
         break;
@@ -2678,59 +2677,38 @@ let SessionStoreInternal = {
       this.restoreNextTab();
     }
 
     // Decrease the busy state counter after we're done.
     this._setWindowStateReady(window);
   },
 
   /**
-   * Restores the specified tab. If the tab can't be restored (eg, no history or
-   * calling gotoIndex fails), then state changes will be rolled back.
-   * This method will check if gTabsProgressListener is attached to the tab's
-   * window, ensuring that we don't get caught without one.
-   * This method removes the session history listener right before starting to
-   * attempt a load. This will prevent cases of "stuck" listeners.
-   * If this method returns false, then it is up to the caller to decide what to
-   * do. In the common case (restoreNextTab), we will want to then attempt to
-   * restore the next tab. In the other case (selecting the tab, reloading the
-   * tab), the caller doesn't actually want to do anything if no page is loaded.
+   * Kicks off restoring the given tab.
    *
    * @param aTab
    *        the tab to restore
-   *
-   * @returns true/false indicating whether or not a load actually happened
+   * @param aLoadArguments
+   *        optional load arguments used for loadURI()
    */
   restoreTabContent: function (aTab, aLoadArguments = null) {
     let window = aTab.ownerDocument.defaultView;
     let browser = aTab.linkedBrowser;
-    let tabData = browser.__SS_data;
 
     // Make sure that this tab is removed from the priority queue.
     TabRestoreQueue.remove(aTab);
 
     // Increase our internal count.
     this._tabsRestoringCount++;
 
     // Set this tab's state to restoring
     browser.__SS_restoreState = TAB_STATE_RESTORING;
     browser.removeAttribute("pending");
     aTab.removeAttribute("pending");
 
-    let activeIndex = tabData.index - 1;
-
-    // Attach data that will be restored on "load" event, after tab is restored.
-    if (tabData.entries.length) {
-      // restore those aspects of the currently active documents which are not
-      // preserved in the plain history entries (mainly scroll state and text data)
-      browser.__SS_restore_data = tabData.entries[activeIndex] || {};
-    } else {
-      browser.__SS_restore_data = {};
-    }
-
     browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent",
       {loadArguments: aLoadArguments});
   },
 
   /**
    * This _attempts_ to restore the next available tab. If the restore fails,
    * then we will attempt the next one.
    * There are conditions where this won't do anything:
--- a/browser/devtools/canvasdebugger/snapshotslist.js
+++ b/browser/devtools/canvasdebugger/snapshotslist.js
@@ -157,17 +157,19 @@ let SnapshotsListView = Heritage.extend(
       this.selectedIndex = 0;
     }
   },
 
   /**
    * The select listener for this container.
    */
   _onSelect: function({ detail: snapshotItem }) {
-    if (!snapshotItem) {
+    // Check to ensure the attachment has an actor, like
+    // an in-progress recording.
+    if (!snapshotItem || !snapshotItem.attachment.actor) {
       return;
     }
     let { calls, thumbnails, screenshot } = snapshotItem.attachment;
 
     $("#reload-notice").hidden = true;
     $("#empty-notice").hidden = true;
     $("#waiting-notice").hidden = false;
 
--- a/browser/devtools/canvasdebugger/test/browser.ini
+++ b/browser/devtools/canvasdebugger/test/browser.ini
@@ -1,14 +1,15 @@
 [DEFAULT]
 subsuite = devtools
 support-files =
   doc_raf-begin.html
   doc_settimeout.html
   doc_no-canvas.html
+  doc_raf-no-canvas.html
   doc_simple-canvas.html
   doc_simple-canvas-bitmasks.html
   doc_simple-canvas-deep-stack.html
   doc_simple-canvas-transparent.html
   doc_webgl-bindings.html
   doc_webgl-enum.html
   head.js
 
@@ -39,12 +40,14 @@ skip-if = e10s # bug 1102301 - leaks whi
 [browser_canvas-frontend-record-01.js]
 [browser_canvas-frontend-record-02.js]
 [browser_canvas-frontend-record-03.js]
 [browser_canvas-frontend-record-04.js]
 [browser_canvas-frontend-reload-01.js]
 [browser_canvas-frontend-reload-02.js]
 [browser_canvas-frontend-slider-01.js]
 [browser_canvas-frontend-slider-02.js]
-[browser_canvas-frontend-snapshot-select.js]
+[browser_canvas-frontend-snapshot-select-01.js]
+[browser_canvas-frontend-snapshot-select-02.js]
 [browser_canvas-frontend-stepping.js]
 [browser_canvas-frontend-stop-01.js]
 [browser_canvas-frontend-stop-02.js]
+[browser_canvas-frontend-stop-03.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/canvasdebugger/test/browser_canvas-frontend-snapshot-select-01.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if selecting snapshots in the frontend displays the appropriate data
+ * respective to their recorded animation frame.
+ */
+
+function ifTestingSupported() {
+  let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+  yield reload(target);
+
+  yield recordAndWaitForFirstSnapshot();
+  info("First snapshot recorded.")
+
+  is(SnapshotsListView.selectedIndex, 0,
+    "A snapshot should be automatically selected after first recording.");
+  is(CallsListView.selectedIndex, -1,
+    "There should be no call item automatically selected in the snapshot.");
+
+  yield recordAndWaitForAnotherSnapshot();
+  info("Second snapshot recorded.")
+
+  is(SnapshotsListView.selectedIndex, 0,
+    "A snapshot should not be automatically selected after another recording.");
+  is(CallsListView.selectedIndex, -1,
+    "There should still be no call item automatically selected in the snapshot.");
+
+  let secondSnapshotTarget = SnapshotsListView.getItemAtIndex(1).target;
+  let snapshotSelected = waitForSnapshotSelection();
+  EventUtils.sendMouseEvent({ type: "mousedown" }, secondSnapshotTarget, window);
+
+  yield snapshotSelected;
+  info("Second snapshot selected.");
+
+  is(SnapshotsListView.selectedIndex, 1,
+    "The second snapshot should now be selected.");
+  is(CallsListView.selectedIndex, -1,
+    "There should still be no call item automatically selected in the snapshot.");
+
+  let firstDrawCallContents = $(".call-item-contents", CallsListView.getItemAtIndex(2).target);
+  let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, firstDrawCallContents, window);
+
+  yield screenshotDisplayed;
+  info("First draw call in the second snapshot selected.");
+
+  is(SnapshotsListView.selectedIndex, 1,
+    "The second snapshot should still be selected.");
+  is(CallsListView.selectedIndex, 2,
+    "The first draw call should now be selected in the snapshot.");
+
+  let firstSnapshotTarget = SnapshotsListView.getItemAtIndex(0).target;
+  snapshotSelected = waitForSnapshotSelection();
+  EventUtils.sendMouseEvent({ type: "mousedown" }, firstSnapshotTarget, window);
+
+  yield snapshotSelected;
+  info("First snapshot re-selected.");
+
+  is(SnapshotsListView.selectedIndex, 0,
+    "The first snapshot should now be re-selected.");
+  is(CallsListView.selectedIndex, -1,
+    "There should still be no call item automatically selected in the snapshot.");
+
+  function recordAndWaitForFirstSnapshot() {
+    let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+    let snapshotSelected = waitForSnapshotSelection();
+    SnapshotsListView._onRecordButtonClick();
+    return promise.all([recordingFinished, snapshotSelected]);
+  }
+
+  function recordAndWaitForAnotherSnapshot() {
+    let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+    SnapshotsListView._onRecordButtonClick();
+    return recordingFinished;
+  }
+
+  function waitForSnapshotSelection() {
+    let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+    let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
+    let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+    return promise.all([
+      callListPopulated,
+      thumbnailsDisplayed,
+      screenshotDisplayed
+    ]);
+  }
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/canvasdebugger/test/browser_canvas-frontend-snapshot-select-02.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if selecting snapshots in the frontend displays the appropriate data
+ * respective to their recorded animation frame.
+ */
+
+function ifTestingSupported() {
+  let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+  yield reload(target);
+
+  SnapshotsListView._onRecordButtonClick();
+  let snapshotTarget = SnapshotsListView.getItemAtIndex(0).target;
+
+  EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window);
+
+  ok(true, "clicking in-progress snapshot does not fail");
+
+  let finished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  SnapshotsListView._onRecordButtonClick();
+  yield finished;
+
+  yield teardown(panel);
+  finish();
+}
deleted file mode 100644
--- a/browser/devtools/canvasdebugger/test/browser_canvas-frontend-snapshot-select.js
+++ /dev/null
@@ -1,93 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Tests if selecting snapshots in the frontend displays the appropriate data
- * respective to their recorded animation frame.
- */
-
-function ifTestingSupported() {
-  let { target, panel } = yield initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
-  let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
-
-  yield reload(target);
-
-  yield recordAndWaitForFirstSnapshot();
-  info("First snapshot recorded.")
-
-  is(SnapshotsListView.selectedIndex, 0,
-    "A snapshot should be automatically selected after first recording.");
-  is(CallsListView.selectedIndex, -1,
-    "There should be no call item automatically selected in the snapshot.");
-
-  yield recordAndWaitForAnotherSnapshot();
-  info("Second snapshot recorded.")
-
-  is(SnapshotsListView.selectedIndex, 0,
-    "A snapshot should not be automatically selected after another recording.");
-  is(CallsListView.selectedIndex, -1,
-    "There should still be no call item automatically selected in the snapshot.");
-
-  let secondSnapshotTarget = SnapshotsListView.getItemAtIndex(1).target;
-  let snapshotSelected = waitForSnapshotSelection();
-  EventUtils.sendMouseEvent({ type: "mousedown" }, secondSnapshotTarget, window);
-
-  yield snapshotSelected;
-  info("Second snapshot selected.");
-
-  is(SnapshotsListView.selectedIndex, 1,
-    "The second snapshot should now be selected.");
-  is(CallsListView.selectedIndex, -1,
-    "There should still be no call item automatically selected in the snapshot.");
-
-  let firstDrawCallContents = $(".call-item-contents", CallsListView.getItemAtIndex(2).target);
-  let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
-  EventUtils.sendMouseEvent({ type: "mousedown" }, firstDrawCallContents, window);
-
-  yield screenshotDisplayed;
-  info("First draw call in the second snapshot selected.");
-
-  is(SnapshotsListView.selectedIndex, 1,
-    "The second snapshot should still be selected.");
-  is(CallsListView.selectedIndex, 2,
-    "The first draw call should now be selected in the snapshot.");
-
-  let firstSnapshotTarget = SnapshotsListView.getItemAtIndex(0).target;
-  snapshotSelected = waitForSnapshotSelection();
-  EventUtils.sendMouseEvent({ type: "mousedown" }, firstSnapshotTarget, window);
-
-  yield snapshotSelected;
-  info("First snapshot re-selected.");
-
-  is(SnapshotsListView.selectedIndex, 0,
-    "The first snapshot should now be re-selected.");
-  is(CallsListView.selectedIndex, -1,
-    "There should still be no call item automatically selected in the snapshot.");
-
-  function recordAndWaitForFirstSnapshot() {
-    let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
-    let snapshotSelected = waitForSnapshotSelection();
-    SnapshotsListView._onRecordButtonClick();
-    return promise.all([recordingFinished, snapshotSelected]);
-  }
-
-  function recordAndWaitForAnotherSnapshot() {
-    let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
-    SnapshotsListView._onRecordButtonClick();
-    return recordingFinished;
-  }
-
-  function waitForSnapshotSelection() {
-    let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
-    let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
-    let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
-    return promise.all([
-      callListPopulated,
-      thumbnailsDisplayed,
-      screenshotDisplayed
-    ]);
-  }
-
-  yield teardown(panel);
-  finish();
-}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/canvasdebugger/test/browser_canvas-frontend-stop-03.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that a recording that has a rAF cycle, but no draw calls, fails
+ * after timeout.
+ */
+
+function ifTestingSupported() {
+  let { target, panel } = yield initCanvasDebuggerFrontend(RAF_NO_CANVAS_URL);
+  let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
+
+  yield reload(target);
+
+  let recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
+  SnapshotsListView._onRecordButtonClick();
+
+  yield recordingStarted;
+
+  is($("#empty-notice").hidden, true, "Empty notice not shown");
+  is($("#waiting-notice").hidden, false, "Waiting notice shown");
+
+  let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  let recordingCancelled = once(window, EVENTS.SNAPSHOT_RECORDING_CANCELLED);
+
+  yield promise.all([recordingFinished, recordingCancelled]);
+
+  ok(true, "Recording stopped and was considered failed.");
+
+  is(SnapshotsListView.itemCount, 0, "No snapshots in the list.");
+  is($("#empty-notice").hidden, false, "Empty notice shown");
+  is($("#waiting-notice").hidden, true, "Waiting notice not shown");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/canvasdebugger/test/doc_raf-no-canvas.html
@@ -0,0 +1,18 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Canvas inspector test page</title>
+  </head>
+
+  <body>
+    <script>
+      function render () { window.requestAnimationFrame(render); }
+      window.requestAnimationFrame(render);
+    </script>
+  </body>
+
+</html>
--- a/browser/devtools/canvasdebugger/test/head.js
+++ b/browser/devtools/canvasdebugger/test/head.js
@@ -25,16 +25,17 @@ let TiltGL = devtools.require("devtools/
 let TargetFactory = devtools.TargetFactory;
 let Toolbox = devtools.Toolbox;
 let mm = null
 
 const FRAME_SCRIPT_UTILS_URL = "chrome://browser/content/devtools/frame-script-utils.js";
 const EXAMPLE_URL = "http://example.com/browser/browser/devtools/canvasdebugger/test/";
 const SET_TIMEOUT_URL = EXAMPLE_URL + "doc_settimeout.html";
 const NO_CANVAS_URL = EXAMPLE_URL + "doc_no-canvas.html";
+const RAF_NO_CANVAS_URL = EXAMPLE_URL + "doc_raf-no-canvas.html";
 const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html";
 const SIMPLE_BITMASKS_URL = EXAMPLE_URL + "doc_simple-canvas-bitmasks.html";
 const SIMPLE_CANVAS_TRANSPARENT_URL = EXAMPLE_URL + "doc_simple-canvas-transparent.html";
 const SIMPLE_CANVAS_DEEP_STACK_URL = EXAMPLE_URL + "doc_simple-canvas-deep-stack.html";
 const WEBGL_ENUM_URL = EXAMPLE_URL + "doc_webgl-enum.html";
 const WEBGL_BINDINGS_URL = EXAMPLE_URL + "doc_webgl-bindings.html";
 const RAF_BEGIN_URL = EXAMPLE_URL + "doc_raf-begin.html";
 
--- a/browser/devtools/markupview/markup-view.css
+++ b/browser/devtools/markupview/markup-view.css
@@ -9,16 +9,21 @@
 /* Force height and width (possibly overflowing) from inline elements.
  * This allows long overflows of text or input fields to still be styled with
  * the container, rather than the background disappearing when scrolling */
 #root {
   float: left;
   min-width: 100%;
 }
 
+body.dragging .tag-line {
+  cursor: grabbing;
+  -moz-user-select: none;
+}
+
 #root-wrapper:after {
    content: "";
    display: block;
    clear: both;
    position:relative;
 }
 
 .html-editor {
@@ -58,16 +63,50 @@
 /* Tags are organized in a UL/LI tree and indented thanks to a left padding.
  * A very large padding is used in combination with a slightly smaller margin
  * to make sure childs actually span from edge-to-edge. */
 .child {
   margin-left: -1000em;
   padding-left: 1001em;
 }
 
+/* Normally this element takes space in the layout even if it's position: relative
+ * by adding height: 0 we let surrounding elements to fill the blank space */
+.child.dragging {
+  position: relative;
+  pointer-events: none;
+  opacity: 0.7;
+  height: 0;
+}
+
+/* Indicates a tag-line in the markup-view as being an active drop target by
+ * drawing a horizontal line where the dragged element would be inserted if
+ * dropped here */
+.tag-line.drop-target::before, .tag-line.drag-target::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+}
+
+.tag-line.drag-target::before {
+  border-top: 2px dashed var(--theme-contrast-background);
+}
+
+.tag-line.drop-target::before {
+  border-top: 2px dashed var(--theme-content-color1);
+}
+
+/* In case the indicator is put on the closing .tag-line, the indentation level
+ * will become misleading, so we push it forward to match the indentation level */
+ul.children + .tag-line::before {
+  margin-left: 14px;
+}
+
 .tag-line {
   min-height: 1.4em;
   line-height: 1.4em;
   position: relative;
 }
 
 .html-editor-container {
   position: relative;
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -9,16 +9,20 @@ const {Cc, Cu, Ci} = require("chrome");
 // Page size for pageup/pagedown
 const PAGE_SIZE = 10;
 const PREVIEW_AREA = 700;
 const DEFAULT_MAX_CHILDREN = 100;
 const COLLAPSE_ATTRIBUTE_LENGTH = 120;
 const COLLAPSE_DATA_URL_REGEX = /^data.+base64/;
 const COLLAPSE_DATA_URL_LENGTH = 60;
 const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000;
+const GRAB_DELAY = 400;
+const DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE = 50;
+const DRAG_DROP_MIN_AUTOSCROLL_SPEED = 5;
+const DRAG_DROP_MAX_AUTOSCROLL_SPEED = 15;
 
 const {UndoStack} = require("devtools/shared/undo");
 const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 const {HTMLEditor} = require("devtools/markupview/html-editor");
 const promise = require("resource://gre/modules/Promise.jsm").Promise;
 const {Tooltip} = require("devtools/shared/widgets/Tooltip");
 const EventEmitter = require("devtools/toolkit/event-emitter");
@@ -57,16 +61,17 @@ loader.lazyGetter(this, "AutocompletePop
  *        The inspector we're watching.
  * @param iframe aFrame
  *        An iframe in which the caller has kindly loaded markup-view.xhtml.
  */
 function MarkupView(aInspector, aFrame, aControllerWindow) {
   this._inspector = aInspector;
   this.walker = this._inspector.walker;
   this._frame = aFrame;
+  this.win = this._frame.contentWindow;
   this.doc = this._frame.contentDocument;
   this._elt = this.doc.querySelector("#root");
   this.htmlEditor = new HTMLEditor(this.doc);
 
   this.layoutHelpers = new LayoutHelpers(this.doc.defaultView);
 
   try {
     this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
@@ -89,16 +94,19 @@ function MarkupView(aInspector, aFrame, 
   this._boundMutationObserver = this._mutationObserver.bind(this);
   this.walker.on("mutations", this._boundMutationObserver);
 
   this._boundOnDisplayChange = this._onDisplayChange.bind(this);
   this.walker.on("display-change", this._boundOnDisplayChange);
 
   this._onMouseClick = this._onMouseClick.bind(this);
 
+  this._onMouseUp = this._onMouseUp.bind(this);
+  this.doc.body.addEventListener("mouseup", this._onMouseUp);
+
   this._boundOnNewSelection = this._onNewSelection.bind(this);
   this._inspector.selection.on("new-node-front", this._boundOnNewSelection);
   this._onNewSelection();
 
   this._boundKeyDown = this._onKeyDown.bind(this);
   this._frame.contentWindow.addEventListener("keydown", this._boundKeyDown, false);
 
   this._boundFocus = this._onFocus.bind(this);
@@ -151,17 +159,59 @@ MarkupView.prototype = {
     if (state) {
       this.tooltip.stopTogglingOnHover();
     } else {
       this.tooltip.startTogglingOnHover(this._elt,
         this._isImagePreviewTarget.bind(this));
     }
   },
 
+  isDragging: false,
+
   _onMouseMove: function(event) {
+    if (this.isDragging) {
+      event.preventDefault();
+      this._dragStartEl = event.target;
+
+      let docEl = this.doc.documentElement;
+
+      if (this._scrollInterval) {
+        this.win.clearInterval(this._scrollInterval);
+      }
+
+      // Auto-scroll when the mouse approaches top/bottom edge
+      let distanceFromBottom = docEl.clientHeight - event.pageY + this.win.scrollY,
+          distanceFromTop = event.pageY - this.win.scrollY;
+
+      if (distanceFromBottom <= DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE) {
+        // Map our distance from 0-50 to 5-15 range so the speed is kept
+        // in a range not too fast, not too slow
+        let speed = map(distanceFromBottom, 0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
+                        DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
+        // Here, we use minus because the value of speed - 15 is always negative
+        // and it makes the speed relative to the distance between mouse and edge
+        // the closer to the edge, the faster
+        this._scrollInterval = this.win.setInterval(() => {
+          docEl.scrollTop -= speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
+        }, 0);
+      }
+
+      if (distanceFromTop <= DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE) {
+        // refer to bottom edge's comments for more info
+        let speed = map(distanceFromTop, 0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
+                        DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
+
+        this._scrollInterval = this.win.setInterval(() => {
+          docEl.scrollTop += speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
+        }, 0);
+      }
+
+      return;
+    };
+
     let target = event.target;
 
     // Search target for a markupContainer reference, if not found, walk up
     while (!target.container) {
       if (target.tagName.toLowerCase() === "body") {
         return;
       }
       target = target.parentNode;
@@ -193,16 +243,28 @@ MarkupView.prototype = {
 
     if (container instanceof MarkupElementContainer) {
       // With the newly found container, delegate the tooltip content creation
       // and decision to show or not the tooltip
       container._buildEventTooltipContent(event.target, this.tooltip);
     }
   },
 
+  _onMouseUp: function() {
+    if (this._lastDropTarget) {
+      this.indicateDropTarget(null);
+    }
+    if (this._lastDragTarget) {
+      this.indicateDragTarget(null);
+    }
+    if (this._scrollInterval) {
+      this.win.clearInterval(this._scrollInterval);
+    }
+  },
+
   _hoveredNode: null,
 
   /**
    * Show a NodeFront's container as being hovered
    * @param {NodeFront} nodeFront The node to show as hovered
    */
   _showContainerAsHovered: function(nodeFront) {
     if (this._hoveredNode === nodeFront) {
@@ -213,16 +275,21 @@ MarkupView.prototype = {
       this.getContainer(this._hoveredNode).hovered = false;
     }
 
     this.getContainer(nodeFront).hovered = true;
     this._hoveredNode = nodeFront;
   },
 
   _onMouseLeave: function() {
+    if (this._scrollInterval) {
+      this.win.clearInterval(this._scrollInterval);
+    }
+    if (this.isDragging) return;
+
     this._hideBoxModel(true);
     if (this._hoveredNode) {
       this.getContainer(this._hoveredNode).hovered = false;
     }
     this._hoveredNode = null;
   },
 
   /**
@@ -785,16 +852,20 @@ MarkupView.prototype = {
     });
   },
 
   /**
    * Expand the container's children.
    */
   _expandContainer: function(aContainer) {
     return this._updateChildren(aContainer, {expand: true}).then(() => {
+      if (this._destroyer) {
+        console.warn("Could not expand the node, the markup-view was destroyed");
+        return;
+      } 
       aContainer.expanded = true;
     });
   },
 
   /**
    * Expand the node's children.
    */
   expandNode: function(aNode) {
@@ -1356,16 +1427,22 @@ MarkupView.prototype = {
     for (let [key, container] of this._containers) {
       container.destroy();
     }
     this._containers = null;
 
     this.tooltip.destroy();
     this.tooltip = null;
 
+    this.win = null;
+    this.doc = null;
+
+    this._lastDropTarget = null;
+    this._lastDragTarget = null;
+
     return this._destroyer;
   },
 
   /**
    * Initialize the preview panel.
    */
   _initPreview: function() {
     this._previewEnabled = Services.prefs.getBoolPref("devtools.inspector.markupPreview");
@@ -1443,16 +1520,90 @@ MarkupView.prototype = {
     let win = this._frame.contentWindow;
     this._previewBar.classList.add("hide");
     win.clearTimeout(this._resizePreviewTimeout);
 
     win.setTimeout(() => {
       this._updatePreview();
       this._previewBar.classList.remove("hide");
     }, 1000);
+  },
+
+  /**
+   * Takes an element as it's only argument and marks the element
+   * as the drop target
+   */
+  indicateDropTarget: function(el) {
+    if (this._lastDropTarget) {
+      this._lastDropTarget.classList.remove("drop-target");
+    }
+
+    if (!el) return;
+
+    let target = el.classList.contains("tag-line") ?
+                 el : el.querySelector(".tag-line") || el.closest(".tag-line");
+    if (!target) return;
+
+    target.classList.add("drop-target");
+    this._lastDropTarget = target;
+  },
+
+  /**
+   * Takes an element to mark it as indicator of dragging target's initial place
+   */
+  indicateDragTarget: function(el) {
+    if (this._lastDragTarget) {
+      this._lastDragTarget.classList.remove("drag-target");
+    }
+
+    if (!el) return;
+
+    let target = el.classList.contains("tag-line") ?
+                 el : el.querySelector(".tag-line") || el.closest(".tag-line");
+
+    if (!target) return;
+
+    target.classList.add("drag-target");
+    this._lastDragTarget = target;
+  },
+
+  /**
+   * Used to get the nodes required to modify the markup after dragging the element (parent/nextSibling)
+   */
+  get dropTargetNodes() {
+    let target = this._lastDropTarget;
+
+    if (!target) {
+      return null;
+    }
+
+    let parent, nextSibling;
+
+    if (this._lastDropTarget.previousElementSibling &&
+        this._lastDropTarget.previousElementSibling.nodeName.toLowerCase() === "ul") {
+      parent = target.parentNode.container.node;
+      nextSibling = null;
+    } else {
+      parent = target.parentNode.container.node.parentNode();
+      nextSibling = target.parentNode.container.node;
+    }
+
+    if (nextSibling && nextSibling.isBeforePseudoElement) {
+      nextSibling = target.parentNode.parentNode.children[1].container.node;
+    }
+    if (nextSibling && nextSibling.isAfterPseudoElement) {
+      parent = target.parentNode.container.node.parentNode();
+      nextSibling = null;
+    }
+
+    if (parent.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
+      return null;
+    }
+
+    return {parent, nextSibling};
   }
 };
 
 /**
  * The main structure for storing a document node in the markup
  * tree.  Manages creation of the editor for the node and
  * a <ul> for placing child elements, and expansion/collapsing
  * of the element.
@@ -1476,35 +1627,37 @@ MarkupContainer.prototype = {
    *        The node to display.
    * @param string templateID
    *        Which template to render for this container
    */
   initialize: function(markupView, node, templateID) {
     this.markup = markupView;
     this.node = node;
     this.undo = this.markup.undo;
+    this.win = this.markup._frame.contentWindow;
 
     // The template will fill the following properties
     this.elt = null;
     this.expander = null;
     this.tagState = null;
     this.tagLine = null;
     this.children = null;
     this.markup.template(templateID, this);
     this.elt.container = this;
 
-    // Binding event listeners
     this._onMouseDown = this._onMouseDown.bind(this);
-    this.elt.addEventListener("mousedown", this._onMouseDown, false);
-
     this._onToggle = this._onToggle.bind(this);
-
-    // Expanding/collapsing the node on dblclick of the whole tag-line element
+    this._onMouseUp = this._onMouseUp.bind(this);
+    this._onMouseMove = this._onMouseMove.bind(this);
+
+    // Binding event listeners
+    this.elt.addEventListener("mousedown", this._onMouseDown, false);
+    this.markup.doc.body.addEventListener("mouseup", this._onMouseUp, true);
+    this.markup.doc.body.addEventListener("mousemove", this._onMouseMove, true);
     this.elt.addEventListener("dblclick", this._onToggle, false);
-
     if (this.expander) {
       this.expander.addEventListener("click", this._onToggle, false);
     }
 
     // Marking the node as shown or hidden
     this.isDisplayed = this.node.isDisplayed;
   },
 
@@ -1613,43 +1766,128 @@ MarkupContainer.prototype = {
       this.expander.removeAttribute("open");
     }
   },
 
   parentContainer: function() {
     return this.elt.parentNode ? this.elt.parentNode.container : null;
   },
 
+  _isMouseDown: false,
+  _isDragging: false,
+  _dragStartY: 0,
+
+  set isDragging(isDragging) {
+    this._isDragging = isDragging;
+    this.markup.isDragging = isDragging;
+
+    if (isDragging) {
+      this.elt.classList.add("dragging");
+      this.markup.doc.body.classList.add("dragging");
+    } else {
+      this.elt.classList.remove("dragging");
+      this.markup.doc.body.classList.remove("dragging");
+    }
+  },
+
+  get isDragging() {
+    return this._isDragging;
+  },
+
   _onMouseDown: function(event) {
     let target = event.target;
 
-    // Target may be a resource link (generated by the output-parser)
+    // The "show more nodes" button (already has its onclick).
+    if (target.nodeName === "button") {
+      return;
+    }
+
+    // output-parser generated links handling.
     if (target.nodeName === "a") {
       event.stopPropagation();
       event.preventDefault();
       let browserWin = this.markup._inspector.target
                            .tab.ownerDocument.defaultView;
       browserWin.openUILinkIn(target.href, "tab");
+      return;
     }
-    // Or it may be the "show more nodes" button (which already has its onclick)
-    // Else, it's the container itself
-    else if (target.nodeName !== "button") {
-      this.hovered = false;
-      this.markup.navigate(this);
-      event.stopPropagation();
+
+    // target is the MarkupContainer itself.
+    this._isMouseDown = true;
+    this.hovered = false;
+    this.markup.navigate(this);
+    event.stopPropagation();
+
+    // Start dragging the container after a delay.
+    this.markup._dragStartEl = target;
+    this.win.setTimeout(() => {
+      // Make sure the mouse is still down and on target.
+      if (!this._isMouseDown || this.markup._dragStartEl !== target ||
+          this.node.isPseudoElement || this.node.isAnonymous ||
+          !this.win.getSelection().isCollapsed) {
+        return;
+      }
+      this.isDragging = true;
+
+      this._dragStartY = event.pageY;
+      this.markup.indicateDropTarget(this.elt);
+
+      // If this is the last child, use the closing <div.tag-line> of parent as indicator
+      this.markup.indicateDragTarget(this.elt.nextElementSibling ||
+                                     this.markup.getContainer(this.node.parentNode()).closeTagLine);
+    }, GRAB_DELAY);
+  },
+
+  /**
+   * On mouse up, stop dragging.
+   */
+  _onMouseUp: function(event) {
+    this._isMouseDown = false;
+
+    if (!this.isDragging) {
+      return;
     }
+
+    this.isDragging = false;
+    this.elt.style.removeProperty("top");
+
+    let dropTargetNodes = this.markup.dropTargetNodes;
+
+    if(!dropTargetNodes) {
+      return;
+    }
+
+    this.markup.walker.insertBefore(this.node, dropTargetNodes.parent,
+                                    dropTargetNodes.nextSibling);
+  },
+
+  /**
+   * On mouse move, move the dragged element if any and indicate the drop target.
+   */
+  _onMouseMove: function(event) {
+    if (!this.isDragging) {
+      return;
+    }
+
+    let diff = event.pageY - this._dragStartY;
+    this.elt.style.top = diff + "px";
+
+    let el = this.markup.doc.elementFromPoint(event.pageX - this.win.scrollX,
+                                              event.pageY - this.win.scrollY);
+
+    this.markup.indicateDropTarget(el);
   },
 
   /**
    * Temporarily flash the container to attract attention.
    * Used for markup mutations.
    */
   flashMutation: function() {
     if (!this.selected) {
-      let contentWin = this.markup._frame.contentWindow;
+      let contentWin = this.win;
       this.flashed = true;
       if (this._flashMutationTimer) {
         contentWin.clearTimeout(this._flashMutationTimer);
         this._flashMutationTimer = null;
       }
       this._flashMutationTimer = contentWin.setTimeout(() => {
         this.flashed = false;
       }, this.markup.CONTAINER_FLASHING_DURATION);
@@ -1772,16 +2010,20 @@ MarkupContainer.prototype = {
   /**
    * Get rid of event listeners and references, when the container is no longer
    * needed
    */
   destroy: function() {
     // Remove event listeners
     this.elt.removeEventListener("mousedown", this._onMouseDown, false);
     this.elt.removeEventListener("dblclick", this._onToggle, false);
+    this.markup.doc.body.removeEventListener("mouseup", this._onMouseUp, true);
+    this.markup.doc.body.removeEventListener("mousemove", this._onMouseMove, true);
+
+    this.win = null;
 
     if (this.expander) {
       this.expander.removeEventListener("click", this._onToggle, false);
     }
 
     // Recursively destroy children containers
     let firstChild;
     while (firstChild = this.children.firstChild) {
@@ -2510,16 +2752,27 @@ function parseAttributeValues(attr, doc)
     }
     catch(e) { }
   }
 
   // Attributes return from DOMParser in reverse order from how they are entered.
   return attributes.reverse();
 }
 
+/**
+ * Map a number from one range to another.
+ */
+function map(value, oldMin, oldMax, newMin, newMax) {
+  let ratio = oldMax - oldMin;
+  if (ratio == 0) {
+    return value;
+  }
+  return newMin + (newMax - newMin) * ((value - oldMin) / ratio);
+}
+
 loader.lazyGetter(MarkupView.prototype, "strings", () => Services.strings.createBundle(
   "chrome://browser/locale/devtools/inspector.properties"
 ));
 
 XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
   return Cc["@mozilla.org/widget/clipboardhelper;1"].
     getService(Ci.nsIClipboardHelper);
 });
--- a/browser/devtools/markupview/test/browser.ini
+++ b/browser/devtools/markupview/test/browser.ini
@@ -1,12 +1,14 @@
 [DEFAULT]
 subsuite = devtools
 support-files =
   doc_markup_anonymous.html
+  doc_markup_dragdrop.html
+  doc_markup_dragdrop_autoscroll.html
   doc_markup_edit.html
   doc_markup_events.html
   doc_markup_events_jquery.html
   doc_markup_events-overflow.html
   doc_markup_flashing.html
   doc_markup_mutation.html
   doc_markup_navigation.html
   doc_markup_not_displayed.html
@@ -32,16 +34,21 @@ support-files =
 
 [browser_markupview_anonymous_01.js]
 [browser_markupview_anonymous_02.js]
 skip-if = e10s # scratchpad.xul is not loading in e10s window
 [browser_markupview_anonymous_03.js]
 [browser_markupview_anonymous_04.js]
 [browser_markupview_copy_image_data.js]
 [browser_markupview_css_completion_style_attribute.js]
+[browser_markupview_dragdrop_autoscroll.js]
+[browser_markupview_dragdrop_invalidNodes.js]
+[browser_markupview_dragdrop_isDragging.js]
+[browser_markupview_dragdrop_reorder.js]
+[browser_markupview_dragdrop_textSelection.js]
 [browser_markupview_events.js]
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
 [browser_markupview_events-overflow.js]
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
 [browser_markupview_events_jquery_1.0.js]
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
 [browser_markupview_events_jquery_1.1.js]
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_dragdrop_autoscroll.js
@@ -0,0 +1,61 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test: Dragging nodes near top/bottom edges of inspector
+// should auto-scroll
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop_autoscroll.html";
+const GRAB_DELAY = 400;
+
+add_task(function*() {
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  let markup = inspector.markup;
+
+  let container = yield getContainerForSelector("#first", inspector);
+  let rect = container.elt.getBoundingClientRect();
+
+  info("Simulating mouseDown on #first");
+  container._onMouseDown({
+    target: container.tagLine,
+    pageX: 10,
+    pageY: rect.top,
+    stopPropagation: function() {},
+    preventDefault: function() {}
+  });
+
+  yield wait(GRAB_DELAY + 1);
+
+  let clientHeight = markup.doc.documentElement.clientHeight;
+  info("Simulating mouseMove on #first with pageY: " + clientHeight);
+
+  let ev = {
+    target: container.tagLine,
+    pageX: 10,
+    pageY: clientHeight,
+    preventDefault: function() {}
+  };
+
+  info("Listening on scroll event");
+  let scroll = onScroll(markup.win);
+
+  markup._onMouseMove(ev);
+
+  yield scroll;
+
+  container._onMouseUp(ev);
+  markup._onMouseUp(ev);
+
+  ok("Scroll event fired");
+});
+
+function onScroll(win) {
+  return new Promise((resolve, reject) => {
+    win.onscroll = function(e) {
+      resolve(e);
+    }
+  });
+};
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_dragdrop_invalidNodes.js
@@ -0,0 +1,58 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test: pseudo-elements and anonymous nodes should not be draggable
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
+const GRAB_DELAY = 400;
+
+add_task(function*() {
+  Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
+
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  info("Expanding #test");
+  let parentFront = yield getNodeFront("#test", inspector);
+  yield inspector.markup.expandNode(parentFront);
+  yield waitForMultipleChildrenUpdates(inspector);
+
+  let parentContainer = yield getContainerForNodeFront(parentFront, inspector);
+  let beforePseudo = parentContainer.elt.children[1].firstChild.container;
+
+  parentContainer.elt.scrollIntoView(true);
+
+  info("Simulating mouseDown on #test::before");
+  beforePseudo._onMouseDown({
+    target: beforePseudo.tagLine,
+    stopPropagation: function() {},
+    preventDefault: function() {}
+  });
+
+  info("Waiting " + (GRAB_DELAY + 1) + "ms")
+  yield wait(GRAB_DELAY + 1);
+  is(beforePseudo.isDragging, false, "[pseudo-element] isDragging is false after GRAB_DELAY has passed");
+
+  let inputFront = yield getNodeFront("#anonymousParent", inspector);
+
+  yield inspector.markup.expandNode(inputFront);
+  yield waitForMultipleChildrenUpdates(inspector);
+
+  let inputContainer = yield getContainerForNodeFront(inputFront, inspector);
+  let anonymousDiv = inputContainer.elt.children[1].firstChild.container;
+
+  inputContainer.elt.scrollIntoView(true);
+
+  info("Simulating mouseDown on input#anonymousParent div");
+  anonymousDiv._onMouseDown({
+    target: anonymousDiv.tagLine,
+    stopPropagation: function() {},
+    preventDefault: function() {}
+  });
+
+  info("Waiting " + (GRAB_DELAY + 1) + "ms")
+  yield wait(GRAB_DELAY + 1);
+  is(anonymousDiv.isDragging, false, "[anonymous element] isDragging is false after GRAB_DELAY has passed");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_dragdrop_isDragging.js
@@ -0,0 +1,41 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test drag mode's delay, it shouldn't enable dragging before
+// GRAB_DELAY = 400 has passed
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
+const GRAB_DELAY = 400;
+
+add_task(function*() {
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  let el = yield getContainerForSelector("#test", inspector);
+  let rect = el.tagLine.getBoundingClientRect();
+
+  info("Simulating mouseDown on #test");
+  el._onMouseDown({
+    target: el.tagLine,
+    pageX: rect.x,
+    pageY: rect.y,
+    stopPropagation: function() {}
+  });
+
+  is(el.isDragging, false, "isDragging should not be set to true immedietly");
+
+  info("Waiting " + (GRAB_DELAY + 1) + "ms");
+  yield wait(GRAB_DELAY + 1);
+  ok(el.isDragging, "isDragging true after GRAB_DELAY has passed");
+
+  info("Simulating mouseUp on #test");
+  el._onMouseUp({
+    target: el.tagLine,
+    pageX: rect.x,
+    pageY: rect.y
+  });
+
+  is(el.isDragging, false, "isDragging false after mouseUp");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_dragdrop_reorder.js
@@ -0,0 +1,109 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test different kinds of drag and drop node re-ordering
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
+const GRAB_DELAY = 400;
+
+add_task(function*() {
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  info("Expanding #test");
+  let parentFront = yield getNodeFront("#test", inspector);
+  let parent = yield getNode("#test");
+  let parentContainer = yield getContainerForNodeFront(parentFront, inspector);
+
+  yield inspector.markup.expandNode(parentFront);
+  yield waitForMultipleChildrenUpdates(inspector);
+
+  parentContainer.elt.scrollIntoView(true);
+
+  info("Testing switching elements inside their parent");
+  yield moveElementDown("#firstChild", "#middleChild", inspector);
+
+  is(parent.children[0].id, "middleChild", "#firstChild is now the second child of #test");
+  is(parent.children[1].id, "firstChild", "#middleChild is now the first child of #test");
+
+  info("Testing switching elements with a last child");
+  yield moveElementDown("#firstChild", "#lastChild", inspector);
+
+  is(parent.children[1].id, "lastChild", "#lastChild is now the second child of #test");
+  is(parent.children[2].id, "firstChild", "#firstChild is now the last child of #test");
+
+  info("Testing appending element to a parent");
+  yield moveElementDown("#before", "#test", inspector);
+
+  is(parent.children.length, 4, "New element appended to #test");
+  is(parent.children[0].id, "before", "New element is appended at the right place (currently first child)");
+
+  info("Testing moving element to after it's parent");
+  yield moveElementDown("#firstChild", "#test", inspector);
+
+  is(parent.children.length, 3, "#firstChild is no longer #test's child");
+  is(parent.nextElementSibling.id, "firstChild", "#firstChild is now #test's nextElementSibling");
+});
+
+function* dragContainer(selector, targetOffset, inspector) {
+  info("Dragging the markup-container for node " + selector);
+
+  let container = yield getContainerForSelector(selector, inspector);
+
+  let updated = inspector.once("inspector-updated");
+
+  let rect = {
+    x: container.tagLine.offsetLeft,
+    y: container.tagLine.offsetTop
+  };
+
+  info("Simulating mouseDown on " + selector);
+  container._onMouseDown({
+    target: container.tagLine,
+    pageX: rect.x,
+    pageY: rect.y,
+    stopPropagation: function() {}
+  });
+
+  let targetX = rect.x + targetOffset.x,
+      targetY = rect.y + targetOffset.y;
+
+  setTimeout(() => {
+    info("Simulating mouseMove on " + selector +
+         " with pageX: " + targetX + " pageY: " + targetY);
+    container._onMouseMove({
+      target: container.tagLine,
+      pageX: targetX,
+      pageY: targetY
+    });
+
+    info("Simulating mouseUp on " + selector +
+         " with pageX: " + targetX + " pageY: " + targetY);
+    container._onMouseUp({
+      target: container.tagLine,
+      pageX: targetX,
+      pageY: targetY
+    });
+
+    container.markup._onMouseUp();
+  }, GRAB_DELAY+1);
+
+  return updated;
+};
+
+function* moveElementDown(selector, next, inspector) {
+  let onMutated = inspector.once("markupmutation");
+  let uiUpdate = inspector.once("inspector-updated");
+
+  let el = yield getContainerForSelector(next, inspector);
+  let height = el.tagLine.getBoundingClientRect().height;
+
+  info("Switching " + selector + ' with ' + next);
+
+  yield dragContainer(selector, {x: 0, y: Math.round(height) + 2}, inspector);
+
+  yield onMutated;
+  yield uiUpdate;
+};
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_dragdrop_textSelection.js
@@ -0,0 +1,48 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test: Nodes should not be draggable if there is a text selected
+// (trying to move selected text around shouldn't trigger node drag and drop)
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
+const GRAB_DELAY = 400;
+
+add_task(function*() {
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+  let markup = inspector.markup;
+
+  info("Expanding span#before");
+  let spanFront = yield getNodeFront("#before", inspector);
+  let spanContainer = yield getContainerForNodeFront(spanFront, inspector);
+  let span = yield getNode("#before");
+
+  yield inspector.markup.expandNode(spanFront);
+  yield waitForMultipleChildrenUpdates(inspector);
+
+  spanContainer.elt.scrollIntoView(true);
+
+  info("Selecting #before's text content");
+
+  let textContent = spanContainer.elt.children[1].firstChild.container;
+
+  let selectRange = markup.doc.createRange();
+  selectRange.selectNode(textContent.editor.elt.querySelector('[tabindex]'));
+  markup.doc.getSelection().addRange(selectRange);
+
+  info("Simulating mouseDown on #before");
+
+  spanContainer._onMouseDown({
+    pageX: 0,
+    pageY: 0,
+    target: spanContainer.tagLine,
+    stopPropagation: function() {},
+    preventDefault: function() {}
+  });
+
+  yield wait(GRAB_DELAY + 1);
+
+  is(spanContainer.isDragging, false, "isDragging should be false if there is a text selected");
+});
\ No newline at end of file
--- a/browser/devtools/markupview/test/browser_markupview_textcontent_edit_01.js
+++ b/browser/devtools/markupview/test/browser_markupview_textcontent_edit_01.js
@@ -26,20 +26,8 @@ add_task(function*() {
   let field = container.elt.querySelector("pre");
   setEditableFieldValue(field, "New text", inspector);
   yield onMutated;
 
   is(node.nodeValue, "New text", "Test test node's text content has changed");
 
   yield inspector.once("inspector-updated");
 });
-
-// The expand all operation of the markup-view calls itself recursively and
-// there's not one event we can wait for to know when it's done
-function* waitForMultipleChildrenUpdates(inspector) {
-  // As long as child updates are queued up while we wait for an update already
-  // wait again
-  if (inspector.markup._queuedChildUpdates &&
-      inspector.markup._queuedChildUpdates.size) {
-    yield waitForChildrenUpdated(inspector);
-    return yield waitForMultipleChildrenUpdates(inspector);
-  }
-}
--- a/browser/devtools/markupview/test/browser_markupview_toggle_03.js
+++ b/browser/devtools/markupview/test/browser_markupview_toggle_03.js
@@ -28,20 +28,8 @@ add_task(function*() {
   let nodeFronts = yield nodeList.items();
   for (let nodeFront of nodeFronts) {
     let nodeContainer = getContainerForNodeFront(nodeFront, inspector);
     ok(nodeContainer, "Container for node " + nodeFront.tagName + " exists");
     ok(nodeContainer.expanded,
       "Container for node " + nodeFront.tagName + " is expanded");
   }
 });
-
-// The expand all operation of the markup-view calls itself recursively and
-// there's not one event we can wait for to know when it's done
-function* waitForMultipleChildrenUpdates(inspector) {
-  // As long as child updates are queued up while we wait for an update already
-  // wait again
-  if (inspector.markup._queuedChildUpdates &&
-      inspector.markup._queuedChildUpdates.size) {
-    yield waitForChildrenUpdated(inspector);
-    return yield waitForMultipleChildrenUpdates(inspector);
-  }
-}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/doc_markup_dragdrop.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=858038
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 858038</title>
+  <style>
+    #test::before {
+      content: 'This should not be draggable';
+    }
+  </style>
+</head>
+<body>
+  <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858038">Mozilla Bug 858038</a>
+  <p id="display"></p>
+  <div id="content" style="display: none">
+
+  </div>
+  <input id="anonymousParent" />
+
+  <span id="before">Before</span>
+  <pre id="test">
+    <span id="firstChild">First</span>
+    <span id="middleChild">Middle</span>
+    <span id="lastChild">Last</span>
+  </pre>
+  <span id="after">After</span>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/doc_markup_dragdrop_autoscroll.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=858038
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 858038 - Autoscroll</title>
+</head>
+<body>
+  <div id="first"></div>
+  <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858038">Mozilla Bug 858038</a>
+  <p id="display">Test</p>
+  <div id="content" style="display: none">
+
+  </div>
+
+  <!-- Make sure the markup-view has enough nodes shown by default that it has a scrollbar -->
+
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+  <div></div>
+</body>
+</html>
--- a/browser/devtools/markupview/test/head.js
+++ b/browser/devtools/markupview/test/head.js
@@ -556,8 +556,21 @@ function collapseSelectionAndShiftTab(in
 function checkFocusedAttribute(attrName, editMode) {
   let focusedAttr = Services.focus.focusedElement;
   is(focusedAttr ? focusedAttr.parentNode.dataset.attr : undefined,
     attrName, attrName + " attribute editor is currently focused.");
   is(focusedAttr ? focusedAttr.tagName : undefined,
     editMode ? "input": "span",
     editMode ? attrName + " is in edit mode" : attrName + " is not in edit mode");
 }
+
+// The expand all operation of the markup-view calls itself recursively and
+// there's not one event we can wait for to know when it's done
+// so use this helper function to wait until all recursive children updates are done.
+function* waitForMultipleChildrenUpdates(inspector) {
+  // As long as child updates are queued up while we wait for an update already
+  // wait again
+  if (inspector.markup._queuedChildUpdates &&
+      inspector.markup._queuedChildUpdates.size) {
+    yield waitForChildrenUpdated(inspector);
+    return yield waitForMultipleChildrenUpdates(inspector);
+  }
+}
--- a/browser/devtools/netmonitor/test/browser_net_prefs-and-l10n.js
+++ b/browser/devtools/netmonitor/test/browser_net_prefs-and-l10n.js
@@ -30,19 +30,16 @@ function test() {
       is(L10N.getFormatStr("networkMenu.totalMS", "foo"),
         stringBundle.formatStringFromName("networkMenu.totalMS", ["foo"], 1),
         "The getFormatStr() method didn't return the expected string.");
     }
 
     function testPrefs() {
       let { Prefs } = aMonitor.panelWin;
 
-      is(Prefs._root, "devtools.netmonitor",
-        "The preferences object should have a correct root path.");
-
       is(Prefs.networkDetailsWidth,
         Services.prefs.getIntPref("devtools.netmonitor.panes-network-details-width"),
         "Getting a pref should work correctly.");
 
       let previousValue = Prefs.networkDetailsWidth;
       let bogusValue = ~~(Math.random() * 100);
       Prefs.networkDetailsWidth = bogusValue;
       is(Prefs.networkDetailsWidth,
--- a/browser/devtools/performance/modules/recording-utils.js
+++ b/browser/devtools/performance/modules/recording-utils.js
@@ -1,13 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
+const { Cc, Ci, Cu, Cr } = require("chrome");
+
 /**
  * Utility functions for managing recording models and their internal data,
  * such as filtering profile samples or offsetting timestamps.
  */
 
 exports.RecordingUtils = {};
 
 /**
@@ -125,8 +127,55 @@ exports.RecordingUtils.getSamplesFromAll
     }
 
     sample.frames.reverse();
   }
 
   gSamplesFromAllocationCache.set(allocations, samples);
   return samples;
 }
+
+/**
+ * Gets the current timeline blueprint without the hidden markers.
+ *
+ * @param blueprint
+ *        The default timeline blueprint.
+ * @param array hiddenMarkers
+ *        A list of hidden markers' names.
+ * @return object
+ *         The filtered timeline blueprint.
+ */
+exports.RecordingUtils.getFilteredBlueprint = function({ blueprint, hiddenMarkers }) {
+  let filteredBlueprint = Cu.cloneInto(blueprint, {});
+  let maybeRemovedGroups = new Set();
+  let removedGroups = new Set();
+
+  // 1. Remove hidden markers from the blueprint.
+
+  for (let hiddenMarkerName of hiddenMarkers) {
+    maybeRemovedGroups.add(filteredBlueprint[hiddenMarkerName].group);
+    delete filteredBlueprint[hiddenMarkerName];
+  }
+
+  // 2. Get a list of all the groups that will be removed.
+
+  for (let maybeRemovedGroup of maybeRemovedGroups) {
+    let markerNames = Object.keys(filteredBlueprint);
+    let isGroupRemoved = markerNames.every(e => filteredBlueprint[e].group != maybeRemovedGroup);
+    if (isGroupRemoved) {
+      removedGroups.add(maybeRemovedGroup);
+    }
+  }
+
+  // 3. Offset groups so that their indices are consecutive.
+
+  for (let removedGroup of removedGroups) {
+    let markerNames = Object.keys(filteredBlueprint);
+    for (let markerName of markerNames) {
+      let markerDetails = filteredBlueprint[markerName];
+      if (markerDetails.group > removedGroup) {
+        markerDetails.group--;
+      }
+    }
+  }
+
+  return filteredBlueprint;
+};
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -52,20 +52,22 @@ devtools.lazyImporter(this, "FlameGraph"
   "resource:///modules/devtools/FlameGraph.jsm");
 devtools.lazyImporter(this, "SideMenuWidget",
   "resource:///modules/devtools/SideMenuWidget.jsm");
 
 const BRANCH_NAME = "devtools.performance.ui.";
 
 // Events emitted by various objects in the panel.
 const EVENTS = {
-  // Fired by the OptionsView when a preference changes.
+  // Fired by the PerformanceController and OptionsView when a pref changes.
   PREF_CHANGED: "Performance:PrefChanged",
 
-  // Emitted by the PerformanceView when the state (display mode) changes.
+  // Emitted by the PerformanceView when the state (display mode) changes,
+  // for example when switching between "empty", "recording" or "recorded".
+  // This causes certain panels to be hidden or visible.
   UI_STATE_CHANGED: "Performance:UI:StateChanged",
 
   // Emitted by the PerformanceView on clear button click
   UI_CLEAR_RECORDINGS: "Performance:UI:ClearRecordings",
 
   // Emitted by the PerformanceView on record button click
   UI_START_RECORDING: "Performance:UI:StartRecording",
   UI_STOP_RECORDING: "Performance:UI:StopRecording",
@@ -171,16 +173,26 @@ let PerformanceController = {
     this.stopRecording = this.stopRecording.bind(this);
     this.importRecording = this.importRecording.bind(this);
     this.exportRecording = this.exportRecording.bind(this);
     this.clearRecordings = this.clearRecordings.bind(this);
     this._onTimelineData = this._onTimelineData.bind(this);
     this._onRecordingSelectFromView = this._onRecordingSelectFromView.bind(this);
     this._onPrefChanged = this._onPrefChanged.bind(this);
 
+    // All boolean prefs should be handled via the OptionsView in the
+    // ToolbarView, so that they may be accessible via the "gear" menu.
+    // Every other pref should be registered here.
+    this._nonBooleanPrefs = new ViewHelpers.Prefs("devtools.performance", {
+      "hidden-markers": ["Json", "timeline.hidden-markers"]
+    });
+
+    this._nonBooleanPrefs.registerObserver();
+    this._nonBooleanPrefs.on("pref-changed", this._onPrefChanged);
+
     ToolbarView.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
     PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
     PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
     PerformanceView.on(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
     RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     RecordingsView.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
 
@@ -190,63 +202,87 @@ let PerformanceController = {
     gFront.on("ticks", this._onTimelineData); // framerate
     gFront.on("allocations", this._onTimelineData); // memory allocations
   }),
 
   /**
    * Remove events handled by the PerformanceController
    */
   destroy: function() {
+    this._nonBooleanPrefs.unregisterObserver();
+    this._nonBooleanPrefs.off("pref-changed", this._onPrefChanged);
+
     ToolbarView.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
     PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
     PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
     PerformanceView.off(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
     RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     RecordingsView.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
 
     gFront.off("markers", this._onTimelineData);
     gFront.off("frames", this._onTimelineData);
     gFront.off("memory", this._onTimelineData);
     gFront.off("ticks", this._onTimelineData);
     gFront.off("allocations", this._onTimelineData);
   },
 
   /**
-   * Get a preference setting from `prefName` via the underlying
+   * Get a boolean preference setting from `prefName` via the underlying
    * OptionsView in the ToolbarView.
+   *
+   * @param string prefName
+   * @return boolean
+   */
+  getOption: function (prefName) {
+    return ToolbarView.optionsView.getPref(prefName);
+  },
+
+  /**
+   * Get a preference setting from `prefName`.
+   * @param string prefName
+   * @return object
    */
   getPref: function (prefName) {
-    return ToolbarView.optionsView.getPref(prefName);
+    return this._nonBooleanPrefs[prefName];
+  },
+
+  /**
+   * Set a preference setting from `prefName`.
+   * @param string prefName
+   * @param object prefValue
+   */
+  setPref: function (prefName, prefValue) {
+    this._nonBooleanPrefs[prefName] = prefValue;
   },
 
   /**
    * Starts recording with the PerformanceFront. Emits `EVENTS.RECORDING_STARTED`
    * when the front has started to record.
    */
   startRecording: Task.async(function *() {
-    let withMemory = this.getPref("enable-memory");
-    let withTicks = this.getPref("enable-framerate");
-    let withAllocations = this.getPref("enable-memory");
+    let withMemory = this.getOption("enable-memory");
+    let withTicks = this.getOption("enable-framerate");
+    let withAllocations = this.getOption("enable-memory");
 
     let recording = this._createRecording({ withMemory, withTicks, withAllocations });
 
     this.emit(EVENTS.RECORDING_WILL_START, recording);
     yield recording.startRecording({ withTicks, withMemory, withAllocations });
     this.emit(EVENTS.RECORDING_STARTED, recording);
 
     this.setCurrentRecording(recording);
   }),
 
   /**
    * Stops recording with the PerformanceFront. Emits `EVENTS.RECORDING_STOPPED`
    * when the front has stopped recording.
    */
   stopRecording: Task.async(function *() {
-    let recording = this._getLatestRecording();
+    let recording = this.getLatestRecording();
 
     this.emit(EVENTS.RECORDING_WILL_STOP, recording);
     yield recording.stopRecording();
     this.emit(EVENTS.RECORDING_STOPPED, recording);
   }),
 
   /**
    * Saves the given recording to a file. Emits `EVENTS.RECORDING_EXPORTED`
@@ -262,17 +298,17 @@ let PerformanceController = {
     this.emit(EVENTS.RECORDING_EXPORTED, recording);
   }),
 
   /**
    * Clears all recordings from the list as well as the current recording.
    * Emits `EVENTS.RECORDINGS_CLEARED` when complete so other components can clean up.
    */
   clearRecordings: Task.async(function* () {
-    let latest = this._getLatestRecording();
+    let latest = this.getLatestRecording();
 
     if (latest && latest.isRecording()) {
       yield this.stopRecording();
     }
 
     this._recordings.length = 0;
     this.setCurrentRecording(null);
     this.emit(EVENTS.RECORDINGS_CLEARED);
@@ -330,24 +366,34 @@ let PerformanceController = {
   getCurrentRecording: function () {
     return this._currentRecording;
   },
 
   /**
    * Get most recently added recording that was triggered manually (via UI).
    * @return RecordingModel
    */
-  _getLatestRecording: function () {
+  getLatestRecording: function () {
     for (let i = this._recordings.length - 1; i >= 0; i--) {
       return this._recordings[i];
     }
     return null;
   },
 
   /**
+   * Gets the current timeline blueprint without the hidden markers.
+   * @return object
+   */
+  getTimelineBlueprint: function() {
+    let blueprint = TIMELINE_BLUEPRINT;
+    let hiddenMarkers = this.getPref("hidden-markers");
+    return RecordingUtils.getFilteredBlueprint({ blueprint, hiddenMarkers });
+  },
+
+  /**
    * Fired whenever the PerformanceFront emits markers, memory or ticks.
    */
   _onTimelineData: function (...data) {
     this._recordings.forEach(e => e.addTimelineData.apply(e, data));
     this.emit(EVENTS.TIMELINE_DATA, ...data);
   },
 
   /**
@@ -357,18 +403,18 @@ let PerformanceController = {
   _onRecordingSelectFromView: function (_, recording) {
     this.setCurrentRecording(recording);
   },
 
   /**
    * Fired when the ToolbarView fires a PREF_CHANGED event.
    * with the value.
    */
-  _onPrefChanged: function (_, prefName, value) {
-    this.emit(EVENTS.PREF_CHANGED, prefName, value);
+  _onPrefChanged: function (_, prefName, prefValue) {
+    this.emit(EVENTS.PREF_CHANGED, prefName, prefValue);
   },
 
   toString: () => "[object PerformanceController]"
 };
 
 /**
  * Convenient way of emitting events from the controller.
  */
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -24,16 +24,17 @@
   <script type="application/javascript" src="performance/views/details-js-call-tree.js"/>
   <script type="application/javascript" src="performance/views/details-js-flamegraph.js"/>
   <script type="application/javascript" src="performance/views/details-memory-call-tree.js"/>
   <script type="application/javascript" src="performance/views/details-memory-flamegraph.js"/>
   <script type="application/javascript" src="performance/views/details.js"/>
   <script type="application/javascript" src="performance/views/recordings.js"/>
 
   <popupset id="performance-options-popupset">
+    <menupopup id="performance-filter-menupopup"/>
     <menupopup id="performance-options-menupopup">
       <menuitem id="option-show-platform-data"
                 type="checkbox"
                 data-pref="show-platform-data"
                 label="&profilerUI.showPlatformData;"
                 tooltiptext="&profilerUI.showPlatformData.tooltiptext;"/>
       <menuitem id="option-enable-memory"
                 type="checkbox"
@@ -80,16 +81,22 @@
                          label="&profilerUI.clearButton;"/>
         </hbox>
       </toolbar>
       <vbox id="recordings-list" flex="1"/>
     </vbox>
 
     <vbox id="performance-pane" flex="1">
       <toolbar id="performance-toolbar" class="devtools-toolbar">
+        <hbox id="performance-toolbar-control-other" class="devtools-toolbarbutton-group">
+          <toolbarbutton id="filter-button"
+                         popup="performance-filter-menupopup"
+                         class="devtools-toolbarbutton"
+                         tooltiptext="&profilerUI.options.filter.tooltiptext;"/>
+        </hbox>
         <hbox id="performance-toolbar-controls-detail-views" class="devtools-toolbarbutton-group">
           <toolbarbutton id="select-waterfall-view"
                          class="devtools-toolbarbutton devtools-button"
                          label="&profilerUI.toolbar.waterfall;"
                          data-view="waterfall" />
           <toolbarbutton id="select-js-calltree-view"
                          class="devtools-toolbarbutton devtools-button"
                          label="&profilerUI.toolbar.js-calltree;"
@@ -107,17 +114,17 @@
                          label="&profilerUI.toolbar.memory-flamegraph;"
                          data-view="memory-flamegraph" />
         </hbox>
         <spacer flex="1"></spacer>
         <hbox id="performance-toolbar-control-options" class="devtools-toolbarbutton-group">
           <toolbarbutton id="performance-options-button"
                          class="devtools-toolbarbutton devtools-option-toolbarbutton"
                          popup="performance-options-menupopup"
-                         tooltiptext="&profilerUI.options.tooltiptext;"/>
+                         tooltiptext="&profilerUI.options.gear.tooltiptext;"/>
         </hbox>
       </toolbar>
 
       <deck id="performance-view" flex="1">
         <hbox id="empty-notice"
               class="notice-container"
               align="center"
               pack="center"
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -92,8 +92,9 @@ support-files =
 [browser_profiler_tree-view-01.js]
 [browser_profiler_tree-view-02.js]
 [browser_profiler_tree-view-03.js]
 [browser_profiler_tree-view-04.js]
 [browser_profiler_tree-view-05.js]
 [browser_profiler_tree-view-06.js]
 [browser_profiler_tree-view-07.js]
 [browser_timeline_blueprint.js]
+[browser_timeline_filters.js]
rename from browser/devtools/timeline/test/browser_timeline_filters.js
rename to browser/devtools/performance/test/browser_timeline_filters.js
--- a/browser/devtools/timeline/test/browser_timeline_filters.js
+++ b/browser/devtools/performance/test/browser_timeline_filters.js
@@ -1,37 +1,38 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests markers filtering mechanism.
  */
 
-add_task(function*() {
-  let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { $, $$, TimelineController, TimelineView } = panel.panelWin;
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { $, $$, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
+  let { MARKERS_GRAPH_ROW_HEIGHT } = panel.panelWin;
 
-  yield TimelineController.toggleRecording();
+  yield startRecording(panel);
   ok(true, "Recording has started.");
 
   yield waitUntil(() => {
     // Wait until we get 3 different markers.
-    let markers = TimelineController.getMarkers();
+    let markers = PerformanceController.getCurrentRecording().getMarkers();
     return markers.some(m => m.name == "Styles") &&
            markers.some(m => m.name == "Reflow") &&
            markers.some(m => m.name == "Paint");
   });
 
-  yield TimelineController.toggleRecording();
+  yield stopRecording(panel);
 
-  let overview = TimelineView.markersOverview;
-  let waterfall = TimelineView.waterfall;
+  let overview = OverviewView.markersOverview;
+  let waterfall = WaterfallView.waterfall;
 
   // Select everything
-  overview.setSelection({ start: 0, end: overview.width })
+  OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE })
 
   $("#filter-button").click();
 
   yield waitUntil(() => !waterfall._outstandingMarkers.length);
 
   let menuItem1 = $("menuitem[marker-type=Styles]");
   let menuItem2 = $("menuitem[marker-type=Reflow]");
   let menuItem3 = $("menuitem[marker-type=Paint]");
@@ -65,18 +66,17 @@ add_task(function*() {
   ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (3)");
 
   heightBefore = overview.fixedHeight;
   EventUtils.synthesizeMouseAtCenter(menuItem3, {type: "mouseup"}, panel.panelWin);
   yield once(menuItem3, "command");
 
   yield waitUntil(() => !waterfall._outstandingMarkers.length);
 
-  // A row is 11px. See markers-overview.js
-  is(overview.fixedHeight, heightBefore - 11, "Overview is smaller");
+  is(overview.fixedHeight, heightBefore - MARKERS_GRAPH_ROW_HEIGHT, "Overview is smaller");
   ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (4)");
   ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (4)");
   ok(!$(".waterfall-marker-bar[type=Paint]"), "No 'Paint' marker (4)");
 
   for (let item of [menuItem1, menuItem2, menuItem3]) {
     EventUtils.synthesizeMouseAtCenter(item, {type: "mouseup"}, panel.panelWin);
     yield once(item, "command");
   }
@@ -84,10 +84,11 @@ add_task(function*() {
   yield waitUntil(() => !waterfall._outstandingMarkers.length);
 
   ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (5)");
   ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (5)");
   ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (5)");
 
   is(overview.fixedHeight, originalHeight, "Overview restored");
 
-  $(".waterfall-marker-bar[type=Styles]");
-});
+  yield teardown(panel);
+  finish();
+}
--- a/browser/devtools/performance/test/doc_simple-test.html
+++ b/browser/devtools/performance/test/doc_simple-test.html
@@ -1,22 +1,25 @@
 <!-- Any copyright is dedicated to the Public Domain.
      http://creativecommons.org/publicdomain/zero/1.0/ -->
 <!doctype html>
 
 <html>
   <head>
     <meta charset="utf-8"/>
-    <title>Profiler test page</title>
+    <title>Performance test page</title>
   </head>
 
   <body>
     <script type="text/javascript">
+      var x = 1;
       function test() {
-        var a = "Hello world!";
+        document.body.style.borderTop = x + "px solid red";
+        x = 1^x;
+        document.body.innerHeight; // flush pending reflows
       }
 
       // Prevent this script from being garbage collected.
       window.setInterval(test, 1);
     </script>
   </body>
 
 </html>
--- a/browser/devtools/performance/views/details-abstract-subview.js
+++ b/browser/devtools/performance/views/details-abstract-subview.js
@@ -54,21 +54,27 @@ let DetailsSubview = {
   /**
    * Flag specifying if this view may get updated even when it's not selected.
    * Should only be used in tests.
    */
   canUpdateWhileHidden: false,
 
   /**
    * An array of preferences under `devtools.performance.ui.` that the view should
-   * rerender upon change.
+   * rerender and callback `this._onRerenderPrefChanged` upon change.
    */
   rerenderPrefs: [],
 
   /**
+   * An array of preferences under `devtools.performance.` that the view should
+   * observe and callback `this._onObservedPrefChange` upon change.
+   */
+  observedPrefs: [],
+
+  /**
    * Called when recording stops or is selected.
    */
   _onRecordingStoppedOrSelected: function(_, recording) {
     if (!recording || recording.isRecording()) {
       return;
     }
     if (DetailsView.isViewSelected(this) || this.canUpdateWhileHidden) {
       this.render();
@@ -98,29 +104,33 @@ let DetailsSubview = {
       this.shouldUpdateWhenShown = false;
     }
   },
 
   /**
    * Fired when a preference in `devtools.performance.ui.` is changed.
    */
   _onPrefChanged: function (_, prefName) {
+    if (~this.observedPrefs.indexOf(prefName) && this._onObservedPrefChange) {
+      this._onObservedPrefChange(_, prefName);
+    }
+
     // All detail views require a recording to be complete, so do not
     // attempt to render if recording is in progress or does not exist.
     let recording = PerformanceController.getCurrentRecording();
     if (!recording || recording.isRecording()) {
       return;
     }
 
     if (!~this.rerenderPrefs.indexOf(prefName)) {
       return;
     }
 
     if (this._onRerenderPrefChanged) {
-      this._onRerenderPrefChanged();
+      this._onRerenderPrefChanged(_, prefName);
     }
 
     if (DetailsView.isViewSelected(this) || this.canUpdateWhileHidden) {
       this.render(OverviewView.getTimeInterval());
     } else {
       this.shouldUpdateWhenShown = true;
     }
   }
--- a/browser/devtools/performance/views/details-js-call-tree.js
+++ b/browser/devtools/performance/views/details-js-call-tree.js
@@ -59,18 +59,18 @@ let JsCallTreeView = Heritage.extend(Det
   },
 
   /**
    * Called when the recording is stopped and prepares data to
    * populate the call tree.
    */
   _prepareCallTree: function (profile, { startTime, endTime }, options) {
     let threadSamples = profile.threads[0].samples;
-    let contentOnly = !PerformanceController.getPref("show-platform-data");
-    let invertTree = PerformanceController.getPref("invert-call-tree");
+    let contentOnly = !PerformanceController.getOption("show-platform-data");
+    let invertTree = PerformanceController.getOption("invert-call-tree");
 
     let threadNode = new ThreadNode(threadSamples,
       { startTime, endTime, contentOnly, invertTree });
 
     // If we have an empty profile (no samples), then don't invert the tree, as
     // it would hide the root node and a completely blank call tree space can be
     // mis-interpreted as an error.
     options.inverted = invertTree && threadNode.samples > 0;
@@ -103,17 +103,17 @@ let JsCallTreeView = Heritage.extend(Det
     container.innerHTML = "";
     root.attachTo(container);
 
     // Profiler data does not contain memory allocations information.
     root.toggleAllocations(false);
 
     // When platform data isn't shown, hide the cateogry labels, since they're
     // only available for C++ frames.
-    let contentOnly = !PerformanceController.getPref("show-platform-data");
+    let contentOnly = !PerformanceController.getOption("show-platform-data");
     root.toggleCategories(!contentOnly);
   },
 
   toString: () => "[object JsCallTreeView]"
 });
 
 /**
  * Opens/selects the debugger in this toolbox and jumps to the specified
--- a/browser/devtools/performance/views/details-js-flamegraph.js
+++ b/browser/devtools/performance/views/details-js-flamegraph.js
@@ -48,20 +48,20 @@ let JsFlameGraphView = Heritage.extend(D
    */
   render: function (interval={}) {
     let recording = PerformanceController.getCurrentRecording();
     let duration = recording.getDuration();
     let profile = recording.getProfile();
     let samples = profile.threads[0].samples;
 
     let data = FlameGraphUtils.createFlameGraphDataFromSamples(samples, {
-      invertStack: PerformanceController.getPref("invert-flame-graph"),
-      flattenRecursion: PerformanceController.getPref("flatten-tree-recursion"),
-      filterFrames: !PerformanceController.getPref("show-platform-data") && FrameNode.isContent,
-      showIdleBlocks: PerformanceController.getPref("show-idle-blocks") && L10N.getStr("table.idle")
+      invertStack: PerformanceController.getOption("invert-flame-graph"),
+      flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"),
+      filterFrames: !PerformanceController.getOption("show-platform-data") && FrameNode.isContent,
+      showIdleBlocks: PerformanceController.getOption("show-idle-blocks") && L10N.getStr("table.idle")
     });
 
     this.graph.setData({ data,
       bounds: {
         startTime: 0,
         endTime: duration
       },
       visible: {
--- a/browser/devtools/performance/views/details-memory-call-tree.js
+++ b/browser/devtools/performance/views/details-memory-call-tree.js
@@ -57,17 +57,17 @@ let MemoryCallTreeView = Heritage.extend
   },
 
   /**
    * Called when the recording is stopped and prepares data to
    * populate the call tree.
    */
   _prepareCallTree: function (allocations, { startTime, endTime }, options) {
     let samples = RecordingUtils.getSamplesFromAllocations(allocations);
-    let invertTree = PerformanceController.getPref("invert-call-tree");
+    let invertTree = PerformanceController.getOption("invert-call-tree");
 
     let threadNode = new ThreadNode(samples,
       { startTime, endTime, invertTree });
 
     // If we have an empty profile (no samples), then don't invert the tree, as
     // it would hide the root node and a completely blank call tree space can be
     // mis-interpreted as an error.
     options.inverted = invertTree && threadNode.samples > 0;
--- a/browser/devtools/performance/views/details-memory-flamegraph.js
+++ b/browser/devtools/performance/views/details-memory-flamegraph.js
@@ -47,19 +47,19 @@ let MemoryFlameGraphView = Heritage.exte
    */
   render: function (interval={}) {
     let recording = PerformanceController.getCurrentRecording();
     let duration = recording.getDuration();
     let allocations = recording.getAllocations();
 
     let samples = RecordingUtils.getSamplesFromAllocations(allocations);
     let data = FlameGraphUtils.createFlameGraphDataFromSamples(samples, {
-      invertStack: PerformanceController.getPref("invert-flame-graph"),
-      flattenRecursion: PerformanceController.getPref("flatten-tree-recursion"),
-      showIdleBlocks: PerformanceController.getPref("show-idle-blocks") && L10N.getStr("table.idle")
+      invertStack: PerformanceController.getOption("invert-flame-graph"),
+      flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"),
+      showIdleBlocks: PerformanceController.getOption("show-idle-blocks") && L10N.getStr("table.idle")
     });
 
     this.graph.setData({ data,
       bounds: {
         startTime: 0,
         endTime: duration
       },
       visible: {
--- a/browser/devtools/performance/views/details-waterfall.js
+++ b/browser/devtools/performance/views/details-waterfall.js
@@ -3,34 +3,44 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 /**
  * Waterfall view containing the timeline markers, controlled by DetailsView.
  */
 let WaterfallView = Heritage.extend(DetailsSubview, {
 
+  observedPrefs: [
+    "hidden-markers"
+  ],
+
+  rerenderPrefs: [
+    "hidden-markers"
+  ],
+
   rangeChangeDebounceTime: 10, // ms
 
   /**
    * Sets up the view with event binding.
    */
   initialize: function () {
     DetailsSubview.initialize.call(this);
 
-    this.waterfall = new Waterfall($("#waterfall-breakdown"), $("#waterfall-view"), TIMELINE_BLUEPRINT);
+    this.waterfall = new Waterfall($("#waterfall-breakdown"), $("#waterfall-view"));
     this.details = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
 
     this._onMarkerSelected = this._onMarkerSelected.bind(this);
     this._onResize = this._onResize.bind(this);
 
     this.waterfall.on("selected", this._onMarkerSelected);
     this.waterfall.on("unselected", this._onMarkerSelected);
     this.details.on("resize", this._onResize);
 
+    let blueprint = PerformanceController.getTimelineBlueprint();
+    this.waterfall.setBlueprint(blueprint);
     this.waterfall.recalculateBounds();
   },
 
   /**
    * Unbinds events.
    */
   destroy: function () {
     DetailsSubview.destroy.call(this);
@@ -74,10 +84,18 @@ let WaterfallView = Heritage.extend(Deta
   /**
    * Called when the marker details view is resized.
    */
   _onResize: function () {
     this.waterfall.recalculateBounds();
     this.render();
   },
 
+  /**
+   * Called whenever an observed pref is changed.
+   */
+  _onObservedPrefChange: function(_, prefName) {
+    let blueprint = PerformanceController.getTimelineBlueprint();
+    this.waterfall.setBlueprint(blueprint);
+  },
+
   toString: () => "[object WaterfallView]"
 });
--- a/browser/devtools/performance/views/details.js
+++ b/browser/devtools/performance/views/details.js
@@ -1,27 +1,49 @@
 /* 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";
 
 /**
- * Details view containing profiler call tree and markers waterfall. Manages
- * subviews and toggles visibility between them.
+ * Details view containing call trees, flamegraphs and markers waterfall.
+ * Manages subviews and toggles visibility between them.
  */
 let DetailsView = {
   /**
-   * Name to node+object mapping of subviews.
+   * Name to (node id, view object, actor requirements, pref killswitch)
+   * mapping of subviews.
    */
   components: {
-    "waterfall": { id: "waterfall-view", view: WaterfallView, requires: ["timeline"] },
-    "js-calltree": { id: "js-calltree-view", view: JsCallTreeView },
-    "js-flamegraph": { id: "js-flamegraph-view", view: JsFlameGraphView, requires: ["timeline"] },
-    "memory-calltree": { id: "memory-calltree-view", view: MemoryCallTreeView, requires: ["memory"], pref: "enable-memory" },
-    "memory-flamegraph": { id: "memory-flamegraph-view", view: MemoryFlameGraphView, requires: ["memory", "timeline"], pref: "enable-memory" }
+    "waterfall": {
+      id: "waterfall-view",
+      view: WaterfallView,
+      requires: ["timeline"]
+    },
+    "js-calltree": {
+      id: "js-calltree-view",
+      view: JsCallTreeView
+    },
+    "js-flamegraph": {
+      id: "js-flamegraph-view",
+      view: JsFlameGraphView,
+      requires: ["timeline"]
+    },
+    "memory-calltree": {
+      id: "memory-calltree-view",
+      view: MemoryCallTreeView,
+      requires: ["memory"],
+      pref: "enable-memory"
+    },
+    "memory-flamegraph": {
+      id: "memory-flamegraph-view",
+      view: MemoryFlameGraphView,
+      requires: ["memory", "timeline"],
+      pref: "enable-memory"
+    }
   },
 
   /**
    * Sets up the view with event binding, initializes subviews.
    */
   initialize: Task.async(function *() {
     this.el = $("#details-pane");
     this.toolbar = $("#performance-toolbar-controls-detail-views");
@@ -61,22 +83,23 @@ let DetailsView = {
 
   /**
    * Sets the possible views based off of prefs and server actor support by hiding/showing the
    * buttons that select them and going to default view if currently selected.
    * Called when a preference changes in `devtools.performance.ui.`.
    */
   setAvailableViews: Task.async(function* () {
     let mocks = gFront.getMocksInUse();
+
     for (let [name, { view, pref, requires }] of Iterator(this.components)) {
       let recording = PerformanceController.getCurrentRecording();
 
       let isRecorded = recording && !recording.isRecording();
-      // View is enabled view prefs
-      let isEnabled = !pref || PerformanceController.getPref(pref);
+      // View is enabled by its corresponding pref
+      let isEnabled = !pref || PerformanceController.getOption(pref);
       // View is supported by the server actor, and the requried actor is not being mocked
       let isSupported = !requires || requires.every(r => !mocks[r]);
 
       $(`toolbarbutton[data-view=${name}]`).hidden = !isRecorded || !(isEnabled && isSupported);
 
       // If the view is currently selected and not enabled, go back to the
       // default view.
       if (!isEnabled && this.isViewSelected(view)) {
--- a/browser/devtools/performance/views/overview.js
+++ b/browser/devtools/performance/views/overview.js
@@ -35,31 +35,41 @@ let OverviewView = {
     this._onRecordingStopped = this._onRecordingStopped.bind(this);
     this._onRecordingSelected = this._onRecordingSelected.bind(this);
     this._onRecordingTick = this._onRecordingTick.bind(this);
     this._onGraphSelecting = this._onGraphSelecting.bind(this);
     this._onPrefChanged = this._onPrefChanged.bind(this);
 
     // Toggle the initial visibility of memory and framerate graph containers
     // based off of prefs.
-    $("#memory-overview").hidden = !PerformanceController.getPref("enable-memory");
-    $("#time-framerate").hidden = !PerformanceController.getPref("enable-framerate");
+    $("#memory-overview").hidden = !PerformanceController.getOption("enable-memory");
+    $("#time-framerate").hidden = !PerformanceController.getOption("enable-framerate");
 
     PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
     PerformanceController.on(EVENTS.RECORDING_WILL_START, this._onRecordingWillStart);
     PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
     PerformanceController.on(EVENTS.RECORDING_WILL_STOP, this._onRecordingWillStop);
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
     PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
   },
 
   /**
    * Unbinds events.
    */
   destroy: function () {
+    if (this.markersOverview) {
+      this.markersOverview.destroy();
+    }
+    if (this.memoryOverview) {
+      this.memoryOverview.destroy();
+    }
+    if (this.framerateGraph) {
+      this.framerateGraph.destroy();
+    }
+
     PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
     PerformanceController.off(EVENTS.RECORDING_WILL_START, this._onRecordingWillStart);
     PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
     PerformanceController.off(EVENTS.RECORDING_WILL_STOP, this._onRecordingWillStop);
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
     PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
   },
 
@@ -81,17 +91,17 @@ let OverviewView = {
   isDisabled: function () {
     return this._disabled;
   },
 
   /**
    * Sets the time interval selection for all graphs in this overview.
    *
    * @param object interval
-   *        The { starTime, endTime }, in milliseconds.
+   *        The { startTime, endTime }, in milliseconds.
    */
   setTimeInterval: function(interval, options = {}) {
     if (this.isDisabled()) {
       return;
     }
 
     let recording = PerformanceController.getCurrentRecording();
     if (recording == null) {
@@ -133,34 +143,35 @@ let OverviewView = {
    * @return object
    *         A promise resolved to `true` when the graph was initialized.
    */
   _markersGraphAvailable: Task.async(function *() {
     if (this.markersOverview) {
       yield this.markersOverview.ready();
       return true;
     }
-    this.markersOverview = new MarkersOverview($("#markers-overview"), TIMELINE_BLUEPRINT);
+    let blueprint = PerformanceController.getTimelineBlueprint();
+    this.markersOverview = new MarkersOverview($("#markers-overview"), blueprint);
     this.markersOverview.headerHeight = MARKERS_GRAPH_HEADER_HEIGHT;
     this.markersOverview.rowHeight = MARKERS_GRAPH_ROW_HEIGHT;
     this.markersOverview.groupPadding = MARKERS_GROUP_VERTICAL_PADDING;
     this.markersOverview.on("selecting", this._onGraphSelecting);
     yield this.markersOverview.ready();
     return true;
   }),
 
   /**
    * Sets up the memory overview graph, if allowed and needed.
    *
    * @return object
    *         A promise resolved to `true` if the graph was initialized and is
    *         ready to use, `false` if the graph is disabled.
    */
   _memoryGraphAvailable: Task.async(function *() {
-    if (!PerformanceController.getPref("enable-memory")) {
+    if (!PerformanceController.getOption("enable-memory")) {
       return false;
     }
     if (this.memoryOverview) {
       yield this.memoryOverview.ready();
       return true;
     }
     this.memoryOverview = new MemoryOverview($("#memory-overview"));
     this.memoryOverview.fixedHeight = MEMORY_GRAPH_HEIGHT;
@@ -174,17 +185,17 @@ let OverviewView = {
   /**
    * Sets up the framerate graph, if allowed and needed.
    *
    * @return object
    *         A promise resolved to `true` if the graph was initialized and is
    *         ready to use, `false` if the graph is disabled.
    */
   _framerateGraphAvailable: Task.async(function *() {
-    if (!PerformanceController.getPref("enable-framerate")) {
+    if (!PerformanceController.getOption("enable-framerate")) {
       return false;
     }
     if (this.framerateGraph) {
       yield this.framerateGraph.ready();
       return true;
     }
     let metric = L10N.getStr("graphs.fps");
     this.framerateGraph = new LineGraphWidget($("#time-framerate"), { metric });
@@ -335,22 +346,34 @@ let OverviewView = {
       this.framerateGraph.selectionEnabled = selectionEnabled;
     }
   }),
 
   /**
    * Called whenever a preference in `devtools.performance.ui.` changes. Used
    * to toggle the visibility of memory and framerate graphs.
    */
-  _onPrefChanged: function (_, prefName) {
-    if (prefName === "enable-memory") {
-      $("#memory-overview").hidden = !PerformanceController.getPref("enable-memory");
+  _onPrefChanged: Task.async(function* (_, prefName, prefValue) {
+    switch (prefName) {
+      case "enable-memory": {
+        $("#memory-overview").hidden = !prefValue;
+        break;
+      }
+      case "enable-framerate": {
+        $("#time-framerate").hidden = !prefValue;
+        break;
+      }
+      case "hidden-markers": {
+        if (yield this._markersGraphAvailable()) {
+          let blueprint = PerformanceController.getTimelineBlueprint();
+          this.markersOverview.setBlueprint(blueprint);
+          this.markersOverview.refresh({ force: true });
+        }
+        break;
+      }
     }
-    if (prefName === "enable-framerate") {
-      $("#time-framerate").hidden = !PerformanceController.getPref("enable-framerate");
-    }
-  },
+  }),
 
   toString: () => "[object OverviewView]"
 };
 
 // Decorates the OverviewView as an EventEmitter
 EventEmitter.decorate(OverviewView);
--- a/browser/devtools/performance/views/toolbar.js
+++ b/browser/devtools/performance/views/toolbar.js
@@ -6,36 +6,110 @@
 /**
  * View handler for toolbar events (mostly option toggling and triggering)
  */
 let ToolbarView = {
   /**
    * Sets up the view with event binding.
    */
   initialize: Task.async(function *() {
+    this._onFilterPopupShowing = this._onFilterPopupShowing.bind(this);
+    this._onFilterPopupHiding = this._onFilterPopupHiding.bind(this);
+    this._onHiddenMarkersChanged = this._onHiddenMarkersChanged.bind(this);
     this._onPrefChanged = this._onPrefChanged.bind(this);
 
     this.optionsView = new OptionsView({
       branchName: BRANCH_NAME,
       menupopup: $("#performance-options-menupopup")
     });
 
     yield this.optionsView.initialize();
     this.optionsView.on("pref-changed", this._onPrefChanged);
+
+    this._buildMarkersFilterPopup();
+    this._updateHiddenMarkersPopup();
+    $("#performance-filter-menupopup").addEventListener("popupshowing", this._onFilterPopupShowing);
+    $("#performance-filter-menupopup").addEventListener("popuphiding",  this._onFilterPopupHiding);
   }),
 
   /**
    * Unbinds events and cleans up view.
    */
   destroy: function () {
+    $("#performance-filter-menupopup").removeEventListener("popupshowing", this._onFilterPopupShowing);
+    $("#performance-filter-menupopup").removeEventListener("popuphiding",  this._onFilterPopupHiding);
+
     this.optionsView.off("pref-changed", this._onPrefChanged);
     this.optionsView.destroy();
   },
 
   /**
+   * Creates the timeline markers filter popup.
+   */
+  _buildMarkersFilterPopup: function() {
+    for (let [markerName, markerDetails] of Iterator(TIMELINE_BLUEPRINT)) {
+      let menuitem = document.createElement("menuitem");
+      menuitem.setAttribute("closemenu", "none");
+      menuitem.setAttribute("type", "checkbox");
+      menuitem.setAttribute("align", "center");
+      menuitem.setAttribute("flex", "1");
+      menuitem.setAttribute("label", markerDetails.label);
+      menuitem.setAttribute("marker-type", markerName);
+
+      menuitem.addEventListener("command", this._onHiddenMarkersChanged);
+
+      // Style used by pseudo element ::before in performance.inc.css
+      let bulletStyle = `--bullet-bg: ${markerDetails.fill};`
+      bulletStyle += `--bullet-border: ${markerDetails.stroke}`;
+      menuitem.setAttribute("style", bulletStyle);
+
+      $("#performance-filter-menupopup").appendChild(menuitem);
+    }
+  },
+
+  /**
+   * Updates the menu items checked state in the timeline markers filter popup.
+   */
+  _updateHiddenMarkersPopup: function() {
+    let menuItems = $$("#performance-filter-menupopup menuitem[marker-type]");
+    let hiddenMarkers = PerformanceController.getPref("hidden-markers");
+
+    for (let menuitem of menuItems) {
+      if (~hiddenMarkers.indexOf(menuitem.getAttribute("marker-type"))) {
+        menuitem.removeAttribute("checked");
+      } else {
+        menuitem.setAttribute("checked", "true");
+      }
+    }
+  },
+
+  /**
+   * Fired when the markers filter popup starts to show.
+   */
+  _onFilterPopupShowing: function() {
+    $("#filter-button").setAttribute("open", "true");
+  },
+
+  /**
+   * Fired when the markers filter popup starts to hide.
+   */
+  _onFilterPopupHiding: function() {
+    $("#filter-button").removeAttribute("open");
+  },
+
+  /**
+   * Fired when a menu item in the markers filter popup is checked or unchecked.
+   */
+  _onHiddenMarkersChanged: function() {
+    let checkedMenuItems = $$("#performance-filter-menupopup menuitem[marker-type]:not([checked])");
+    let hiddenMarkers = Array.map(checkedMenuItems, e => e.getAttribute("marker-type"));
+    PerformanceController.setPref("hidden-markers", hiddenMarkers);
+  },
+
+  /**
    * Fired when a preference changes in the underlying OptionsView.
    * Propogated by the PerformanceController.
    */
   _onPrefChanged: function (_, prefName) {
     let value = Services.prefs.getBoolPref(BRANCH_NAME + prefName);
     this.emit(EVENTS.PREF_CHANGED, prefName, value);
   },
 
--- a/browser/devtools/profiler/profiler.js
+++ b/browser/devtools/profiler/profiler.js
@@ -143,18 +143,16 @@ let PrefObserver = {
   register: function() {
     this.branch = Services.prefs.getBranch("devtools.profiler.");
     this.branch.addObserver("", this, false);
   },
   unregister: function() {
     this.branch.removeObserver("", this);
   },
   observe: function(subject, topic, pref) {
-    Prefs.refresh();
-
     if (pref == "ui.show-platform-data") {
       RecordingsListView.forceSelect(RecordingsListView.selectedItem);
     }
   }
 };
 
 /**
  * Functions handling target-related lifetime events.
--- a/browser/devtools/shared/test/browser_prefs.js
+++ b/browser/devtools/shared/test/browser_prefs.js
@@ -15,19 +15,28 @@ function test() {
 
   Prefs.foo = !originalPrefValue;
   is(Prefs.foo, !originalPrefValue,
     "The pref was was correctly changed (1).");
   is(Services.prefs.getBoolPref("devtools.debugger.enabled"), !originalPrefValue,
     "The pref was was correctly changed (2).");
 
   Services.prefs.setBoolPref("devtools.debugger.enabled", originalPrefValue);
-  info("The pref value was reset.");
+  info("The pref value was reset (1).");
+  is(Prefs.foo, !originalPrefValue,
+    "The cached pref value hasn't changed yet (1).");
 
+  Services.prefs.setBoolPref("devtools.debugger.enabled", !originalPrefValue);
+  info("The pref value was reset (2).");
   is(Prefs.foo, !originalPrefValue,
-    "The cached pref value hasn't changed yet.");
+    "The cached pref value hasn't changed yet (2).");
 
-  Prefs.refresh();
+  Prefs.registerObserver();
+
+  Services.prefs.setBoolPref("devtools.debugger.enabled", originalPrefValue);
+  info("The pref value was reset (3).");
   is(Prefs.foo, originalPrefValue,
     "The cached pref value has changed now.");
 
+  Prefs.unregisterObserver();
+
   finish();
 }
--- a/browser/devtools/shared/widgets/ViewHelpers.jsm
+++ b/browser/devtools/shared/widgets/ViewHelpers.jsm
@@ -12,16 +12,17 @@ const Cu = Components.utils;
 const PANE_APPEARANCE_DELAY = 50;
 const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
 const WIDGET_FOCUSABLE_NODES = new Set(["vbox", "hbox"]);
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
+Cu.import("resource://gre/modules/devtools/event-emitter.js");
 
 this.EXPORTED_SYMBOLS = [
   "Heritage", "ViewHelpers", "WidgetMethods",
   "setNamedTimeout", "clearNamedTimeout",
   "setConditionalTimeout", "clearConditionalTimeout",
 ];
 
 /**
@@ -387,94 +388,138 @@ ViewHelpers.L10N.prototype = {
 };
 
 /**
  * Shortcuts for lazily accessing and setting various preferences.
  * Usage:
  *   let prefs = new ViewHelpers.Prefs("root.path.to.branch", {
  *     myIntPref: ["Int", "leaf.path.to.my-int-pref"],
  *     myCharPref: ["Char", "leaf.path.to.my-char-pref"],
+ *     myJsonPref: ["Json", "leaf.path.to.my-json-pref"]
  *     ...
  *   });
  *
+ * Get/set:
  *   prefs.myCharPref = "foo";
  *   let aux = prefs.myCharPref;
  *
+ * Observe:
+ *   prefs.registerObserver();
+ *   prefs.on("pref-changed", (prefName, prefValue) => {
+ *     ...
+ *   });
+ *
  * @param string aPrefsRoot
  *        The root path to the required preferences branch.
- * @param object aPrefsObject
+ * @param object aPrefsBlueprint
  *        An object containing { accessorName: [prefType, prefName] } keys.
  */
-ViewHelpers.Prefs = function(aPrefsRoot = "", aPrefsObject = {}) {
-  this._root = aPrefsRoot;
+ViewHelpers.Prefs = function(aPrefsRoot = "", aPrefsBlueprint = {}) {
+  EventEmitter.decorate(this);
+
   this._cache = new Map();
+  let self = this;
+
+  for (let [accessorName, [prefType, prefName]] of Iterator(aPrefsBlueprint)) {
+    this._map(accessorName, prefType, aPrefsRoot, prefName);
+  }
 
-  for (let accessorName in aPrefsObject) {
-    let [prefType, prefName] = aPrefsObject[accessorName];
-    this.map(accessorName, prefType, prefName);
-  }
+  let observer = {
+    register: function() {
+      this.branch = Services.prefs.getBranch(aPrefsRoot + ".");
+      this.branch.addObserver("", this, false);
+    },
+    unregister: function() {
+      this.branch.removeObserver("", this);
+    },
+    observe: function(_, __, aPrefName) {
+      // If this particular pref isn't handled by the blueprint object,
+      // even though it's in the specified branch, ignore it.
+      let accessor = self._accessor(aPrefsBlueprint, aPrefName);
+      if (!(accessor in self)) {
+        return;
+      }
+      self._cache.delete(aPrefName);
+      self.emit("pref-changed", accessor, self[accessor]);
+    }
+  };
+
+  this.registerObserver = () => observer.register();
+  this.unregisterObserver = () => observer.unregister();
 };
 
 ViewHelpers.Prefs.prototype = {
   /**
    * Helper method for getting a pref value.
    *
    * @param string aType
+   * @param string aPrefsRoot
    * @param string aPrefName
    * @return any
    */
-  _get: function(aType, aPrefName) {
+  _get: function(aType, aPrefsRoot, aPrefName) {
     let cachedPref = this._cache.get(aPrefName);
     if (cachedPref !== undefined) {
       return cachedPref;
     }
-    let value = Services.prefs["get" + aType + "Pref"](aPrefName);
+    let value = Services.prefs["get" + aType + "Pref"]([aPrefsRoot, aPrefName].join("."));
     this._cache.set(aPrefName, value);
     return value;
   },
 
   /**
    * Helper method for setting a pref value.
    *
    * @param string aType
+   * @param string aPrefsRoot
    * @param string aPrefName
    * @param any aValue
    */
-  _set: function(aType, aPrefName, aValue) {
-    Services.prefs["set" + aType + "Pref"](aPrefName, aValue);
+  _set: function(aType, aPrefsRoot, aPrefName, aValue) {
+    Services.prefs["set" + aType + "Pref"]([aPrefsRoot, aPrefName].join("."), aValue);
     this._cache.set(aPrefName, aValue);
   },
 
   /**
    * Maps a property name to a pref, defining lazy getters and setters.
    * Supported types are "Bool", "Char", "Int" and "Json" (which is basically
    * just sugar for "Char" using the standard JSON serializer).
    *
    * @param string aAccessorName
    * @param string aType
+   * @param string aPrefsRoot
    * @param string aPrefName
    * @param array aSerializer
    */
-  map: function(aAccessorName, aType, aPrefName, aSerializer = { in: e => e, out: e => e }) {
+  _map: function(aAccessorName, aType, aPrefsRoot, aPrefName, aSerializer = { in: e => e, out: e => e }) {
+    if (aPrefName in this) {
+      throw new Error(`Can't use ${aPrefName} because it's already a property.`);
+    }
     if (aType == "Json") {
-      this.map(aAccessorName, "Char", aPrefName, { in: JSON.parse, out: JSON.stringify });
+      this._map(aAccessorName, "Char", aPrefsRoot, aPrefName, { in: JSON.parse, out: JSON.stringify });
       return;
     }
 
     Object.defineProperty(this, aAccessorName, {
-      get: () => aSerializer.in(this._get(aType, [this._root, aPrefName].join("."))),
-      set: (e) => this._set(aType, [this._root, aPrefName].join("."), aSerializer.out(e))
+      get: () => aSerializer.in(this._get(aType, aPrefsRoot, aPrefName)),
+      set: (e) => this._set(aType, aPrefsRoot, aPrefName, aSerializer.out(e))
     });
   },
 
   /**
-   * Clears all the cached preferences' values.
+   * Finds the accessor in this object for the provided property name,
+   * based on the blueprint object used in the constructor.
    */
-  refresh: function() {
-    this._cache.clear();
+  _accessor: function(aPrefsBlueprint, aPrefName) {
+    for (let [accessorName, [, prefName]] of Iterator(aPrefsBlueprint)) {
+      if (prefName == aPrefName) {
+        return accessorName;
+      }
+    }
+    return null;
   }
 };
 
 /**
  * A generic Item is used to describe children present in a Widget.
  *
  * This is basically a very thin wrapper around an nsIDOMNode, with a few
  * characteristics, like a `value` and an `attachment`.
--- a/browser/devtools/sourceeditor/test/browser_css_autocompletion.js
+++ b/browser/devtools/sourceeditor/test/browser_css_autocompletion.js
@@ -133,14 +133,14 @@ function checkState(expected, actual) {
   ok(true, "Test " + index + " passed. ");
 }
 
 function finishUp() {
   completer.walker.release().then(() => {
     inspector.destroy();
     inspector = null;
     completer = null;
+    gBrowser.removeCurrentTab();
+    finish();
   });
   progress = null;
   progressDiv = null;
-  gBrowser.removeCurrentTab();
-  finish();
 }
--- a/browser/devtools/timeline/test/browser.ini
+++ b/browser/devtools/timeline/test/browser.ini
@@ -1,16 +1,15 @@
 [DEFAULT]
 subsuite = devtools
 support-files =
   doc_simple-test.html
   head.js
 
 [browser_timeline_aaa_run_first_leaktest.js]
-[browser_timeline_filters.js]
 [browser_timeline_overview-initial-selection-01.js]
 [browser_timeline_overview-initial-selection-02.js]
 [browser_timeline_overview-update.js]
 [browser_timeline_overview-theme.js]
 [browser_timeline_panels.js]
 [browser_timeline_recording-without-memory.js]
 [browser_timeline_recording.js]
 [browser_timeline_waterfall-background.js]
--- a/browser/devtools/webaudioeditor/test/browser_wa_inspector-toggle.js
+++ b/browser/devtools/webaudioeditor/test/browser_wa_inspector-toggle.js
@@ -44,19 +44,17 @@ add_task(function*() {
   // Open again to test node loading while open
   $("#inspector-pane-toggle").click();
   yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
 
   ok(InspectorView.isVisible(), "InspectorView being shown.");
   ok(!isVisible($("#web-audio-editor-tabs")),
     "InspectorView tabs are still hidden.");
 
-  let nodeSet = once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
-  click(panelWin, findGraphNode(panelWin, nodeIds[1]));
-  yield nodeSet;
+  yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[1]));
 
   ok(!isVisible($("#web-audio-editor-details-pane-empty")),
     "Empty message hides even when loading node while open.");
   ok(isVisible($("#web-audio-editor-tabs")),
     "Switches to tab view when loading node while open.");
 
   yield teardown(target);
 });
--- a/browser/devtools/webaudioeditor/test/browser_wa_inspector-width.js
+++ b/browser/devtools/webaudioeditor/test/browser_wa_inspector-width.js
@@ -1,63 +1,56 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
- 
+
 /**
  * Test that the WebAudioInspector's Width is saved as
  * a preference
  */
- 
+
 add_task(function*() {
   let { target, panel } = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
   let { panelWin } = panel;
   let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
   let gVars = InspectorView._propsView;
- 
+
   let started = once(gFront, "start-context");
- 
+
   reload(target);
- 
+
   let [actors] = yield Promise.all([
     get3(gFront, "create-node"),
     waitForGraphRendered(panelWin, 3, 2)
   ]);
   let nodeIds = actors.map(actor => actor.actorID);
- 
+
   ok(!InspectorView.isVisible(), "InspectorView hidden on start.");
- 
+
   // Open inspector pane
   $("#inspector-pane-toggle").click();
   yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
- 
+
   let newInspectorWidth = 500;
- 
+
   // Setting width to new_inspector_width
   $("#web-audio-inspector").setAttribute("width", newInspectorWidth);
   reload(target);
- 
+
   //Width should be 500 after reloading
   [actors] = yield Promise.all([
     get3(gFront, "create-node"),
     waitForGraphRendered(panelWin, 3, 2)
   ]);
   nodeIds = actors.map(actor => actor.actorID);
- 
+
   // Open inspector pane
   $("#inspector-pane-toggle").click();
   yield once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED);
- 
-  let nodeSet = Promise.all([
-    once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET),
-    once(panelWin, EVENTS.UI_PROPERTIES_TAB_RENDERED),
-    once(panelWin, EVENTS.UI_AUTOMATION_TAB_RENDERED)
-  ]);
- 
-  click(panelWin, findGraphNode(panelWin, nodeIds[1]));
-  yield nodeSet;
+
+  yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[1]));
  
   // Getting the width of the audio inspector
   let width = $("#web-audio-inspector").getAttribute("width");
- 
+
   is(width, newInspectorWidth, "WebAudioEditor's Inspector width should be saved as a preference");
- 
+
   yield teardown(target);
-});
\ No newline at end of file
+});
--- a/browser/devtools/webaudioeditor/test/browser_wa_inspector.js
+++ b/browser/devtools/webaudioeditor/test/browser_wa_inspector.js
@@ -24,30 +24,23 @@ add_task(function*() {
 
   ok(!InspectorView.isVisible(), "InspectorView hidden on start.");
   ok(isVisible($("#web-audio-editor-details-pane-empty")),
     "InspectorView empty message should show when no node's selected.");
   ok(!isVisible($("#web-audio-editor-tabs")),
     "InspectorView tabs view should be hidden when no node's selected.");
 
   // Wait for the node to be set as well as the inspector to come fully into the view
-  let nodeSet = Promise.all([
-    once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET),
-    once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED)
-  ]);
-  click(panelWin, findGraphNode(panelWin, nodeIds[1]));
-  yield nodeSet;
+  yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[1]), true);
 
   ok(InspectorView.isVisible(), "InspectorView shown once node selected.");
   ok(!isVisible($("#web-audio-editor-details-pane-empty")),
     "InspectorView empty message hidden when node selected.");
   ok(isVisible($("#web-audio-editor-tabs")),
     "InspectorView tabs view visible when node selected.");
 
   is($("#web-audio-editor-tabs").selectedIndex, 0,
     "default tab selected should be the parameters tab.");
 
-  nodeSet = once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
-  click(panelWin, findGraphNode(panelWin, nodeIds[2]));
-  yield nodeSet;
+  yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[2]));
 
   yield teardown(target);
 });
--- a/browser/devtools/webaudioeditor/test/head.js
+++ b/browser/devtools/webaudioeditor/test/head.js
@@ -105,16 +105,17 @@ function once(aTarget, aEventName, aUseC
   for (let [add, remove] of [
     ["on", "off"], // Use event emitter before DOM events for consistency
     ["addEventListener", "removeEventListener"],
     ["addListener", "removeListener"]
   ]) {
     if ((add in aTarget) && (remove in aTarget)) {
       aTarget[add](aEventName, function onEvent(...aArgs) {
         aTarget[remove](aEventName, onEvent, aUseCapture);
+        info("Got event: '" + aEventName + "' on " + aTarget + ".");
         deferred.resolve(...aArgs);
       }, aUseCapture);
       break;
     }
   }
 
   return deferred.promise;
 }
@@ -347,23 +348,25 @@ function wait (n) {
   let { promise, resolve } = Promise.defer();
   setTimeout(resolve, n);
   info("Waiting " + n/1000 + " seconds.");
   return promise;
 }
 
 /**
  * Clicks a graph node based on actorID or passing in an element.
- * Returns a promise that resolves once
- * UI_INSPECTOR_NODE_SET is fired.
+ * Returns a promise that resolves once UI_INSPECTOR_NODE_SET is fired and
+ * the tabs have rendered, completing all RDP requests for the node.
  */
 function clickGraphNode (panelWin, el, waitForToggle = false) {
   let { promise, resolve } = Promise.defer();
   let promises = [
-   once(panelWin, panelWin.EVENTS.UI_INSPECTOR_NODE_SET)
+   once(panelWin, panelWin.EVENTS.UI_INSPECTOR_NODE_SET),
+   once(panelWin, panelWin.EVENTS.UI_PROPERTIES_TAB_RENDERED),
+   once(panelWin, panelWin.EVENTS.UI_AUTOMATION_TAB_RENDERED)
   ];
 
   if (waitForToggle) {
     promises.push(once(panelWin, panelWin.EVENTS.UI_INSPECTOR_TOGGLED));
   }
 
   // Use `el` as the element if it is one, otherwise
   // assume it's an ID and find the related graph node
--- a/browser/devtools/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js
@@ -34,17 +34,17 @@ let test = asyncTest(function* () {
         text: "Loading mixed (insecure) display content \"http://example.com/tests/image/test/mochitest/blue.png\" on a secure page",
         category: CATEGORY_SECURITY,
         severity: SEVERITY_WARNING,
         objects: true,
       },
     ],
   });
 
-  testClickOpenNewTab(hud, results);
+  yield testClickOpenNewTab(hud, results);
 });
 
 function pushPrefEnv()
 {
   let deferred = promise.defer();
   let options = {"set":
       [["security.mixed_content.block_active_content", false],
        ["security.mixed_content.block_display_content", false]
@@ -52,23 +52,10 @@ function pushPrefEnv()
   SpecialPowers.pushPrefEnv(options, deferred.resolve);
   return deferred.promise;
 }
 
 function testClickOpenNewTab(hud, results) {
   let warningNode = results[0].clickableElements[0];
   ok(warningNode, "link element");
   ok(warningNode.classList.contains("learn-more-link"), "link class name");
-
-  // Invoke the click event and check if a new tab would open to the correct page.
-  let linkOpened = false;
-  let oldOpenUILinkIn = window.openUILinkIn;
-  window.openUILinkIn = function(aLink) {
-    if (aLink == LEARN_MORE_URI) {
-      linkOpened = true;
-    }
-  }
-
-  EventUtils.synthesizeMouse(warningNode, 2, 2, {},
-                             warningNode.ownerDocument.defaultView);
-  ok(linkOpened, "Clicking the Learn More Warning node opens the desired page");
-  window.openUILinkIn = oldOpenUILinkIn;
+  return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js
@@ -38,21 +38,21 @@ let test = asyncTest(function* () {
         text: "Blocked loading mixed active content \"http://example.com/\"",
         category: CATEGORY_SECURITY,
         severity: SEVERITY_ERROR,
         objects: true,
       },
     ],
   });
 
-  testClickOpenNewTab(hud, results[0]);
+  yield testClickOpenNewTab(hud, results[0]);
 
   let results2 = yield mixedContentOverrideTest2(hud, browser);
 
-  testClickOpenNewTab(hud, results2[0]);
+  yield testClickOpenNewTab(hud, results2[0]);
 });
 
 function pushPrefEnv()
 {
   let deferred = promise.defer();
   let options = {"set": [["security.mixed_content.block_active_content", true],
                             ["security.mixed_content.block_display_content", true]]};
   SpecialPowers.pushPrefEnv(options, deferred.resolve);
@@ -113,24 +113,10 @@ function afterNotificationShown(hud, not
     ],
   }).then(msgs => deferred.resolve(msgs), Cu.reportError);
 }
 
 function testClickOpenNewTab(hud, match) {
   let warningNode = match.clickableElements[0];
   ok(warningNode, "link element");
   ok(warningNode.classList.contains("learn-more-link"), "link class name");
-
-  // Invoke the click event and check if a new tab would
-  // open to the correct page.
-  let linkOpened = false;
-  let oldOpenUILinkIn = window.openUILinkIn;
-  window.openUILinkIn = function(aLink) {
-    if (aLink == LEARN_MORE_URI) {
-      linkOpened = true;
-    }
-  }
-
-  EventUtils.synthesizeMouse(warningNode, 2, 2, {},
-                             warningNode.ownerDocument.defaultView);
-  ok(linkOpened, "Clicking the Learn More Warning node opens the desired page");
-  window.openUILinkIn = oldOpenUILinkIn;
+  return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js
@@ -1,18 +1,11 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-///////////////////
-//
-// Whitelisting this test.
-// As part of bug 1077403, the leaking uncaught rejection should be fixed. 
-//
-thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Connection closed");
-
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-632275-getters.html";
 
 let getterValue = null;
 
 function test() {
   loadTab(TEST_URI).then(() => {
     openConsole().then(consoleOpened);
   });
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js
@@ -7,65 +7,54 @@
  *  Tanvi Vyas <tanvi@mozilla.com>
  *
  * ***** END LICENSE BLOCK ***** */
 
 // Tests that the Web Console Mixed Content messages are displayed
 
 const TEST_URI = "data:text/html;charset=utf8,Web Console mixed content test";
 const TEST_HTTPS_URI = "https://example.com/browser/browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Security/MixedContent";
 
 let test = asyncTest(function* () {
   Services.prefs.setBoolPref("security.mixed_content.block_display_content", false);
   Services.prefs.setBoolPref("security.mixed_content.block_active_content", false);
 
   yield loadTab(TEST_URI);
 
   let hud = yield openConsole();
 
   yield testMixedContent(hud);
 
   Services.prefs.clearUserPref("security.mixed_content.block_display_content");
   Services.prefs.clearUserPref("security.mixed_content.block_active_content");
 });
 
-function testMixedContent(hud) {
+let testMixedContent = Task.async(function*(hud) {
   content.location = TEST_HTTPS_URI;
 
-  return waitForMessages({
+  let results = yield waitForMessages({
     webconsole: hud,
     messages: [{
       text: "example.com",
       category: CATEGORY_NETWORK,
       severity: SEVERITY_WARNING,
     }],
-  }).then((results) => {
-    let msg = [...results[0].matched][0];
-    ok(msg, "page load logged");
-    ok(msg.classList.contains("mixed-content"), ".mixed-content element");
+  });
 
-    let link = msg.querySelector(".learn-more-link");
-    ok(link, "mixed content link element");
-    is(link.textContent, "[Mixed Content]", "link text is accurate");
+  let msg = [...results[0].matched][0];
+  ok(msg, "page load logged");
+  ok(msg.classList.contains("mixed-content"), ".mixed-content element");
+
+  let link = msg.querySelector(".learn-more-link");
+  ok(link, "mixed content link element");
+  is(link.textContent, "[Mixed Content]", "link text is accurate");
 
-    let oldOpenLink = hud.openLink;
-    let linkOpened = false;
-    hud.openLink = (url) => {
-      is(url, "https://developer.mozilla.org/docs/Security/MixedContent",
-         "url opened");
-      linkOpened = true;
-    };
+  yield simulateMessageLinkClick(link, LEARN_MORE_URI);
 
-    EventUtils.synthesizeMouse(link, 2, 2, {}, link.ownerDocument.defaultView);
-
-    ok(linkOpened, "clicking the Mixed Content link opened a page");
+  ok(!msg.classList.contains("filtered-by-type"), "message is not filtered");
 
-    hud.openLink = oldOpenLink;
-
-    ok(!msg.classList.contains("filtered-by-type"), "message is not filtered");
-
-    hud.setFilterState("netwarn", false);
+  hud.setFilterState("netwarn", false);
 
-    ok(msg.classList.contains("filtered-by-type"), "message is filtered");
+  ok(msg.classList.contains("filtered-by-type"), "message is filtered");
 
-    hud.setFilterState("netwarn", true);
-  });
-}
+  hud.setFilterState("netwarn", true);
+});
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js
@@ -35,31 +35,17 @@ let test = asyncTest(function* () {
         name: "Insecure form action error displayed successfully",
         text: INSECURE_FORM_ACTION_MSG,
         category: CATEGORY_SECURITY,
         severity: SEVERITY_WARNING
       },
     ],
   });
 
-  testClickOpenNewTab(hud, result);
+  yield testClickOpenNewTab(hud, result);
 });
 
 function testClickOpenNewTab(hud, [result]) {
   let msg = [...result.matched][0];
   let warningNode = msg.querySelector(".learn-more-link");
   ok(warningNode, "learn more link");
-
-  // Invoke the click event and check if a new tab would open to the correct
-  // page
-  let linkOpened = false;
-  let oldOpenUILinkIn = window.openUILinkIn;
-  window.openUILinkIn = function(aLink) {
-    if (aLink == INSECURE_PASSWORDS_URI) {
-      linkOpened = true;
-    }
-  }
-
-  EventUtils.synthesizeMouse(warningNode, 2, 2, {},
-                             warningNode.ownerDocument.defaultView);
-  ok(linkOpened, "Clicking the Insecure Passwords Warning node opens the desired page");
-  window.openUILinkIn = oldOpenUILinkIn;
+  return simulateMessageLinkClick(warningNode, INSECURE_PASSWORDS_URI);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_846918_hsts_invalid-headers.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_846918_hsts_invalid-headers.js
@@ -1,50 +1,35 @@
  /* Any copyright is dedicated to the Public Domain.
   * http://creativecommons.org/publicdomain/zero/1.0/ */
 /* Tests that errors about invalid HSTS security headers are logged
  *  to the web console */
 const TEST_URI = "https://example.com/browser/browser/devtools/webconsole/test/test-bug-846918-hsts-invalid-headers.html";
 const HSTS_INVALID_HEADER_MSG = "The site specified an invalid Strict-Transport-Security header.";
 const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security";
 
-function test()
-{
-  loadTab(TEST_URI).then(() => {
-    openConsole().then((hud) => {
-      waitForMessages({
-        webconsole: hud,
-        messages: [
-          {
-            name: "Invalid HSTS header error displayed successfully",
-            text: HSTS_INVALID_HEADER_MSG,
-            category: CATEGORY_SECURITY,
-            severity: SEVERITY_WARNING,
-            objects: true,
-          },
-        ],
-      }).then((results) => testClickOpenNewTab(hud, results));
-    })
+let test = asyncTest(function* () {
+  yield loadTab(TEST_URI);
+
+  let hud = yield openConsole();
+
+  let results = yield waitForMessages({
+    webconsole: hud,
+    messages: [
+      {
+        name: "Invalid HSTS header error displayed successfully",
+        text: HSTS_INVALID_HEADER_MSG,
+        category: CATEGORY_SECURITY,
+        severity: SEVERITY_WARNING,
+        objects: true,
+      },
+    ],
   });
-}
+
+  yield testClickOpenNewTab(hud, results);
+});
 
 function testClickOpenNewTab(hud, results) {
   let warningNode = results[0].clickableElements[0];
   ok(warningNode, "link element");
   ok(warningNode.classList.contains("learn-more-link"), "link class name");
-
-  // Invoke the click event and check if a new tab would
-  // open to the correct page.
-  let linkOpened = false;
-  let oldOpenUILinkIn = window.openUILinkIn;
-  window.openUILinkIn = function(aLink) {
-    if (aLink == LEARN_MORE_URI) {
-      linkOpened = true;
-    }
-  }
-
-  EventUtils.synthesizeMouse(warningNode, 2, 2, {},
-                             warningNode.ownerDocument.defaultView);
-  ok(linkOpened, "Clicking the Learn More Warning node opens the desired page");
-  window.openUILinkIn = oldOpenUILinkIn;
-
-  finishTest();
+  return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
 }
--- a/browser/devtools/webconsole/test/head.js
+++ b/browser/devtools/webconsole/test/head.js
@@ -1641,8 +1641,37 @@ function once(target, eventName, useCapt
 
   return deferred.promise;
 }
 
 function getSourceActor(aSources, aURL) {
   let item = aSources.getItemForAttachment(a => a.source.url === aURL);
   return item && item.value;
 }
+
+/**
+ * Verify that clicking on a link from a popup notification message tries to
+ * open the expected URL.
+ */
+function simulateMessageLinkClick(element, expectedLink) {
+  let deferred = promise.defer();
+
+  // Invoke the click event and check if a new tab would
+  // open to the correct page.
+  let oldOpenUILinkIn = window.openUILinkIn;
+  window.openUILinkIn = function(link) {
+    if (link == expectedLink) {
+      ok(true, "Clicking the message link opens the desired page");
+      window.openUILinkIn = oldOpenUILinkIn;
+      deferred.resolve();
+    }
+  };
+
+  let event = new MouseEvent("click", {
+    detail: 1,
+    button: 0,
+    bubbles: true,
+    cancelable: true
+  });
+  element.dispatchEvent(event);
+
+  return deferred.promise;
+}
--- a/browser/experiments/test/xpcshell/head.js
+++ b/browser/experiments/test/xpcshell/head.js
@@ -148,16 +148,25 @@ function loadAddonManager() {
   let head = "../../../../toolkit/mozapps/extensions/test/xpcshell/head_addons.js";
   let file = do_get_file(head);
   let uri = ns.Services.io.newFileURI(file);
   ns.Services.scriptloader.loadSubScript(uri.spec, gGlobalScope);
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
   startupManager();
 }
 
+// Starts the addon manager without creating app info. We can't directly use
+// |loadAddonManager| defined above in test_conditions.js as it would make the test fail.
+function startAddonManagerOnly() {
+  let addonManager = Cc["@mozilla.org/addons/integration;1"]
+                       .getService(Ci.nsIObserver)
+                       .QueryInterface(Ci.nsITimerCallback);
+  addonManager.observe(null, "addons-startup", null);
+}
+
 function getExperimentAddons(previous=false) {
   let deferred = Promise.defer();
 
   AddonManager.getAddonsByTypes(["experiment"], (addons) => {
     if (previous) {
       deferred.resolve(addons);
     } else {
       deferred.resolve([a for (a of addons) if (!a.appDisabled)]);
--- a/browser/experiments/test/xpcshell/test_conditions.js
+++ b/browser/experiments/test/xpcshell/test_conditions.js
@@ -1,17 +1,23 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 
 Cu.import("resource:///modules/experiments/Experiments.jsm");
+Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
 Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
 
+XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
+  () => Cc["@mozilla.org/datareporting/service;1"]
+          .getService(Ci.nsISupports)
+          .wrappedJSObject);
+
 const FILE_MANIFEST            = "experiments.manifest";
 const SEC_IN_ONE_DAY = 24 * 60 * 60;
 const MS_IN_ONE_DAY  = SEC_IN_ONE_DAY * 1000;
 
 let gProfileDir = null;
 let gHttpServer = null;
 let gHttpRoot   = null;
 let gReporter   = null;
@@ -40,24 +46,36 @@ function ManifestEntry(data) {
 
 function applicableFromManifestData(data, policy) {
   let manifestData = new ManifestEntry(data);
   let entry = new Experiments.ExperimentEntry(policy);
   entry.initFromManifestData(manifestData);
   return entry.isApplicable();
 }
 
+function initialiseTelemetry() {
+  // Send the needed startup notifications to the datareporting service
+  // to ensure that it has been initialized.
+  if ("@mozilla.org/datareporting/service;1" in Cc) {
+    gDatareportingService.observe(null, "app-startup", null);
+    gDatareportingService.observe(null, "profile-after-change", null);
+  }
+
+  return TelemetryPing.setup().then(TelemetrySession.setup);
+}
+
 function run_test() {
   run_next_test();
 }
 
 add_task(function* test_setup() {
   createAppInfo();
   gProfileDir = do_get_profile();
-  yield TelemetrySession.setup();
+  startAddonManagerOnly();
+  yield initialiseTelemetry();
   gPolicy = new Experiments.Policy();
 
   gReporter = yield getReporter("json_payload_simple");
   yield gReporter.collectMeasurements();
   let payload = yield gReporter.getJSONPayload(false);
   do_register_cleanup(() => gReporter._shutdown());
 
   patchPolicy(gPolicy, {
@@ -307,10 +325,10 @@ add_task(function* test_times() {
       + i + ": " + JSON.stringify([entry[2], entry[3]]));
     if (!applicable && entry[1]) {
       Assert.equal(reason, entry[1], "Experiment rejection reason should match for test " + i);
     }
   }
 });
 
 add_task(function* test_shutdown() {
-  yield TelemetrySession.shutdown();
+  yield TelemetrySession.shutdown(false);
 });
--- a/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
@@ -59,19 +59,23 @@
 <!ENTITY profilerUI.table.totalAlloc      "Total Allocations">
 <!ENTITY profilerUI.table.selfAlloc       "Self Allocations">
 <!ENTITY profilerUI.table.function        "Function">
 
 <!-- LOCALIZATION NOTE (profilerUI.newtab.tooltiptext): The tooltiptext shown
   -  on the "+" (new tab) button for a profile when a selection is available. -->
 <!ENTITY profilerUI.newtab.tooltiptext "Add new tab from selection">
 
+<!-- LOCALIZATION NOTE (profilerUI.toolbar.filter.tooltiptext): This string
+  -  is displayed next to the filter button-->
+<!ENTITY profilerUI.options.filter.tooltiptext "Select what data to display in the timeline">
+
 <!-- LOCALIZATION NOTE (profilerUI.options.tooltiptext): This is the tooltip
   -  for the options button. -->
-<!ENTITY profilerUI.options.tooltiptext "Configure performance preferences.">
+<!ENTITY profilerUI.options.gear.tooltiptext "Configure performance preferences.">
 
 <!-- LOCALIZATION NOTE (profilerUI.invertTree): This is the label shown next to
   -  a checkbox that inverts and un-inverts the profiler's call tree. -->
 <!ENTITY profilerUI.invertTree             "Invert Call Tree">
 <!ENTITY profilerUI.invertTree.tooltiptext "Inverting the call tree displays the profiled call paths starting from the youngest frames and expanding out to the older frames.">
 
 <!-- LOCALIZATION NOTE (profilerUI.invertFlameGraph): This is the label shown next to
   -  a checkbox that inverts and un-inverts the profiler's flame graph. -->
--- a/browser/themes/shared/devtools/performance.inc.css
+++ b/browser/themes/shared/devtools/performance.inc.css
@@ -20,25 +20,54 @@
 
 /* Toolbar */
 
 #performance-toolbar > tabs,
 #performance-toolbar {
   -moz-border-end-color: var(--theme-splitter-color);
 }
 
+#performance-toolbar-control-other {
+  -moz-padding-end: 5px;
+}
+
 #performance-toolbar-controls-detail-views > toolbarbutton {
   min-width: 0;
 }
 
 #performance-toolbar-controls-detail-views .toolbarbutton-text {
   -moz-padding-start: 4px;
   -moz-padding-end: 8px;
 }
 
+#filter-button {
+  list-style-image: url(timeline-filter.svg#filter);
+  min-width: 24px;
+}
+
+#filter-button[disabled] {
+  list-style-image: url(timeline-filter.svg#filter-disabled);
+}
+
+#filter-button[open] {
+  list-style-image: url(timeline-filter.svg#filter-open);
+}
+
+#performance-filter-menupopup > menuitem:before {
+  content: "";
+  display: block;
+  width: 8px;
+  height: 8px;
+  margin: 0 8px;
+  border: 1px solid;
+  border-radius: 1px;
+  background-color: var(--bullet-bg);
+  border-color: var(--bullet-border);
+}
+
 /* Recording Notice */
 
 #performance-view .notice-container {
   font-size: 120%;
   background-color: var(--theme-toolbar-background);
   color: var(--theme-body-color);
   padding-bottom: 20vh;
 }
index 9de7f3ba1070494fb6d3f367d9f112e1af432484..20c1d0728cf70a7e0cacb0006ae79bfbb165c164
GIT binary patch
literal 13854
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
S37P@?y_DqCWh<mjga03Luhl^S
index ca727dd443bec2998c7b8334b040b0493810fab1..b672d2f4bde9943a71ddfa9dbfbf389357479827
GIT binary patch
literal 7611
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
index d1bfb2602f2505d06acf40a317a978e88acb347b..e77e84fbb800f9e19c57d529ec320af8ec0d04b9
GIT binary patch
literal 20892
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
index 6e41f409eb727bcb8cf1e93fcd0d6e49ba0d75bc..81a15709369ea4c58f011d9d738d17fe0c7f3c1c
GIT binary patch
literal 37290
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
index 1fd7d89e2cc005ee95a7565d692fc97f158323b3..c4d71316e0665992582b8c31399bd50830402ae0
GIT binary patch
literal 17359
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
GLSTYx%jZ`B
--- a/mobile/android/themes/core/aboutReader.css
+++ b/mobile/android/themes/core/aboutReader.css
@@ -28,22 +28,58 @@ body {
 .sans-serif {
   font-family: sans-serif;
 }
 
 .serif {
   font-family: serif;
 }
 
+.font-size1 {
+  font-size: 10px;
+}
+
+.font-size2 {
+  font-size: 12px;
+}
+
+.font-size3 {
+  font-size: 14px;
+}
+
+.font-size4  {
+  font-size: 16px;
+}
+
+.font-size5 {
+  font-size: 18px;
+}
+
+.font-size6 {
+  font-size: 20px;
+}
+
+.font-size7 {
+  font-size: 22px;
+}
+
+.font-size8 {
+  font-size: 24px;
+}
+
+.font-size9 {
+  font-size: 26px;
+}
+
 .message {
   margin-top: 40px;
   display: none;
   text-align: center;
   width: 100%;
-  font-size: 16px;
+  font-size: 0.9rem;
 }
 
 .header {
   text-align: start;
   display: none;
 }
 
 .domain,
@@ -60,17 +96,17 @@ body {
 
 .domain-border {
   margin-top: 15px;
   border-bottom: 1.5px solid #777777;
   width: 50%;
 }
 
 .header > h1 {
-  font-size: 2.625em;
+  font-size: 1.33rem;
   font-weight: 700;
   line-height: 1.1em;
   width: 100%;
   margin: 0px;
   margin-top: 32px;
   margin-bottom: 16px;
   padding: 0px;
 }
@@ -102,76 +138,29 @@ body {
 .dark > .header > h1 {
   color: #eeeeee;
 }
 
 .dark > .header > .credits {
   color: #aaaaaa;
 }
 
-.font-size1 > body > .header > h1 {
-  font-size: 27px;
-}
-
-.font-size2 > body > .header > h1 {
-  font-size: 29px;
-}
-
-.font-size3 > body > .header > h1 {
-  font-size: 31px;
-}
-
-.font-size4 > body > .header > h1 {
-  font-size: 33px;
-}
-
-.font-size5 > body > .header > h1 {
-  font-size: 35px;
-}
-
 /* This covers caption, domain, and credits
    texts in the reader UI */
 
-.font-size1 > body > .content .wp-caption-text,
-.font-size1 > body > .content figcaption,
-.font-size1 > body > .header > .domain,
-.font-size1 > body > .header > .credits {
-  font-size: 10px;
-}
-
-.font-size2 > body > .content .wp-caption-text,
-.font-size2 > body > .content figcaption,
-.font-size2 > body > .header > .domain,
-.font-size2 > body > .header > .credits {
-  font-size: 13px;
-}
-
-.font-size3 > body > .content .wp-caption-text,
-.font-size3 > body > .content figcaption,
-.font-size3 > body > .header > .domain,
-.font-size3 > body > .header > .credits {
-  font-size: 15px;
-}
-
-.font-size4 > body > .content .wp-caption-text,
-.font-size4 > body > .content figcaption,
-.font-size4 > body > .header > .domain,
-.font-size4 > body > .header > .credits {
-  font-size: 17px;
-}
-
-.font-size5 > body > .content .wp-caption-text,
-.font-size5 > body > .content figcaption,
-.font-size5 > body > .header > .domain,
-.font-size5 > body > .header > .credits {
-  font-size: 19px;
+.content .wp-caption-text,
+.content figcaption,
+.header > .domain,
+.header > .credits {
+  font-size: 0.9rem;
 }
 
 .content {
   display: none;
+  font-size: 1rem;
 }
 
 .content a {
   text-decoration: underline !important;
   font-weight: normal;
 }
 
 .light > .content a,
@@ -290,41 +279,16 @@ body {
   list-style: disk !important;
 }
 
 .content ol {
   -moz-padding-start: 35px !important;
   list-style: decimal !important;
 }
 
-.font-size1-sample,
-.font-size1 > body > .content {
-  font-size: 14px !important;
-}
-
-.font-size2-sample,
-.font-size2 > body > .content {
-  font-size: 16px !important;
-}
-
-.font-size3-sample,
-.font-size3 > body > .content {
-  font-size: 18px !important;
-}
-
-.font-size4-sample,
-.font-size4 > body > .content {
-  font-size: 20px !important;
-}
-
-.font-size5-sample,
-.font-size5 > body > .content {
-  font-size: 22px !important;
-}
-
 .toolbar {
   font-family: "Clear Sans",sans-serif;
   transition-property: visibility, opacity;
   transition-duration: 0.7s;
   visibility: visible;
   opacity: 1.0;
   position: fixed;
   width: 100%;
@@ -356,17 +320,18 @@ body {
   background-size: 30px 24px;
   background-repeat: no-repeat;
   background-color: transparent;
   border: 0;
   width: 100%;
 }
 
 /* Remove dotted border when button is focused */
-.button::-moz-focus-inner {
+.button::-moz-focus-inner,
+#font-size-buttons > button::-moz-focus-inner {
   border: 0;
 }
 
 .dropdown {
   text-align: center;
   display: inline-block;
   list-style: none;
   margin: 0px;
@@ -422,40 +387,51 @@ body {
   bottom: -18px;
   background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-mdpi.png');
   background-size: 40px 18px;
   background-position: center;
   display: block;
 }
 
 #font-type-buttons,
+#font-size-buttons,
 .segmented-button {
   display: flex;
   flex-direction: row;
   list-style: none;
   padding: 10px 5px;
   white-space: nowrap;
 }
 
 #font-type-buttons > li,
+#font-size-buttons > button,
 .segmented-button > li {
-  width: 50px; /* combined with flex, this acts as a minimum width */
   flex: 1 0 auto;
   text-align: center;
   line-height: 20px;
 }
 
+#font-type-buttons > li,
+#font-size-buttons > button {
+  width: 100px; /* combined with flex, this acts as a minimum width */
+}
+
+.segmented-button > li {
+  width: 50px; /* combined with flex, this acts as a minimum width */
+}
+
 #font-type-buttons > li {
   padding: 10px 0;
 }
 
 .segmented-button > li {
   border-left: 1px solid #B5B5B5;
 }
 
+#font-size-buttons > button:first-child,
 .segmented-button > li:first-child {
   border-left: 0px;
 }
 
 #font-type-buttons > li > a,
 .segmented-button > li > a {
   vertical-align: middle;
   text-decoration: none;
@@ -491,16 +467,34 @@ body {
   font-weight: lighter;
 }
 
 #font-type-buttons > li > div {
   color: #666;
   font-size: 12px;
 }
 
+.minus-button,
+.plus-button {
+  background-color: transparent;
+  border: 0;
+  height: 30px;
+  background-size: 18px 18px;
+  background-repeat: no-repeat;
+  background-position: center;
+}
+
+.minus-button {
+  background-size: 24px 6px;
+}
+
+.plus-button {
+  background-size: 24px 24px;
+}
+
  /* desktop-only controls */
 .close-button,
 .list-button {
   display: none;
 }
 
 .toggle-button.on {
   background-image: url('chrome://browser/skin/images/reader-toggle-on-icon-mdpi.png');
@@ -513,16 +507,24 @@ body {
 .share-button {
   background-image: url('chrome://browser/skin/images/reader-share-icon-mdpi.png');
 }
 
 .style-button {
   background-image: url('chrome://browser/skin/images/reader-style-icon-mdpi.png');
 }
 
+.minus-button {
+  background-image: url('chrome://browser/skin/images/reader-minus-mdpi.png');
+}
+
+.plus-button {
+  background-image: url('chrome://browser/skin/images/reader-plus-mdpi.png');
+}
+
 @media screen and (min-resolution: 1.25dppx) {
   .dropdown-arrow {
     background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-hdpi.png');
   }
 
   .toggle-button.on {
     background-image: url('chrome://browser/skin/images/reader-toggle-on-icon-hdpi.png');
   }
@@ -533,16 +535,24 @@ body {
 
   .share-button {
     background-image: url('chrome://browser/skin/images/reader-share-icon-hdpi.png');
   }
 
   .style-button {
     background-image: url('chrome://browser/skin/images/reader-style-icon-hdpi.png');
   }
+
+  .minus-button {
+    background-image: url('chrome://browser/skin/images/reader-minus-hdpi.png');
+  }
+
+  .plus-button {
+    background-image: url('chrome://browser/skin/images/reader-plus-hdpi.png');
+  }
 }
 
 @media screen and (min-resolution: 2dppx) {
   .dropdown-arrow {
     background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-xhdpi.png');
   }
 
   .toggle-button.on {
@@ -555,16 +565,24 @@ body {
 
   .share-button {
     background-image: url('chrome://browser/skin/images/reader-share-icon-xhdpi.png');
   }
 
   .style-button {
     background-image: url('chrome://browser/skin/images/reader-style-icon-xhdpi.png');
   }
+
+  .minus-button {
+    background-image: url('chrome://browser/skin/images/reader-minus-xhdpi.png');
+  }
+
+  .plus-button {
+    background-image: url('chrome://browser/skin/images/reader-plus-xhdpi.png');
+  }
 }
 
 @media screen and (orientation: portrait) {
   .button {
     height: 48px;
   }
 }
 
new file mode 100644
index 0000000000000000000000000000000000000000..2b2e483c7ae293af13d1ad193c35780655e22c82
GIT binary patch
literal 195
zc%17D@N?(olHy`uVBq!ia0vp^DnQJ^!3HEJ=EYtGQjEnx?oJHr&dIz4a%w$Y978Mw
z&t5*r+n~V1df|T8;~6W2CFgnzub(%=<&OK(ZR|p3D||Uv`D)qiWDU!7E7$}18%ipt
zI6SG6pV^dTc9ci1D@$$WLe7ZDhK*GPxn~c#99h?uwIi~wSyNAJVhrEC-H{Q=rB%+S
sYBq0h%e{C)b?+7Xr(t5hOO(&czh_Z(&xo>U06K)h)78&qol`;+0NsZ~NdN!<
new file mode 100644
index 0000000000000000000000000000000000000000..53b6e71bd0dc88a64238a5465e82d8daf4693fbb
GIT binary patch
literal 155
zc%17D@N?(olHy`uVBq!ia0vp^5<twx!3HF6u8tE1QjEnx?oJHr&dIz4azZ^_978lj
zlVf(*B})nm2MaSZGg}J;3;+4^{e8Twu(EQp?uC#nX}%&2>k~N>SZ%J9m~Dzw*gSIr
zYsSH}U;qFA7mt$QF%nKOJJFP;@LQc%=O?F;G{X#L$$pRS-0eUM7(8A5T-G@yGywn~
C7%n>i
new file mode 100644
index 0000000000000000000000000000000000000000..78e97b4a4e48243a2e992c48ccfdf56503ed3296
GIT binary patch
literal 223
zc%17D@N?(olHy`uVBq!ia0vp^20+Zk!3HEhJ-+J$q!^2X+?^QKos)S9<jnMRaSX8#
zOrB$sDYUz1juSUCbFzuDa<j@|0fztojcw8se(e8paxuH9j?t!1xjj8SuFePU7N|Qh
z&DzNFqD)PV&Fz@Pjxzzt40q45@YHTx_$+TmwPRj-Q(>Ey?W@MdKFekQ|Nq}FmEk@i
z+y2B`AD;jJ^%bu-FkWXbvEICO>s3d)I!3+(*JT_!@s;d%0$nT^q9&fL`Sc^vl%esC
WN=M$I|EGbDWbkzLb6Mw<&;$VR1yvLP
new file mode 100644
index 0000000000000000000000000000000000000000..80f96968ce84b7c9acb1fb412039c2a2b4bcc22b
GIT binary patch
literal 506
zc$@+H0R{evP)<h;3K|Lk000e1NJLTq001Na001Ni1^@s6;Q*MJ00001b5ch_0Itp)
z=>Px$v`IukR9Fe^Sg}e2K@gqUyCkHsuoFQDf(Yg}ENp_}5%d!*{0JefV59zmgm}SJ
zR(^nWEW|<(Y%~Z~lDN0yTvQUh4ZCbYg1B4l&6|1m_T3&kEW#M0*=%iqfMWnGr8GqJ
zL;wfdo0St+T68U}*X#LQq4>=8bHf}gN-5b}jiTsoIBq)^wu#xi!V<@Rh)Hu0QD7LB
zv-x%rXF*|@jI1RRBQcXK=Ry;)EA9oEobwKOhfEHbaKDHCoJ0M?l@H1QptzXir1dAv
z3sDjvu5>Hr6MwI(<fRVN#e(-x0lYm{tJMVK_+lM|I75VaUGlVIMDPMi?eHSS>8!=1
zi|%V+ErKV@({jP|o>YtVo-vV43THvonH1UhJk&`A)tHEXjF}JCK(+pF-)OYfFvPOp
zR?k)z30&afLw!CSfr4v2mW?VzMUvW4RJn0(^5ZyO!60{=r3Xk9MbTE}T!)!r){dBn
zCWU)JCMUf^-XW6%Cfx6#|KpI90k@s{!AH|bOkXjGj<JWj58QUX4yZn7>4g<Z+5R23
weRdYa4S5fVcDZG^|9;ummEOMV?ODq60SoYSa8PR@+W-In07*qoM6N<$f{Z!mVgLXD
new file mode 100644
index 0000000000000000000000000000000000000000..4603a204f467ff1bf472d15da6d172afb1549c02
GIT binary patch
literal 394
zc$@)>0d@X~P)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv00001b5ch_0Itp)
z=>Px$L`g(JR7eeDWFQi(TfccOBNId1|NsA)7#JA0U%GHMprN7R4<YS9#e~7Sb?bzI
zh9?14vM@3-GBGkTcx&nF>k!t-z(AA*Kv%P&8wRw69bF6?pC}8kX(U!`)B=JbHS~js
z5#+~Jt5)#?DU75D^FIp<i#Q`Q>tz@pMF0Q$&-2fppJ$Q9(AdAi!oof>uHU$6CNO8F
zp-EDS`+pWN4j7ZDWEvJ@7?VJe%#1~bT0+cB{~7-8q?T#u=77v*dHv$KpNNR00WjKd
zMl!H~6Jca#+<;=hzyIm~{{6p-B8I|Y{PpI|^9$fqgd&cGvv%!TNhTJyXUI~2|NpfJ
z2?#ieEQXCe^fTJ153t26irC-?B49rFh2ktA=imSTKhedo@rm+5P*Bj@|Nj`~0S)*A
oYybewX4rb|;>9c23`7?L0O812980c4+5i9m07*qoM6N<$f)ll|EC2ui
new file mode 100644
index 0000000000000000000000000000000000000000..d2f26364951f02f8afe024240fe0a4c22f6136d7
GIT binary patch
literal 609
zc$@)Y0-pVeP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00001b5ch_0Itp)
z=>Px%8%ab#RA>e5T1!d;K@hDPN6b$#;3m3r>DCJf3L<_;3<w$!Jb>mXiWd-shzMZ<
z3L?62BZ#>34#_5<;wJ{pwABM6+4S@z-HfAjFEUA0b=9l)LWeGbpm`k~osK0^u!q1F
z0dbV4X8<3_;Pw+bJ8Sv;gU*tc&SX4SGR2M&q2z<-HUvn}%Zv>J7YB~xHO1{ZmevNU
z)oPA_&x(LiW4RH;Ig!5~yVeFQ%Nmz~Kp2q5$Z~Z1wKmY5II8!ifvA`k)iDZ)n2E}6
z6o`sxQ5~azh?%JDMuDi97S%Bdh?t4WZWM@$X;B^9cT_5s>|i>b^FR0VWQdb=`;sh{
zJE_&CBte1h&$qWmsZ@I8LCAq8GsA}nxD>c6W=;m|to8b`X(uxyyTsa`0`D<Tgmdf`
zfnSLkxuM~3?EDnUhTajs!0f*`M%(ctHZ-EPMPswO(BW|lB<xlP-7*dTnsLJ>$HST&
zyZ5`p=b1xEwm_3JrJ)lgo1n?uc3^SFPH9$p6Br|LG6wZ_3(q$;mYZG!lK9YW$8n~?
zvhRa7Q1S9=etADArWfD8IE8v?ICil{f!~(IC}0#&WWaod6bV$`ZWK_aP%(i~K#@S@
z?M4A*3KbJ*Rls%Kx)cw(4QWgd0a_a<7K;tKV_%8t(`Bad?7EjCe?NAu4R8RhcUUK&
v6=3Yi2gLtwLycX3L1%ueQ24R?ewF7p+0b#o#PU7H00000NkvXXu0mjf_O=jj
--- a/mobile/android/themes/core/jar.mn
+++ b/mobile/android/themes/core/jar.mn
@@ -84,16 +84,22 @@ chrome.jar:
   skin/images/scrubber-hdpi.png             (images/scrubber-hdpi.png)
   skin/images/about-btn-darkgrey.png        (images/about-btn-darkgrey.png)
   skin/images/logo-hdpi.png                 (images/logo-hdpi.png)
   skin/images/wordmark-hdpi.png             (images/wordmark-hdpi.png)
   skin/images/config-plus.png               (images/config-plus.png)
   skin/images/reader-dropdown-arrow-mdpi.png     (images/reader-dropdown-arrow-mdpi.png)
   skin/images/reader-dropdown-arrow-hdpi.png     (images/reader-dropdown-arrow-hdpi.png)
   skin/images/reader-dropdown-arrow-xhdpi.png    (images/reader-dropdown-arrow-xhdpi.png)
+  skin/images/reader-minus-mdpi.png              (images/reader-minus-mdpi.png)
+  skin/images/reader-minus-hdpi.png              (images/reader-minus-hdpi.png)
+  skin/images/reader-minus-xhdpi.png             (images/reader-minus-xhdpi.png)
+  skin/images/reader-plus-mdpi.png               (images/reader-plus-mdpi.png)
+  skin/images/reader-plus-hdpi.png               (images/reader-plus-hdpi.png)
+  skin/images/reader-plus-xhdpi.png              (images/reader-plus-xhdpi.png)
   skin/images/reader-toggle-on-icon-mdpi.png     (images/reader-toggle-on-icon-mdpi.png)
   skin/images/reader-toggle-on-icon-hdpi.png     (images/reader-toggle-on-icon-hdpi.png)
   skin/images/reader-toggle-on-icon-xhdpi.png    (images/reader-toggle-on-icon-xhdpi.png)
   skin/images/reader-toggle-off-icon-mdpi.png    (images/reader-toggle-off-icon-mdpi.png)
   skin/images/reader-toggle-off-icon-hdpi.png    (images/reader-toggle-off-icon-hdpi.png)
   skin/images/reader-toggle-off-icon-xhdpi.png   (images/reader-toggle-off-icon-xhdpi.png)
   skin/images/reader-share-icon-mdpi.png         (images/reader-share-icon-mdpi.png)
   skin/images/reader-share-icon-hdpi.png         (images/reader-share-icon-hdpi.png)
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4555,18 +4555,18 @@ pref("media.gmp-eme-adobe.hidden", true)
 // Whether or not to perform reader mode article parsing on page load.
 // If this pref is disabled, we will never show a reader mode icon in the toolbar.
 pref("reader.parse-on-load.enabled", true);
 
 // Force-enables reader mode parsing, even on low-memory platforms, where it
 // is disabled by default.
 pref("reader.parse-on-load.force-enabled", false);
 
-// The default relative font size in reader mode (1-5)
-pref("reader.font_size", 3);
+// The default relative font size in reader mode (1-9)
+pref("reader.font_size", 5);
 
 // The default color scheme in reader mode (light, dark, sepia, auto)
 // auto = color automatically adjusts according to ambient light level
 // (auto only works on platforms where the 'devicelight' event is enabled)
 pref("reader.color_scheme", "light");
 
 // Color scheme values available in reader mode UI.
 pref("reader.color_scheme.values", "[\"light\",\"dark\",\"sepia\"]");
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -225,16 +225,21 @@ user_pref('browser.contentHandlers.types
 user_pref('browser.contentHandlers.types.5.uri', 'http://test1.example.org/rss?url=%%s')
 
 // Set dummy server for Android tiles testing.
 user_pref('browser.tiles.reportURL', 'http://%(server)s/tests/robocop/robocop_tiles.sjs')
 
 // We want to collect telemetry, but we don't want to send in the results.
 user_pref('toolkit.telemetry.server', 'https://%(server)s/telemetry-dummy/');
 
+// A couple of preferences with default values to test that telemetry preference
+// watching is working.
+user_pref('toolkit.telemetry.test.pref1', true);
+user_pref('toolkit.telemetry.test.pref2', false);
+
 // We don't want to hit the real Firefox Accounts server for tests.  We don't
 // actually need a functioning FxA server, so just set it to something that
 // resolves and accepts requests, even if they all fail.
 user_pref('identity.fxaccounts.auth.uri', 'https://%(server)s/fxa-dummy/');
 
 // Ditto for all the other Firefox accounts URIs used for about:accounts et al.:
 user_pref("identity.fxaccounts.remote.signup.uri", "https://%(server)s/fxa-signup");
 user_pref("identity.fxaccounts.remote.force_auth.uri", "https://%(server)s/fxa-force-auth");
--- a/toolkit/components/printing/content/printUtils.js
+++ b/toolkit/components/printing/content/printUtils.js
@@ -164,16 +164,39 @@ var PrintUtils = {
     if (!aBrowser) {
       throw new Error("PrintUtils.print could not resolve content window " +
                       "to a browser.");
     }
 
     let printSettings = this.getPrintSettings();
 
     let mm = aBrowser.messageManager;
+
+    mm.addMessageListener("Printing:Print:Done", function onPrintingDone(msg) {
+      mm.removeMessageListener("Printing:Print:Done", onPrintingDone);
+      let rv = msg.data.rv;
+      let printSettings = msg.objects.printSettings;
+      if (rv != Components.results.NS_OK &&
+          rv != Components.results.NS_ERROR_ABORT) {
+        Cu.reportError(`In Printing:Print:Done handler, got unexpected rv
+                        ${rv}.`);
+      }
+
+      if (gPrintSettingsAreGlobal && gSavePrintSettings) {
+        let PSSVC =
+          Components.classes["@mozilla.org/gfx/printsettings-service;1"]
+                    .getService(Components.interfaces.nsIPrintSettingsService);
+
+        PSSVC.savePrintSettingsToPrefs(printSettings, true,
+                                       printSettings.kInitSaveAll);
+        PSSVC.savePrintSettingsToPrefs(printSettings, false,
+                                       printSettings.kInitSavePrinterName);
+      }
+    });
+
     mm.sendAsyncMessage("Printing:Print", null, {
       printSettings: printSettings,
       contentWindow: aWindow,
     });
   },
 
   /**
    * Initializes print preview.
--- a/toolkit/components/reader/AboutReader.jsm
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -70,52 +70,31 @@ let AboutReader = function(mm, win) {
 
   let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
   this._setupSegmentedButton("color-scheme-buttons", colorSchemeOptions, colorScheme, this._setColorSchemePref.bind(this));
   this._setColorSchemePref(colorScheme);
 
   let fontTypeSample = gStrings.GetStringFromName("aboutReader.fontTypeSample");
   let fontTypeOptions = [
     { name: fontTypeSample,
-      description: gStrings.GetStringFromName("aboutReader.fontType.serif"),
-      value: "serif",
-      linkClass: "serif" },
-    { name: fontTypeSample,
       description: gStrings.GetStringFromName("aboutReader.fontType.sans-serif"),
       value: "sans-serif",
       linkClass: "sans-serif"
     },
+    { name: fontTypeSample,
+      description: gStrings.GetStringFromName("aboutReader.fontType.serif"),
+      value: "serif",
+      linkClass: "serif" },
   ];
 
   let fontType = Services.prefs.getCharPref("reader.font_type");
   this._setupSegmentedButton("font-type-buttons", fontTypeOptions, fontType, this._setFontType.bind(this));
   this._setFontType(fontType);
 
-  let fontSizeSample = gStrings.GetStringFromName("aboutReader.fontSizeSample");
-  let fontSizeOptions = [
-    { name: fontSizeSample,
-      value: 1,
-      linkClass: "font-size1-sample" },
-    { name: fontSizeSample,
-      value: 2,
-      linkClass: "font-size2-sample" },
-    { name: fontSizeSample,
-      value: 3,
-      linkClass: "font-size3-sample" },
-    { name: fontSizeSample,
-      value: 4,
-      linkClass: "font-size4-sample" },
-    { name: fontSizeSample,
-      value: 5,
-      linkClass: "font-size5-sample" }
-  ];
-
-  let fontSize = Services.prefs.getIntPref("reader.font_size");
-  this._setupSegmentedButton("font-size-buttons", fontSizeOptions, fontSize, this._setFontSize.bind(this));
-  this._setFontSize(fontSize);
+  this._setupFontSizeButtons();
 
   // Track status of reader toolbar add/remove toggle button
   this._isReadingListItem = -1;
   this._updateToggleButton();
 
   this._loadArticle();
 }
 
@@ -302,16 +281,73 @@ AboutReader.prototype = {
     htmlClasses.add("font-size" + this._fontSize);
 
     this._mm.sendAsyncMessage("Reader:SetIntPref", {
       name: "reader.font_size",
       value: this._fontSize
     });
   },
 
+  _setupFontSizeButtons: function() {
+    const FONT_SIZE_MIN = 1;
+    const FONT_SIZE_MAX = 9;
+
+    let currentSize = Services.prefs.getIntPref("reader.font_size");
+    currentSize = Math.max(FONT_SIZE_MIN, Math.min(FONT_SIZE_MAX, currentSize));
+
+    let plusButton = this._doc.getElementById("font-size-plus");
+    let minusButton = this._doc.getElementById("font-size-minus");
+
+    function updateControls() {
+      if (currentSize === FONT_SIZE_MIN) {
+        minusButton.setAttribute("disabled", true);
+      } else {
+        minusButton.removeAttribute("disabled");
+      }
+      if (currentSize === FONT_SIZE_MAX) {
+        plusButton.setAttribute("disabled", true);
+      } else {
+        plusButton.removeAttribute("disabled");
+      }
+    }
+
+    updateControls();
+    this._setFontSize(currentSize);
+
+    plusButton.addEventListener("click", (event) => {
+      if (!event.isTrusted) {
+        return;
+      }
+      event.stopPropagation();
+
+      if (currentSize >= FONT_SIZE_MAX) {
+        return;
+      }
+
+      currentSize++;
+      updateControls();
+      this._setFontSize(currentSize);
+    }, true);
+
+    minusButton.addEventListener("click", (event) => {
+      if (!event.isTrusted) {
+        return;
+      }
+      event.stopPropagation();
+
+      if (currentSize <= FONT_SIZE_MIN) {
+        return;
+      }
+
+      currentSize--;
+      updateControls();
+      this._setFontSize(currentSize);
+    }, true);
+  },
+
   _handleDeviceLight: function Reader_handleDeviceLight(newLux) {
     // Desired size of the this._luxValues array.
     let luxValuesSize = 10;
     // Add new lux value at the front of the array.
     this._luxValues.unshift(newLux);
     // Add new lux value to this._totalLux for averaging later.
     this._totalLux += newLux;
 
--- a/toolkit/components/reader/content/aboutReader.html
+++ b/toolkit/components/reader/content/aboutReader.html
@@ -27,17 +27,20 @@
   <ul id="reader-toolbar" class="toolbar toolbar-hidden">
     <li><button id="close-button" class="button close-button"/></li>
     <li><button id="share-button" class="button share-button"/></li>
     <ul id="style-dropdown" class="dropdown">
       <li><button class="dropdown-toggle button style-button"/></li>
       <li class="dropdown-popup">
         <ul id="font-type-buttons"></ul>
         <hr></hr>
-        <ul id="font-size-buttons" class="segmented-button"></ul>
+        <div id="font-size-buttons">
+          <button id="font-size-minus" class="minus-button"/>
+          <button id="font-size-plus" class="plus-button"/>
+        </div>
         <hr></hr>
         <ul id="color-scheme-buttons" class="segmented-button"></ul>
         <div class="dropdown-arrow"/>
       </li>
     </ul>
     <li><button id="toggle-button" class="button toggle-button"/></li>
     <li><button id="list-button" class="button list-button"/></li>
   </ul>
--- a/toolkit/components/telemetry/Makefile.in
+++ b/toolkit/components/telemetry/Makefile.in
@@ -37,8 +37,19 @@ data_python_deps := \
   $(NULL)
 
 $(histogram_enum_file): $(histograms_file) $(enum_python_deps)
 	$(PYTHON) $(srcdir)/gen-histogram-enum.py $< > $@
 $(histogram_data_file): $(histograms_file) $(data_python_deps)
 	$(PYTHON) $(srcdir)/gen-histogram-data.py $< > $@
 
 GARBAGE += $(histogram_enum_file)
+
+# This is so hacky. Waiting on bug 988938.
+addondir = $(srcdir)/tests/addons
+testdir = $(abspath $(DEPTH)/_tests/xpcshell/toolkit/components/telemetry/tests/unit)
+
+misc:: $(call mkdir_deps,$(testdir))
+	$(EXIT_ON_ERROR) \
+	for dir in $(addondir)/*; do \
+	  base=`basename $$dir`; \
+	  (cd $$dir && zip -qr $(testdir)/$$base.xpi *); \
+	done
--- a/toolkit/components/telemetry/Telemetry.cpp
+++ b/toolkit/components/telemetry/Telemetry.cpp
@@ -79,16 +79,19 @@ using namespace mozilla::HangMonitor;
 using base::BooleanHistogram;
 using base::CountHistogram;
 using base::FlagHistogram;
 using base::Histogram;
 using base::LinearHistogram;
 using base::StatisticsRecorder;
 
 const char KEYED_HISTOGRAM_NAME_SEPARATOR[] = "#";
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+const char SUBSESSION_HISTOGRAM_PREFIX[] = "sub#";
+#endif
 
 enum reflectStatus {
   REFLECT_OK,
   REFLECT_CORRUPT,
   REFLECT_FAILURE
 };
 
 nsresult
@@ -694,16 +697,20 @@ private:
                   bool privateSQL);
   bool GetSQLStats(JSContext *cx, JS::MutableHandle<JS::Value> ret,
                    bool includePrivateSql);
 
   // Like GetHistogramById, but returns the underlying C++ object, not the JS one.
   nsresult GetHistogramByName(const nsACString &name, Histogram **ret);
   bool ShouldReflectHistogram(Histogram *h);
   void IdentifyCorruptHistograms(StatisticsRecorder::Histograms &hs);
+  nsresult CreateHistogramSnapshots(JSContext *cx,
+                                    JS::MutableHandle<JS::Value> ret,
+                                    bool subsession,
+                                    bool clearSubsession);
   typedef StatisticsRecorder::Histograms::iterator HistogramIterator;
 
   struct AddonHistogramInfo {
     uint32_t min;
     uint32_t max;
     uint32_t bucketCount;
     uint32_t histogramType;
     Histogram *h;
@@ -769,28 +776,34 @@ TelemetryImpl::CollectReports(nsIHandleR
     "Memory used by the telemetry system.");
 }
 
 class KeyedHistogram {
 public:
   KeyedHistogram(const nsACString &name, const nsACString &expiration,
                  uint32_t histogramType, uint32_t min, uint32_t max,
                  uint32_t bucketCount);
-  nsresult GetHistogram(const nsCString& name, Histogram** histogram);
-  Histogram* GetHistogram(const nsCString& name);
+  nsresult GetHistogram(const nsCString& name, Histogram** histogram, bool subsession);
+  Histogram* GetHistogram(const nsCString& name, bool subsession);
   uint32_t GetHistogramType() const { return mHistogramType; }
   nsresult GetDataset(uint32_t* dataset) const;
   nsresult GetJSKeys(JSContext* cx, JS::CallArgs& args);
-  nsresult GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj);
-  void Clear();
+  nsresult GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj,
+                         bool subsession, bool clearSubsession);
+
+  nsresult Add(const nsCString& key, uint32_t aSample);
+  void Clear(bool subsession);
 
 private:
   typedef nsBaseHashtableET<nsCStringHashKey, Histogram*> KeyedHistogramEntry;
   typedef AutoHashtable<KeyedHistogramEntry> KeyedHistogramMapType;
   KeyedHistogramMapType mHistogramMap;
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+  KeyedHistogramMapType mSubsessionMap;
+#endif
 
   struct ReflectKeysArgs {
     JSContext* jsContext;
     JS::AutoValueVector* vector;
   };
   static PLDHashOperator ReflectKeys(KeyedHistogramEntry* entry, void* arg);
 
   static bool ReflectKeyedHistogram(KeyedHistogramEntry* entry,
@@ -986,16 +999,103 @@ GetHistogramByEnumId(Telemetry::ID id, H
 
   if (p.extendedStatisticsOK) {
     h->SetFlags(Histogram::kExtendedStatisticsFlag);
   }
   *ret = knownHistograms[id] = h;
   return NS_OK;
 }
 
+/**
+ * This clones a histogram |existing| with the id |existingId| to a
+ * new histogram with the name |newName|.
+ * For simplicity this is limited to registered histograms.
+ */
+Histogram*
+CloneHistogram(const nsACString& newName, Telemetry::ID existingId,
+               Histogram& existing)
+{
+  const TelemetryHistogram &info = gHistograms[existingId];
+  Histogram *clone = nullptr;
+  nsresult rv;
+
+  rv = HistogramGet(PromiseFlatCString(newName).get(), info.expiration(),
+                    info.histogramType, existing.declared_min(),
+                    existing.declared_max(), existing.bucket_count(),
+                    true, &clone);
+  if (NS_FAILED(rv)) {
+    return nullptr;
+  }
+
+  Histogram::SampleSet ss;
+  existing.SnapshotSample(&ss);
+  clone->AddSampleSet(ss);
+
+  return clone;
+}
+
+/**
+ * This clones a histogram with the id |existingId| to a new histogram
+ * with the name |newName|.
+ * For simplicity this is limited to registered histograms.
+ */
+Histogram*
+CloneHistogram(const nsACString& newName, Telemetry::ID existingId)
+{
+  Histogram *existing = nullptr;
+  nsresult rv = GetHistogramByEnumId(existingId, &existing);
+  if (NS_FAILED(rv)) {
+    return nullptr;
+  }
+
+  return CloneHistogram(newName, existingId, *existing);
+}
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+Histogram*
+GetSubsessionHistogram(Histogram& existing)
+{
+  Telemetry::ID id;
+  nsresult rv = TelemetryImpl::GetHistogramEnumId(existing.histogram_name().c_str(), &id);
+  if (NS_FAILED(rv) || gHistograms[id].keyed) {
+    return nullptr;
+  }
+
+  static Histogram* subsession[Telemetry::HistogramCount] = {};
+  if (subsession[id]) {
+    return subsession[id];
+  }
+
+  NS_NAMED_LITERAL_CSTRING(prefix, SUBSESSION_HISTOGRAM_PREFIX);
+  nsDependentCString existingName(gHistograms[id].id());
+  if (StringBeginsWith(existingName, prefix)) {
+    return nullptr;
+  }
+
+  nsCString subsessionName(prefix);
+  subsessionName.Append(existingName);
+
+  subsession[id] = CloneHistogram(subsessionName, id, existing);
+  return subsession[id];
+}
+#endif
+
+nsresult
+HistogramAdd(Histogram& histogram, int32_t value)
+{
+  histogram.Add(value);
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+  if (Histogram* subsession = GetSubsessionHistogram(histogram)) {
+    subsession->Add(value);
+  }
+#endif
+
+  return NS_OK;
+}
+
 bool
 FillRanges(JSContext *cx, JS::Handle<JSObject*> array, Histogram *h)
 {
   JS::Rooted<JS::Value> range(cx);
   for (size_t i = 0; i < h->bucket_count(); i++) {
     range = INT_TO_JSVAL(h->ranges(i));
     if (!JS_DefineElement(cx, array, i, range, JSPROP_ENUMERATE))
       return false;
@@ -1084,16 +1184,17 @@ bool
 JSHistogram_Add(JSContext *cx, unsigned argc, JS::Value *vp)
 {
   JSObject *obj = JS_THIS_OBJECT(cx, vp);
   if (!obj) {
     return false;
   }
 
   Histogram *h = static_cast<Histogram*>(JS_GetPrivate(obj));
+  MOZ_ASSERT(h);
   Histogram::ClassType type = h->histogram_type();
 
   int32_t value = 1;
   if (type != base::CountHistogram::COUNT_HISTOGRAM) {
     JS::CallArgs args = CallArgsFromVp(argc, vp);
     if (!args.length()) {
       JS_ReportError(cx, "Expected one argument");
       return false;
@@ -1105,17 +1206,17 @@ JSHistogram_Add(JSContext *cx, unsigned 
     }
 
     if (!JS::ToInt32(cx, args[0], &value)) {
       return false;
     }
   }
 
   if (TelemetryImpl::CanRecord()) {
-    h->Add(value);
+    HistogramAdd(*h, value);
   }
 
   return true;
 }
 
 bool
 JSHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp)
 {
@@ -1147,18 +1248,42 @@ JSHistogram_Snapshot(JSContext *cx, unsi
 bool
 JSHistogram_Clear(JSContext *cx, unsigned argc, JS::Value *vp)
 {
   JSObject *obj = JS_THIS_OBJECT(cx, vp);
   if (!obj) {
     return false;
   }
 
+  bool onlySubsession = false;
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+  JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+  if (args.length() >= 1) {
+    if (!args[0].isBoolean()) {
+      JS_ReportError(cx, "Not a boolean");
+      return false;
+    }
+
+    onlySubsession = JS::ToBoolean(args[0]);
+  }
+#endif
+
   Histogram *h = static_cast<Histogram*>(JS_GetPrivate(obj));
-  h->Clear();
+  MOZ_ASSERT(h);
+  if(!onlySubsession) {
+    h->Clear();
+  }
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+  if (Histogram* subsession = GetSubsessionHistogram(*h)) {
+    subsession->Clear();
+  }
+#endif
+
   return true;
 }
 
 bool
 JSHistogram_Dataset(JSContext *cx, unsigned argc, JS::Value *vp)
 {
   JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
   JSObject *obj = JS_THIS_OBJECT(cx, vp);
@@ -1238,27 +1363,17 @@ JSKeyedHistogram_Add(JSContext *cx, unsi
       return false;
     }
 
     if (!JS::ToInt32(cx, args[1], &value)) {
       return false;
     }
   }
 
-  Histogram* h = nullptr;
-  nsresult rv = keyed->GetHistogram(NS_ConvertUTF16toUTF8(key), &h);
-  if (NS_FAILED(rv)) {
-    JS_ReportError(cx, "Failed to get histogram");
-    return false;
-  }
-
-  if (TelemetryImpl::CanRecord()) {
-    h->Add(value);
-  }
-
+  keyed->Add(NS_ConvertUTF16toUTF8(key), value);
   return true;
 }
 
 bool
 JSKeyedHistogram_Keys(JSContext *cx, unsigned argc, JS::Value *vp)
 {
   JSObject *obj = JS_THIS_OBJECT(cx, vp);
   if (!obj) {
@@ -1270,17 +1385,18 @@ JSKeyedHistogram_Keys(JSContext *cx, uns
     return false;
   }
 
   JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
   return NS_SUCCEEDED(keyed->GetJSKeys(cx, args));
 }
 
 bool
-JSKeyedHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp)
+KeyedHistogram_SnapshotImpl(JSContext *cx, unsigned argc, JS::Value *vp,
+                            bool subsession, bool clearSubsession)
 {
   JSObject *obj = JS_THIS_OBJECT(cx, vp);
   if (!obj) {
     return false;
   }
 
   KeyedHistogram* keyed = static_cast<KeyedHistogram*>(JS_GetPrivate(obj));
   if (!keyed) {
@@ -1291,33 +1407,33 @@ JSKeyedHistogram_Snapshot(JSContext *cx,
 
   if (args.length() == 0) {
     JS::RootedObject snapshot(cx, JS_NewPlainObject(cx));
     if (!snapshot) {
       JS_ReportError(cx, "Failed to create object");
       return false;
     }
 
-    if (!NS_SUCCEEDED(keyed->GetJSSnapshot(cx, snapshot))) {
+    if (!NS_SUCCEEDED(keyed->GetJSSnapshot(cx, snapshot, subsession, clearSubsession))) {
       JS_ReportError(cx, "Failed to reflect keyed histograms");
       return false;
     }
 
     args.rval().setObject(*snapshot);
     return true;
   }
 
   nsAutoJSString key;
   if (!args[0].isString() || !key.init(cx, args[0])) {
     JS_ReportError(cx, "Not a string");
     return false;
   }
 
   Histogram* h = nullptr;
-  nsresult rv = keyed->GetHistogram(NS_ConvertUTF16toUTF8(key), &h);
+  nsresult rv = keyed->GetHistogram(NS_ConvertUTF16toUTF8(key), &h, subsession);
   if (NS_FAILED(rv)) {
     JS_ReportError(cx, "Failed to get histogram");
     return false;
   }
 
   JS::RootedObject snapshot(cx, JS_NewPlainObject(cx));
   if (!snapshot) {
     return false;
@@ -1333,29 +1449,72 @@ JSKeyedHistogram_Snapshot(JSContext *cx,
     args.rval().setObject(*snapshot);
     return true;
   default:
     MOZ_CRASH("unhandled reflection status");
   }
 }
 
 bool
+JSKeyedHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp)
+{
+  return KeyedHistogram_SnapshotImpl(cx, argc, vp, false, false);
+}
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+bool
+JSKeyedHistogram_SubsessionSnapshot(JSContext *cx, unsigned argc, JS::Value *vp)
+{
+  return KeyedHistogram_SnapshotImpl(cx, argc, vp, true, false);
+}
+#endif
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+bool
+JSKeyedHistogram_SnapshotSubsessionAndClear(JSContext *cx, unsigned argc, JS::Value *vp)
+{
+  JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+  if (args.length() != 0) {
+    JS_ReportError(cx, "No key arguments supported for snapshotSubsessionAndClear");
+  }
+
+  return KeyedHistogram_SnapshotImpl(cx, argc, vp, true, true);
+}
+#endif
+
+bool
 JSKeyedHistogram_Clear(JSContext *cx, unsigned argc, JS::Value *vp)
 {
   JSObject *obj = JS_THIS_OBJECT(cx, vp);
   if (!obj) {
     return false;
   }
 
   KeyedHistogram* keyed = static_cast<KeyedHistogram*>(JS_GetPrivate(obj));
   if (!keyed) {
     return false;
   }
 
-  keyed->Clear();
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+  bool onlySubsession = false;
+  JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+  if (args.length() >= 1) {
+    if (!(args[0].isNumber() || args[0].isBoolean())) {
+      JS_ReportError(cx, "Not a boolean");
+      return false;
+    }
+
+    onlySubsession = JS::ToBoolean(args[0]);
+  }
+
+  keyed->Clear(onlySubsession);
+#else
+  keyed->Clear(false);
+#endif
   return true;
 }
 
 bool
 JSKeyedHistogram_Dataset(JSContext *cx, unsigned argc, JS::Value *vp)
 {
   JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
   JSObject *obj = JS_THIS_OBJECT(cx, vp);
@@ -1386,16 +1545,20 @@ WrapAndReturnKeyedHistogram(KeyedHistogr
     JSCLASS_HAS_PRIVATE  /* flags */
   };
 
   JS::Rooted<JSObject*> obj(cx, JS_NewObject(cx, &JSHistogram_class));
   if (!obj)
     return NS_ERROR_FAILURE;
   if (!(JS_DefineFunction(cx, obj, "add", JSKeyedHistogram_Add, 2, 0)
         && JS_DefineFunction(cx, obj, "snapshot", JSKeyedHistogram_Snapshot, 1, 0)
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+        && JS_DefineFunction(cx, obj, "subsessionSnapshot", JSKeyedHistogram_SubsessionSnapshot, 1, 0)
+        && JS_DefineFunction(cx, obj, "snapshotSubsessionAndClear", JSKeyedHistogram_SnapshotSubsessionAndClear, 0, 0)
+#endif
         && JS_DefineFunction(cx, obj, "keys", JSKeyedHistogram_Keys, 0, 0)
         && JS_DefineFunction(cx, obj, "clear", JSKeyedHistogram_Clear, 0, 0)
         && JS_DefineFunction(cx, obj, "dataset", JSKeyedHistogram_Dataset, 0, 0))) {
     return NS_ERROR_FAILURE;
   }
 
   JS_SetPrivate(obj, h);
   ret.setObject(*obj);
@@ -1662,16 +1825,28 @@ mFailedLockCount(0)
   for (size_t i = 0; i < ArrayLength(trackedDBs); i++)
     mTrackedDBs.PutEntry(nsDependentCString(trackedDBs[i]));
 
 #ifdef DEBUG
   // Mark immutable to prevent asserts on simultaneous access from multiple threads
   mTrackedDBs.MarkImmutable();
 #endif
 
+  // Populate the static histogram name->id cache.
+  // Note that the histogram names are statically allocated.
+  for (uint32_t i = 0; i < Telemetry::HistogramCount; i++) {
+    CharPtrEntryType *entry = mHistogramMap.PutEntry(gHistograms[i].id());
+    entry->mData = (Telemetry::ID) i;
+  }
+
+#ifdef DEBUG
+  mHistogramMap.MarkImmutable();
+#endif
+
+  // Create registered keyed histograms
   for (size_t i = 0; i < ArrayLength(gHistograms); ++i) {
     const TelemetryHistogram& h = gHistograms[i];
     if (!h.keyed) {
       continue;
     }
 
     const nsDependentCString id(h.id());
     const nsDependentCString expiration(h.expiration());
@@ -1791,31 +1966,18 @@ TelemetryImpl::AddSQLInfo(JSContext *cx,
 
 nsresult
 TelemetryImpl::GetHistogramEnumId(const char *name, Telemetry::ID *id)
 {
   if (!sTelemetry) {
     return NS_ERROR_FAILURE;
   }
 
-  // Cache names
-  // Note the histogram names are statically allocated
-  TelemetryImpl::HistogramMapType *map = &sTelemetry->mHistogramMap;
-  if (!map->Count()) {
-    for (uint32_t i = 0; i < Telemetry::HistogramCount; i++) {
-      CharPtrEntryType *entry = map->PutEntry(gHistograms[i].id());
-      if (MOZ_UNLIKELY(!entry)) {
-        map->Clear();
-        return NS_ERROR_OUT_OF_MEMORY;
-      }
-      entry->mData = (Telemetry::ID) i;
-    }
-  }
-
-  CharPtrEntryType *entry = map->GetEntry(name);
+  const TelemetryImpl::HistogramMapType& map = sTelemetry->mHistogramMap;
+  CharPtrEntryType *entry = map.GetEntry(name);
   if (!entry) {
     return NS_ERROR_INVALID_ARG;
   }
   *id = entry->mData;
   return NS_OK;
 }
 
 nsresult
@@ -1838,35 +2000,22 @@ NS_IMETHODIMP
 TelemetryImpl::HistogramFrom(const nsACString &name, const nsACString &existing_name,
                              JSContext *cx, JS::MutableHandle<JS::Value> ret)
 {
   Telemetry::ID id;
   nsresult rv = GetHistogramEnumId(PromiseFlatCString(existing_name).get(), &id);
   if (NS_FAILED(rv)) {
     return rv;
   }
-  const TelemetryHistogram &p = gHistograms[id];
-
-  Histogram *existing;
-  rv = GetHistogramByEnumId(id, &existing);
-  if (NS_FAILED(rv)) {
-    return rv;
+
+  Histogram* clone = CloneHistogram(name, id);
+  if (!clone) {
+    return NS_ERROR_FAILURE;
   }
 
-  Histogram *clone;
-  rv = HistogramGet(PromiseFlatCString(name).get(), p.expiration(),
-                    p.histogramType, existing->declared_min(),
-                    existing->declared_max(), existing->bucket_count(),
-                    true, &clone);
-  if (NS_FAILED(rv))
-    return rv;
-
-  Histogram::SampleSet ss;
-  existing->SnapshotSample(&ss);
-  clone->AddSampleSet(ss);
   return WrapAndReturnHistogram(clone, cx, ret);
 }
 
 void
 TelemetryImpl::IdentifyCorruptHistograms(StatisticsRecorder::Histograms &hs)
 {
   for (HistogramIterator it = hs.begin(); it != hs.end(); ++it) {
     Histogram *h = *it;
@@ -2039,18 +2188,21 @@ TelemetryImpl::UnregisterAddonHistograms
     // the same names.
     delete addonEntry->mData;
     mAddonMap.RemoveEntry(id);
   }
 
   return NS_OK;
 }
 
-NS_IMETHODIMP
-TelemetryImpl::GetHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value> ret)
+nsresult
+TelemetryImpl::CreateHistogramSnapshots(JSContext *cx,
+                                        JS::MutableHandle<JS::Value> ret,
+                                        bool subsession,
+                                        bool clearSubsession)
 {
   JS::Rooted<JSObject*> root_obj(cx, JS_NewPlainObject(cx));
   if (!root_obj)
     return NS_ERROR_FAILURE;
   ret.setObject(*root_obj);
 
   // Ensure that all the HISTOGRAM_FLAG & HISTOGRAM_COUNT histograms have
   // been created, so that their values are snapshotted.
@@ -2081,38 +2233,72 @@ TelemetryImpl::GetHistogramSnapshots(JSC
   // OK, now we can actually reflect things.
   JS::Rooted<JSObject*> hobj(cx);
   for (HistogramIterator it = hs.begin(); it != hs.end(); ++it) {
     Histogram *h = *it;
     if (!ShouldReflectHistogram(h) || IsEmpty(h) || IsExpired(h)) {
       continue;
     }
 
+    Histogram* original = h;
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+    if (subsession) {
+      h = GetSubsessionHistogram(*h);
+      if (!h) {
+        continue;
+      }
+    }
+#endif
+
     hobj = JS_NewPlainObject(cx);
     if (!hobj) {
       return NS_ERROR_FAILURE;
     }
     switch (ReflectHistogramSnapshot(cx, hobj, h)) {
     case REFLECT_CORRUPT:
       // We can still hit this case even if ShouldReflectHistograms
       // returns true.  The histogram lies outside of our control
       // somehow; just skip it.
       continue;
     case REFLECT_FAILURE:
       return NS_ERROR_FAILURE;
     case REFLECT_OK:
-      if (!JS_DefineProperty(cx, root_obj, h->histogram_name().c_str(), hobj,
-                             JSPROP_ENUMERATE)) {
+      if (!JS_DefineProperty(cx, root_obj, original->histogram_name().c_str(),
+                             hobj, JSPROP_ENUMERATE)) {
         return NS_ERROR_FAILURE;
       }
     }
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+    if (subsession && clearSubsession) {
+      h->Clear();
+    }
+#endif
   }
   return NS_OK;
 }
 
+NS_IMETHODIMP
+TelemetryImpl::GetHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value> ret)
+{
+  return CreateHistogramSnapshots(cx, ret, false, false);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SnapshotSubsessionHistograms(bool clearSubsession,
+                                            JSContext *cx,
+                                            JS::MutableHandle<JS::Value> ret)
+{
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+  return CreateHistogramSnapshots(cx, ret, true, clearSubsession);
+#else
+  return NS_OK;
+#endif
+}
+
 bool
 TelemetryImpl::CreateHistogramForAddon(const nsACString &name,
                                        AddonHistogramInfo &info)
 {
   Histogram *h;
   nsresult rv = HistogramGet(PromiseFlatCString(name).get(), "never",
                              info.histogramType, info.min, info.max,
                              info.bucketCount, true, &h);
@@ -2208,17 +2394,17 @@ TelemetryImpl::KeyedHistogramsReflector(
 {
   KeyedHistogramReflectArgs* args = static_cast<KeyedHistogramReflectArgs*>(arg);
   JSContext *cx = args->jsContext;
   JS::RootedObject snapshot(cx, JS_NewPlainObject(cx));
   if (!snapshot) {
     return PL_DHASH_STOP;
   }
 
-  if (!NS_SUCCEEDED(entry->GetJSSnapshot(cx, snapshot))) {
+  if (!NS_SUCCEEDED(entry->GetJSSnapshot(cx, snapshot, false, false))) {
     return PL_DHASH_STOP;
   }
 
   if (!JS_DefineProperty(cx, args->object, PromiseFlatCString(key).get(),
                          snapshot, JSPROP_ENUMERATE)) {
     return PL_DHASH_STOP;
   }
 
@@ -3333,34 +3519,30 @@ void
 Accumulate(ID aHistogram, uint32_t aSample)
 {
   if (!TelemetryImpl::CanRecord()) {
     return;
   }
   Histogram *h;
   nsresult rv = GetHistogramByEnumId(aHistogram, &h);
   if (NS_SUCCEEDED(rv))
-    h->Add(aSample);
+    HistogramAdd(*h, aSample);
 }
 
 void
 Accumulate(ID aID, const nsCString& aKey, uint32_t aSample)
 {
   if (!TelemetryImpl::CanRecord()) {
     return;
   }
 
   const TelemetryHistogram& th = gHistograms[aID];
   KeyedHistogram* keyed = TelemetryImpl::GetKeyedHistogramById(nsDependentCString(th.id()));
   MOZ_ASSERT(keyed);
-
-  Histogram* histogram = keyed->GetHistogram(aKey);
-  if (histogram) {
-    histogram->Add(aSample);
-  }
+  keyed->Add(aKey, aSample);
 }
 
 void
 Accumulate(const char* name, uint32_t sample)
 {
   if (!TelemetryImpl::CanRecord()) {
     return;
   }
@@ -3368,17 +3550,17 @@ Accumulate(const char* name, uint32_t sa
   nsresult rv = TelemetryImpl::GetHistogramEnumId(name, &id);
   if (NS_FAILED(rv)) {
     return;
   }
 
   Histogram *h;
   rv = GetHistogramByEnumId(id, &h);
   if (NS_SUCCEEDED(rv)) {
-    h->Add(sample);
+    HistogramAdd(*h, sample);
   }
 }
 
 void
 AccumulateTimeDelta(ID aHistogram, TimeStamp start, TimeStamp end)
 {
   Accumulate(aHistogram,
              static_cast<uint32_t>((end - start).ToMilliseconds()));
@@ -3771,64 +3953,79 @@ XRE_TelemetryAccumulate(int aID, uint32_
 {
   mozilla::Telemetry::Accumulate((mozilla::Telemetry::ID) aID, aSample);
 }
 
 KeyedHistogram::KeyedHistogram(const nsACString &name, const nsACString &expiration,
                                uint32_t histogramType, uint32_t min, uint32_t max,
                                uint32_t bucketCount)
   : mHistogramMap()
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+  , mSubsessionMap()
+#endif
   , mName(name)
   , mExpiration(expiration)
   , mHistogramType(histogramType)
   , mMin(min)
   , mMax(max)
   , mBucketCount(bucketCount)
 {
 }
 
 nsresult
-KeyedHistogram::GetHistogram(const nsCString& key, Histogram** histogram)
+KeyedHistogram::GetHistogram(const nsCString& key, Histogram** histogram,
+                             bool subsession)
 {
-  KeyedHistogramEntry* entry = mHistogramMap.GetEntry(key);
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+  KeyedHistogramMapType& map = subsession ? mSubsessionMap : mHistogramMap;
+#else
+  KeyedHistogramMapType& map = mHistogramMap;
+#endif
+  KeyedHistogramEntry* entry = map.GetEntry(key);
   if (entry) {
     *histogram = entry->mData;
     return NS_OK;
   }
 
-  nsCString histogramName = mName;
+  nsCString histogramName;
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+  if (subsession) {
+    histogramName.Append(SUBSESSION_HISTOGRAM_PREFIX);
+  }
+#endif
+  histogramName.Append(mName);
   histogramName.Append(KEYED_HISTOGRAM_NAME_SEPARATOR);
   histogramName.Append(key);
 
   Histogram* h;
   nsresult rv = HistogramGet(histogramName.get(), mExpiration.get(),
                              mHistogramType, mMin, mMax, mBucketCount,
                              true, &h);
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   h->ClearFlags(Histogram::kUmaTargetedHistogramFlag);
   h->SetFlags(Histogram::kExtendedStatisticsFlag);
   *histogram = h;
 
-  entry = mHistogramMap.PutEntry(key);
+  entry = map.PutEntry(key);
   if (MOZ_UNLIKELY(!entry)) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
   entry->mData = h;
   return NS_OK;
 }
 
 Histogram*
-KeyedHistogram::GetHistogram(const nsCString& key)
+KeyedHistogram::GetHistogram(const nsCString& key, bool subsession)
 {
   Histogram* h = nullptr;
-  if (NS_FAILED(GetHistogram(key, &h))) {
+  if (NS_FAILED(GetHistogram(key, &h, subsession))) {
     return nullptr;
   }
   return h;
 }
 
 nsresult
 KeyedHistogram::GetDataset(uint32_t* dataset) const
 {
@@ -3847,19 +4044,54 @@ KeyedHistogram::GetDataset(uint32_t* dat
 /* static */
 PLDHashOperator
 KeyedHistogram::ClearHistogramEnumerator(KeyedHistogramEntry* entry, void*)
 {
   entry->mData->Clear();
   return PL_DHASH_NEXT;
 }
 
+nsresult
+KeyedHistogram::Add(const nsCString& key, uint32_t sample)
+{
+  if (!TelemetryImpl::CanRecord()) {
+    return NS_OK;
+  }
+
+  Histogram* histogram = GetHistogram(key, false);
+  MOZ_ASSERT(histogram);
+  if (!histogram) {
+    return NS_ERROR_FAILURE;
+  }
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+  Histogram* subsession = GetHistogram(key, true);
+  MOZ_ASSERT(subsession);
+  if (!subsession) {
+    return NS_ERROR_FAILURE;
+  }
+#endif
+
+  histogram->Add(sample);
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+  subsession->Add(sample);
+#endif
+  return NS_OK;
+}
+
 void
-KeyedHistogram::Clear()
+KeyedHistogram::Clear(bool onlySubsession)
 {
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+  mSubsessionMap.EnumerateEntries(&KeyedHistogram::ClearHistogramEnumerator, nullptr);
+  mSubsessionMap.Clear();
+  if (onlySubsession) {
+    return;
+  }
+#endif
+
   mHistogramMap.EnumerateEntries(&KeyedHistogram::ClearHistogramEnumerator, nullptr);
   mHistogramMap.Clear();
 }
 
 /* static */
 PLDHashOperator
 KeyedHistogram::ReflectKeys(KeyedHistogramEntry* entry, void* arg)
 {
@@ -3915,16 +4147,28 @@ KeyedHistogram::ReflectKeyedHistogram(Ke
                            histogramSnapshot, JSPROP_ENUMERATE)) {
     return false;
   }
 
   return true;
 }
 
 nsresult
-KeyedHistogram::GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj)
+KeyedHistogram::GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj,
+                              bool subsession, bool clearSubsession)
 {
-  if (!mHistogramMap.ReflectIntoJS(&KeyedHistogram::ReflectKeyedHistogram, cx, obj)) {
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+  KeyedHistogramMapType& map = subsession ? mSubsessionMap : mHistogramMap;
+#else
+  KeyedHistogramMapType& map = mHistogramMap;
+#endif
+  if (!map.ReflectIntoJS(&KeyedHistogram::ReflectKeyedHistogram, cx, obj)) {
     return NS_ERROR_FAILURE;
   }
 
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+  if (subsession && clearSubsession) {
+    Clear(true);
+  }
+#endif
+
   return NS_OK;
 }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -0,0 +1,1069 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = [
+  "TelemetryEnvironment",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/PromiseUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
+                                  "resource://gre/modules/ctypes.jsm");
+#ifndef MOZ_WIDGET_GONK
+XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
+                                  "resource://gre/modules/LightweightThemeManager.jsm");
+#endif
+XPCOMUtils.defineLazyModuleGetter(this, "ProfileTimesAccessor",
+                                  "resource://gre/modules/services/healthreport/profile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
+                                  "resource://gre/modules/UpdateChannel.jsm");
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+
+const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
+const PREF_DISTRIBUTION_ID = "distribution.id";
+const PREF_DISTRIBUTION_VERSION = "distribution.version";
+const PREF_DISTRIBUTOR = "app.distributor";
+const PREF_DISTRIBUTOR_CHANNEL = "app.distributor.channel";
+const PREF_E10S_ENABLED = "browser.tabs.remote.autostart";
+const PREF_HOTFIX_LASTVERSION = "extensions.hotfix.lastVersion";
+const PREF_APP_PARTNER_BRANCH = "app.partner.";
+const PREF_PARTNER_ID = "mozilla.partner.id";
+const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
+const PREF_UPDATE_ENABLED = "app.update.enabled";
+const PREF_UPDATE_AUTODOWNLOAD = "app.update.auto";
+
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
+
+const EXPERIMENTS_CHANGED_TOPIC = "experiments-changed";
+
+/**
+ * Turn a millisecond timestamp into a day timestamp.
+ *
+ * @param aMsec a number of milliseconds since epoch.
+ * @return the number of whole days denoted by the input.
+ */
+function truncateToDays(aMsec) {
+  return Math.floor(aMsec / MILLISECONDS_PER_DAY);
+}
+
+/**
+ * Get the current browser.
+ * @return a string with the locale or null on failure.
+ */
+function getBrowserLocale() {
+  try {
+    return Cc["@mozilla.org/chrome/chrome-registry;1"].
+             getService(Ci.nsIXULChromeRegistry).
+             getSelectedLocale('global');
+  } catch (e) {
+    return null;
+  }
+}
+
+/**
+ * Get the current OS locale.
+ * @return a string with the OS locale or null on failure.
+ */
+function getSystemLocale() {
+  try {
+    return Services.locale.getLocaleComponentForUserAgent();
+  } catch (e) {
+    return null;
+  }
+}
+
+/**
+ * Asynchronously get a list of addons of the specified type from the AddonManager.
+ * @param aTypes An array containing the types of addons to request.
+ * @return Promise<Array> resolved when AddonManager has finished, returning an
+ *         array of addons.
+ */
+function promiseGetAddonsByTypes(aTypes) {
+  return new Promise((resolve) =>
+                     AddonManager.getAddonsByTypes(aTypes, (addons) => resolve(addons)));
+}
+
+/**
+ * Safely get a sysinfo property and return its value. If the property is not
+ * available, return aDefault.
+ *
+ * @param aPropertyName the property name to get.
+ * @param aDefault the value to return if aPropertyName is not available.
+ * @return The property value, if available, or aDefault.
+ */
+function getSysinfoProperty(aPropertyName, aDefault) {
+  try {
+    // |getProperty| may throw if |aPropertyName| does not exist.
+    return Services.sysinfo.getProperty(aPropertyName);
+  } catch (e) {}
+
+  return aDefault;
+}
+
+/**
+ * Safely get a gfxInfo field and return its value. If the field is not available, return
+ * aDefault.
+ *
+ * @param aPropertyName the property name to get.
+ * @param aDefault the value to return if aPropertyName is not available.
+ * @return The property value, if available, or aDefault.
+ */
+function getGfxField(aPropertyName, aDefault) {
+  let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+  try {
+    // Accessing the field may throw if |aPropertyName| does not exist.
+    let gfxProp = gfxInfo[aPropertyName];
+    if (gfxProp !== "") {
+      return gfxProp;
+    }
+  } catch (e) {}
+
+  return aDefault;
+}
+
+/**
+ * Get the information about a graphic adapter.
+ *
+ * @param aSuffix A suffix to add to the properties names.
+ * @return An object containing the adapter properties.
+ */
+function getGfxAdapter(aSuffix = "") {
+  let memoryMB = getGfxField("adapterRAM" + aSuffix, null);
+  if (memoryMB) {
+    memoryMB = parseInt(memoryMB, 10);
+  }
+
+  return {
+    description: getGfxField("adapterDescription" + aSuffix, null),
+    vendorID: getGfxField("adapterVendorID" + aSuffix, null),
+    deviceID: getGfxField("adapterDeviceID" + aSuffix, null),
+    subsysID: getGfxField("adapterSubsysID" + aSuffix, null),
+    RAM: memoryMB,
+    driver: getGfxField("adapterDriver" + aSuffix, null),
+    driverVersion: getGfxField("adapterDriverVersion" + aSuffix, null),
+    driverDate: getGfxField("adapterDriverDate" + aSuffix, null),
+  };
+}
+
+#ifdef XP_WIN
+/**
+ * Gets the service pack information on Windows platforms. This was copied from
+ * nsUpdateService.js.
+ *
+ * @return An object containing the service pack major and minor versions.
+ */
+function getServicePack() {
+  const BYTE = ctypes.uint8_t;
+  const WORD = ctypes.uint16_t;
+  const DWORD = ctypes.uint32_t;
+  const WCHAR = ctypes.char16_t;
+  const BOOL = ctypes.int;
+
+  // This structure is described at:
+  // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx
+  const SZCSDVERSIONLENGTH = 128;
+  const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW',
+      [
+      {dwOSVersionInfoSize: DWORD},
+      {dwMajorVersion: DWORD},
+      {dwMinorVersion: DWORD},
+      {dwBuildNumber: DWORD},
+      {dwPlatformId: DWORD},
+      {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)},
+      {wServicePackMajor: WORD},
+      {wServicePackMinor: WORD},
+      {wSuiteMask: WORD},
+      {wProductType: BYTE},
+      {wReserved: BYTE}
+      ]);
+
+  let kernel32 = ctypes.open("kernel32");
+  try {
+    let GetVersionEx = kernel32.declare("GetVersionExW",
+                                        ctypes.default_abi,
+                                        BOOL,
+                                        OSVERSIONINFOEXW.ptr);
+    let winVer = OSVERSIONINFOEXW();
+    winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size;
+
+    if(0 === GetVersionEx(winVer.address())) {
+      throw("Failure in GetVersionEx (returned 0)");
+    }
+
+    return {
+      major: winVer.wServicePackMajor,
+      minor: winVer.wServicePackMinor,
+    };
+  } catch (e) {
+    return {
+      major: null,
+      minor: null,
+    };
+  } finally {
+    kernel32.close();
+  }
+}
+#endif
+
+this.TelemetryEnvironment = {
+  _shutdown: true,
+
+  // A map of (sync) listeners that will be called on environment changes.
+  _changeListeners: new Map(),
+  // Async task for collecting the environment data.
+  _collectTask: null,
+
+  // Policy to use when saving preferences. Exported for using them in tests.
+  RECORD_PREF_STATE: 1, // Don't record the preference value
+  RECORD_PREF_VALUE: 2, // We only record user-set prefs.
+  RECORD_PREF_NOTIFY_ONLY: 3, // Record nothing, just notify of changes.
+
+  // A map of watched preferences which trigger an Environment change when modified.
+  // Every entry contains a recording policy (RECORD_PREF_*).
+  _watchedPrefs: null,
+
+  // The Addons change listener, init by |_startWatchingAddons| .
+  _addonsListener: null,
+
+  // AddonManager may shut down before us, in which case we cache the addons here.
+  // It is always null if the AM didn't shut down before us.
+  // If cached, it is an object containing the addon information, suitable for use
+  // in the environment data.
+  _cachedAddons: null,
+
+  /**
+   * Initialize TelemetryEnvironment.
+   */
+  init: function () {
+    if (!this._shutdown) {
+      this._log.error("init - Already initialized");
+      return;
+    }
+
+    this._configureLog();
+    this._log.trace("init");
+    this._shutdown = false;
+    this._startWatchingPrefs();
+    this._startWatchingAddons();
+
+    AddonManager.shutdown.addBlocker("TelemetryEnvironment: caching addons",
+                                      () => this._blockAddonManagerShutdown(),
+                                      () => this._getState());
+  },
+
+  /**
+   * Shutdown TelemetryEnvironment.
+   * @return Promise<> that is resolved when shutdown is finished.
+   */
+  shutdown: Task.async(function* () {
+    if (this._shutdown) {
+      if (this._log) {
+        this._log.error("shutdown - Already shut down");
+      } else {
+        Cu.reportError("TelemetryEnvironment.shutdown - Already shut down");
+      }
+      return;
+    }
+
+    this._log.trace("shutdown");
+    this._shutdown = true;
+    this._stopWatchingPrefs();
+    this._stopWatchingAddons();
+    this._changeListeners.clear();
+    yield this._collectTask;
+
+    this._cachedAddons = null;
+  }),
+
+  _configureLog: function () {
+    if (this._log) {
+      return;
+    }
+    this._log = Log.repository.getLoggerWithMessagePrefix(
+                                 LOGGER_NAME, "TelemetryEnvironment::");
+  },
+
+  /**
+   * Register a listener for environment changes.
+   * It's fine to call this on an unitialized TelemetryEnvironment.
+   * @param name The name of the listener - good for debugging purposes.
+   * @param listener A JS callback function.
+   */
+  registerChangeListener: function (name, listener) {
+    this._configureLog();
+    this._log.trace("registerChangeListener for " + name);
+    if (this._shutdown) {
+      this._log.warn("registerChangeListener - already shutdown")
+      return;
+    }
+    this._changeListeners.set(name, listener);
+  },
+
+  /**
+   * Unregister from listening to environment changes.
+   * It's fine to call this on an unitialized TelemetryEnvironment.
+   * @param name The name of the listener to remove.
+   */
+  unregisterChangeListener: function (name) {
+    this._configureLog();
+    this._log.trace("unregisterChangeListener for " + name);
+    if (this._shutdown) {
+      this._log.warn("registerChangeListener - already shutdown")
+      return;
+    }
+    this._changeListeners.delete(name);
+  },
+
+  /**
+   * Only used in tests, set the preferences to watch.
+   * @param aPreferences A map of preferences names and their recording policy.
+   */
+  _watchPreferences: function (aPreferences) {
+    if (this._watchedPrefs) {
+      this._stopWatchingPrefs();
+    }
+
+    this._watchedPrefs = aPreferences;
+    this._startWatchingPrefs();
+  },
+
+  /**
+   * Get an object containing the values for the watched preferences. Depending on the
+   * policy, the value for a preference or whether it was changed by user is reported.
+   *
+   * @return An object containing the preferences values.
+   */
+  _getPrefData: function () {
+    if (!this._watchedPrefs) {
+      return {};
+    }
+
+    let prefData = {};
+    for (let pref in this._watchedPrefs) {
+      // Only record preferences if they are non-default and policy allows recording.
+      if (!Preferences.isSet(pref) ||
+          this._watchedPrefs[pref] == this.RECORD_PREF_NOTIFY_ONLY) {
+        continue;
+      }
+
+      // Check the policy for the preference and decide if we need to store its value
+      // or whether it changed from the default value.
+      let prefValue = undefined;
+      if (this._watchedPrefs[pref] == this.RECORD_PREF_STATE) {
+        prefValue = null;
+      } else {
+        prefValue = Preferences.get(pref, null);
+      }
+      prefData[pref] = prefValue;
+    }
+    return prefData;
+  },
+
+  /**
+   * Start watching the preferences.
+   */
+  _startWatchingPrefs: function () {
+    this._log.trace("_startWatchingPrefs - " + this._watchedPrefs);
+
+    if (!this._watchedPrefs) {
+      return;
+    }
+
+    for (let pref in this._watchedPrefs) {
+      Preferences.observe(pref, this._onPrefChanged, this);
+    }
+  },
+
+  /**
+   * Do not receive any more change notifications for the preferences.
+   */
+  _stopWatchingPrefs: function () {
+    this._log.trace("_stopWatchingPrefs");
+
+    if (!this._watchedPrefs) {
+      return;
+    }
+
+    for (let pref in this._watchedPrefs) {
+      Preferences.ignore(pref, this._onPrefChanged, this);
+    }
+
+    this._watchedPrefs = null;
+  },
+
+  _onPrefChanged: function () {
+    this._log.trace("_onPrefChanged");
+    this._onEnvironmentChange("pref-changed");
+  },
+
+  /**
+   * Get the build data in object form.
+   * @return Object containing the build data.
+   */
+  _getBuild: function () {
+    let buildData = {
+      applicationId: Services.appinfo.ID,
+      applicationName: Services.appinfo.name,
+      architecture: Services.sysinfo.get("arch"),
+      buildId: Services.appinfo.appBuildID,
+      version: Services.appinfo.version,
+      vendor: Services.appinfo.vendor,
+      platformVersion: Services.appinfo.platformVersion,
+      xpcomAbi: Services.appinfo.XPCOMABI,
+      hotfixVersion: Preferences.get(PREF_HOTFIX_LASTVERSION, null),
+    };
+
+    // Add |architecturesInBinary| only for Mac Universal builds.
+    if ("@mozilla.org/xpcom/mac-utils;1" in Cc) {
+      let macUtils = Cc["@mozilla.org/xpcom/mac-utils;1"].getService(Ci.nsIMacUtils);
+      if (macUtils && macUtils.isUniversalBinary) {
+        buildData.architecturesInBinary = macUtils.architecturesInBinary;
+      }
+    }
+
+    return buildData;
+  },
+
+  /**
+   * Determine if Firefox is the default browser.
+   * @returns null on error, true if we are the default browser, or false otherwise.
+   */
+  _isDefaultBrowser: function () {
+    if (!("@mozilla.org/browser/shell-service;1" in Cc)) {
+      this._log.error("_isDefaultBrowser - Could not obtain shell service");
+      return null;
+    }
+
+    let shellService;
+    try {
+      shellService = Cc["@mozilla.org/browser/shell-service;1"]
+                       .getService(Ci.nsIShellService);
+    } catch (ex) {
+      this._log.error("_isDefaultBrowser - Could not obtain shell service", ex);
+      return null;
+    }
+
+    if (shellService) {
+      try {
+        // This uses the same set of flags used by the pref pane.
+        return shellService.isDefaultBrowser(false, true) ? true : false;
+      } catch (ex) {
+        this._log.error("_isDefaultBrowser - Could not determine if default browser", ex);
+        return null;
+      }
+    }
+
+    return null;
+  },
+
+  /**
+   * Get the settings data in object form.
+   * @return Object containing the settings.
+   */
+  _getSettings: function () {
+    let updateChannel = null;
+    try {
+      updateChannel = UpdateChannel.get();
+    } catch (e) {}
+
+    return {
+      blocklistEnabled: Preferences.get(PREF_BLOCKLIST_ENABLED, true),
+#ifndef MOZ_WIDGET_ANDROID
+      isDefaultBrowser: this._isDefaultBrowser(),
+#endif
+      e10sEnabled: Preferences.get(PREF_E10S_ENABLED, false),
+      telemetryEnabled: Preferences.get(PREF_TELEMETRY_ENABLED, false),
+      locale: getBrowserLocale(),
+      update: {
+        channel: updateChannel,
+        enabled: Preferences.get(PREF_UPDATE_ENABLED, true),
+        autoDownload: Preferences.get(PREF_UPDATE_AUTODOWNLOAD, true),
+      },
+      userPrefs: this._getPrefData(),
+    };
+  },
+
+  /**
+   * Get the profile data in object form.
+   * @return Object containing the profile data.
+   */
+  _getProfile: Task.async(function* () {
+    let profileAccessor = new ProfileTimesAccessor(null, this._log);
+
+    let creationDate = yield profileAccessor.created;
+    let resetDate = yield profileAccessor.reset;
+
+    let profileData = {
+      creationDate: truncateToDays(creationDate),
+    };
+
+    if (resetDate) {
+      profileData.resetDate = truncateToDays(resetDate);
+    }
+    return profileData;
+  }),
+
+  /**
+   * Get the partner data in object form.
+   * @return Object containing the partner data.
+   */
+  _getPartner: function () {
+    let partnerData = {
+      distributionId: Preferences.get(PREF_DISTRIBUTION_ID, null),
+      distributionVersion: Preferences.get(PREF_DISTRIBUTION_VERSION, null),
+      partnerId: Preferences.get(PREF_PARTNER_ID, null),
+      distributor: Preferences.get(PREF_DISTRIBUTOR, null),
+      distributorChannel: Preferences.get(PREF_DISTRIBUTOR_CHANNEL, null),
+    };
+
+    // Get the PREF_APP_PARTNER_BRANCH branch and append its children to partner data.
+    let partnerBranch = Services.prefs.getBranch(PREF_APP_PARTNER_BRANCH);
+    partnerData.partnerNames = partnerBranch.getChildList("")
+
+    return partnerData;
+  },
+
+  /**
+   * Get the CPU information.
+   * @return Object containing the CPU information data.
+   */
+  _getCpuData: function () {
+    let cpuData = {
+      count: getSysinfoProperty("cpucount", null),
+      vendor: null, // TODO: bug 1128472
+      family: null, // TODO: bug 1128472
+      model: null, // TODO: bug 1128472
+      stepping: null, // TODO: bug 1128472
+    };
+
+    const CPU_EXTENSIONS = ["hasMMX", "hasSSE", "hasSSE2", "hasSSE3", "hasSSSE3",
+                            "hasSSE4A", "hasSSE4_1", "hasSSE4_2", "hasEDSP", "hasARMv6",
+                            "hasARMv7", "hasNEON"];
+
+    // Enumerate the available CPU extensions.
+    let availableExts = [];
+    for (let ext of CPU_EXTENSIONS) {
+      try {
+        Services.sysinfo.getProperty(ext);
+        // If it doesn't throw, add it to the list.
+        availableExts.push(ext);
+      } catch (e) {}
+    }
+
+    cpuData.extensions = availableExts;
+
+    return cpuData;
+  },
+
+#if defined(MOZ_WIDGET_GONK) || defined(MOZ_WIDGET_ANDROID)
+  /**
+   * Get the device information, if we are on a portable device.
+   * @return Object containing the device information data.
+   */
+  _getDeviceData: function () {
+    return {
+      model: getSysinfoProperty("device", null),
+      manufacturer: getSysinfoProperty("manufacturer", null),
+      hardware: getSysinfoProperty("hardware", null),
+      isTablet: getSysinfoProperty("tablet", null),
+    };
+  },
+#endif
+
+  /**
+   * Get the OS information.
+   * @return Object containing the OS data.
+   */
+  _getOSData: function () {
+#ifdef XP_WIN
+    // Try to get service pack information.
+    let servicePack = getServicePack();
+#endif
+
+    return {
+      name: getSysinfoProperty("name", null),
+      version: getSysinfoProperty("version", null),
+#if defined(MOZ_WIDGET_GONK) || defined(MOZ_WIDGET_ANDROID)
+      kernelVersion: getSysinfoProperty("kernel_version", null),
+#elif defined(XP_WIN)
+      servicePackMajor: servicePack.major,
+      servicePackMinor: servicePack.minor,
+#endif
+      locale: getSystemLocale(),
+    };
+  },
+
+  /**
+   * Get the HDD information.
+   * @return Object containing the HDD data.
+   */
+  _getHDDData: function () {
+    return {
+      profile: { // hdd where the profile folder is located
+        model: getSysinfoProperty("profileHDDModel", null),
+        revision: getSysinfoProperty("profileHDDRevision", null),
+      },
+      binary:  { // hdd where the application binary is located
+        model: getSysinfoProperty("binHDDModel", null),
+        revision: getSysinfoProperty("binHDDRevision", null),
+      },
+      system:  { // hdd where the system files are located
+        model: getSysinfoProperty("winHDDModel", null),
+        revision: getSysinfoProperty("winHDDRevision", null),
+      },
+    };
+  },
+
+  /**
+   * Get the GFX information.
+   * @return Object containing the GFX data.
+   */
+  _getGFXData: function () {
+    let gfxData = {
+      D2DEnabled: getGfxField("D2DEnabled", null),
+      DWriteEnabled: getGfxField("DWriteEnabled", null),
+      DWriteVersion: getGfxField("DWriteVersion", null),
+      adapters: [],
+    };
+
+    // GfxInfo does not yet expose a way to iterate through all the adapters.
+    gfxData.adapters.push(getGfxAdapter(""));
+    gfxData.adapters[0].GPUActive = true;
+
+    // If we have a second adapter add it to the gfxData.adapters section.
+    let hasGPU2 = getGfxField("adapterDeviceID2", null) !== null;
+    if (!hasGPU2) {
+      this._log.trace("_getGFXData - Only one display adapter detected.");
+      return gfxData;
+    }
+
+    this._log.trace("_getGFXData - Two display adapters detected.");
+
+    gfxData.adapters.push(getGfxAdapter("2"));
+    gfxData.adapters[1].GPUActive = getGfxField("isGPU2Active ", null);
+
+    return gfxData;
+  },
+
+  /**
+   * Get the system data in object form.
+   * @return Object containing the system data.
+   */
+  _getSystem: function () {
+    let memoryMB = getSysinfoProperty("memsize", null);
+    if (memoryMB) {
+      // Send RAM size in megabytes. Rounding because sysinfo doesn't
+      // always provide RAM in multiples of 1024.
+      memoryMB = Math.round(memoryMB / 1024 / 1024);
+    }
+
+    return {
+      memoryMB: memoryMB,
+#ifdef XP_WIN
+      isWow64: getSysinfoProperty("isWow64", null),
+#endif
+      cpu: this._getCpuData(),
+#if defined(MOZ_WIDGET_GONK) || defined(MOZ_WIDGET_ANDROID)
+      device: this._getDeviceData(),
+#endif
+      os: this._getOSData(),
+      hdd: this._getHDDData(),
+      gfx: this._getGFXData(),
+    };
+  },
+
+  /**
+   * Get the addon data in object form.
+   * @return Object containing the addon data.
+   *
+   * This should only be called from the environment collection task
+   * or _blockAddonManagerShutdown, otherwise we risk running this
+   * during addon manager shutdown.
+   */
+  _getActiveAddons: Task.async(function* () {
+
+    // Request addons, asynchronously.
+    let allAddons = yield promiseGetAddonsByTypes(["extension", "service"]);
+
+    let activeAddons = {};
+    for (let addon of allAddons) {
+      // Skip addons which are not active.
+      if (!addon.isActive) {
+        continue;
+      }
+
+      activeAddons[addon.id] = {
+        blocklisted: (addon.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED),
+        description: addon.description,
+        name: addon.name,
+        userDisabled: addon.userDisabled,
+        appDisabled: addon.appDisabled,
+        version: addon.version,
+        scope: addon.scope,
+        type: addon.type,
+        foreignInstall: addon.foreignInstall,
+        hasBinaryComponents: addon.hasBinaryComponents,
+        installDay: truncateToDays(addon.installDate.getTime()),
+        updateDay: truncateToDays(addon.updateDate.getTime()),
+      };
+    }
+
+    return activeAddons;
+  }),
+
+  /**
+   * Get the currently active theme data in object form.
+   * @return Object containing the active theme data.
+   *
+   * This should only be called from the environment collection task
+   * or _blockAddonManagerShutdown, otherwise we risk running this
+   * during addon manager shutdown.
+   */
+  _getActiveTheme: Task.async(function* () {
+    // Request themes, asynchronously.
+    let themes = yield promiseGetAddonsByTypes(["theme"]);
+
+    let activeTheme = {};
+    // We only store information about the active theme.
+    let theme = themes.find(theme => theme.isActive);
+    if (theme) {
+      activeTheme = {
+        id: theme.id,
+        blocklisted: (theme.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED),
+        description: theme.description,
+        name: theme.name,
+        userDisabled: theme.userDisabled,
+        appDisabled: theme.appDisabled,
+        version: theme.version,
+        scope: theme.scope,
+        foreignInstall: theme.foreignInstall,
+        hasBinaryComponents: theme.hasBinaryComponents,
+        installDay: truncateToDays(theme.installDate.getTime()),
+        updateDay: truncateToDays(theme.updateDate.getTime()),
+      };
+    }
+
+    return activeTheme;
+  }),
+
+  /**
+   * Get the plugins data in object form.
+   * @return Object containing the plugins data.
+   */
+  _getActivePlugins: function () {
+    let pluginTags =
+      Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost).getPluginTags({});
+
+    let activePlugins = [];
+    for (let tag of pluginTags) {
+      // Skip plugins which are not active.
+      if (tag.disabled) {
+        continue;
+      }
+
+      // Make sure to have a valid date.
+      let updateDate = new Date(Math.max(0, tag.lastModifiedTime));
+
+      activePlugins.push({
+        name: tag.name,
+        version: tag.version,
+        description: tag.description,
+        blocklisted: tag.blocklisted,
+        disabled: tag.disabled,
+        clicktoplay: tag.clicktoplay,
+        mimeTypes: tag.getMimeTypes({}),
+        updateDay: truncateToDays(updateDate.getTime()),
+      });
+    }
+
+    return activePlugins;
+  },
+
+  /**
+   * Get the GMPlugins data in object form.
+   * @return Object containing the GMPlugins data.
+   *
+   * This should only be called from the environment collection task
+   * or _blockAddonManagerShutdown, otherwise we risk running this
+   * during addon manager shutdown.
+   */
+  _getActiveGMPlugins: Task.async(function* () {
+    // Request plugins, asynchronously.
+    let allPlugins = yield promiseGetAddonsByTypes(["plugin"]);
+
+    let activeGMPlugins = {};
+    for (let plugin of allPlugins) {
+      // Only get GM Plugin info.
+      if (!plugin.isGMPlugin) {
+        continue;
+      }
+
+      activeGMPlugins[plugin.id] = {
+        version: plugin.version,
+        userDisabled: plugin.userDisabled,
+        applyBackgroundUpdates: plugin.applyBackgroundUpdates,
+      };
+    }
+
+    return activeGMPlugins;
+  }),
+
+  /**
+   * Get the active experiment data in object form.
+   * @return Object containing the active experiment data.
+   */
+  _getActiveExperiment: function () {
+    let experimentInfo = {};
+    try {
+      let scope = {};
+      Cu.import("resource:///modules/experiments/Experiments.jsm", scope);
+      let experiments = scope.Experiments.instance()
+      let activeExperiment = experiments.getActiveExperimentID();
+      if (activeExperiment) {
+        experimentInfo.id = activeExperiment;
+        experimentInfo.branch = experiments.getActiveExperimentBranch();
+      }
+    } catch(e) {
+      // If this is not Firefox, the import will fail.
+      return experimentInfo;
+    }
+
+    return experimentInfo;
+  },
+
+  /**
+   * Get the addon data in object form.
+   * @return Object containing the addon data.
+   *
+   * This should only be called from the environment collection task
+   * or _blockAddonManagerShutdown, otherwise we risk running this
+   * during addon manager shutdown.
+   */
+  _getAddons: Task.async(function* () {
+    // AddonManager may have shutdown already, in which case we should have cached addon data.
+    // It can't shutdown during the collection here because we have a blocker on the AMs
+    // shutdown barrier that waits for the collect task.
+    let addons = this._cachedAddons || {};
+    if (!this._cachedAddons) {
+      addons.activeAddons = yield this._getActiveAddons();
+      addons.activeTheme = yield this._getActiveTheme();
+      addons.activeGMPlugins = yield this._getActiveGMPlugins();
+    }
+
+    let personaId = null;
+#ifndef MOZ_WIDGET_GONK
+    let theme = LightweightThemeManager.currentTheme;
+    if (theme) {
+      personaId = theme.id;
+    }
+#endif
+
+    let addonData = {
+      activeAddons: addons.activeAddons,
+      theme: addons.activeTheme,
+      activePlugins: this._getActivePlugins(),
+      activeGMPlugins: addons.activeGMPlugins,
+      activeExperiment: this._getActiveExperiment(),
+      persona: personaId,
+    };
+
+    return addonData;
+  }),
+
+  /**
+   * Start watching the addons for changes.
+   */
+  _startWatchingAddons: function () {
+    // Define a listener to catch addons changes from the AddonManager. This part is
+    // tricky, as we only want to detect when the set of active addons changes without
+    // getting double notifications.
+    //
+    // We identified the following cases:
+    //
+    // * onEnabled:   Gets called when a restartless addon is enabled. Doesn't get called
+    //                if the restartless addon is installed and directly enabled.
+    // * onDisabled:  Gets called when disabling a restartless addon or can get called when
+    //                uninstalling a restartless addon from the UI (see bug 612168).
+    // * onInstalled: Gets called for all addon installs.
+    // * onUninstalling: Called the moment before addon uninstall happens.
+
+    this._addonsListener = {
+      onEnabled: addon => {
+        this._log.trace("_addonsListener - onEnabled " + addon.id);
+        this._onActiveAddonsChanged(addon)
+      },
+      onDisabled: addon => {
+        this._log.trace("_addonsListener - onDisabled " + addon.id);
+        this._onActiveAddonsChanged(addon);
+      },
+      onInstalled: addon => {
+        this._log.trace("_addonsListener - onInstalled " + addon.id +
+                        ", isActive: " + addon.isActive);
+        if (addon.isActive) {
+          this._onActiveAddonsChanged(addon);
+        }
+      },
+      onUninstalling: (addon, requiresRestart) => {
+        this._log.trace("_addonsListener - onUninstalling " + addon.id +
+                        ", isActive: " + addon.isActive +
+                        ", requiresRestart: " + requiresRestart);
+        if (!addon.isActive || requiresRestart) {
+          return;
+        }
+        this._onActiveAddonsChanged(addon);
+      },
+    };
+
+    AddonManager.addAddonListener(this._addonsListener);
+
+    // Watch for experiment changes as well.
+    Services.obs.addObserver(this, EXPERIMENTS_CHANGED_TOPIC, false);
+  },
+
+  /**
+   * Stop watching addons for changes.
+   */
+  _stopWatchingAddons: function () {
+    if (this._addonsListener) {
+      AddonManager.removeAddonListener(this._addonsListener);
+      Services.obs.removeObserver(this, EXPERIMENTS_CHANGED_TOPIC);
+    }
+    this._addonsListener = null;
+  },
+
+  /**
+   * Triggered when an addon changes its state.
+   * @param aAddon The addon which triggered the change.
+   */
+  _onActiveAddonsChanged: function (aAddon) {
+    const INTERESTING_ADDONS = [ "extension", "plugin", "service", "theme" ];
+
+    this._log.trace("_onActiveAddonsChanged - id " + aAddon.id + ", type " + aAddon.type);
+
+    if (INTERESTING_ADDONS.find(addon => addon == aAddon.type)) {
+      this._onEnvironmentChange("addons-changed");
+    }
+  },
+
+  /**
+   * Handle experiment change notifications.
+   */
+  observe: function (aSubject, aTopic, aData) {
+    this._log.trace("observe - Topic " + aTopic);
+
+    if (aTopic == EXPERIMENTS_CHANGED_TOPIC) {
+      this._onEnvironmentChange("experiment-changed");
+    }
+  },
+
+  /**
+   * Get the environment data in object form.
+   * @return Promise<Object> Resolved with the data on success, otherwise rejected.
+   */
+  getEnvironmentData: function() {
+    if (this._shutdown) {
+      this._log.error("getEnvironmentData - Already shut down");
+      return Promise.reject("Already shutdown");
+    }
+
+    this._log.trace("getEnvironmentData");
+    if (this._collectTask) {
+      return this._collectTask;
+    }
+
+    this._collectTask = this._doGetEnvironmentData();
+    let clear = () => this._collectTask = null;
+    this._collectTask.then(clear, clear);
+    return this._collectTask;
+  },
+
+  _doGetEnvironmentData: Task.async(function* () {
+    this._log.trace("getEnvironmentData");
+
+    // Define the data collection function for each section.
+    let sections = {
+      "build" : () => this._getBuild(),
+      "settings": () => this._getSettings(),
+#ifndef MOZ_WIDGET_ANDROID
+      "profile": () => this._getProfile(),
+#endif
+      "partner": () => this._getPartner(),
+      "system": () => this._getSystem(),
+      "addons": () => this._getAddons(),
+    };
+
+    let data = {};
+    // Recover from exceptions in the collection functions and populate the data
+    // object. We want to recover so that, in the worst-case, we only lose some data
+    // sections instead of all.
+    for (let s in sections) {
+      try {
+        data[s] = yield sections[s]();
+      } catch (e) {
+        this._log.error("_doGetEnvironmentData - There was an exception collecting " + s, e);
+      }
+    }
+
+    return data;
+  }),
+
+  _onEnvironmentChange: function (what) {
+    this._log.trace("_onEnvironmentChange for " + what);
+    if (this._shutdown) {
+      this._log.trace("_onEnvironmentChange - Already shut down.");
+      return;
+    }
+
+    for (let [name, listener] of this._changeListeners) {
+      try {
+        this._log.debug("_onEnvironmentChange - calling " + name);
+        listener();
+      } catch (e) {
+        this._log.warning("_onEnvironmentChange - listener " + name + " caught error", e);
+      }
+    }
+  },
+
+  /**
+   * This blocks the AddonManager shutdown barrier, it caches addons we might need later.
+   * It also lets an active collect task finish first as it may still access the AM.
+   */
+  _blockAddonManagerShutdown: Task.async(function*() {
+    this._log.trace("_blockAddonManagerShutdown");
+
+    this._stopWatchingAddons();
+
+    this._cachedAddons = {
+      activeAddons: yield this._getActiveAddons(),
+      activeTheme: yield this._getActiveTheme(),
+      activeGMPlugins: yield this._getActiveGMPlugins(),
+    };
+
+    yield this._collectTask;
+  }),
+
+  /**
+   * Get an object describing the current state of this module for AsyncShutdown diagnostics.
+   */
+  _getState: function() {
+    return {
+      shutdown: this._shutdown,
+      hasCollectTask: !!this._collectTask,
+      hasAddonsListener: !!this._addonsListener,
+      hasCachedAddons: !!this._cachedAddons,
+    };
+  },
+};
--- a/toolkit/components/telemetry/TelemetryFile.jsm
+++ b/toolkit/components/telemetry/TelemetryFile.jsm
@@ -239,36 +239,34 @@ this.TelemetryFile = {
     return pingsDiscarded;
   },
 
   /**
    * Iterate destructively through the pending pings.
    *
    * @return {iterator}
    */
-  popPendingPings: function*(reason) {
+  popPendingPings: function*() {
     while (pendingPings.length > 0) {
       let data = pendingPings.pop();
-      // Send persisted pings to the test URL too.
-      if (reason == "test-ping") {
-        data.reason = reason;
-      }
       yield data;
     }
   },
 
   testLoadHistograms: function(file) {
     pingsLoaded = 0;
     return this.loadHistograms(file.path);
   }
 };
 
 ///// Utility functions
 function pingFilePath(ping) {
-  return OS.Path.join(TelemetryFile.pingDirectoryPath, ping.slug);
+  // Support legacy ping formats, who don't have an "id" field, but a "slug" field.
+  let pingIdentifier = (ping.slug) ? ping.slug : ping.id;
+  return OS.Path.join(TelemetryFile.pingDirectoryPath, pingIdentifier);
 }
 
 function getPingDirectory() {
   return Task.spawn(function*() {
     let directory = TelemetryFile.pingDirectoryPath;
 
     if (!isPingDirectoryCreated) {
       yield OS.File.makeDir(directory, { unixMode: OS.Constants.S_IRWXU });
--- a/toolkit/components/telemetry/TelemetryPing.jsm
+++ b/toolkit/components/telemetry/TelemetryPing.jsm
@@ -25,32 +25,40 @@ 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_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
 
+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.
+const DEFAULT_RETENTION_DAYS = 14;
 
 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
                                    "@mozilla.org/base/telemetry;1",
                                    "nsITelemetry");
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
 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, "UpdateChannel",
+                                  "resource://gre/modules/UpdateChannel.jsm");
 
 /**
  * Setup Telemetry logging. This function also gets called when loggin related
  * preferences change.
  */
 let gLogger = null;
 let gLogAppenderDump = null;
 function configureLogging() {
@@ -81,16 +89,24 @@ function configureLogging() {
 }
 
 function generateUUID() {
   let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString();
   // strip {}
   return str.substring(1, str.length - 1);
 }
 
+/**
+ * Determine if the ping has new ping format or a legacy one.
+ */
+function isNewPingFormat(aPing) {
+  return ("id" in aPing) && ("application" in aPing) &&
+         ("version" in aPing) && (aPing.version >= 2);
+}
+
 this.EXPORTED_SYMBOLS = ["TelemetryPing"];
 
 this.TelemetryPing = Object.freeze({
   Constants: Object.freeze({
     PREF_ENABLED: PREF_ENABLED,
     PREF_LOG_LEVEL: PREF_LOG_LEVEL,
     PREF_LOG_DUMP: PREF_LOG_DUMP,
     PREF_SERVER: PREF_SERVER,
@@ -126,46 +142,230 @@ this.TelemetryPing = Object.freeze({
    * Sets a server to send pings to.
    */
   setServer: function(aServer) {
     return Impl.setServer(aServer);
   },
 
   /**
    * Send payloads to the server.
+   *
+   * @param {String} aType The type of the ping.
+   * @param {Object} aPayload The actual data payload for the ping.
+   * @param {Object} [aOptions] Options object.
+   * @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
+   *                 if sending fails.
+   * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+   *                  id, false otherwise.
+   * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+   *                  environment data.
+   * @returns {Promise} A promise that resolves when the ping is sent.
    */
-  send: function(aReason, aPingPayload) {
-    return Impl.send(aReason, aPingPayload);
+  send: function(aType, aPayload, aOptions = {}) {
+    let options = aOptions;
+    options.retentionDays = aOptions.retentionDays || DEFAULT_RETENTION_DAYS;
+    options.addClientId = aOptions.addClientId || false;
+    options.addEnvironment = aOptions.addEnvironment || false;
+
+    return Impl.send(aType, aPayload, options);
+  },
+
+  /**
+   * Add the ping to the pending ping list and save all pending pings.
+   *
+   * @param {String} aType The type of the ping.
+   * @param {Object} aPayload The actual data payload for the ping.
+   * @param {Object} [aOptions] Options object.
+   * @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
+   *                 if sending fails.
+   * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+   *                  id, false otherwise.
+   * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+   *                  environment data.
+   * @returns {Promise} A promise that resolves when the pings are saved.
+   */
+  savePendingPings: function(aType, aPayload, aOptions = {}) {
+    let options = aOptions;
+    options.retentionDays = aOptions.retentionDays || DEFAULT_RETENTION_DAYS;
+    options.addClientId = aOptions.addClientId || false;
+    options.addEnvironment = aOptions.addEnvironment || false;
+
+    return Impl.savePendingPings(aType, aPayload, options);
+  },
+
+  /**
+   * Save a ping to disk.
+   *
+   * @param {String} aType The type of the ping.
+   * @param {Object} aPayload The actual data payload for the ping.
+   * @param {Object} [aOptions] Options object.
+   * @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
+   *                 if sending fails.
+   * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+   *                  id, false otherwise.
+   * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+   *                  environment data.
+   * @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
+   *                  if found.
+   *
+   * @returns {Promise} A promise that resolves when the ping is saved to disk.
+   */
+  savePing: function(aType, aPayload, aOptions = {}) {
+    let options = aOptions;
+    options.retentionDays = aOptions.retentionDays || DEFAULT_RETENTION_DAYS;
+    options.addClientId = aOptions.addClientId || false;
+    options.addEnvironment = aOptions.addEnvironment || false;
+    options.overwrite = aOptions.overwrite || false;
+
+    return Impl.savePing(aType, aPayload, options);
+  },
+
+  /**
+   * Only used for testing. Saves a ping to disk and return the ping id once done.
+   *
+   * @param {String} aType The type of the ping.
+   * @param {Object} aPayload The actual data payload for the ping.
+   * @param {Object} [aOptions] Options object.
+   * @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
+   *                 if sending fails.
+   * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+   *                  id, false otherwise.
+   * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+   *                  environment data.
+   * @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
+   *                  if found.
+   * @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
+   *                 ping location if not provided.
+   *
+   * @returns {Promise<Integer>} A promise that resolves with the ping id when the ping is
+   *                             saved to disk.
+   */
+  testSavePingToFile: function(aType, aPayload, aOptions = {}) {
+    let options = aOptions;
+    options.retentionDays = aOptions.retentionDays || DEFAULT_RETENTION_DAYS;
+    options.addClientId = aOptions.addClientId || false;
+    options.addEnvironment = aOptions.addEnvironment || false;
+    options.overwrite = aOptions.overwrite || false;
+
+    return Impl.testSavePingToFile(aType, aPayload, options);
   },
 
   /**
    * The client id send with the telemetry ping.
    *
    * @return The client id as string, or null.
    */
    get clientID() {
     return Impl.clientID;
    },
+
+   /**
+    * The AsyncShutdown.Barrier to synchronize with TelemetryPing shutdown.
+    */
+   get shutdown() {
+    return Impl._shutdownBarrier.client;
+   },
 });
 
 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,
 
-  popPayloads: function popPayloads(reason, externalPayload) {
+  _shutdownBarrier: new AsyncShutdown.Barrier("TelemetryPing: Waiting for clients."),
+
+  /**
+   * Get the data for the "application" section of the ping.
+   */
+  _getApplicationSection: function() {
+    // Querying architecture and update channel can throw. Make sure to recover and null
+    // those fields.
+    let arch = null;
+    try {
+      arch = Services.sysinfo.get("arch");
+    } catch (e) {
+      this._log.trace("assemblePing - Unable to get system architecture.", e);
+    }
+
+    let updateChannel = null;
+    try {
+      updateChannel = UpdateChannel.get();
+    } catch (e) {
+      this._log.trace("assemblePing - Unable to get update channel.", e);
+    }
+
+    return {
+      architecture: arch,
+      buildId: Services.appinfo.appBuildID,
+      name: Services.appinfo.name,
+      version: Services.appinfo.version,
+      vendor: Services.appinfo.vendor,
+      platformVersion: Services.appinfo.platformVersion,
+      xpcomAbi: Services.appinfo.XPCOMABI,
+      channel: updateChannel,
+    };
+  },
+
+  /**
+   * Assemble a complete ping following the common ping format specification.
+   *
+   * @param {String} aType The type of the ping.
+   * @param {Object} aPayload The actual data payload for the ping.
+   * @param {Object} aOptions Options object.
+   * @param {Boolean} aOptions.addClientId true if the ping should contain the client
+   *                  id, false otherwise.
+   * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
+   *                  environment data.
+   *
+   * @returns Promise<Object> A promise that resolves when the ping is completely assembled.
+   */
+  assemblePing: function assemblePing(aType, aPayload, aOptions = {}) {
+    this._log.trace("assemblePing - Type " + aType + ", Server " + this._server +
+                    ", aOptions " + JSON.stringify(aOptions));
+
+    // Fill the common ping fields.
+    let pingData = {
+      type: aType,
+      id: generateUUID(),
+      creationDate: (new Date()).toISOString(),
+      version: PING_FORMAT_VERSION,
+      application: this._getApplicationSection(),
+      payload: aPayload,
+    };
+
+    if (aOptions.addClientId) {
+      pingData.clientId = this._clientID;
+    }
+
+    if (aOptions.addEnvironment) {
+      return TelemetryEnvironment.getEnvironmentData().then(environment => {
+        pingData.environment = environment;
+        return pingData;
+      },
+      error => {
+        this._log.error("assemblePing - Rejection", error);
+      });
+    }
+
+    return Promise.resolve(pingData);
+  },
+
+  popPayloads: function popPayloads() {
+    this._log.trace("popPayloads");
     function payloadIter() {
-      if (externalPayload && reason != "overdue-flush") {
-        yield externalPayload;
-      }
-      let iterator = TelemetryFile.popPendingPings(reason);
+      let iterator = TelemetryFile.popPendingPings();
       for (let data of iterator) {
         yield data;
       }
     }
 
     let payloadIterWithThis = payloadIter.bind(this);
     return { __iterator__: payloadIterWithThis };
   },
@@ -173,91 +373,235 @@ let Impl = {
   /**
    * Only used in tests.
    */
   setServer: function (aServer) {
     this._server = aServer;
   },
 
   /**
-   * Send data to the server. Record success/send-time in histograms
+   * Build a complete ping and send data to the server. Record success/send-time in
+   * histograms.
+   *
+   * @param {String} aType The type of the ping.
+   * @param {Object} aPayload The actual data payload for the ping.
+   * @param {Object} aOptions Options object.
+   * @param {Number} aOptions.retentionDays The number of days to keep the ping on disk
+   *                 if sending fails.
+   * @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
+   *                  false otherwise.
+   * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
+   *                  environment data.
+   *
+   * @returns {Promise} A promise that resolves when the ping is sent.
    */
-  send: function send(reason, aPayload) {
-    this._log.trace("send - Reason " + reason + ", Server " + this._server);
-    return this.sendPingsFromIterator(this._server, reason,
-                                      Iterator(this.popPayloads(reason, aPayload)));
+  send: function send(aType, aPayload, aOptions) {
+    this._log.trace("send - Type " + aType + ", Server " + this._server +
+                    ", aOptions " + JSON.stringify(aOptions));
+
+    return this.assemblePing(aType, aPayload, aOptions)
+        .then(pingData => {
+          // Once ping is assembled, send it along with the persisted ping in the backlog.
+          let p = [
+            // Persist the ping if sending it fails.
+            this.doPing(pingData, false)
+                .catch(() => TelemetryFile.savePing(pingData, true)),
+            this.sendPersistedPings(),
+          ];
+          return Promise.all(p);
+        },
+        error => this._log.error("send - Rejection", error));
   },
 
-  sendPingsFromIterator: function sendPingsFromIterator(server, reason, i) {
-    let p = [data for (data in i)].map((data) =>
-      this.doPing(server, data).then(null, () => TelemetryFile.savePing(data, true)));
-
+  /**
+   * Send the persisted pings to the server.
+   */
+  sendPersistedPings: function sendPersistedPings() {
+    this._log.trace("sendPersistedPings");
+    let pingsIterator = Iterator(this.popPayloads());
+    let p = [data for (data in pingsIterator)].map(data => this.doPing(data, true));
     return Promise.all(p);
   },
 
-  finishPingRequest: function finishPingRequest(success, startTime, ping) {
+  /**
+   * Saves all the pending pings, plus the passed one, to disk.
+   *
+   * @param {String} aType The type of the ping.
+   * @param {Object} aPayload The actual data payload for the ping.
+   * @param {Object} aOptions Options object.
+   * @param {Number} aOptions.retentionDays The number of days to keep the ping on disk
+   *                 if sending fails.
+   * @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
+   *                  false otherwise.
+   * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
+   *                  environment data.
+   *
+   * @returns {Promise} A promise that resolves when all the pings are saved to disk.
+   */
+  savePendingPings: function savePendingPings(aType, aPayload, aOptions) {
+    this._log.trace("savePendingPings - Type " + aType + ", Server " + this._server +
+                    ", aOptions " + JSON.stringify(aOptions));
+
+    return this.assemblePing(aType, aPayload, aOptions)
+        .then(pingData => TelemetryFile.savePendingPings(pingData),
+              error => this._log.error("savePendingPings - Rejection", error));
+  },
+
+  /**
+   * Save a ping to disk.
+   *
+   * @param {String} aType The type of the ping.
+   * @param {Object} aPayload The actual data payload for the ping.
+   * @param {Object} aOptions Options object.
+   * @param {Number} aOptions.retentionDays The number of days to keep the ping on disk
+   *                 if sending fails.
+   * @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
+   *                  false otherwise.
+   * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
+   *                  environment data.
+   * @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
+   *
+   * @returns {Promise} A promise that resolves when the ping is saved to disk.
+   */
+  savePing: function savePing(aType, aPayload, aOptions) {
+    this._log.trace("savePing - Type " + aType + ", Server " + this._server +
+                    ", aOptions " + JSON.stringify(aOptions));
+
+    return this.assemblePing(aType, aPayload, aOptions)
+        .then(pingData => TelemetryFile.savePing(pingData, aOptions.overwrite),
+              error => this._log.error("savePing - Rejection", error));
+  },
+
+  /**
+   * Save a ping to disk and return the ping id when done.
+   *
+   * @param {String} aType The type of the ping.
+   * @param {Object} aPayload The actual data payload for the ping.
+   * @param {Object} aOptions Options object.
+   * @param {Number} aOptions.retentionDays The number of days to keep the ping on disk
+   *                 if sending fails.
+   * @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
+   *                  false otherwise.
+   * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
+   *                  environment data.
+   * @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
+   * @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
+   *                 ping location if not provided.
+   *
+   * @returns {Promise} A promise that resolves with the ping id when the ping is saved to
+   *                    disk.
+   */
+  testSavePingToFile: function testSavePingToFile(aType, aPayload, aOptions) {
+    this._log.trace("testSavePingToFile - Type " + aType + ", Server " + this._server +
+                    ", aOptions " + JSON.stringify(aOptions));
+    return this.assemblePing(aType, aPayload, aOptions)
+        .then(pingData => {
+            if (aOptions.filePath) {
+              return TelemetryFile.savePingToFile(pingData, aOptions.filePath, aOptions.overwrite)
+                                  .then(() => { return pingData.id; });
+            } else {
+              return TelemetryFile.savePing(pingData, aOptions.overwrite)
+                                  .then(() => { return pingData.id; });
+            }
+        }, error => this._log.error("testSavePing - Rejection", error));
+  },
+
+  finishPingRequest: function finishPingRequest(success, startTime, ping, isPersisted) {
+    this._log.trace("finishPingRequest - Success " + success + ", Persisted " + isPersisted);
+
     let hping = Telemetry.getHistogramById("TELEMETRY_PING");
     let hsuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
 
     hsuccess.add(success);
     hping.add(new Date() - startTime);
 
-    if (success) {
+    if (success && isPersisted) {
       return TelemetryFile.cleanupPingFile(ping);
     } else {
       return Promise.resolve();
     }
   },
 
   submissionPath: function submissionPath(ping) {
-    let slug;
-    if (!ping) {
-      slug = this._uuid;
+    // The new ping format contains an "application" section, the old one doesn't.
+    let pathComponents;
+    if (isNewPingFormat(ping)) {
+      // We insert the Ping id in the URL to simplify server handling of duplicated
+      // pings.
+      let app = ping.application;
+      pathComponents = [
+        ping.id, ping.type, app.name, app.version, app.channel, app.buildId
+      ];
     } else {
-      let info = ping.payload.info;
-      let pathComponents = [ping.slug, info.reason, info.appName,
-                            info.appVersion, info.appUpdateChannel,
-                            info.appBuildID];
-      slug = pathComponents.join("/");
+      // This is a ping in the old format.
+      if (!("slug" in ping)) {
+        // That's odd, we don't have a slug. Generate one so that TelemetryFile.jsm works.
+        ping.slug = generateUUID();
+      }
+
+      // Do we have enough info to build a submission URL?
+      let payload = ("payload" in ping) ? ping.payload : null;
+      if (payload && ("info" in payload)) {
+        let info = ping.payload.info;
+        pathComponents = [ ping.slug, info.reason, info.appName, info.appVersion,
+                           info.appUpdateChannel, info.appBuildID ];
+      } else {
+        // Only use the UUID as the slug.
+        pathComponents = [ ping.slug ];
+      }
     }
+
+    let slug = pathComponents.join("/");
     return "/submit/telemetry/" + slug;
   },
 
-  doPing: function doPing(server, ping) {
-    this._log.trace("doPing - Server " + server);
+  doPing: function doPing(ping, isPersisted) {
+    this._log.trace("doPing - Server " + this._server + ", Persisted " + isPersisted);
     let deferred = Promise.defer();
-    let url = server + this.submissionPath(ping);
+    let isNewPing = isNewPingFormat(ping);
+    let version = isNewPing ? PING_FORMAT_VERSION : 1;
+    let url = this._server + this.submissionPath(ping) + "?v=" + version;
     let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
                   .createInstance(Ci.nsIXMLHttpRequest);
     request.mozBackgroundRequest = true;
     request.open("POST", url, true);
     request.overrideMimeType("text/plain");
     request.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
 
     let startTime = new Date();
 
     function handler(success) {
+      let handleCompletion = event => {
+        if (success) {
+          deferred.resolve();
+        } else {
+          deferred.reject(event);
+        }
+      };
+
       return function(event) {
-        this.finishPingRequest(success, startTime, ping).then(() => {
-          if (success) {
-            deferred.resolve();
-          } else {
-            deferred.reject(event);
-          }
-        });
+        this.finishPingRequest(success, startTime, ping, isPersisted)
+          .then(() => handleCompletion(event),
+                error => {
+                  this._log.error("doPing - Request Success " + success + ", Error " +
+                                  error);
+                  handleCompletion(event);
+                });
       };
     }
     request.addEventListener("error", handler(false).bind(this), false);
     request.addEventListener("load", handler(true).bind(this), false);
 
+    // If that's a legacy ping format, just send its payload.
+    let networkPayload = isNewPing ? ping : ping.payload;
     request.setRequestHeader("Content-Encoding", "gzip");
     let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                     .createInstance(Ci.nsIScriptableUnicodeConverter);
     converter.charset = "UTF-8";
-    let utf8Payload = converter.ConvertFromUnicode(JSON.stringify(ping.payload));
+    let utf8Payload = converter.ConvertFromUnicode(JSON.stringify(networkPayload));
     utf8Payload += converter.Finish();
     let payloadStream = Cc["@mozilla.org/io/string-input-stream;1"]
                         .createInstance(Ci.nsIStringInputStream);
     payloadStream.data = this.gzipCompressString(utf8Payload);
     request.send(payloadStream);
     return deferred.promise;
   },
 
@@ -311,24 +655,42 @@ let Impl = {
       return false;
     }
 
     return true;
   },
 
   /**
    * Initializes telemetry within a timer. If there is no PREF_SERVER set, don't turn on telemetry.
+   *
+   * This delayed initialization means TelemetryPing init can be in the following states:
+   * 1) setupTelemetry was never called
+   * or it was called and
+   *   2) _delayedInitTask was scheduled, but didn't run yet.
+   *   3) _delayedInitTask is currently running.
+   *   4) _delayedInitTask finished running and is nulled out.
    */
   setupTelemetry: function setupTelemetry(testing) {
+    this._initStarted = true;
     if (testing && !this._log) {
       this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
     }
 
     this._log.trace("setupTelemetry");
 
+    if (this._delayedInitTask) {
+      this._log.error("setupTelemetry - init task already running");
+      return this._delayedInitTaskDeferred.promise;
+    }
+
+    if (this._initialized && !testing) {
+      this._log.error("setupTelemetry - already initialized");
+      return Promise.resolve();
+    }
+
     // 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();
     }
@@ -337,51 +699,101 @@ let Impl = {
     // id from disk.
     // We try to cache it in prefs to avoid this, even though this may
     // lead to some stale client ids.
     this._clientID = Preferences.get(PREF_CACHED_CLIENTID, null);
 
     // Delay full telemetry initialization to give the browser time to
     // run various late initializers. Otherwise our gathered memory
     // footprint and other numbers would be too optimistic.
-    let deferred = Promise.defer();
-    let delayedTask = new DeferredTask(function* () {
-      this._initialized = true;
+    this._delayedInitTaskDeferred = Promise.defer();
+    this._delayedInitTask = new DeferredTask(function* () {
+      try {
+        this._initialized = true;
+
+        yield TelemetryEnvironment.init();
 
-      yield TelemetryFile.loadSavedPings();
-      // If we have any TelemetryPings lying around, we'll be aggressive
-      // and try to send them all off ASAP.
-      if (TelemetryFile.pingsOverdue > 0) {
-        this._log.trace("setupChromeProcess - Sending " + TelemetryFile.pingsOverdue +
-                        " overdue pings now.");
-        // It doesn't really matter what we pass to this.send as a reason,
-        // since it's never sent to the server. All that this.send does with
-        // the reason is check to make sure it's not a test-ping.
-        yield this.send("overdue-flush");
-      }
+        yield TelemetryFile.loadSavedPings();
+        // If we have any TelemetryPings lying around, we'll be aggressive
+        // and try to send them all off ASAP.
+        if (TelemetryFile.pingsOverdue > 0) {
+          this._log.trace("setupChromeProcess - Sending " + TelemetryFile.pingsOverdue +
+                          " overdue pings now.");
+          // It doesn't really matter what we pass to this.send as a reason,
+          // since it's never sent to the server. All that this.send does with
+          // the reason is check to make sure it's not a test-ping.
+          yield this.sendPersistedPings();
+        }
 
-      if ("@mozilla.org/datareporting/service;1" in Cc) {
-        let drs = Cc["@mozilla.org/datareporting/service;1"]
-                    .getService(Ci.nsISupports)
-                    .wrappedJSObject;
-        this._clientID = yield drs.getClientID();
-        // Update cached client id.
-        Preferences.set(PREF_CACHED_CLIENTID, this._clientID);
-      } else {
-        // Nuke potentially cached client id.
-        Preferences.reset(PREF_CACHED_CLIENTID);
+        if ("@mozilla.org/datareporting/service;1" in Cc) {
+          let drs = Cc["@mozilla.org/datareporting/service;1"]
+                      .getService(Ci.nsISupports)
+                      .wrappedJSObject;
+          this._clientID = yield drs.getClientID();
+          // Update cached client id.
+          Preferences.set(PREF_CACHED_CLIENTID, this._clientID);
+        } else {
+          // Nuke potentially cached client id.
+          Preferences.reset(PREF_CACHED_CLIENTID);
+        }
+
+        Telemetry.asyncFetchTelemetryData(function () {});
+        this._delayedInitTaskDeferred.resolve();
+      } catch (e) {
+        this._delayedInitTaskDeferred.reject(e);
+      } finally {
+        this._delayedInitTask = null;
+        this._delayedInitTaskDeferred = null;
       }
-
-      Telemetry.asyncFetchTelemetryData(function () {});
-      deferred.resolve();
-
     }.bind(this), testing ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY);
 
-    delayedTask.arm();
-    return deferred.promise;
+    AsyncShutdown.sendTelemetry.addBlocker("TelemetryPing: shutting down",
+                                           () => this.shutdown(),
+                                           () => this._getState());
+
+    this._delayedInitTask.arm();
+    return this._delayedInitTaskDeferred.promise;
+  },
+
+  shutdown: function() {
+    this._log.trace("shutdown");
+
+    let cleanup = () => {
+      if (!this._initialized) {
+        return;
+      }
+      let reset = () => {
+        this._initialized = false;
+        this._initStarted = false;
+      };
+      return this._shutdownBarrier.wait().then(
+               () => TelemetryEnvironment.shutdown().then(reset, reset));
+    };
+
+    // We can be in one the following states here:
+    // 1) setupTelemetry was never called
+    // or it was called and
+    //   2) _delayedInitTask was scheduled, but didn't run yet.
+    //   3) _delayedInitTask is running now.
+    //   4) _delayedInitTask finished running already.
+
+    // This handles 1).
+    if (!this._initStarted) {
+      return Promise.resolve();
+    }
+
+    // This handles 4).
+    if (!this._delayedInitTask) {
+      // We already ran the delayed initialization.
+      return cleanup();
+    }
+
+    // This handles 2) and 3).
+    this._delayedInitTask.disarm();
+    return this._delayedInitTask.finalize().then(cleanup);
   },
 
   /**
    * This observer drives telemetry.
    */
   observe: function (aSubject, aTopic, aData) {
     // The logger might still be not available at this point.
     if (!this._log) {
@@ -407,9 +819,20 @@ let Impl = {
       Preferences.ignore(PREF_BRANCH_LOG, configureLogging);
       break;
     }
   },
 
   get clientID() {
     return this._clientID;
   },
+
+  /**
+   * Get an object describing the current state of this module for AsyncShutdown diagnostics.
+   */
+  _getState: function() {
+    return {
+      initialized: this._initialized,
+      initStarted: this._initStarted,
+      haveDelayedInitTask: !!this._delayedInitTask,
+    };
+  },
 };
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -7,31 +7,51 @@
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/debug.js", this);
 Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Services.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/DeferredTask.jsm", this);
 Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
 
 const IS_CONTENT_PROCESS = (function() {
   // We cannot use Services.appinfo here because in telemetry xpcshell tests,
   // appinfo is initially unavailable, and becomes available only later on.
   let runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
   return runtime.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
 })();
 
 // When modifying the payload in incompatible ways, please bump this version number
-const PAYLOAD_VERSION = 1;
+const PAYLOAD_VERSION = 4;
+const PING_TYPE_MAIN = "main";
+const PING_TYPE_SAVED_SESSION = "saved-session";
+const RETENTION_DAYS = 14;
+
+const REASON_DAILY = "daily";
+const REASON_SAVED_SESSION = "saved-session";
+const REASON_IDLE_DAILY = "idle-daily";
+const REASON_GATHER_PAYLOAD = "gather-payload";
+const REASON_TEST_PING = "test-ping";
+const REASON_ENVIRONMENT_CHANGE = "environment-change";
+
+const ENVIRONMENT_CHANGE_LISTENER = "TelemetrySession::onEnvironmentChange";
+
+const SEC_IN_ONE_DAY  = 24 * 60 * 60;
+const MS_IN_ONE_DAY   = SEC_IN_ONE_DAY * 1000;
+
+const MIN_SUBSESSION_LENGTH_MS = 10 * 60 * 1000;
 
 // This is the HG changeset of the Histogram.json file, used to associate
 // submitted ping data with its histogram definition (bug 832007)
 #expand const HISTOGRAMS_FILE_VERSION = "__HISTOGRAMS_FILE_VERSION__";
 
 const LOGGER_NAME = "Toolkit.Telemetry";
 const LOGGER_PREFIX = "TelemetrySession::";
 
@@ -40,16 +60,18 @@ const PREF_SERVER = PREF_BRANCH + "serve
 const PREF_ENABLED = PREF_BRANCH + "enabled";
 const PREF_PREVIOUS_BUILDID = PREF_BRANCH + "previousBuildID";
 const PREF_CACHED_CLIENTID = PREF_BRANCH + "cachedClientID"
 const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
 const PREF_ASYNC_PLUGIN_INIT = "dom.ipc.plugins.asyncInit";
 
 const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload";
 
+const SESSION_STATE_FILE_NAME = "session-state.json";
+
 // Do not gather data more than once a minute
 const TELEMETRY_INTERVAL = 60000;
 // Delay before intializing telemetry (ms)
 const TELEMETRY_DELAY = 60000;
 // Delay before initializing telemetry if we're testing (ms)
 const TELEMETRY_TEST_DELAY = 100;
 
 // Seconds of idle time before pinging.
@@ -79,40 +101,104 @@ XPCOMUtils.defineLazyServiceGetter(this,
 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
                                    "@mozilla.org/parentprocessmessagemanager;1",
                                    "nsIMessageListenerManager");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
-#ifndef MOZ_WIDGET_GONK
-XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
-                                  "resource://gre/modules/LightweightThemeManager.jsm");
-#endif
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryPing",
                                   "resource://gre/modules/TelemetryPing.jsm");
 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, "UITelemetry",
                                   "resource://gre/modules/UITelemetry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
                                   "resource://gre/modules/UpdateChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment",
+                                  "resource://gre/modules/TelemetryEnvironment.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
+                                  "resource://services-common/utils.js");
 
 function generateUUID() {
   let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString();
   // strip {}
   return str.substring(1, str.length - 1);
 }
 
 /**
+ * This is a policy object used to override behavior for testing.
+ */
+let Policy = {
+  now: () => new Date(),
+  generateSessionUUID: () => generateUUID(),
+  generateSubsessionUUID: () => generateUUID(),
+  setDailyTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
+  clearDailyTimeout: (id) => clearTimeout(id),
+};
+
+/**
+ * Takes a date and returns it trunctated to a date with daily precision.
+ */
+function truncateToDays(date) {
+  return new Date(date.getFullYear(),
+                  date.getMonth(),
+                  date.getDate(),
+                  0, 0, 0, 0);
+}
+
+/**
+ * Get the ping type based on the payload.
+ * @param {Object} aPayload The ping payload.
+ * @return {String} A string representing the ping type.
+ */
+function getPingType(aPayload) {
+  // To remain consistent with server-side ping handling, set "saved-session" as the ping
+  // type for "saved-session" payload reasons.
+  if (aPayload.info.reason == REASON_SAVED_SESSION) {
+    return PING_TYPE_SAVED_SESSION;
+  }
+
+  return PING_TYPE_MAIN;
+}
+
+/**
+ * Date.toISOString() gives us UTC times, this gives us local times in the ISO date format.
+ * http://www.w3.org/TR/NOTE-datetime
+ */
+function toLocalTimeISOString(date) {
+  function padNumber(number, places) {
+    number = number.toString();
+    while (number.length < places) {
+      number = "0" + number;
+    }
+    return number;
+  }
+
+  let sign = (n) => n >= 0 ? "+" : "-";
+  let tzOffset = date.getTimezoneOffset();
+
+  // YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)
+  return    padNumber(date.getFullYear(), 4)
+    + "-" + padNumber(date.getMonth() + 1, 2)
+    + "-" + padNumber(date.getDate(), 2)
+    + "T" + padNumber(date.getHours(), 2)
+    + ":" + padNumber(date.getMinutes(), 2)
+    + ":" + padNumber(date.getSeconds(), 2);
+    + "." + date.getMilliseconds()
+    + sign(tzOffset) + Math.abs(Math.floor(tzOffset / 60))
+    + ":" + Math.abs(tzOffset % 60);
+}
+
+/**
  * Read current process I/O counters.
  */
 let processInfo = {
   _initialized: false,
   _IO_COUNTERS: null,
   _kernel32: null,
   _GetProcessIoCounters: null,
   _GetCurrentProcess: null,
@@ -149,36 +235,127 @@ let processInfo = {
     }
     let io = new this._IO_COUNTERS();
     if(!this._GetProcessIoCounters(this._GetCurrentProcess(), io.address()))
       return null;
     return [parseInt(io.readBytes), parseInt(io.writeBytes)];
   }
 };
 
+/**
+ * This object allows the serialisation of asynchronous tasks. This is particularly
+ * useful to serialise write access to the disk in order to prevent race conditions
+ * to corrupt the data being written.
+ * We are using this to synchronize saving to the file that TelemetrySession persists
+ * its state in.
+ */
+let gStateSaveSerializer = {
+  _queuedOperations: [],
+  _queuedInProgress: false,
+  _log: Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX),
+
+  /**
+   * Enqueues an operation to a list to serialise their execution in order to prevent race
+   * conditions. Useful to serialise access to disk.
+   *
+   * @param {Function} aFunction The task function to enqueue. It must return a promise.
+   * @return {Promise} A promise resolved when the enqueued task completes.
+   */
+  enqueueTask: function (aFunction) {
+    let promise = new Promise((resolve, reject) =>
+      this._queuedOperations.push([aFunction, resolve, reject]));
+
+    if (this._queuedOperations.length == 1) {
+      this._popAndPerformQueuedOperation();
+    }
+    return promise;
+  },
+
+  /**
+   * Make sure to flush all the pending operations.
+   * @return {Promise} A promise resolved when all the pending operations have completed.
+   */
+  flushTasks: function () {
+    let dummyTask = () => new Promise(resolve => resolve());
+    return this.enqueueTask(dummyTask);
+  },
+
+  /**
+   * Pop a task from the queue, executes it and continue to the next one.
+   * This function recursively pops all the tasks.
+   */
+  _popAndPerformQueuedOperation: function () {
+    if (!this._queuedOperations.length || this._queuedInProgress) {
+      return;
+    }
+
+    this._log.trace("_popAndPerformQueuedOperation - Performing queued operation.");
+    let [func, resolve, reject] = this._queuedOperations.shift();
+    let promise;
+
+    try {
+      this._queuedInProgress = true;
+      promise = func();
+    } catch (ex) {
+      this._log.warn("_popAndPerformQueuedOperation - Queued operation threw during execution. ",
+                     ex);
+      this._queuedInProgress = false;
+      reject(ex);
+      this._popAndPerformQueuedOperation();
+      return;
+    }
+
+    if (!promise || typeof(promise.then) != "function") {
+      let msg = "Queued operation did not return a promise: " + func;
+      this._log.warn("_popAndPerformQueuedOperation - " + msg);
+
+      this._queuedInProgress = false;
+      reject(new Error(msg));
+      this._popAndPerformQueuedOperation();
+      return;
+    }
+
+    promise.then(result => {
+        this._log.trace("_popAndPerformQueuedOperation - Queued operation completed.");
+        this._queuedInProgress = false;
+        resolve(result);
+        this._popAndPerformQueuedOperation();
+      },
+      error => {
+        this._log.warn("_popAndPerformQueuedOperation - Failure when performing queued operation.",
+                       error);
+        this._queuedInProgress = false;
+        reject(error);
+        this._popAndPerformQueuedOperation();
+      });
+  },
+};
+
 this.EXPORTED_SYMBOLS = ["TelemetrySession"];
 
 this.TelemetrySession = Object.freeze({
   Constants: Object.freeze({
     PREF_ENABLED: PREF_ENABLED,
     PREF_SERVER: PREF_SERVER,
     PREF_PREVIOUS_BUILDID: PREF_PREVIOUS_BUILDID,
   }),
   /**
    * Send a ping to a test server. Used only for testing.
    */
   testPing: function() {
     return Impl.testPing();
   },
   /**
    * Returns the current telemetry payload.
+   * @param reason Optional, the reason to trigger the payload.
+   * @param clearSubsession Optional, whether to clear subsession specific data.
    * @returns Object
    */
-  getPayload: function() {
-    return Impl.getPayload();
+  getPayload: function(reason, clearSubsession = false) {
+    return Impl.getPayload(reason, clearSubsession);
   },
   /**
    * Save histograms to a file.
    * Used only for testing purposes.
    *
    * @param {nsIFile} aFile The file to load from.
    */
   testSaveHistograms: function(aFile) {
@@ -194,47 +371,45 @@ this.TelemetrySession = Object.freeze({
    * Inform the ping which AddOns are installed.
    *
    * @param aAddOns - The AddOns.
    */
   setAddOns: function(aAddOns) {
     return Impl.setAddOns(aAddOns);
   },
   /**
-   * Load histograms from a file.
-   * Used only for testing purposes.
-   *
-   * @param aFile - File to load from.
-   */
-  testLoadHistograms: function(aFile) {
-    return Impl.testLoadHistograms(aFile);
-  },
-  /**
    * Descriptive metadata
    *
    * @param  reason
    *         The reason for the telemetry ping, this will be included in the
    *         returned metadata,
    * @return The metadata as a JS object
    */
   getMetadata: function(reason) {
     return Impl.getMetadata(reason);
   },
   /**
    * Used only for testing purposes.
    */
   reset: function() {
+    Impl._sessionId = null;
+    Impl._subsessionId = null;
+    Impl._previousSubsessionId = null;
+    Impl._subsessionCounter = 0;
+    Impl._profileSubsessionCounter = 0;
     this.uninstall();
     return this.setup();
   },
   /**
    * Used only for testing purposes.
+   * @param {Boolean} [aForceSavePending=true] If true, always saves the ping whether Telemetry
+   *        can send pings or not, which is used for testing.
    */
-  shutdown: function() {
-    return Impl.shutdown(true);
+  shutdown: function(aForceSavePending = true) {
+    return Impl.shutdownChromeProcess(aForceSavePending);
   },
   /**
    * Used only for testing purposes.
    */
   setup: function() {
     return Impl.setupChromeProcess(true);
   },
   /**
@@ -263,35 +438,51 @@ this.TelemetrySession = Object.freeze({
    },
 });
 
 let Impl = {
   _histograms: {},
   _initialized: false,
   _log: null,
   _prevValues: {},
-  // Generate a unique id once per session so the server can cope with
-  // duplicate submissions.
-  _uuid: generateUUID(),
   // Regex that matches histograms we care about during startup.
   // Keep this in sync with gen-histogram-bucket-ranges.py.
   _startupHistogramRegex: /SQLITE|HTTP|SPDY|CACHE|DNS/,
   _slowSQLStartup: {},
-  _prevSession: null,
   _hasWindowRestoredObserver: false,
   _hasXulWindowVisibleObserver: false,
   _startupIO : {},
   // 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,
+  // Null if this is the first run, or the previous build ID is unknown.
+  _previousBuildId: null,
   // Telemetry payloads sent by child processes.
   // Each element is in the format {source: <weak-ref>, payload: <object>},
   // where source is a weak reference to the child process,
   // and payload is the telemetry payload from that child process.
   _childTelemetry: [],
+  // Unique id that identifies this session so the server can cope with duplicate
+  // submissions, orphaning and other oddities. The id is shared across subsessions.
+  _sessionId: null,
+  // Random subsession id.
+  _subsessionId: null,
+  // Subsession id of the previous subsession (even if it was in a different session),
+  // null on first run.
+  _previousSubsessionId: null,
+  // The running no. of subsessions since the start of the browser session
+  _subsessionCounter: 0,
+  // The running no. of all subsessions for the whole profile life time
+  _profileSubsessionCounter: 0,
+  // Date of the last session split
+  _subsessionStartDate: null,
+  // The timer used for daily collections.
+  _dailyTimerId: null,
+  // A task performing delayed initialization of the chrome process
+  _delayedInitTask: null,
+  // The deferred promise resolved when the initialization task completes.
+  _delayedInitTaskDeferred: null,
 
   /**
    * Gets a series of simple measurements (counters). At the moment, this
    * only returns startup data from nsIAppStartup.getStartupInfo().
    *
    * @return simple measurements as a dictionary.
    */
   getSimpleMeasurements: function getSimpleMeasurements(forSavedSession) {
@@ -453,21 +644,23 @@ let Impl = {
     }
 
     // add an upper bound
     if (last && last < c.length)
       retgram.values[r[last]] = 0;
     return retgram;
   },
 
-  getHistograms: function getHistograms(hls) {
-    this._log.trace("getHistograms");
+  getHistograms: function getHistograms(subsession, clearSubsession) {
+    this._log.trace("getHistograms - subsession: " + subsession + ", clearSubsession: " + clearSubsession);
 
     let registered =
       Telemetry.registeredHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, []);
+    let hls = subsession ? Telemetry.snapshotSubsessionHistograms(clearSubsession)
+                         : Telemetry.histogramSnapshots;
     let ret = {};
 
     for (let name of registered) {
       for (let n of [name, "STARTUP_" + name]) {
         if (n in hls) {
           ret[n] = this.packHistogram(hls[n]);
         }
       }
@@ -490,27 +683,33 @@ let Impl = {
       }
       if (Object.keys(packedHistograms).length != 0)
         ret[addonName] = packedHistograms;
     }
 
     return ret;
   },
 
-  getKeyedHistograms: function() {
-    this._log.trace("getKeyedHistograms");
+  getKeyedHistograms: function(subsession, clearSubsession) {
+    this._log.trace("getKeyedHistograms - subsession: " + subsession + ", clearSubsession: " + clearSubsession);
 
     let registered =
       Telemetry.registeredKeyedHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, []);
     let ret = {};
 
     for (let id of registered) {
       ret[id] = {};
       let keyed = Telemetry.getKeyedHistogramById(id);
-      let snapshot = keyed.snapshot();
+      let snapshot = null;
+      if (subsession) {
+        snapshot = clearSubsession ? keyed.snapshotSubsessionAndClear()
+                                   : keyed.subsessionSnapshot();
+      } else {
+        snapshot = keyed.snapshot();
+      }
       for (let key of Object.keys(snapshot)) {
         ret[id][key] = this.packHistogram(snapshot[key]);
       }
     }
 
     return ret;
   },
 
@@ -532,121 +731,53 @@ let Impl = {
    * @param  reason
    *         The reason for the telemetry ping, this will be included in the
    *         returned metadata,
    * @return The metadata as a JS object
    */
   getMetadata: function getMetadata(reason) {
     this._log.trace("getMetadata - Reason " + reason);
 
-    let ai = Services.appinfo;
+    let sessionStartDate = toLocalTimeISOString(truncateToDays(this._sessionStartDate));
+    let subsessionStartDate = toLocalTimeISOString(truncateToDays(this._subsessionStartDate));
+    // Compute the subsession length in milliseconds, then convert to seconds.
+    let subsessionLength =
+      Math.floor((Policy.now() - this._subsessionStartDate.getTime()) / 1000);
+
     let ret = {
       reason: reason,
-      OS: ai.OS,
-      appID: ai.ID,
-      appVersion: ai.version,
-      appName: ai.name,
-      appBuildID: ai.appBuildID,
-      appUpdateChannel: UpdateChannel.get(),
-      platformBuildID: ai.platformBuildID,
       revision: HISTOGRAMS_FILE_VERSION,
-      locale: getLocale(),
-      asyncPluginInit: Preferences.get(PREF_ASYNC_PLUGIN_INIT, false)
+      asyncPluginInit: Preferences.get(PREF_ASYNC_PLUGIN_INIT, false),
+
+      // Date.getTimezoneOffset() unintuitively returns negative values if we are ahead of
+      // UTC and vice versa (e.g. -60 for UTC+1). We invert the sign here.
+      timezoneOffset: -this._subsessionStartDate.getTimezoneOffset(),
+      previousBuildId: this._previousBuildId,
+
+      sessionId: this._sessionId,
+      subsessionId: this._subsessionId,
+      previousSubsessionId: this._previousSubsessionId,
+
+      subsessionCounter: this._subsessionCounter,
+      profileSubsessionCounter: this._profileSubsessionCounter,
+
+      sessionStartDate: sessionStartDate,
+      subsessionStartDate: subsessionStartDate,
+      subsessionLength: subsessionLength,
     };
 
-    // In order to share profile data, the appName used for Metro Firefox is "Firefox",
-    // (the same as desktop Firefox). We set it to "MetroFirefox" here in order to
-    // differentiate telemetry pings sent by desktop vs. metro Firefox.
-    if(Services.metro && Services.metro.immersive) {
-      ret.appName = "MetroFirefox";
-    }
-
-    if (this._previousBuildID) {
-      ret.previousBuildID = this._previousBuildID;
-    }
-
-    // sysinfo fields are not always available, get what we can.
-    let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
-    let fields = ["cpucount", "memsize", "arch", "version", "kernel_version",
-                  "device", "manufacturer", "hardware", "tablet",
-                  "hasMMX", "hasSSE", "hasSSE2", "hasSSE3",
-                  "hasSSSE3", "hasSSE4A", "hasSSE4_1", "hasSSE4_2",
-                  "hasEDSP", "hasARMv6", "hasARMv7", "hasNEON", "isWow64",
-                  "profileHDDModel", "profileHDDRevision", "binHDDModel",
-                  "binHDDRevision", "winHDDModel", "winHDDRevision"];
-    for each (let field in fields) {
-      let value;
-      try {
-        value = sysInfo.getProperty(field);
-      } catch (e) {
-        continue;
-      }
-      if (field == "memsize") {
-        // Send RAM size in megabytes. Rounding because sysinfo doesn't
-        // always provide RAM in multiples of 1024.
-        value = Math.round(value / 1024 / 1024);
-      }
-      ret[field] = value;
-    }
-
-    // gfxInfo fields are not always available, get what we can.
-    let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
-    let gfxfields = ["adapterDescription", "adapterVendorID", "adapterDeviceID",
-                     "adapterSubsysID", "adapterRAM", "adapterDriver",
-                     "adapterDriverVersion", "adapterDriverDate",
-                     "adapterDescription2", "adapterVendorID2",
-                     "adapterDeviceID2", "adapterSubsysID2", "adapterRAM2",
-                     "adapterDriver2", "adapterDriverVersion2",
-                     "adapterDriverDate2", "isGPU2Active", "D2DEnabled",
-                     "DWriteEnabled", "DWriteVersion"
-                    ];
-
-    if (gfxInfo) {
-      for each (let field in gfxfields) {
-        try {
-          let value = gfxInfo[field];
-          // bug 940806: We need to do a strict equality comparison here,
-          // otherwise a type conversion will occur and boolean false values
-          // will get filtered out
-          if (value !== "") {
-            ret[field] = value;
-          }
-        } catch (e) {
-          continue
-        }
-      }
-    }
-
-#ifndef MOZ_WIDGET_GONK
-    let theme = LightweightThemeManager.currentTheme;
-    if (theme) {
-      ret.persona = theme.id;
-    }
-#endif
-
+    // TODO: Remove this when bug 1124128 lands.
     if (this._addons)
       ret.addons = this._addons;
 
+    // TODO: Remove this when bug 1124128 lands.
     let flashVersion = this.getFlashVersion();
     if (flashVersion)
       ret.flashVersion = flashVersion;
 
-    try {
-      let scope = {};
-      Cu.import("resource:///modules/experiments/Experiments.jsm", scope);
-      let experiments = scope.Experiments.instance()
-      let activeExperiment = experiments.getActiveExperimentID();
-      if (activeExperiment) {
-        ret.activeExperiment = activeExperiment;
-	ret.activeExperimentBranch = experiments.getActiveExperimentBranch();
-      }
-    } catch(e) {
-      // If this is not Firefox, the import will fail.
-    }
-
     return ret;
   },
 
   /**
    * Pull values from about:memory into corresponding histograms
    */
   gatherMemory: function gatherMemory() {
     this._log.trace("gatherMemory");
@@ -787,25 +918,32 @@ let Impl = {
   },
 
   /**
    * Get the current session's payload using the provided
    * simpleMeasurements and info, which are typically obtained by a call
    * to |this.getSimpleMeasurements| and |this.getMetadata|,
    * respectively.
    */
-  assemblePayloadWithMeasurements: function assemblePayloadWithMeasurements(simpleMeasurements, info) {
-    this._log.trace("assemblePayloadWithMeasurements");
+  assemblePayloadWithMeasurements: function(simpleMeasurements, info, reason, clearSubsession) {
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+    const isSubsession = !this._isClassicReason(reason);
+#else
+    const isSubsession = false;
+    clearSubsession = false;
+#endif
+    this._log.trace("assemblePayloadWithMeasurements - reason: " + reason +
+                    ", submitting subsession data: " + isSubsession);
 
     // Payload common to chrome and content processes.
     let payloadObj = {
       ver: PAYLOAD_VERSION,
       simpleMeasurements: simpleMeasurements,
-      histograms: this.getHistograms(Telemetry.histogramSnapshots),
-      keyedHistograms: this.getKeyedHistograms(),
+      histograms: this.getHistograms(isSubsession, clearSubsession),
+      keyedHistograms: this.getKeyedHistograms(isSubsession, clearSubsession),
       chromeHangs: Telemetry.chromeHangs,
       threadHangStats: this.getThreadHangStats(Telemetry.threadHangStats),
       log: TelemetryLog.entries(),
     };
 
     if (IS_CONTENT_PROCESS) {
       return payloadObj;
     }
@@ -820,66 +958,71 @@ let Impl = {
     payloadObj.UIMeasurements = UITelemetry.getUIMeasurements();
 
     if (Object.keys(this._slowSQLStartup).length != 0 &&
         (Object.keys(this._slowSQLStartup.mainThread).length ||
          Object.keys(this._slowSQLStartup.otherThreads).length)) {
       payloadObj.slowSQLStartup = this._slowSQLStartup;
     }
 
-    let clientID = TelemetryPing.clientID;
-    if (clientID && Preferences.get(PREF_FHR_UPLOAD_ENABLED, false)) {
-      payloadObj.clientID = clientID;
-    }
-
     if (this._childTelemetry.length) {
       payloadObj.childPayloads = this.getChildPayloads();
     }
+
     return payloadObj;
   },
 
-  getSessionPayload: function getSessionPayload(reason) {
-    this._log.trace("getSessionPayload - Reason " + reason);
-    let measurements = this.getSimpleMeasurements(reason == "saved-session");
-    let info = !IS_CONTENT_PROCESS ? this.getMetadata(reason) : null;
-    return this.assemblePayloadWithMeasurements(measurements, info);
+  /**
+   * Start a new subsession.
+   */
+  startNewSubsession: function () {
+    this._subsessionStartDate = Policy.now();
+    this._previousSubsessionId = this._subsessionId;
+    this._subsessionId = Policy.generateSubsessionUUID();
+    this._subsessionCounter++;
+    this._profileSubsessionCounter++;
   },
 
-  assemblePing: function assemblePing(payloadObj, reason) {
-    let slug = this._uuid;
-    return { slug: slug, reason: reason, payload: payloadObj };
-  },
+  getSessionPayload: function getSessionPayload(reason, clearSubsession) {
+    this._log.trace("getSessionPayload - reason: " + reason + ", clearSubsession: " + clearSubsession);
+#if defined(MOZ_WIDGET_GONK) || defined(MOZ_WIDGET_ANDROID)
+    clearSubsession = false;
+#endif
+
+    let measurements = this.getSimpleMeasurements(reason == REASON_SAVED_SESSION);
+    let info = !IS_CONTENT_PROCESS ? this.getMetadata(reason) : null;
+    let payload = this.assemblePayloadWithMeasurements(measurements, info, reason, clearSubsession);
 
-  getSessionPayloadAndSlug: function getSessionPayloadAndSlug(reason) {
-    this._log.trace("getSessionPayloadAndSlug - Reason " + reason);
-    return this.assemblePing(this.getSessionPayload(reason), reason);
+    if (!IS_CONTENT_PROCESS && clearSubsession) {
+      this.startNewSubsession();
+      // Persist session data to disk (don't wait until it completes).
+      let sessionData = this._getSessionDataObject();
+      gStateSaveSerializer.enqueueTask(() => this._saveSessionData(sessionData));
+      this._rescheduleDailyTimer();
+    }
+
+    return payload;
   },
 
   /**
    * Send data to the server. Record success/send-time in histograms
    */
   send: function send(reason) {
     this._log.trace("send - Reason " + reason);
     // populate histograms one last time
     this.gatherMemory();
-    return TelemetryPing.send(reason, this.getSessionPayloadAndSlug(reason));
-  },
 
-  submissionPath: function submissionPath(ping) {
-    let slug;
-    if (!ping) {
-      slug = this._uuid;
-    } else {
-      let info = ping.payload.info;
-      let pathComponents = [ping.slug, info.reason, info.appName,
-                            info.appVersion, info.appUpdateChannel,
-                            info.appBuildID];
-      slug = pathComponents.join("/");
-    }
-    return "/submit/telemetry/" + slug;
+    const isSubsession = !this._isClassicReason(reason);
+    let payload = this.getSessionPayload(reason, isSubsession);
+    let options = {
+      retentionDays: RETENTION_DAYS,
+      addClientId: true,
+      addEnvironment: true,
+    };
+    return TelemetryPing.send(getPingType(payload), payload, options);
   },
 
   attachObservers: function attachObservers() {
     if (!this._initialized)
       return;
     Services.obs.addObserver(this, "cycle-collector-begin", false);
     Services.obs.addObserver(this, "idle-daily", false);
   },
@@ -923,78 +1066,110 @@ let Impl = {
 
     return true;
   },
 
   /**
    * Initializes telemetry within a timer. If there is no PREF_SERVER set, don't turn on telemetry.
    */
   setupChromeProcess: function setupChromeProcess(testing) {
+    this._initStarted = true;
     if (testing && !this._log) {
       this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
     }
 
     this._log.trace("setupChromeProcess");
 
+    if (this._delayedInitTask) {
+      this._log.error("setupChromeProcess - init task already running");
+      return this._delayedInitTaskDeferred.promise;
+    }
+
+    if (this._initialized && !testing) {
+      this._log.error("setupChromeProcess - already initialized");
+      return Promise.resolve();
+    }
+
+    // Generate a unique id once per session so the server can cope with duplicate
+    // submissions, orphaning and other oddities. The id is shared across subsessions.
+    this._sessionId = Policy.generateSessionUUID();
+    this.startNewSubsession();
+    // startNewSubsession sets |_subsessionStartDate| to the current date/time. Use
+    // the very same value for |_sessionStartDate|.
+    this._sessionStartDate = this._subsessionStartDate;
+
     // Initialize some probes that are kept in their own modules
     this._thirdPartyCookies = new ThirdPartyCookieProbe();
     this._thirdPartyCookies.init();
 
     // Record old value and update build ID preference if this is the first
     // run with a new build ID.
-    let previousBuildID = Preferences.get(PREF_PREVIOUS_BUILDID, undefined);
+    let previousBuildId = Preferences.get(PREF_PREVIOUS_BUILDID, null);
     let thisBuildID = Services.appinfo.appBuildID;
-    // If there is no previousBuildID preference, this._previousBuildID remains
-    // undefined so no value is sent in the telemetry metadata.
-    if (previousBuildID != thisBuildID) {
-      this._previousBuildID = previousBuildID;
+    // If there is no previousBuildId preference, we send null to the server.
+    if (previousBuildId != thisBuildID) {
+      this._previousBuildId = previousBuildId;
       Preferences.set(PREF_PREVIOUS_BUILDID, thisBuildID);
     }
 
     if (!this.enableTelemetryRecording(testing)) {
       this._log.config("setupChromeProcess - Telemetry recording is disabled, skipping Chrome process setup.");
       return Promise.resolve();
     }
 
-    AsyncShutdown.sendTelemetry.addBlocker(
-      "Telemetry: shutting down",
-      function condition(){
-        this.uninstall();
-        if (Telemetry.canSend) {
-          return this.savePendingPings();
-        }
-      }.bind(this));
+    TelemetryPing.shutdown.addBlocker("TelemetrySession: shutting down",
+                                      () => this.shutdownChromeProcess(),
+                                      () => this._getState());
 
     Services.obs.addObserver(this, "sessionstore-windows-restored", false);
 #ifdef MOZ_WIDGET_ANDROID
     Services.obs.addObserver(this, "application-background", false);
 #endif
     Services.obs.addObserver(this, "xul-window-visible", false);
     this._hasWindowRestoredObserver = true;
     this._hasXulWindowVisibleObserver = true;
 
     ppmm.addMessageListener(MESSAGE_TELEMETRY_PAYLOAD, this);
 
     // Delay full telemetry initialization to give the browser time to
     // run various late initializers. Otherwise our gathered memory
     // footprint and other numbers would be too optimistic.
-    let deferred = Promise.defer();
-    let delayedTask = new DeferredTask(function* () {
-      this._initialized = true;
+    this._delayedInitTaskDeferred = Promise.defer();
+    this._delayedInitTask = new DeferredTask(function* () {
+      try {
+        this._initialized = true;
+
+        let hasLoaded = yield this._loadSessionData();
+        if (!hasLoaded) {
+          // We could not load a valid session data file. Create one.
+          yield this._saveSessionData(this._getSessionDataObject()).catch(() =>
+            this._log.error("setupChromeProcess - Could not write session data to disk."));
+        }
+        this.attachObservers();
+        this.gatherMemory();
 
-      this.attachObservers();
-      this.gatherMemory();
+        Telemetry.asyncFetchTelemetryData(function () {});
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+        this._rescheduleDailyTimer();
+        TelemetryEnvironment.registerChangeListener(ENVIRONMENT_CHANGE_LISTENER,
+                                                    () => this._onEnvironmentChange());
+#endif
 
-      Telemetry.asyncFetchTelemetryData(function () {});
-      deferred.resolve();
-
+        this._delayedInitTaskDeferred.resolve();
+      } catch (e) {
+        this._delayedInitTaskDeferred.reject();
+      } finally {
+        this._delayedInitTask = null;
+        this._delayedInitTaskDeferred = null;
+      }
     }.bind(this), testing ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY);
 
-    delayedTask.arm();
-    return deferred.promise;
+    this._delayedInitTask.arm();
+    return this._delayedInitTaskDeferred.promise;
   },
 
   /**
    * Initializes telemetry for a content process.
    */
   setupContentProcess: function setupContentProcess() {
     this._log.trace("setupContentProcess");
 
@@ -1011,20 +1186,16 @@ let Impl = {
 
       this.attachObservers();
       this.gatherMemory();
     }.bind(this), TELEMETRY_DELAY);
 
     delayedTask.arm();
   },
 
-  testLoadHistograms: function testLoadHistograms(file) {
-    return TelemetryFile.testLoadHistograms(file);
-  },
-
   getFlashVersion: function getFlashVersion() {
     this._log.trace("getFlashVersion");
     let host = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
     let tags = host.getPluginTags();
 
     for (let i = 0; i < tags.length; i++) {
       if (tags[i].name == "Shockwave Flash")
         return tags[i].version;
@@ -1055,29 +1226,43 @@ let Impl = {
     }
     default:
       throw new Error("Telemetry.receiveMessage: bad message name");
     }
   },
 
   sendContentProcessPing: function sendContentProcessPing(reason) {
     this._log.trace("sendContentProcessPing - Reason " + reason);
-    let payload = this.getSessionPayload(reason);
+    const isSubsession = !this._isClassicReason(reason);
+    let payload = this.getSessionPayload(reason, isSubsession);
     cpmm.sendAsyncMessage(MESSAGE_TELEMETRY_PAYLOAD, payload);
   },
 
   savePendingPings: function savePendingPings() {
     this._log.trace("savePendingPings");
-    let sessionPing = this.getSessionPayloadAndSlug("saved-session");
-    return TelemetryFile.savePendingPings(sessionPing);
+    let payload = this.getSessionPayload(REASON_SAVED_SESSION, false);
+    let options = {
+      retentionDays: RETENTION_DAYS,
+      addClientId: true,
+      addEnvironment: true,
+    };
+    return TelemetryPing.savePendingPings(getPingType(payload), payload, options);
   },
 
   testSaveHistograms: function testSaveHistograms(file) {
-    return TelemetryFile.savePingToFile(this.getSessionPayloadAndSlug("saved-session"),
-      file.path, true);
+    this._log.trace("testSaveHistograms - Path: " + file.path);
+    let payload = this.getSessionPayload(REASON_SAVED_SESSION, false);
+    let options = {
+      retentionDays: RETENTION_DAYS,
+      addClientId: true,
+      addEnvironment: true,
+      overwrite: true,
+      filePath: file.path,
+    };
+    return TelemetryPing.testSavePingToFile(getPingType(payload), payload, options);
   },
 
   /**
    * Remove observers to avoid leaks
    */
   uninstall: function uninstall() {
     this.detachObservers();
     if (this._hasWindowRestoredObserver) {
@@ -1088,26 +1273,27 @@ let Impl = {
       Services.obs.removeObserver(this, "xul-window-visible");
       this._hasXulWindowVisibleObserver = false;
     }
 #ifdef MOZ_WIDGET_ANDROID
     Services.obs.removeObserver(this, "application-background", false);
 #endif
   },
 
-  getPayload: function getPayload() {
-    this._log.trace("getPayload");
+  getPayload: function getPayload(reason, clearSubsession) {
+    this._log.trace("getPayload - clearSubsession: " + clearSubsession);
+    reason = reason || REASON_GATHER_PAYLOAD;
     // This function returns the current Telemetry payload to the caller.
     // We only gather startup info once.
     if (Object.keys(this._slowSQLStartup).length == 0) {
       this.gatherStartupHistograms();
       this._slowSQLStartup = Telemetry.slowSQL;
     }
     this.gatherMemory();
-    return this.getSessionPayload("gather-payload");
+    return this.getSessionPayload(reason, clearSubsession);
   },
 
   gatherStartup: function gatherStartup() {
     this._log.trace("gatherStartup");
     let counters = processInfo.getCounters();
     if (counters) {
       [this._startupIO.startupSessionRestoreReadBytes,
         this._startupIO.startupSessionRestoreWriteBytes] = counters;
@@ -1122,19 +1308,19 @@ let Impl = {
 
   sendIdlePing: function sendIdlePing(aTest) {
     this._log.trace("sendIdlePing");
     if (this._isIdleObserver) {
       idleService.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
       this._isIdleObserver = false;
     }
     if (aTest) {
-      return this.send("test-ping");
+      return this.send(REASON_TEST_PING);
     } else if (Telemetry.canSend) {
-      return this.send("idle-daily");
+      return this.send(REASON_IDLE_DAILY);
     }
   },
 
   testPing: function testPing() {
     return this.sendIdlePing(true);
   },
 
   /**
@@ -1155,17 +1341,17 @@ let Impl = {
       // app-startup is only registered for content processes.
       return this.setupContentProcess();
     case "content-child-shutdown":
       // content-child-shutdown is only registered for content processes.
       Services.obs.removeObserver(this, "content-child-shutdown");
       this.uninstall();
 
       if (Telemetry.canSend) {
-        this.sendContentProcessPing("saved-session");
+        this.sendContentProcessPing(REASON_SAVED_SESSION);
       }
       break;
     case "cycle-collector-begin":
       let now = new Date();
       if (!gLastMemoryPoll
           || (TELEMETRY_INTERVAL <= now - gLastMemoryPoll)) {
         gLastMemoryPoll = now;
         this.gatherMemory();
@@ -1183,17 +1369,17 @@ let Impl = {
     case "sessionstore-windows-restored":
       Services.obs.removeObserver(this, "sessionstore-windows-restored");
       this._hasWindowRestoredObserver = false;
       // Check whether debugger was attached during startup
       let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
       gWasDebuggerAttached = debugService.isDebuggerAttached;
       this.gatherStartup();
       break;
-    case "idle-daily":
+    case REASON_IDLE_DAILY:
       // Enqueue to main-thread, otherwise components may be inited by the
       // idle-daily category and miss the gather-telemetry notification.
       Services.tm.mainThread.dispatch((function() {
         // Notify that data should be gathered now, since ping will happen soon.
         Services.obs.notifyObservers(null, "gather-telemetry", null);
         // The ping happens at the first idle of length IDLE_TIMEOUT_SECONDS.
         idleService.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
         this._isIdleObserver = true;
@@ -1216,28 +1402,210 @@ let Impl = {
     // 3) We submit the data, and then come back into the foreground. Same as case (2).
     // 4) We do not submit the data, but come back into the foreground. In this case
     //    we have the option of either deleting the file that we saved (since we will either
     //    send the live data while in the foreground, or create the file again on the next
     //    backgrounding), or not (in which case we will delete it on submit, or overwrite
     //    it on the next backgrounding). Not deleting it is faster, so that's what we do.
     case "application-background":
       if (Telemetry.canSend) {
-        let ping = this.getSessionPayloadAndSlug("saved-session");
-        TelemetryFile.savePing(ping, true);
+        let payload = this.getSessionPayload(REASON_SAVED_SESSION, false);
+        let options = {
+          retentionDays: RETENTION_DAYS,
+          addClientId: true,
+          addEnvironment: true,
+          overwrite: true,
+        };
+        TelemetryPing.savePing(getPingType(payload), payload, options);
       }
       break;
 #endif
     }
   },
 
   /**
-   * This tells TelemetryPing to uninitialize and save any pending pings.
+   * This tells TelemetrySession to uninitialize and save any pending pings.
    * @param testing Optional. If true, always saves the ping whether Telemetry
    *                can send pings or not, which is used for testing.
    */
-  shutdown: function(testing = false) {
-    this.uninstall();
-    if (Telemetry.canSend || testing) {
-      return this.savePendingPings();
+  shutdownChromeProcess: function(testing = false) {
+    this._log.trace("shutdownChromeProcess - testing: " + testing);
+
+    let cleanup = () => {
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+      TelemetryEnvironment.unregisterChangeListener(ENVIRONMENT_CHANGE_LISTENER);
+#endif
+      if (this._dailyTimerId) {
+        Policy.clearDailyTimeout(this._dailyTimerId);
+        this._dailyTimerId = null;
+      }
+      this.uninstall();
+
+      let reset = () => {
+        this._initStarted = false;
+        this._initialized = false;
+      };
+
+      if (Telemetry.canSend || testing) {
+        return this.savePendingPings()
+                .then(() => gStateSaveSerializer.flushTasks())
+                .then(reset);
+      }
+
+      reset();
+      return Promise.resolve();
+    };
+
+    // We can be in one the following states here:
+    // 1) setupChromeProcess was never called
+    // or it was called and
+    //   2) _delayedInitTask was scheduled, but didn't run yet.
+    //   3) _delayedInitTask is running now.
+    //   4) _delayedInitTask finished running already.
+
+    // This handles 1).
+    if (!this._initStarted) {
+      return Promise.resolve();
+     }
+
+    // This handles 4).
+    if (!this._delayedInitTask) {
+      // We already ran the delayed initialization.
+      return cleanup();
+     }
+
+    // This handles 2) and 3).
+    this._delayedInitTask.disarm();
+    return this._delayedInitTask.finalize().then(cleanup);
+   },
+
+  _rescheduleDailyTimer: function() {
+    if (this._dailyTimerId) {
+      this._log.trace("_rescheduleDailyTimer - clearing existing timeout");
+      Policy.clearDailyTimeout(this._dailyTimerId);
+    }
+
+    let now = Policy.now();
+    let midnight = truncateToDays(now).getTime() + MS_IN_ONE_DAY;
+    let msUntilCollection = midnight - now.getTime();
+    if (msUntilCollection < MIN_SUBSESSION_LENGTH_MS) {
+      msUntilCollection += MS_IN_ONE_DAY;
+    }
+
+    this._log.trace("_rescheduleDailyTimer - now: " + now
+                    + ", scheduled: " + new Date(now.getTime() + msUntilCollection));
+    this._dailyTimerId = Policy.setDailyTimeout(() => this._onDailyTimer(), msUntilCollection);
+  },
+
+  _onDailyTimer: function() {
+    if (!this._initStarted) {
+      if (this._log) {
+        this._log.warn("_onDailyTimer - not initialized");
+      } else {
+        Cu.reportError("TelemetrySession._onDailyTimer - not initiali