Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 04 May 2016 11:58:38 +0200
changeset 296087 a36a2b5275b7600d33a174758ec58f175bbae7b0
parent 296010 b97e26486559686ad20229845e1c4f6712f652fe (current diff)
parent 296086 311c7ea8803d326cec715404256c86beb8af3804 (diff)
child 296088 d30f9cddfa7c87430f05a254bac9685fd22199e2
push id76181
push usercbook@mozilla.com
push dateWed, 04 May 2016 09:58:49 +0000
treeherdermozilla-inbound@a36a2b5275b7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound
devtools/client/aboutdebugging/components/addon-target.js
devtools/client/aboutdebugging/components/addons-controls.js
devtools/client/aboutdebugging/components/addons-install-error.js
devtools/client/aboutdebugging/components/addons-tab.js
devtools/client/aboutdebugging/components/service-worker-target.js
devtools/client/aboutdebugging/components/tab-header.js
devtools/client/aboutdebugging/components/tab-menu-entry.js
devtools/client/aboutdebugging/components/tab-menu.js
devtools/client/aboutdebugging/components/worker-target.js
devtools/client/aboutdebugging/components/workers-tab.js
devtools/shared/css-color.js
devtools/shared/tests/unit/test_cssColor.js
mobile/android/base/java/org/mozilla/gecko/telemetry/pings/TelemetryCorePingBuilder.java
mobile/android/base/java/org/mozilla/gecko/telemetry/pings/TelemetryPing.java
mobile/android/base/java/org/mozilla/gecko/telemetry/pings/TelemetryPingBuilder.java
mobile/android/services/src/main/java/org/mozilla/gecko/background/BackgroundService.java
mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestBrowserContractHelpers.java
mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/BackgroundServiceTestCase.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/pings/TestTelemetryPingBuilder.java
toolkit/components/passwordmgr/test/subtst_master_pass.html
toolkit/components/passwordmgr/test/test_master_password.html
toolkit/mozapps/update/tests/chrome/test_0017_check_staging_basic.xul
toolkit/mozapps/update/tests/chrome/test_0021_check_billboard.xul
toolkit/mozapps/update/tests/chrome/test_0031_available_basic.xul
toolkit/mozapps/update/tests/chrome/test_0041_available_billboard.xul
--- a/.eslintignore
+++ b/.eslintignore
@@ -95,26 +95,27 @@ devtools/client/netmonitor/test/**
 devtools/client/netmonitor/har/test/**
 devtools/client/performance/**
 devtools/client/projecteditor/**
 devtools/client/promisedebugger/**
 devtools/client/responsivedesign/**
 devtools/client/scratchpad/**
 devtools/client/shadereditor/**
 devtools/client/shared/**
+!devtools/client/shared/css-color.js
+!devtools/client/shared/css-color-db.js
 devtools/client/sourceeditor/**
 devtools/client/webaudioeditor/**
 devtools/client/webconsole/**
 !devtools/client/webconsole/panel.js
 !devtools/client/webconsole/jsterm.js
 devtools/client/webide/**
 devtools/server/**
 !devtools/server/actors/webbrowser.js
 devtools/shared/*.js
-!devtools/shared/css-color.js
 devtools/shared/*.jsm
 devtools/shared/apps/**
 devtools/shared/client/**
 devtools/shared/discovery/**
 devtools/shared/gcli/**
 devtools/shared/heapsnapshot/**
 devtools/shared/inspector/**
 devtools/shared/layout/**
--- a/browser/components/extensions/ext-history.js
+++ b/browser/components/extensions/ext-history.js
@@ -1,9 +1,22 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+XPCOMUtils.defineLazyGetter(this, "History", () => {
+  Cu.import("resource://gre/modules/PlacesUtils.jsm");
+  return PlacesUtils.history;
+});
+
 extensions.registerSchemaAPI("history", "history", (extension, context) => {
   return {
-    history: {},
+    history: {
+      deleteUrl: function(details) {
+        let url = details.url;
+        // History.remove returns a boolean, but our API should return nothing
+        return History.remove(url).then(() => undefined);
+      },
+    },
   };
 });
--- a/browser/components/extensions/schemas/history.json
+++ b/browser/components/extensions/schemas/history.json
@@ -195,17 +195,16 @@
             "type": "function",
             "optional": true,
             "parameters": []
           }
         ]
       },
       {
         "name": "deleteUrl",
-        "unsupported": true,
         "type": "function",
         "description": "Removes all occurrences of the given URL from the history.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
--- a/browser/components/extensions/test/browser/browser_ext_history.js
+++ b/browser/components/extensions/test/browser/browser_ext_history.js
@@ -1,20 +1,58 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+                                  "resource://testing-common/PlacesTestUtils.jsm");
+
 add_task(function* test_history_schema() {
   function background() {
     browser.test.assertTrue(browser.history, "browser.history API exists");
     browser.test.notifyPass("history-schema");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["history"],
     },
     background: `(${background})()`,
   });
   yield extension.startup();
   yield extension.awaitFinish("history-schema");
   yield extension.unload();
 });
+
+add_task(function* test_delete_url() {
+  const TEST_URL = `http://example.com/${Math.random()}`;
+
+  function background() {
+    browser.test.onMessage.addListener((msg, url) => {
+      browser.history.deleteUrl({url: url}).then(result => {
+        browser.test.assertEq(undefined, result, "browser.history.deleteUrl returns nothing");
+        browser.test.sendMessage("url-deleted");
+      });
+    });
+
+    browser.test.sendMessage("ready");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["history"],
+    },
+    background: `(${background})()`,
+  });
+
+  yield extension.startup();
+  yield PlacesTestUtils.clearHistory();
+  yield extension.awaitMessage("ready");
+
+  yield PlacesTestUtils.addVisits(TEST_URL);
+  ok(yield PlacesTestUtils.isPageInDB(TEST_URL), `${TEST_URL} found in history database`);
+
+  extension.sendMessage("delete-url", TEST_URL);
+  yield extension.awaitMessage("url-deleted");
+  ok(!(yield PlacesTestUtils.isPageInDB(TEST_URL)), `${TEST_URL} not found in history database`);
+
+  yield extension.unload();
+});
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -131,9 +131,8 @@ function closePageAction(extension, win 
   if (node) {
     return promisePopupShown(node).then(() => {
       node.hidePopup();
     });
   }
 
   return Promise.resolve();
 }
-
--- a/browser/themes/shared/syncedtabs/sidebar.inc.css
+++ b/browser/themes/shared/syncedtabs/sidebar.inc.css
@@ -121,16 +121,17 @@ body {
 }
 
 .item-title {
   flex-grow: 1;
   overflow: hidden;
   text-overflow: ellipsis;
   margin: 0px;
   line-height: 1.3;
+  cursor: default;
 }
 
 .item[hidden] {
   opacity: 0;
   max-height: 0;
   transition: opacity 150ms ease-in-out, max-height 150ms ease-in-out 150ms;
 }
 
--- a/devtools/client/aboutdebugging/aboutdebugging.css
+++ b/devtools/client/aboutdebugging/aboutdebugging.css
@@ -12,17 +12,17 @@ h2, h3, h4 {
 }
 
 button {
   padding-left: 20px;
   padding-right: 20px;
   min-width: 100px;
 }
 
-/* Category tabs */
+/* Category panels */
 
 .category {
   display: flex;
   flex-direction: row;
   align-content: center;
 }
 
 .category-name {
@@ -35,17 +35,17 @@ button {
   display: flex;
   flex-direction: row;
 }
 
 .main-content {
   flex: 1;
 }
 
-.tab {
+.panel {
   max-width: 800px;
 }
 
 /* Targets */
 
 .targets {
   margin-bottom: 35px;
 }
--- a/devtools/client/aboutdebugging/components/aboutdebugging.js
+++ b/devtools/client/aboutdebugging/components/aboutdebugging.js
@@ -1,53 +1,61 @@
 /* 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/. */
 
 /* eslint-env browser */
-/* globals AddonsTab, WorkersTab */
+/* globals AddonsPanel, WorkersPanel */
 
 "use strict";
 
 const { createFactory, createClass, DOM: dom } =
   require("devtools/client/shared/vendor/react");
 const Services = require("Services");
 
-const TabMenu = createFactory(require("./tab-menu"));
+const PanelMenu = createFactory(require("./panel-menu"));
 
-loader.lazyGetter(this, "AddonsTab",
-  () => createFactory(require("./addons-tab")));
-loader.lazyGetter(this, "WorkersTab",
-  () => createFactory(require("./workers-tab")));
+loader.lazyGetter(this, "AddonsPanel",
+  () => createFactory(require("./addons/panel")));
+loader.lazyGetter(this, "TabsPanel",
+  () => createFactory(require("./tabs/panel")));
+loader.lazyGetter(this, "WorkersPanel",
+  () => createFactory(require("./workers/panel")));
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
-const tabs = [{
+const panels = [{
   id: "addons",
-  panelId: "tab-addons",
+  panelId: "addons-panel",
   name: Strings.GetStringFromName("addons"),
   icon: "chrome://devtools/skin/images/debugging-addons.svg",
-  component: AddonsTab
+  component: AddonsPanel
+}, {
+  id: "tabs",
+  panelId: "tabs-panel",
+  name: Strings.GetStringFromName("tabs"),
+  icon: "chrome://devtools/skin/images/debugging-tabs.svg",
+  component: TabsPanel
 }, {
   id: "workers",
-  panelId: "tab-workers",
+  panelId: "workers-panel",
   name: Strings.GetStringFromName("workers"),
   icon: "chrome://devtools/skin/images/debugging-workers.svg",
-  component: WorkersTab
+  component: WorkersPanel
 }];
 
-const defaultTabId = "addons";
+const defaultPanelId = "addons";
 
 module.exports = createClass({
   displayName: "AboutDebuggingApp",
 
   getInitialState() {
     return {
-      selectedTabId: defaultTabId
+      selectedPanelId: defaultPanelId
     };
   },
 
   componentDidMount() {
     window.addEventListener("hashchange", this.onHashChange);
     this.onHashChange();
     this.props.telemetry.toolOpened("aboutdebugging");
   },
@@ -56,39 +64,39 @@ module.exports = createClass({
     window.removeEventListener("hashchange", this.onHashChange);
     this.props.telemetry.toolClosed("aboutdebugging");
     this.props.telemetry.destroy();
   },
 
   onHashChange() {
     let hash = window.location.hash;
     // Default to defaultTabId if no hash is provided.
-    let tabId = hash ? hash.substr(1) : defaultTabId;
+    let panelId = hash ? hash.substr(1) : defaultPanelId;
 
-    let isValid = tabs.some(t => t.id == tabId);
+    let isValid = panels.some(p => p.id == panelId);
     if (isValid) {
-      this.setState({ selectedTabId: tabId });
+      this.setState({ selectedPanelId: panelId });
     } else {
       // If the current hash matches no valid category, navigate to the default
-      // tab.
-      this.selectTab(defaultTabId);
+      // panel.
+      this.selectPanel(defaultPanelId);
     }
   },
 
-  selectTab(tabId) {
-    window.location.hash = "#" + tabId;
+  selectPanel(panelId) {
+    window.location.hash = "#" + panelId;
   },
 
   render() {
     let { client } = this.props;
-    let { selectedTabId } = this.state;
-    let selectTab = this.selectTab;
+    let { selectedPanelId } = this.state;
+    let selectPanel = this.selectPanel;
 
-    let selectedTab = tabs.find(t => t.id == selectedTabId);
+    let selectedPanel = panels.find(p => p.id == selectedPanelId);
 
     return dom.div({ className: "app" },
-      TabMenu({ tabs, selectedTabId, selectTab }),
+      PanelMenu({ panels, selectedPanelId, selectPanel }),
       dom.div({ className: "main-content" },
-        selectedTab.component({ client, id: selectedTab.panelId })
+        selectedPanel.component({ client, id: selectedPanel.panelId })
       )
     );
   }
 });
rename from devtools/client/aboutdebugging/components/addons-controls.js
rename to devtools/client/aboutdebugging/components/addons/controls.js
--- a/devtools/client/aboutdebugging/components/addons-controls.js
+++ b/devtools/client/aboutdebugging/components/addons/controls.js
@@ -9,18 +9,17 @@
 
 loader.lazyImporter(this, "AddonManager",
   "resource://gre/modules/AddonManager.jsm");
 
 const { Cc, Ci, Cu } = require("chrome");
 const { createFactory, createClass, DOM: dom } =
   require("devtools/client/shared/vendor/react");
 const Services = require("Services");
-
-const AddonsInstallError = createFactory(require("./addons-install-error"));
+const AddonsInstallError = createFactory(require("./install-error"));
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 const MORE_INFO_URL = "https://developer.mozilla.org/docs/Tools" +
                       "/about:debugging#Enabling_add-on_debugging";
 
 module.exports = createClass({
rename from devtools/client/aboutdebugging/components/addons-install-error.js
rename to devtools/client/aboutdebugging/components/addons/install-error.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/addons/moz.build
@@ -0,0 +1,10 @@
+# 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/.
+
+DevToolsModules(
+    'controls.js',
+    'install-error.js',
+    'panel.js',
+    'target.js',
+)
rename from devtools/client/aboutdebugging/components/addons-tab.js
rename to devtools/client/aboutdebugging/components/addons/panel.js
--- a/devtools/client/aboutdebugging/components/addons-tab.js
+++ b/devtools/client/aboutdebugging/components/addons/panel.js
@@ -4,30 +4,30 @@
 
 "use strict";
 
 const { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
 const { createFactory, createClass, DOM: dom } =
   require("devtools/client/shared/vendor/react");
 const Services = require("Services");
 
-const AddonsControls = createFactory(require("./addons-controls"));
-const AddonTarget = createFactory(require("./addon-target"));
-const TabHeader = createFactory(require("./tab-header"));
-const TargetList = createFactory(require("./target-list"));
+const AddonsControls = createFactory(require("./controls"));
+const AddonTarget = createFactory(require("./target"));
+const PanelHeader = createFactory(require("../panel-header"));
+const TargetList = createFactory(require("../target-list"));
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 const ExtensionIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
 const CHROME_ENABLED_PREF = "devtools.chrome.enabled";
 const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled";
 
 module.exports = createClass({
-  displayName: "AddonsTab",
+  displayName: "AddonsPanel",
 
   getInitialState() {
     return {
       extensions: [],
       debugDisabled: false,
     };
   },
 
@@ -107,23 +107,30 @@ module.exports = createClass({
 
   render() {
     let { client, id } = this.props;
     let { debugDisabled, extensions: targets } = this.state;
     let name = Strings.GetStringFromName("extensions");
     let targetClass = AddonTarget;
 
     return dom.div({
-      id: id,
-      className: "tab",
+      id,
+      className: "panel",
       role: "tabpanel",
-      "aria-labelledby": "tab-addons-header-name"
+      "aria-labelledby": "panel-addons-header-name"
     },
-    TabHeader({
-      id: "tab-addons-header-name",
+    PanelHeader({
+      id: "addons-panel-header-name",
       name: Strings.GetStringFromName("addons")
     }),
     AddonsControls({ debugDisabled }),
     dom.div({ id: "addons" },
-      TargetList({ name, targets, client, debugDisabled, targetClass })
+      TargetList({
+        name,
+        targets,
+        client,
+        debugDisabled,
+        targetClass,
+        sort: true
+      })
     ));
   }
 });
rename from devtools/client/aboutdebugging/components/addon-target.js
rename to devtools/client/aboutdebugging/components/addons/target.js
--- a/devtools/client/aboutdebugging/components/moz.build
+++ b/devtools/client/aboutdebugging/components/moz.build
@@ -1,18 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+DIRS += [
+    'addons',
+    'tabs',
+    'workers',
+]
+
 DevToolsModules(
     'aboutdebugging.js',
-    'addon-target.js',
-    'addons-controls.js',
-    'addons-install-error.js',
-    'addons-tab.js',
-    'service-worker-target.js',
-    'tab-header.js',
-    'tab-menu-entry.js',
-    'tab-menu.js',
+    'panel-header.js',
+    'panel-menu-entry.js',
+    'panel-menu.js',
     'target-list.js',
-    'worker-target.js',
-    'workers-tab.js',
 )
rename from devtools/client/aboutdebugging/components/tab-header.js
rename to devtools/client/aboutdebugging/components/panel-header.js
--- a/devtools/client/aboutdebugging/components/tab-header.js
+++ b/devtools/client/aboutdebugging/components/panel-header.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { createClass, DOM: dom } =
   require("devtools/client/shared/vendor/react");
 
 module.exports = createClass({
-  displayName: "TabHeader",
+  displayName: "PanelHeader",
 
   render() {
     let { name, id } = this.props;
 
     return dom.div({ className: "header" },
       dom.h1({ id, className: "header-name" }, name));
   },
 });
rename from devtools/client/aboutdebugging/components/tab-menu-entry.js
rename to devtools/client/aboutdebugging/components/panel-menu-entry.js
--- a/devtools/client/aboutdebugging/components/tab-menu-entry.js
+++ b/devtools/client/aboutdebugging/components/panel-menu-entry.js
@@ -3,25 +3,25 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { createClass, DOM: dom } =
   require("devtools/client/shared/vendor/react");
 
 module.exports = createClass({
-  displayName: "TabMenuEntry",
+  displayName: "PanelMenuEntry",
 
   onClick() {
-    this.props.selectTab(this.props.tabId);
+    this.props.selectPanel(this.props.id);
   },
 
   onKeyUp(event) {
     if ([" ", "Enter"].includes(event.key)) {
-      this.props.selectTab(this.props.tabId);
+      this.props.selectPanel(this.props.id);
     }
   },
 
   render() {
     let { panelId, icon, name, selected } = this.props;
 
     // Here .category, .category-icon, .category-name classnames are used to
     // apply common styles defined.
rename from devtools/client/aboutdebugging/components/tab-menu.js
rename to devtools/client/aboutdebugging/components/panel-menu.js
--- a/devtools/client/aboutdebugging/components/tab-menu.js
+++ b/devtools/client/aboutdebugging/components/panel-menu.js
@@ -1,26 +1,31 @@
 /* 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 { createClass, createFactory, DOM: dom } =
   require("devtools/client/shared/vendor/react");
-const TabMenuEntry = createFactory(require("./tab-menu-entry"));
+const PanelMenuEntry = createFactory(require("./panel-menu-entry"));
 
 module.exports = createClass({
-  displayName: "TabMenu",
+  displayName: "PanelMenu",
 
   render() {
-    let { tabs, selectedTabId, selectTab } = this.props;
-    let tabLinks = tabs.map(({ panelId, id, name, icon }) => {
-      let selected = id == selectedTabId;
-      return TabMenuEntry({
-        tabId: id, panelId, name, icon, selected, selectTab
+    let { panels, selectedPanelId, selectPanel } = this.props;
+    let panelLinks = panels.map(({ id, panelId, name, icon }) => {
+      let selected = id == selectedPanelId;
+      return PanelMenuEntry({
+        id,
+        panelId,
+        name,
+        icon,
+        selected,
+        selectPanel
       });
     });
 
     // "categories" id used for styling purposes
-    return dom.div({ id: "categories", role: "tablist" }, tabLinks);
+    return dom.div({ id: "categories", role: "tablist" }, panelLinks);
   },
 });
copy from devtools/client/aboutdebugging/components/moz.build
copy to devtools/client/aboutdebugging/components/tabs/moz.build
--- a/devtools/client/aboutdebugging/components/moz.build
+++ b/devtools/client/aboutdebugging/components/tabs/moz.build
@@ -1,18 +1,8 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
-    'aboutdebugging.js',
-    'addon-target.js',
-    'addons-controls.js',
-    'addons-install-error.js',
-    'addons-tab.js',
-    'service-worker-target.js',
-    'tab-header.js',
-    'tab-menu-entry.js',
-    'tab-menu.js',
-    'target-list.js',
-    'worker-target.js',
-    'workers-tab.js',
+    'panel.js',
+    'target.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/tabs/panel.js
@@ -0,0 +1,90 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { createClass, createFactory, DOM: dom } =
+  require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+
+const PanelHeader = createFactory(require("../panel-header"));
+const TargetList = createFactory(require("../target-list"));
+const TabTarget = createFactory(require("./target"));
+
+const Strings = Services.strings.createBundle(
+  "chrome://devtools/locale/aboutdebugging.properties");
+
+module.exports = createClass({
+  displayName: "TabsPanel",
+
+  getInitialState() {
+    return {
+      tabs: []
+    };
+  },
+
+  componentDidMount() {
+    let { client } = this.props;
+    client.addListener("tabListChanged", this.update);
+    this.update();
+  },
+
+  componentWillUnmount() {
+    let { client } = this.props;
+    client.removeListener("tabListChanged", this.update);
+  },
+
+  update() {
+    this.props.client.mainRoot.listTabs().then(({ tabs }) => {
+      // Filter out closed tabs (represented as `null`).
+      tabs = tabs.filter(tab => !!tab);
+      tabs.forEach(tab => {
+        // FIXME Also try to fetch low-res favicon. But we should use actor
+        // support for this to get the high-res one (bug 1061654).
+        let url = new URL(tab.url);
+        if (url.protocol.startsWith("http")) {
+          let prePath = url.origin;
+          let idx = url.pathname.lastIndexOf("/");
+          if (idx === -1) {
+            prePath += url.pathname;
+          } else {
+            prePath += url.pathname.substr(0, idx);
+          }
+          tab.icon = prePath + "/favicon.ico";
+        } else {
+          tab.icon = "chrome://devtools/skin/images/tabs-icon.svg";
+        }
+      });
+      this.setState({ tabs });
+    });
+  },
+
+  render() {
+    let { client } = this.props;
+    let { tabs } = this.state;
+
+    return dom.div({
+      id: "tabs-panel",
+      className: "panel",
+      role: "tabpanel",
+      "aria-labelledby": "tabs-panel-header-name"
+    },
+    PanelHeader({
+      id: "tabs-panel-header-name",
+      name: Strings.GetStringFromName("tabs")
+    }),
+    dom.div({},
+      TargetList({
+        client,
+        id: "tabs",
+        name: Strings.GetStringFromName("tabs"),
+        sort: false,
+        targetClass: TabTarget,
+        targets: tabs
+      })
+    ));
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/tabs/target.js
@@ -0,0 +1,44 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { createClass, DOM: dom } =
+  require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+
+const Strings = Services.strings.createBundle(
+  "chrome://devtools/locale/aboutdebugging.properties");
+
+module.exports = createClass({
+  displayName: "TabTarget",
+
+  debug() {
+    let { target } = this.props;
+    window.open("about:devtools-toolbox?type=tab&id=" + target.outerWindowID);
+  },
+
+  render() {
+    let { target } = this.props;
+
+    return dom.div({ className: "target-container" },
+      dom.img({
+        className: "target-icon",
+        role: "presentation",
+        src: target.icon
+      }),
+      dom.div({ className: "target" },
+        // If the title is empty, display the url instead.
+        dom.div({ className: "target-name", title: target.url },
+          target.title || target.url)
+      ),
+      dom.button({
+        className: "debug-button",
+        onClick: this.debug,
+      }, Strings.GetStringFromName("debug"))
+    );
+  }
+});
--- a/devtools/client/aboutdebugging/components/target-list.js
+++ b/devtools/client/aboutdebugging/components/target-list.js
@@ -14,18 +14,21 @@ const Strings = Services.strings.createB
 const LocaleCompare = (a, b) => {
   return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
 };
 
 module.exports = createClass({
   displayName: "TargetList",
 
   render() {
-    let { client, debugDisabled, targetClass } = this.props;
-    let targets = this.props.targets.sort(LocaleCompare).map(target => {
+    let { client, debugDisabled, targetClass, targets, sort } = this.props;
+    if (sort) {
+      targets = targets.sort(LocaleCompare);
+    }
+    targets = targets.map(target => {
       return targetClass({ client, target, debugDisabled });
     });
 
     return dom.div({ id: this.props.id, className: "targets" },
       dom.h2(null, this.props.name),
       targets.length > 0 ?
         dom.ul({ className: "target-list" }, targets) :
         dom.p(null, Strings.GetStringFromName("nothing"))
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/components/workers/moz.build
@@ -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/.
+
+DevToolsModules(
+    'panel.js',
+    'service-worker-target.js',
+    'target.js',
+)
rename from devtools/client/aboutdebugging/components/workers-tab.js
rename to devtools/client/aboutdebugging/components/workers/panel.js
--- a/devtools/client/aboutdebugging/components/workers-tab.js
+++ b/devtools/client/aboutdebugging/components/workers/panel.js
@@ -2,31 +2,31 @@
  * 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 { Ci } = require("chrome");
 const { createClass, createFactory, DOM: dom } =
   require("devtools/client/shared/vendor/react");
-const { getWorkerForms } = require("../modules/worker");
+const { getWorkerForms } = require("../../modules/worker");
 const Services = require("Services");
 
-const TabHeader = createFactory(require("./tab-header"));
-const TargetList = createFactory(require("./target-list"));
-const WorkerTarget = createFactory(require("./worker-target"));
+const PanelHeader = createFactory(require("../panel-header"));
+const TargetList = createFactory(require("../target-list"));
+const WorkerTarget = createFactory(require("./target"));
 const ServiceWorkerTarget = createFactory(require("./service-worker-target"));
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 const WorkerIcon = "chrome://devtools/skin/images/debugging-workers.svg";
 
 module.exports = createClass({
-  displayName: "WorkersTab",
+  displayName: "WorkersPanel",
 
   getInitialState() {
     return {
       workers: {
         service: [],
         shared: [],
         other: []
       }
@@ -99,42 +99,45 @@ module.exports = createClass({
     });
   },
 
   render() {
     let { client, id } = this.props;
     let { workers } = this.state;
 
     return dom.div({
-      id: id,
-      className: "tab",
+      id,
+      className: "panel",
       role: "tabpanel",
-      "aria-labelledby": "tab-workers-header-name"
+      "aria-labelledby": "panel-workers-header-name"
     },
-    TabHeader({
-      id: "tab-workers-header-name",
+    PanelHeader({
+      id: "workers-panel-header-name",
       name: Strings.GetStringFromName("workers")
     }),
     dom.div({ id: "workers", className: "inverted-icons" },
       TargetList({
         client,
         id: "service-workers",
         name: Strings.GetStringFromName("serviceWorkers"),
+        sort: true,
         targetClass: ServiceWorkerTarget,
         targets: workers.service
       }),
       TargetList({
         client,
         id: "shared-workers",
         name: Strings.GetStringFromName("sharedWorkers"),
+        sort: true,
         targetClass: WorkerTarget,
         targets: workers.shared
       }),
       TargetList({
         client,
         id: "other-workers",
         name: Strings.GetStringFromName("otherWorkers"),
+        sort: true,
         targetClass: WorkerTarget,
         targets: workers.other
       })
     ));
   }
 });
rename from devtools/client/aboutdebugging/components/service-worker-target.js
rename to devtools/client/aboutdebugging/components/workers/service-worker-target.js
--- a/devtools/client/aboutdebugging/components/service-worker-target.js
+++ b/devtools/client/aboutdebugging/components/workers/service-worker-target.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* eslint-env browser */
 
 "use strict";
 
 const { createClass, DOM: dom } =
   require("devtools/client/shared/vendor/react");
-const { debugWorker } = require("../modules/worker");
+const { debugWorker } = require("../../modules/worker");
 const Services = require("Services");
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 module.exports = createClass({
   displayName: "ServiceWorkerTarget",
 
rename from devtools/client/aboutdebugging/components/worker-target.js
rename to devtools/client/aboutdebugging/components/workers/target.js
--- a/devtools/client/aboutdebugging/components/worker-target.js
+++ b/devtools/client/aboutdebugging/components/workers/target.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* eslint-env browser */
 
 "use strict";
 
 const { createClass, DOM: dom } =
   require("devtools/client/shared/vendor/react");
-const { debugWorker } = require("../modules/worker");
+const { debugWorker } = require("../../modules/worker");
 const Services = require("Services");
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 module.exports = createClass({
   displayName: "WorkerTarget",
 
--- a/devtools/client/aboutdebugging/test/browser.ini
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -17,8 +17,9 @@ support-files =
 [browser_addons_reload.js]
 [browser_addons_toggle_debug.js]
 [browser_service_workers.js]
 [browser_service_workers_push.js]
 [browser_service_workers_start.js]
 [browser_service_workers_timeout.js]
 skip-if = true # Bug 1232931
 [browser_service_workers_unregister.js]
+[browser_tabs.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_tabs.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TAB_URL = "data:text/html,<title>foo</title>";
+
+add_task(function* () {
+  let { tab, document } = yield openAboutDebugging("tabs");
+
+  // Wait for initial tabs list which may be empty
+  let tabsElement = getTabList(document);
+  if (tabsElement.querySelectorAll(".target-name").length == 0) {
+    yield waitForMutation(tabsElement, { childList: true });
+  }
+  // Refresh tabsElement to get the .target-list element
+  tabsElement = getTabList(document);
+
+  let names = [...tabsElement.querySelectorAll(".target-name")];
+  let initialTabCount = names.length;
+
+  // Open a new tab in background and wait for its addition in the UI
+  let onNewTab = waitForMutation(tabsElement, { childList: true });
+  let newTab = yield addTab(TAB_URL, null, true);
+  yield onNewTab;
+
+  // Check that the new tab appears in the UI, but with an empty name
+  let newNames = [...tabsElement.querySelectorAll(".target-name")];
+  newNames = newNames.filter(node => !names.includes(node));
+  is(newNames.length, 1, "A new tab appeared in the list");
+  let newTabTarget = newNames[0];
+
+  // Then wait for title update, but on slow test runner, the title may already
+  // be set to the expected value
+  if (newTabTarget.textContent != "foo") {
+    yield waitForMutation(newTabTarget, { childList: true });
+  }
+
+  // Check that the new tab appears in the UI
+  is(newTabTarget.textContent, "foo", "The tab title got updated");
+  is(newTabTarget.title, TAB_URL, "The tab tooltip is the url");
+
+  // Finally, close the tab
+  let onTabsUpdate = waitForMutation(tabsElement, { childList: true });
+  yield removeTab(newTab);
+  yield onTabsUpdate;
+
+  // Check that the tab disappeared from the UI
+  names = [...tabsElement.querySelectorAll("#tabs .target-name")];
+  is(names.length, initialTabCount, "The tab disappeared from the UI");
+
+  yield closeAboutDebugging(tab);
+});
--- a/devtools/client/aboutdebugging/test/head.js
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* eslint-env browser */
 /* eslint-disable mozilla/no-cpows-in-tests */
 /* exported openAboutDebugging, closeAboutDebugging, installAddon,
    uninstallAddon, waitForMutation, assertHasTarget, getServiceWorkerList,
-   waitForInitialAddonList, waitForServiceWorkerRegistered,
+   getTabList, waitForInitialAddonList, waitForServiceWorkerRegistered,
    unregisterServiceWorker */
 /* global sendAsyncMessage */
 
 "use strict";
 
 var { utils: Cu, classes: Cc, interfaces: Ci } = Components;
 
 const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
@@ -43,25 +43,28 @@ function* openAboutDebugging(page) {
   return { tab, document };
 }
 
 function closeAboutDebugging(tab) {
   info("Closing about:debugging");
   return removeTab(tab);
 }
 
-function addTab(url, win) {
+function addTab(url, win, backgroundTab = false) {
   info("Adding tab: " + url);
 
   return new Promise(done => {
     let targetWindow = win || window;
     let targetBrowser = targetWindow.gBrowser;
 
     targetWindow.focus();
-    let tab = targetBrowser.selectedTab = targetBrowser.addTab(url);
+    let tab = targetBrowser.addTab(url);
+    if (!backgroundTab) {
+      targetBrowser.selectedTab = tab;
+    }
     let linkedBrowser = tab.linkedBrowser;
 
     linkedBrowser.addEventListener("load", function onLoad() {
       linkedBrowser.removeEventListener("load", onLoad, true);
       info("Tab added and finished loading: " + url);
       done(tab);
     }, true);
   });
@@ -110,16 +113,27 @@ function getAddonList(document) {
  * @param  {DOMDocument}  document   #service-workers section container document
  * @return {DOMNode}                 target list or container element
  */
 function getServiceWorkerList(document) {
   return document.querySelector("#service-workers .target-list") ||
     document.querySelector("#service-workers.targets");
 }
 
+/**
+ * Depending on whether there are tabs opened, return either a
+ * target list element or its container.
+ * @param  {DOMDocument}  document   #tabs section container document
+ * @return {DOMNode}                 target list or container element
+ */
+function getTabList(document) {
+  return document.querySelector("#tabs .target-list") ||
+    document.querySelector("#tabs.targets");
+}
+
 function* installAddon(document, path, name, evt) {
   // Mock the file picker to select a test addon
   let MockFilePicker = SpecialPowers.MockFilePicker;
   MockFilePicker.init(null);
   let file = getSupportsFile(path);
   MockFilePicker.returnFiles = [file.file];
 
   let addonList = getAddonList(document);
--- a/devtools/client/animationinspector/components/animation-details.js
+++ b/devtools/client/animationinspector/components/animation-details.js
@@ -52,16 +52,39 @@ AnimationDetails.prototype = {
     }
     this.keyframeComponents = [];
 
     while (this.containerEl.firstChild) {
       this.containerEl.firstChild.remove();
     }
   },
 
+  getPerfDataForProperty: function (animation, propertyName) {
+    let warning = "";
+    let className = "";
+    if (animation.state.propertyState) {
+      let isRunningOnCompositor;
+      for (let propState of animation.state.propertyState) {
+        if (propState.property == propertyName) {
+          isRunningOnCompositor = propState.runningOnCompositor;
+          if (typeof propState.warning != "undefined") {
+            warning = propState.warning;
+          }
+          break;
+        }
+      }
+      if (isRunningOnCompositor && warning == "") {
+        className = "oncompositor";
+      } else if (!isRunningOnCompositor && warning != "") {
+        className = "warning";
+      }
+    }
+    return {className, warning};
+  },
+
   /**
    * Get a list of the tracks of the animation actor
    * @return {Object} A list of tracks, one per animated property, each
    * with a list of keyframes
    */
   getTracks: Task.async(function* () {
     let tracks = {};
 
@@ -132,26 +155,29 @@ AnimationDetails.prototype = {
     // Useful for tests to know when the keyframes have been retrieved.
     this.emit("keyframes-retrieved");
 
     for (let propertyName in this.tracks) {
       let line = createNode({
         parent: this.containerEl,
         attributes: {"class": "property"}
       });
-
+      let {warning, className} =
+        this.getPerfDataForProperty(animation, propertyName);
       createNode({
         // text-overflow doesn't work in flex items, so we need a second level
         // of container to actually have an ellipsis on the name.
         // See bug 972664.
         parent: createNode({
           parent: line,
-          attributes: {"class": "name"},
+          attributes: {"class": "name"}
         }),
-        textContent: getCssPropertyName(propertyName)
+        textContent: getCssPropertyName(propertyName),
+        attributes: {"title": warning,
+                     "class": className}
       });
 
       // Add the keyframes diagram for this property.
       let framesWrapperEl = createNode({
         parent: line,
         attributes: {"class": "track-container"}
       });
 
--- a/devtools/client/animationinspector/components/animation-time-block.js
+++ b/devtools/client/animationinspector/components/animation-time-block.js
@@ -170,17 +170,25 @@ AnimationTimeBlock.prototype = {
     if (state.playbackRate !== 1) {
       text += L10N.getStr("player.animationRateLabel") + " ";
       text += state.playbackRate;
       text += "\n";
     }
 
     // Adding a note that the animation is running on the compositor thread if
     // needed.
-    if (state.isRunningOnCompositor) {
+    if (state.propertyState) {
+      if (state.propertyState
+          .every(propState => propState.runningOnCompositor)) {
+        text += L10N.getStr("player.allPropertiesOnCompositorTooltip");
+      } else if (state.propertyState
+                 .some(propState => propState.runningOnCompositor)) {
+        text += L10N.getStr("player.somePropertiesOnCompositorTooltip");
+      }
+    } else if (state.isRunningOnCompositor) {
       text += L10N.getStr("player.runningOnCompositorTooltip");
     }
 
     return text;
   },
 
   onClick: function (e) {
     e.stopPropagation();
--- a/devtools/client/animationinspector/components/animation-timeline.js
+++ b/devtools/client/animationinspector/components/animation-timeline.js
@@ -266,16 +266,31 @@ AnimationsTimeline.prototype = {
     this.emit("timeline-data-changed", {
       isPaused: true,
       isMoving: false,
       isUserDrag: true,
       time: time
     });
   },
 
+  getCompositorStatusClassName: function (state) {
+    let className = state.isRunningOnCompositor
+                    ? " fast-track"
+                    : "";
+
+    if (state.isRunningOnCompositor && state.propertyState) {
+      className +=
+        state.propertyState.some(propState => !propState.runningOnCompositor)
+        ? " some-properties"
+        : " all-properties";
+    }
+
+    return className;
+  },
+
   render: function (animations, documentCurrentTime) {
     this.unrender();
 
     this.animations = animations;
     if (!this.animations.length) {
       return;
     }
 
@@ -283,36 +298,35 @@ AnimationsTimeline.prototype = {
     for (let {state} of animations) {
       TimeScale.addAnimation(state);
     }
 
     this.drawHeaderAndBackground();
 
     for (let animation of this.animations) {
       animation.on("changed", this.onAnimationStateChanged);
-
       // Each line contains the target animated node and the animation time
       // block.
       let animationEl = createNode({
         parent: this.animationsEl,
         nodeType: "li",
         attributes: {
           "class": "animation " +
                    animation.state.type +
-                   (animation.state.isRunningOnCompositor ? " fast-track" : "")
+                   this.getCompositorStatusClassName(animation.state)
         }
       });
 
       // Right below the line is a hidden-by-default line for displaying the
       // inline keyframes.
       let detailsEl = createNode({
         parent: this.animationsEl,
         nodeType: "li",
         attributes: {
-          "class": "animated-properties"
+          "class": "animated-properties " + animation.state.type
         }
       });
 
       let details = new AnimationDetails(this.serverTraits);
       details.init(detailsEl);
       details.on("frame-selected", this.onFrameSelected);
       this.details.push(details);
 
--- a/devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js
+++ b/devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js
@@ -1,14 +1,18 @@
 /* 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";
 
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
+const STRINGS_URI = "chrome://global/locale/layout_errors.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
 // Test that when an animation is selected, its list of animated properties is
 // displayed below it.
 
 const EXPECTED_PROPERTIES = [
   "background-color",
   "background-position-x",
   "background-position-y",
   "background-size",
@@ -37,16 +41,19 @@ add_task(function* () {
 
   ok(isNodeVisible(propertiesList),
      "The list of properties panel is shown");
   ok(propertiesList.querySelectorAll(".property").length,
      "The list of properties panel actually contains properties");
   ok(hasExpectedProperties(propertiesList),
      "The list of properties panel contains the right properties");
 
+  ok(hasExpectedWarnings(propertiesList),
+     "The list of properties panel contains the right warnings");
+
   info("Click to unselect the animation");
   yield clickOnAnimation(panel, 0, true);
 
   ok(!isNodeVisible(propertiesList),
      "The list of properties panel is hidden again");
 });
 
 function hasExpectedProperties(containerEl) {
@@ -61,8 +68,19 @@ function hasExpectedProperties(container
   for (let i = 0; i < names.length; i++) {
     if (names[i] !== EXPECTED_PROPERTIES[i]) {
       return false;
     }
   }
 
   return true;
 }
+
+function hasExpectedWarnings(containerEl) {
+  let warnings = [...containerEl.querySelectorAll(".warning")];
+  for (let warning of warnings) {
+    if (warning.getAttribute("title") ==
+         L10N.getStr("AnimationWarningTransformWithGeometricProperties")) {
+      return true;
+    }
+  }
+  return false;
+}
--- a/devtools/client/animationinspector/test/browser_animation_running_on_compositor.js
+++ b/devtools/client/animationinspector/test/browser_animation_running_on_compositor.js
@@ -20,28 +20,43 @@ add_task(function* () {
   let timeline = panel.animationsTimelineComponent;
 
   info("Select a test node we know has an animation running on the compositor");
   yield selectNodeAndWaitForAnimations(".animated", inspector);
 
   let animationEl = timeline.animationsEl.querySelector(".animation");
   ok(animationEl.classList.contains("fast-track"),
      "The animation element has the fast-track css class");
-  ok(hasTooltip(animationEl),
+  ok(hasTooltip(animationEl,
+                L10N.getStr("player.allPropertiesOnCompositorTooltip")),
      "The animation element has the right tooltip content");
 
   info("Select a node we know doesn't have an animation on the compositor");
   yield selectNodeAndWaitForAnimations(".no-compositor", inspector);
 
   animationEl = timeline.animationsEl.querySelector(".animation");
   ok(!animationEl.classList.contains("fast-track"),
      "The animation element does not have the fast-track css class");
-  ok(!hasTooltip(animationEl),
+  ok(!hasTooltip(animationEl,
+                 L10N.getStr("player.allPropertiesOnCompositorTooltip")),
+     "The animation element does not have oncompositor tooltip content");
+  ok(!hasTooltip(animationEl,
+                 L10N.getStr("player.somePropertiesOnCompositorTooltip")),
+     "The animation element does not have oncompositor tooltip content");
+
+  info("Select a node we know has animation on the compositor and not on the" +
+       " compositor");
+  yield selectNodeAndWaitForAnimations(".compositor-notall", inspector);
+
+  animationEl = timeline.animationsEl.querySelector(".animation");
+  ok(animationEl.classList.contains("fast-track"),
+     "The animation element has the fast-track css class");
+  ok(hasTooltip(animationEl,
+                L10N.getStr("player.somePropertiesOnCompositorTooltip")),
      "The animation element has the right tooltip content");
 });
 
-function hasTooltip(animationEl) {
+function hasTooltip(animationEl, expected) {
   let el = animationEl.querySelector(".name");
   let tooltip = el.getAttribute("title");
 
-  let expected = L10N.getStr("player.runningOnCompositorTooltip");
   return tooltip.indexOf(expected) !== -1;
 }
--- a/devtools/client/animationinspector/test/doc_keyframes.html
+++ b/devtools/client/animationinspector/test/doc_keyframes.html
@@ -1,16 +1,16 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
   <meta charset="UTF-8">
   <title>Yay! Keyframes!</title>
   <style>
     div {
-      animation: wow 10s forwards;
+      animation: wow 100s forwards;
     }
     @keyframes wow {
       0% {
         width: 100px;
         height: 100px;
         border-radius: 0px;
         background: #f06;
       }
--- a/devtools/client/animationinspector/test/doc_simple_animation.html
+++ b/devtools/client/animationinspector/test/doc_simple_animation.html
@@ -77,16 +77,20 @@
     .no-compositor {
       top: 0;
       right: 10px;
       background: gold;
 
       animation: no-compositor 10s cubic-bezier(.57,-0.02,1,.31) forwards;
     }
 
+    .compositor-notall {
+      animation: compositor-notall 2s infinite;
+    }
+
     @keyframes simple-animation {
       100% {
         transform: translateX(300px);
       }
     }
 
     @keyframes other-animation {
       100% {
@@ -94,30 +98,44 @@
       }
     }
 
     @keyframes no-compositor {
       100% {
         margin-right: 600px;
       }
     }
+
+    @keyframes compositor-notall {
+      from {
+        opacity: 0;
+        width: 0px;
+        transform: translate(0px);
+      }
+      to {
+        opacity: 1;
+        width: 100px;
+        transform: translate(100px);
+      }
+    }
   </style>
 </head>
 <body>
   <!-- Comment node -->
   <div class="ball still"></div>
   <div class="ball animated"></div>
   <div class="ball multi"></div>
   <div class="ball delayed"></div>
   <div class="ball multi-finite"></div>
   <div class="ball short"></div>
   <div class="ball long"></div>
   <div class="ball negative-delay"></div>
   <div class="ball no-compositor"></div>
   <div class="ball" id="endDelayed"></div>
+  <div class="ball compositor-notall"></div>
   <script>
     /* globals KeyframeEffect, Animation */
     "use strict";
 
     var el = document.getElementById("endDelayed");
     let effect = new KeyframeEffect(el, [
       { opacity: 0, offset: 0 },
       { opacity: 1, offset: 1 }
--- a/devtools/client/debugger/test/mochitest/browser.ini
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -334,16 +334,17 @@ skip-if = e10s && debug
 skip-if = e10s && debug
 [browser_dbg_parser-08.js]
 skip-if = e10s && debug
 [browser_dbg_parser-09.js]
 skip-if = e10s && debug
 [browser_dbg_parser-10.js]
 skip-if = e10s && debug
 [browser_dbg_parser-11.js]
+[browser_dbg_parser-computed-name.js]
 [browser_dbg_parser-function-defaults.js]
 [browser_dbg_parser-spread-expression.js]
 [browser_dbg_parser-template-strings.js]
 skip-if = e10s && debug
 [browser_dbg_pause-exceptions-01.js]
 skip-if = e10s && debug
 [browser_dbg_pause-exceptions-02.js]
 skip-if = e10s && debug
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_parser-computed-name.js
@@ -0,0 +1,32 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that template strings are correctly processed.
+ */
+
+"use strict";
+
+function test() {
+  let { Parser, SyntaxTreeVisitor } =
+    Cu.import("resource://devtools/shared/Parser.jsm", {});
+
+  let ast = Parser.reflectionAPI.parse("({ [i]: 1 })");
+  let nodes = SyntaxTreeVisitor.filter(ast, e => e.type == "ComputedName");
+  ok(nodes && nodes.length === 1, "Found the ComputedName node");
+
+  let name = nodes[0].name;
+  ok(name, "The ComputedName node has a name property");
+  is(name.type, "Identifier", "The name has a correct type");
+  is(name.name, "i", "The name has a correct name");
+
+  let identNodes = SyntaxTreeVisitor.filter(ast, e => e.type == "Identifier");
+  ok(identNodes && identNodes.length === 1, "Found the Identifier node");
+
+  is(identNodes[0].type, "Identifier", "The identifier has a correct type");
+  is(identNodes[0].name, "i", "The identifier has a correct name");
+
+  finish();
+}
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -1153,17 +1153,17 @@ function source(sourceClient) {
 // console if necessary.  This cleans up the split console pref so
 // it won't pollute other tests.
 function getSplitConsole(toolbox, win) {
   registerCleanupFunction(() => {
     Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
   });
 
   if (!win) {
-    win = toolbox.doc.defaultView;
+    win = toolbox.win;
   }
 
   if (!toolbox.splitConsole) {
     EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
   }
 
   return new Promise(resolve => {
     toolbox.getPanelWhenReady("webconsole").then(() => {
--- a/devtools/client/eyedropper/eyedropper.js
+++ b/devtools/client/eyedropper/eyedropper.js
@@ -1,14 +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/. */
 
 const {Cc, Ci, Cu} = require("chrome");
-const {rgbToHsl} = require("devtools/shared/css-color").colorUtils;
+const {rgbToHsl, rgbToColorName} =
+      require("devtools/client/shared/css-color").colorUtils;
 const Telemetry = require("devtools/client/shared/telemetry");
 const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js");
 const promise = require("promise");
 const Services = require("Services");
 const {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {});
 
 loader.lazyGetter(this, "clipboardHelper", function() {
   return Cc["@mozilla.org/widget/clipboardhelper;1"]
@@ -801,17 +802,17 @@ function toColorString(rgb, format) {
     case "rgb":
       return "rgb(" + r + ", " + g + ", " + b + ")";
     case "hsl":
       let [h,s,l] = rgbToHsl(rgb);
       return "hsl(" + h + ", " + s + "%, " + l + "%)";
     case "name":
       let str;
       try {
-        str = DOMUtils.rgbToColorName(r, g, b);
+        str = rgbToColorName(r, g, b);
       } catch(e) {
         str = hexString(rgb);
       }
       return str;
     default:
       return hexString(rgb);
   }
 }
--- a/devtools/client/framework/devtools-browser.js
+++ b/devtools/client/framework/devtools-browser.js
@@ -579,21 +579,21 @@ var gDevToolsBrowser = exports.gDevTools
    * Update the "Toggle Tools" checkbox in the developer tools menu. This is
    * called when a toolbox is created or destroyed.
    */
   _updateMenuCheckbox: function DT_updateMenuCheckbox() {
     for (let win of gDevToolsBrowser._trackedBrowserWindows) {
 
       let hasToolbox = gDevToolsBrowser.hasToolboxOpened(win);
 
-      let broadcaster = win.document.getElementById("menu_devToolbox");
+      let menu = win.document.getElementById("menu_devToolbox");
       if (hasToolbox) {
-        broadcaster.setAttribute("checked", "true");
+        menu.setAttribute("checked", "true");
       } else {
-        broadcaster.removeAttribute("checked");
+        menu.removeAttribute("checked");
       }
     }
   },
 
   /**
    * Remove the menuitem for a tool to all open browser windows.
    *
    * @param {string} toolId
--- a/devtools/client/framework/menu-item.js
+++ b/devtools/client/framework/menu-item.js
@@ -1,58 +1,65 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+"use strict";
+
 /**
  * A partial implementation of the MenuItem API provided by electron:
  * https://github.com/electron/electron/blob/master/docs/api/menu-item.md.
  *
  * Missing features:
  *   - id String - Unique within a single menu. If defined then it can be used
  *                 as a reference to this item by the position attribute.
  *   - role String - Define the action of the menu item; when specified the
  *                   click property will be ignored
  *   - sublabel String
  *   - accelerator Accelerator
  *   - icon NativeImage
- *   - visible Boolean - If false, the menu item will be entirely hidden.
  *   - position String - This field allows fine-grained definition of the
  *                       specific location within a given menu.
  *
  * Implemented features:
  *  @param Object options
  *    Function click
- *      Will be called with click(menuItem, browserWindow) when the menu item is clicked
+ *      Will be called with click(menuItem, browserWindow) when the menu item
+ *       is clicked
  *    String type
  *      Can be normal, separator, submenu, checkbox or radio
  *    String label
- *      Boolean enabled
- *    If false, the menu item will be greyed out and unclickable.
- *      Boolean checked
- *    Should only be specified for checkbox or radio type menu items.
- *      Menu submenu
- *    Should be specified for submenu type menu items. If submenu is specified, the type: 'submenu' can be omitted. If the value is not a Menu then it will be automatically converted to one using Menu.buildFromTemplate.
- *
+ *    Boolean enabled
+ *      If false, the menu item will be greyed out and unclickable.
+ *    Boolean checked
+ *      Should only be specified for checkbox or radio type menu items.
+ *    Menu submenu
+ *      Should be specified for submenu type menu items. If submenu is specified,
+ *      the type: 'submenu' can be omitted. If the value is not a Menu then it
+ *      will be automatically converted to one using Menu.buildFromTemplate.
+ *    Boolean visible
+ *      If false, the menu item will be entirely hidden.
  */
 function MenuItem({
     accesskey = null,
     checked = false,
     click = () => {},
     disabled = false,
     label = "",
     id = null,
     submenu = null,
     type = "normal",
+    visible = true,
 } = { }) {
   this.accesskey = accesskey;
   this.checked = checked;
   this.click = click;
   this.disabled = disabled;
   this.id = id;
   this.label = label;
   this.submenu = submenu;
   this.type = type;
+  this.visible = visible;
 }
 
 module.exports = MenuItem;
--- a/devtools/client/framework/menu.js
+++ b/devtools/client/framework/menu.js
@@ -1,28 +1,29 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const MenuItem = require("./menu-item");
+"use strict";
+
 const EventEmitter = require("devtools/shared/event-emitter");
 
 /**
  * A partial implementation of the Menu API provided by electron:
  * https://github.com/electron/electron/blob/master/docs/api/menu.md.
  *
  * Extra features:
  *  - Emits an 'open' and 'close' event when the menu is opened/closed
 
  * @param String id (non standard)
  *        Needed so tests can confirm the XUL implementation is working
  */
-function Menu({id=null} = {}) {
+function Menu({ id = null } = {}) {
   this.menuitems = [];
   this.id = id;
 
   Object.defineProperty(this, "items", {
     get() {
       return this.menuitems;
     }
   });
@@ -30,43 +31,43 @@ function Menu({id=null} = {}) {
   EventEmitter.decorate(this);
 }
 
 /**
  * Add an item to the end of the Menu
  *
  * @param {MenuItem} menuItem
  */
-Menu.prototype.append = function(menuItem) {
+Menu.prototype.append = function (menuItem) {
   this.menuitems.push(menuItem);
 };
 
 /**
  * Add an item to a specified position in the menu
  *
  * @param {int} pos
  * @param {MenuItem} menuItem
  */
-Menu.prototype.insert = function(pos, menuItem) {
-  throw "Not implemented";
+Menu.prototype.insert = function (pos, menuItem) {
+  throw Error("Not implemented");
 };
 
 /**
  * Show the Menu at a specified location on the screen
  *
  * Missing features:
  *   - browserWindow - BrowserWindow (optional) - Default is null.
  *   - positioningItem Number - (optional) OS X
  *
  * @param {int} screenX
  * @param {int} screenY
  * @param Toolbox toolbox (non standard)
  *        Needed so we in which window to inject XUL
  */
-Menu.prototype.popup = function(screenX, screenY, toolbox) {
+Menu.prototype.popup = function (screenX, screenY, toolbox) {
   let doc = toolbox.doc;
   let popup = doc.createElement("menupopup");
   popup.setAttribute("menu-api", "true");
 
   if (this.id) {
     popup.id = this.id;
   }
   this._createMenuItems(popup);
@@ -84,19 +85,23 @@ Menu.prototype.popup = function(screenX,
       this.emit("open");
     }
   });
 
   doc.querySelector("popupset").appendChild(popup);
   popup.openPopupAtScreen(screenX, screenY, true);
 };
 
-Menu.prototype._createMenuItems = function(parent) {
+Menu.prototype._createMenuItems = function (parent) {
   let doc = parent.ownerDocument;
   this.menuitems.forEach(item => {
+    if (!item.visible) {
+      return;
+    }
+
     if (item.submenu) {
       let menupopup = doc.createElement("menupopup");
       item.submenu._createMenuItems(menupopup);
 
       let menu = doc.createElement("menu");
       menu.appendChild(menupopup);
       menu.setAttribute("label", item.label);
       parent.appendChild(menu);
@@ -130,20 +135,20 @@ Menu.prototype._createMenuItems = functi
       }
 
       parent.appendChild(menuitem);
     }
   });
 };
 
 Menu.setApplicationMenu = () => {
-  throw "Not implemented";
+  throw Error("Not implemented");
 };
 
 Menu.sendActionToFirstResponder = () => {
-  throw "Not implemented";
+  throw Error("Not implemented");
 };
 
 Menu.buildFromTemplate = () => {
-  throw "Not implemented";
+  throw Error("Not implemented");
 };
 
 module.exports = Menu;
--- a/devtools/client/framework/test/browser_keybindings_02.js
+++ b/devtools/client/framework/test/browser_keybindings_02.js
@@ -35,17 +35,17 @@ add_task(function*() {
 function zoomWithKey(toolbox, key) {
   if (!key) {
     info("Key was empty, skipping zoomWithKey");
     return;
   }
 
   info("Zooming with key: " + key);
   let currentZoom = toolbox.zoomValue;
-  EventUtils.synthesizeKey(key, {accelKey: true}, toolbox.doc.defaultView);
+  EventUtils.synthesizeKey(key, {accelKey: true}, toolbox.win);
   isnot(toolbox.zoomValue, currentZoom, "The zoom level was changed in the toolbox");
 }
 
 function* checkKeyBindings(toolbox) {
   zoomWithKey(toolbox, toolbox.doc.getElementById("toolbox-zoom-in-key").getAttribute("key"));
   zoomWithKey(toolbox, toolbox.doc.getElementById("toolbox-zoom-in-key2").getAttribute("key"));
   zoomWithKey(toolbox, toolbox.doc.getElementById("toolbox-zoom-in-key3").getAttribute("key"));
 
--- a/devtools/client/framework/test/browser_menu_api.js
+++ b/devtools/client/framework/test/browser_menu_api.js
@@ -6,17 +6,17 @@
 "use strict";
 
 // Test that the Menu API works
 
 const URL = "data:text/html;charset=utf8,test page for menu api";
 const Menu = require("devtools/client/framework/menu");
 const MenuItem = require("devtools/client/framework/menu-item");
 
-add_task(function*() {
+add_task(function* () {
   info("Create a test tab and open the toolbox");
   let tab = yield addTab(URL);
   let target = TargetFactory.forTab(tab);
   let toolbox = yield gDevTools.showToolbox(target, "webconsole");
 
   yield testMenuItems();
   yield testMenuPopup(toolbox);
   yield testSubmenu(toolbox);
@@ -66,47 +66,54 @@ function* testMenuPopup(toolbox) {
       disabled: true,
     }),
   ];
 
   for (let item of MENU_ITEMS) {
     menu.append(item);
   }
 
+  // Append an invisible MenuItem, which shouldn't show up in the DOM
+  menu.append(new MenuItem({
+    label: "Invisible",
+    visible: false,
+  }));
+
   menu.popup(0, 0, toolbox);
 
   ok(toolbox.doc.querySelector("#menu-popup"), "A popup is in the DOM");
 
-  let menuSeparators = toolbox.doc.querySelectorAll("#menu-popup > menuseparator");
+  let menuSeparators =
+    toolbox.doc.querySelectorAll("#menu-popup > menuseparator");
   is(menuSeparators.length, 1, "A separator is in the menu");
 
   let menuItems = toolbox.doc.querySelectorAll("#menu-popup > menuitem");
   is(menuItems.length, MENU_ITEMS.length, "Correct number of menuitems");
 
   is(menuItems[0].id, MENU_ITEMS[0].id, "Correct id for menuitem");
   is(menuItems[0].getAttribute("label"), MENU_ITEMS[0].label, "Correct label");
 
   is(menuItems[1].getAttribute("label"), MENU_ITEMS[1].label, "Correct label");
-  is(menuItems[1].getAttribute("type"), "checkbox", "Correct type attribute");
-  is(menuItems[1].getAttribute("checked"), "true", "Has checked attribute");
+  is(menuItems[1].getAttribute("type"), "checkbox", "Correct type attr");
+  is(menuItems[1].getAttribute("checked"), "true", "Has checked attr");
 
   is(menuItems[2].getAttribute("label"), MENU_ITEMS[2].label, "Correct label");
-  is(menuItems[2].getAttribute("type"), "radio", "Correct type attribute");
-  ok(!menuItems[2].hasAttribute("checked"), "Doesn't have checked attribute");
+  is(menuItems[2].getAttribute("type"), "radio", "Correct type attr");
+  ok(!menuItems[2].hasAttribute("checked"), "Doesn't have checked attr");
 
   is(menuItems[3].getAttribute("label"), MENU_ITEMS[3].label, "Correct label");
-  is(menuItems[3].getAttribute("disabled"), "true", "disabled attribute menuitem");
+  is(menuItems[3].getAttribute("disabled"), "true", "disabled attr menuitem");
 
   yield once(menu, "open");
   let closed = once(menu, "close");
-  EventUtils.synthesizeMouseAtCenter(menuItems[0], {}, toolbox.doc.defaultView);
+  EventUtils.synthesizeMouseAtCenter(menuItems[0], {}, toolbox.win);
   yield closed;
   ok(clickFired, "Click has fired");
 
-  ok(!toolbox.doc.querySelector("#menu-popup"), "The popup is removed from the DOM");
+  ok(!toolbox.doc.querySelector("#menu-popup"), "Popup removed from the DOM");
 }
 
 function* testSubmenu(toolbox) {
   let clickFired = false;
   let menu = new Menu({
     id: "menu-popup",
   });
   let submenu = new Menu({
@@ -121,21 +128,22 @@ function* testSubmenu(toolbox) {
   }));
   menu.append(new MenuItem({
     label: "Submenu parent",
     submenu: submenu,
   }));
 
   menu.popup(0, 0, toolbox);
   ok(toolbox.doc.querySelector("#menu-popup"), "A popup is in the DOM");
-  is(toolbox.doc.querySelectorAll("#menu-popup > menuitem").length, 0, "No menuitem children");
+  is(toolbox.doc.querySelectorAll("#menu-popup > menuitem").length, 0,
+    "No menuitem children");
 
   let menus = toolbox.doc.querySelectorAll("#menu-popup > menu");
   is(menus.length, 1, "Correct number of menus");
-  is(menus[0].getAttribute("label"), "Submenu parent", "Correct label for menus");
+  is(menus[0].getAttribute("label"), "Submenu parent", "Correct label");
 
   let subMenuItems = menus[0].querySelectorAll("menupopup > menuitem");
   is(subMenuItems.length, 1, "Correct number of submenu items");
   is(subMenuItems[0].getAttribute("label"), "Submenu item", "Correct label");
 
   yield once(menu, "open");
   let closed = once(menu, "close");
 
@@ -149,13 +157,13 @@ function* testSubmenu(toolbox) {
   EventUtils.synthesizeKey("VK_LEFT", {});
   yield hidden;
 
   shown = once(menus[0], "popupshown");
   EventUtils.synthesizeKey("VK_RIGHT", {});
   yield shown;
 
   info("Clicking the submenu item");
-  EventUtils.synthesizeMouseAtCenter(subMenuItems[0], {}, toolbox.doc.defaultView);
+  EventUtils.synthesizeMouseAtCenter(subMenuItems[0], {}, toolbox.win);
 
   yield closed;
   ok(clickFired, "Click has fired");
 }
--- a/devtools/client/framework/test/browser_toolbox_custom_host.js
+++ b/devtools/client/framework/test/browser_toolbox_custom_host.js
@@ -33,17 +33,17 @@ function test() {
       ok("Got the `toolbox-close` message");
       window.removeEventListener("message", onMessage);
       cleanup();
     }
   }
 
   function testCustomHost(t) {
     toolbox = t;
-    is(toolbox.doc.defaultView.top, window, "Toolbox is included in browser.xul");
+    is(toolbox.win.top, window, "Toolbox is included in browser.xul");
     is(toolbox.doc, iframe.contentDocument, "Toolbox is in the custom iframe");
     executeSoon(() => gBrowser.removeCurrentTab());
   }
 
   function cleanup() {
     iframe.remove();
 
     // Even if we received "toolbox-close", the toolbox may still be destroying
--- a/devtools/client/framework/test/browser_toolbox_minimize.js
+++ b/devtools/client/framework/test/browser_toolbox_minimize.js
@@ -48,59 +48,59 @@ add_task(function*() {
   let onMaximized = toolbox._host.once("maximized");
   yield toolbox.selectTool("inspector");
   yield onMaximized;
 
   info("Minimize again and click on the tab of the current tool");
   yield minimize(toolbox);
   onMaximized = toolbox._host.once("maximized");
   let tabButton = toolbox.doc.querySelector("#toolbox-tab-inspector");
-  EventUtils.synthesizeMouseAtCenter(tabButton, {}, toolbox.doc.defaultView);
+  EventUtils.synthesizeMouseAtCenter(tabButton, {}, toolbox.win);
   yield onMaximized;
 
   info("Minimize again and click on the settings tab");
   yield minimize(toolbox);
   onMaximized = toolbox._host.once("maximized");
   let settingsButton = toolbox.doc.querySelector("#toolbox-tab-options");
-  EventUtils.synthesizeMouseAtCenter(settingsButton, {}, toolbox.doc.defaultView);
+  EventUtils.synthesizeMouseAtCenter(settingsButton, {}, toolbox.win);
   yield onMaximized;
 
   info("Switch to a different host");
   yield toolbox.switchHost(Toolbox.HostType.SIDE);
   button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
   ok(!button, "The minimize button doesn't exist in the side host");
 
   Services.prefs.clearUserPref("devtools.toolbox.host");
   yield toolbox.destroy();
   gBrowser.removeCurrentTab();
 });
 
 function* minimize(toolbox) {
   let button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
   let onMinimized = toolbox._host.once("minimized");
-  EventUtils.synthesizeMouseAtCenter(button, {}, toolbox.doc.defaultView);
+  EventUtils.synthesizeMouseAtCenter(button, {}, toolbox.win);
   yield onMinimized;
 }
 
 function* minimizeWithShortcut(toolbox) {
   let key = toolbox.doc.getElementById("toolbox-minimize-key")
                        .getAttribute("key");
   let onMinimized = toolbox._host.once("minimized");
   EventUtils.synthesizeKey(key, {accelKey: true, shiftKey: true},
-                           toolbox.doc.defaultView);
+                           toolbox.win);
   yield onMinimized;
 }
 
 function* maximize(toolbox) {
   let button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
   let onMaximized = toolbox._host.once("maximized");
-  EventUtils.synthesizeMouseAtCenter(button, {}, toolbox.doc.defaultView);
+  EventUtils.synthesizeMouseAtCenter(button, {}, toolbox.win);
   yield onMaximized;
 }
 
 function* maximizeWithShortcut(toolbox) {
   let key = toolbox.doc.getElementById("toolbox-minimize-key")
                        .getAttribute("key");
   let onMaximized = toolbox._host.once("maximized");
   EventUtils.synthesizeKey(key, {accelKey: true, shiftKey: true},
-                           toolbox.doc.defaultView);
+                           toolbox.win);
   yield onMaximized;
 }
--- a/devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js
+++ b/devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js
@@ -51,16 +51,16 @@ add_task(function*() {
   gBrowser.removeCurrentTab();
 });
 
 function* testShortcuts(toolbox, index, key, toolIDs) {
   info("Testing shortcut to switch to tool " + index + ":" + toolIDs[index] +
        " using key " + key);
 
   let onToolSelected = toolbox.once("select");
-  EventUtils.synthesizeKey(key, {accelKey: true}, toolbox.doc.defaultView);
+  EventUtils.synthesizeKey(key, {accelKey: true}, toolbox.win);
   let id = yield onToolSelected;
 
   info("toolbox-select event from " + id);
 
   is(toolIDs.indexOf(id), index,
      "Correct tool is selected on pressing the shortcut for " + id);
 }
--- a/devtools/client/framework/test/browser_toolbox_toggle.js
+++ b/devtools/client/framework/test/browser_toolbox_toggle.js
@@ -66,17 +66,17 @@ function* testToggleDetachedToolbox(tab,
 
   info("change the toolbox hostType to WINDOW");
 
   yield toolbox.switchHost(Toolbox.HostType.WINDOW);
   is(toolbox.hostType, Toolbox.HostType.WINDOW,
     "Toolbox opened on separate window");
 
   info("Wait for focus on the toolbox window");
-  yield new Promise(res => waitForFocus(res, toolbox.frame.contentWindow));
+  yield new Promise(res => waitForFocus(res, toolbox.win));
 
   info("Focus main window to put the toolbox window in the background");
 
   let onMainWindowFocus = once(window, "focus");
   window.focus();
   yield onMainWindowFocus;
   ok(true, "Main window focused");
 
--- a/devtools/client/framework/test/browser_toolbox_window_reload_target.js
+++ b/devtools/client/framework/test/browser_toolbox_window_reload_target.js
@@ -38,17 +38,17 @@ function startReloadTest(aToolbox) {
     info("Detected reload #"+reloads);
     is(reloads, reloadsSent, "Reloaded from devtools window once and only for "+description+"");
   };
   gBrowser.selectedBrowser.messageManager.addMessageListener("devtools:test:load", reloadCounter);
 
   testAllTheTools("docked", () => {
     let origHostType = toolbox.hostType;
     toolbox.switchHost(Toolbox.HostType.WINDOW).then(() => {
-      toolbox.doc.defaultView.focus();
+      toolbox.win.focus();
       testAllTheTools("undocked", () => {
         toolbox.switchHost(origHostType).then(() => {
           gBrowser.selectedBrowser.messageManager.removeMessageListener("devtools:test:load", reloadCounter);
           // If we finish too early, the inspector breaks promises:
           toolbox.getPanel("inspector").once("new-root", finishUp);
         });
       });
     });
--- a/devtools/client/framework/test/browser_toolbox_window_shortcuts.js
+++ b/devtools/client/framework/test/browser_toolbox_window_shortcuts.js
@@ -54,17 +54,17 @@ function testShortcuts(aToolbox, aIndex)
   let modifiers = {
     accelKey: toolModifiers.includes("accel"),
     altKey: toolModifiers.includes("alt"),
     shiftKey: toolModifiers.includes("shift"),
   };
   idIndex = aIndex;
   info("Testing shortcut for tool " + aIndex + ":" + toolIDs[aIndex] +
        " using key " + key);
-  EventUtils.synthesizeKey(key, modifiers, toolbox.doc.defaultView.parent);
+  EventUtils.synthesizeKey(key, modifiers, toolbox.win.parent);
 }
 
 function selectCB(event, id) {
   info("toolbox-select event from " + id);
 
   is(toolIDs.indexOf(id), idIndex,
      "Correct tool is selected on pressing the shortcut for " + id);
 
--- a/devtools/client/framework/test/browser_toolbox_zoom.js
+++ b/devtools/client/framework/test/browser_toolbox_zoom.js
@@ -40,17 +40,17 @@ function testZoomLevel(type, times, expe
 
   is(toolbox.zoomValue.toFixed(2), expected,
      "saved zoom level is correct after zoom " + type);
 }
 
 function sendZoomKey(id, times) {
   let key = toolbox.doc.getElementById(id).getAttribute("key");
   for (let i = 0; i < times; i++) {
-    EventUtils.synthesizeKey(key, modifiers, toolbox.doc.defaultView);
+    EventUtils.synthesizeKey(key, modifiers, toolbox.win);
   }
 }
 
 function getCurrentZoom() {
   var contViewer = toolbox.frame.docShell.contentViewer;
   return contViewer.fullZoom;
 }
 
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -280,18 +280,17 @@ var openToolboxForTab = Task.async(funct
       return toolbox;
     }
   }
 
   // If not, load it now.
   toolbox = yield gDevTools.showToolbox(target, toolId, hostType);
 
   // Make sure that the toolbox frame is focused.
-  yield new Promise(resolve => waitForFocus(resolve,
-    toolbox.frame.contentWindow));
+  yield new Promise(resolve => waitForFocus(resolve, toolbox.win));
 
   info("Toolbox opened and focused");
 
   return toolbox;
 });
 
 /**
  * Add a new tab and open the toolbox in it.
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -272,16 +272,23 @@ Toolbox.prototype = {
   /**
    * Get the iframe containing the toolbox UI.
    */
   get frame() {
     return this._host.frame;
   },
 
   /**
+   * Shortcut to the window containing the toolbox UI
+   */
+  get win() {
+    return this.frame.contentWindow;
+  },
+
+  /**
    * Shortcut to the document containing the toolbox UI
    */
   get doc() {
     return this.frame.contentDocument;
   },
 
   /**
    * Get current zoom level of toolbox
@@ -702,17 +709,17 @@ Toolbox.prototype = {
    * @param {number} zoomValue
    *        Zoom level e.g. 1.2
    */
   setZoom: function(zoomValue) {
     // cap zoom value
     zoomValue = Math.max(zoomValue, MIN_ZOOM);
     zoomValue = Math.min(zoomValue, MAX_ZOOM);
 
-    let docShell = this.frame.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+    let docShell = this.win.QueryInterface(Ci.nsIInterfaceRequestor)
       .getInterface(Ci.nsIWebNavigation)
       .QueryInterface(Ci.nsIDocShell);
     let contViewer = docShell.contentViewer;
 
     contViewer.fullZoom = zoomValue;
 
     Services.prefs.setCharPref(ZOOM_PREF, zoomValue);
   },
@@ -720,17 +727,17 @@ Toolbox.prototype = {
   /**
    * Adds the keys and commands to the Toolbox Window in window mode.
    */
   _addKeysToWindow: function() {
     if (this.hostType != Toolbox.HostType.WINDOW) {
       return;
     }
 
-    let doc = this.doc.defaultView.parent.document;
+    let doc = this.win.parent.document;
 
     for (let [id, toolDefinition] of gDevTools.getToolDefinitionMap()) {
       // Prevent multiple entries for the same tool.
       if (!toolDefinition.key || doc.getElementById("key_" + id)) {
         continue;
       }
 
       let toolId = id;
@@ -931,17 +938,17 @@ Toolbox.prototype = {
         // focus to current aria-activedescendant.
         event.preventDefault();
         control.focus();
       }
     }, true)
 
     toolbar.addEventListener("keypress", event => {
       let { key, target } = event;
-      let win = this.doc.defaultView;
+      let win = this.win;
       let elm, type;
       if (key === "Tab") {
         // Tabbing when toolbar or its contents are focused should move focus to
         // next/previous focusable element relative to toolbar itself.
         if (event.shiftKey) {
           elm = toolbar;
           type = Services.focus.MOVEFOCUS_BACKWARD;
         } else {
@@ -1803,17 +1810,17 @@ Toolbox.prototype = {
     return newHost.create().then(iframe => {
       // change toolbox document's parent to the new host
       iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
       iframe.swapFrameLoaders(this.frame);
 
       // See bug 1022726, most probably because of swapFrameLoaders we need to
       // first focus the window here, and then once again further below to make
       // sure focus actually happens.
-      this.frame.contentWindow.focus();
+      this.win.focus();
 
       this._host.off("window-closed", this.destroy);
       this.destroyHost();
 
       let prevHostType = this._host.type;
       this._host = newHost;
 
       if (this.hostType != Toolbox.HostType.CUSTOM) {
@@ -1821,17 +1828,17 @@ Toolbox.prototype = {
         Services.prefs.setCharPref(this._prefs.PREVIOUS_HOST, prevHostType);
       }
 
       this._buildDockButtons();
       this._addKeysToWindow();
 
       // Focus the contentWindow to make sure keyboard shortcuts work straight
       // away.
-      this.frame.contentWindow.focus();
+      this.win.focus();
 
       this.emit("host-changed");
     });
   },
 
   /**
    * Return if the tool is available as a tab (i.e. if it's checked
    * in the options panel). This is different from Toolbox.getPanel -
@@ -1895,17 +1902,17 @@ Toolbox.prototype = {
       radio.parentNode.removeChild(radio);
     }
 
     if (panel) {
       panel.parentNode.removeChild(panel);
     }
 
     if (this.hostType == Toolbox.HostType.WINDOW) {
-      let doc = this.doc.defaultView.parent.document;
+      let doc = this.win.parent.document;
       let key = doc.getElementById("key_" + toolId);
       if (key) {
         key.parentNode.removeChild(key);
       }
     }
     // Emit the event so tools can listen to it from the toolbox level
     // instead of gDevTools
     this.emit("tool-unregistered", toolId);
@@ -2159,25 +2166,24 @@ Toolbox.prototype = {
    * For displaying the promotional Doorhanger on first opening of
    * the developer tools, promoting the Developer Edition.
    */
   _showDevEditionPromo: function() {
     // Do not display in browser toolbox
     if (this.target.chrome) {
       return;
     }
-    let window = this.frame.contentWindow;
-    showDoorhanger({ window, type: "deveditionpromo" });
+    showDoorhanger({ window: this.win, type: "deveditionpromo" });
   },
 
   /**
    * Enable / disable necessary textbox menu items using globalOverlay.js.
    */
   _updateTextboxMenuItems: function() {
-    let window = this.doc.defaultView;
+    let window = this.win;
     ["cmd_undo", "cmd_delete", "cmd_cut",
      "cmd_copy", "cmd_paste", "cmd_selectAll"].forEach(window.goUpdateCommand);
   },
 
   /**
    * Connects to the SPS profiler when the developer tools are open. This is
    * necessary because of the WebConsole's `profile` and `profileEnd` methods.
    */
@@ -2257,17 +2263,17 @@ Toolbox.prototype = {
       recordings.push(recording);
     }
   }),
 
   /**
    * Returns gViewSourceUtils for viewing source.
    */
   get gViewSourceUtils() {
-    return this.frame.contentWindow.gViewSourceUtils;
+    return this.win.gViewSourceUtils;
   },
 
   /**
    * Opens source in style editor. Falls back to plain "view-source:".
    * @see devtools/client/shared/source-utils.js
    */
   viewSourceInStyleEditor: function(sourceURL, sourceLine) {
     return viewSource.viewSourceInStyleEditor(this, sourceURL, sourceLine);
new file mode 100644
--- /dev/null
+++ b/devtools/client/fronts/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'storage.js',
+    'stylesheets.js'
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/fronts/storage.js
@@ -0,0 +1,32 @@
+/* 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 protocol = require("devtools/server/protocol");
+const specs = require("devtools/shared/specs/storage");
+
+for (let childSpec of Object.values(specs.childSpecs)) {
+  protocol.FrontClassWithSpec(childSpec, {
+    form(form, detail) {
+      if (detail === "actorid") {
+        this.actorID = form;
+        return null;
+      }
+
+      this.actorID = form.actor;
+      this.hosts = form.hosts;
+      return null;
+    }
+  });
+}
+
+const StorageFront = protocol.FrontClassWithSpec(specs.storageSpec, {
+  initialize(client, tabForm) {
+    protocol.Front.prototype.initialize.call(this, client);
+    this.actorID = tabForm.storageActor;
+    this.manage(this);
+  }
+});
+
+exports.StorageFront = StorageFront;
new file mode 100644
--- /dev/null
+++ b/devtools/client/fronts/stylesheets.js
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Front, FrontClassWithSpec } = require("devtools/server/protocol.js");
+const { originalSourceSpec } = require("devtools/shared/specs/stylesheets.js");
+
+/**
+ * The client-side counterpart for an OriginalSourceActor.
+ */
+const OriginalSourceFront = FrontClassWithSpec(originalSourceSpec, {
+  initialize: function (client, form) {
+    Front.prototype.initialize.call(this, client, form);
+
+    this.isOriginalSource = true;
+  },
+
+  form: function (form, detail) {
+    if (detail === "actorid") {
+      this.actorID = form;
+      return;
+    }
+    this.actorID = form.actor;
+    this._form = form;
+  },
+
+  get href() {
+    return this._form.url;
+  },
+  get url() {
+    return this._form.url;
+  }
+});
+
+exports.OriginalSourceFront = OriginalSourceFront;
--- a/devtools/client/inspector/inspector-panel.js
+++ b/devtools/client/inspector/inspector-panel.js
@@ -983,18 +983,17 @@ InspectorPanel.prototype = {
 
   _onMarkupFrameLoad: function() {
     this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
 
     this._markupFrame.contentWindow.focus();
 
     this._markupBox.removeAttribute("collapsed");
 
-    let controllerWindow = this._toolbox.doc.defaultView;
-    this.markup = new MarkupView(this, this._markupFrame, controllerWindow);
+    this.markup = new MarkupView(this, this._markupFrame, this._toolbox.win);
 
     this.emit("markuploaded");
   },
 
   _destroyMarkup: function() {
     let destroyPromise;
 
     if (this._markupFrame) {
--- a/devtools/client/inspector/inspector-search.js
+++ b/devtools/client/inspector/inspector-search.js
@@ -175,16 +175,17 @@ SelectorAutocompleter.prototype = {
     return this.inspector.walker;
   },
 
   // The possible states of the query.
   States: {
     CLASS: "class",
     ID: "id",
     TAG: "tag",
+    ATTRIBUTE: "attribute",
   },
 
   // The current state of the query.
   _state: null,
 
   // The query corresponding to last state computation.
   _lastStateCheckAt: null,
 
@@ -223,42 +224,69 @@ SelectorAutocompleter.prototype = {
       // Calculate the state.
       subQuery = query.slice(0, i);
       let [secondLastChar, lastChar] = subQuery.slice(-2);
       switch (this._state) {
         case null:
           // This will happen only in the first iteration of the for loop.
           lastChar = secondLastChar;
         case this.States.TAG:
-          this._state = lastChar == "."
-            ? this.States.CLASS
-            : lastChar == "#"
-              ? this.States.ID
-              : this.States.TAG;
+          if (lastChar == ".") {
+            this._state = this.States.CLASS;
+          } else if (lastChar == "#") {
+            this._state = this.States.ID;
+          } else if (lastChar == "[") {
+            this._state = this.States.ATTRIBUTE;
+          } else {
+            this._state = this.States.TAG;
+          }
           break;
 
         case this.States.CLASS:
           if (subQuery.match(/[\.]+[^\.]*$/)[0].length > 2) {
             // Checks whether the subQuery has atleast one [a-zA-Z] after the '.'.
-            this._state = (lastChar == " " || lastChar == ">")
-            ? this.States.TAG
-            : lastChar == "#"
-              ? this.States.ID
-              : this.States.CLASS;
+            if (lastChar == " " || lastChar == ">") {
+              this._state = this.States.TAG;
+            } else if(lastChar == "#") {
+              this._state = this.States.ID;
+            } else if(lastChar == "[") {
+              this._state = this.States.ATTRIBUTE;
+            } else {
+              this._state = this.States.CLASS;
+            }
           }
           break;
 
         case this.States.ID:
           if (subQuery.match(/[#]+[^#]*$/)[0].length > 2) {
             // Checks whether the subQuery has atleast one [a-zA-Z] after the '#'.
-            this._state = (lastChar == " " || lastChar == ">")
-            ? this.States.TAG
-            : lastChar == "."
-              ? this.States.CLASS
-              : this.States.ID;
+            if (lastChar == " " || lastChar == ">") {
+              this._state = this.States.TAG;
+            } else if (lastChar == ".") {
+              this._state = this.States.CLASS;
+            } else if (lastChar == "[") {
+              this._state = this.States.ATTRIBUTE;
+            } else {
+              this._state = this.States.ID;
+            }
+          }
+          break;
+
+        case this.States.ATTRIBUTE:
+          if (subQuery.match(/[\[][^\]]+[\]]/) != null) {
+            // Checks whether the subQuery has at least one ']' after the '['.
+            if (lastChar == " " || lastChar == ">") {
+              this._state = this.States.TAG;
+            } else if (lastChar == ".") {
+              this._state = this.States.CLASS;
+            } else if (lastChar == "#") {
+              this._state = this.States.ID;
+            } else {
+              this._state = this.States.ATTRIBUTE;
+            }
           }
           break;
       }
     }
     return this._state;
   },
 
   /**
@@ -407,19 +435,20 @@ SelectorAutocompleter.prototype = {
       } else if (query.match(/[\s>+][\.#a-zA-Z][^\s>+\.#\[]*$/)) {
         // for cases like 'div #a' or 'div .a' or 'div > d' and likewise
         let lastPart = query.match(/[\s>+][\.#a-zA-Z][^\s>+\.#\[]*$/)[0];
         value = query.slice(0, -1 * lastPart.length + 1) + value;
       } else if (query.match(/[a-zA-Z][#\.][^#\.\s+>]*$/)) {
         // for cases like 'div.class' or '#foo.bar' and likewise
         let lastPart = query.match(/[a-zA-Z][#\.][^#\.\s+>]*$/)[0];
         value = query.slice(0, -1 * lastPart.length + 1) + value;
-      } else if (query.match(/[a-zA-Z]\[[^\]]*\]?$/)) {
-        // for cases like 'div[foo=bar]' and likewise
-        value = query;
+      } else if (query.match(/[a-zA-Z]*\[[^\]]*\][^\]]*/)) {
+        // for cases like '[foo].bar' and likewise
+        let attrPart = query.substring(0, query.lastIndexOf("]") + 1);
+        value = attrPart + value;
       }
 
       let item = {
         preLabel: query,
         label: value
       };
 
       // In case of tagNames, change the case to small
@@ -464,19 +493,20 @@ SelectorAutocompleter.prototype = {
    * Suggests classes,ids and tags based on the user input as user types in the
    * searchbox.
    */
   showSuggestions: function() {
     let query = this.searchBox.value;
     let state = this.state;
     let firstPart = "";
 
-    if (query.endsWith("*")) {
-      // Hide the popup if the query ends with * because we don't want to
-      // suggest all nodes.
+    if (query.endsWith("*") || state === this.States.ATTRIBUTE) {
+      // Hide the popup if the query ends with * (because we don't want to
+      // suggest all nodes) or if it is an attribute selector (because
+      // it would give a lot of useless results).
       this.hidePopup();
       return;
     }
 
     if (state === this.States.TAG) {
       // gets the tag that is being completed. For ex. 'div.foo > s' returns 's',
       // 'di' returns 'di' and likewise.
       firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["", query])[1];
--- a/devtools/client/inspector/test/browser_inspector_search-03.js
+++ b/devtools/client/inspector/test/browser_inspector_search-03.js
@@ -184,19 +184,35 @@ var TEST_DATA = [
   },
   {
     key: "=", suggestions: []
   },
   {
     key: "p", suggestions: []
   },
   {
-    key: "]",
+    key: "]", suggestions: []
+  },
+  {
+    key: ".",
     suggestions: [
-      {label: "p[id*=p]"}
+      {label: "p[id*=p].c1"},
+      {label: "p[id*=p].c2"}
+    ]
+  },
+  {
+    key: "VK_BACK_SPACE",
+    suggestions: []
+  },
+  {
+    key: "#",
+    suggestions: [
+      {label: "p[id*=p]#p1"},
+      {label: "p[id*=p]#p2"},
+      {label: "p[id*=p]#p3"}
     ]
   }
 ];
 
 add_task(function* () {
   let { inspector } = yield openInspectorForURL(TEST_URL);
   let searchBox = inspector.searchBox;
   let popup = inspector.searchSuggestions.searchPopup;
--- a/devtools/client/inspector/test/browser_inspector_search-reserved.js
+++ b/devtools/client/inspector/test/browser_inspector_search-reserved.js
@@ -85,18 +85,17 @@ const TEST_DATA = [
     suggestions: [{label: "body .c1\\.c2"}]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [{label: "body div"}]
   },
   {
     key: "#",
-    suggestions: [{label: "body #"},
-                  {label: "body #d1\\.d2"}]
+    suggestions: [{label: "body #d1\\.d2"}]
   }
 ];
 
 add_task(function* () {
   let { inspector } = yield openInspectorForURL(TEST_URL);
   let searchBox = inspector.searchBox;
   let popup = inspector.searchSuggestions.searchPopup;
 
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -431,17 +431,17 @@ var clickContainer = Task.async(function
 function mouseLeaveMarkupView(inspector) {
   info("Leaving the markup-view area");
   let def = promise.defer();
 
   // Find another element to mouseover over in order to leave the markup-view
   let btn = inspector.toolbox.doc.querySelector("#toolbox-controls");
 
   EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"},
-    inspector.toolbox.doc.defaultView);
+    inspector.toolbox.win);
   executeSoon(def.resolve);
 
   return def.promise;
 }
 
 /**
  * Dispatch the copy event on the given element
  */
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -279,24 +279,26 @@ devtools.jar:
     skin/images/emojis/emoji-tool-styleeditor.svg (themes/images/emojis/emoji-tool-styleeditor.svg)
     skin/images/emojis/emoji-tool-storage.svg (themes/images/emojis/emoji-tool-storage.svg)
     skin/images/emojis/emoji-tool-profiler.svg (themes/images/emojis/emoji-tool-profiler.svg)
     skin/images/emojis/emoji-tool-network.svg (themes/images/emojis/emoji-tool-network.svg)
     skin/images/emojis/emoji-tool-scratchpad.svg (themes/images/emojis/emoji-tool-scratchpad.svg)
     skin/images/emojis/emoji-tool-webaudio.svg (themes/images/emojis/emoji-tool-webaudio.svg)
     skin/images/emojis/emoji-tool-memory.svg (themes/images/emojis/emoji-tool-memory.svg)
     skin/images/emojis/emoji-tool-dom.svg (themes/images/emojis/emoji-tool-dom.svg)
+    skin/images/debugging-addons.svg (themes/images/debugging-addons.svg)
+    skin/images/debugging-devices.svg (themes/images/debugging-devices.svg)
+    skin/images/debugging-tabs.svg (themes/images/debugging-tabs.svg)
+    skin/images/debugging-workers.svg (themes/images/debugging-workers.svg)
+    skin/images/tabs-icon.svg (themes/images/tabs-icon.svg)
     skin/images/tool-options.svg (themes/images/tool-options.svg)
     skin/images/tool-webconsole.svg (themes/images/tool-webconsole.svg)
     skin/images/tool-canvas.svg (themes/images/tool-canvas.svg)
     skin/images/tool-debugger.svg (themes/images/tool-debugger.svg)
     skin/images/tool-debugger-paused.svg (themes/images/tool-debugger-paused.svg)
-    skin/images/debugging-addons.svg (themes/images/debugging-addons.svg)
-    skin/images/debugging-devices.svg (themes/images/debugging-devices.svg)
-    skin/images/debugging-workers.svg (themes/images/debugging-workers.svg)
     skin/images/tool-inspector.svg (themes/images/tool-inspector.svg)
     skin/images/tool-shadereditor.svg (themes/images/tool-shadereditor.svg)
     skin/images/tool-styleeditor.svg (themes/images/tool-styleeditor.svg)
     skin/images/tool-storage.svg (themes/images/tool-storage.svg)
     skin/images/tool-profiler.svg (themes/images/tool-profiler.svg)
     skin/images/tool-profiler-active.svg (themes/images/tool-profiler-active.svg)
     skin/images/tool-network.svg (themes/images/tool-network.svg)
     skin/images/tool-scratchpad.svg (themes/images/tool-scratchpad.svg)
--- a/devtools/client/locales/en-US/aboutdebugging.properties
+++ b/devtools/client/locales/en-US/aboutdebugging.properties
@@ -18,10 +18,12 @@ extensions = Extensions
 selectAddonFromFile2 = Select Manifest File or Package (.xpi)
 reload = Reload
 
 workers = Workers
 serviceWorkers = Service Workers
 sharedWorkers = Shared Workers
 otherWorkers = Other Workers
 
+tabs = Tabs
+
 nothing = Nothing yet.
 
--- a/devtools/client/locales/en-US/animationinspector.properties
+++ b/devtools/client/locales/en-US/animationinspector.properties
@@ -83,16 +83,26 @@ player.timeLabel=%Ss
 # animation runs (1× being the default, 2× being twice as fast).
 player.playbackRateLabel=%S×
 
 # LOCALIZATION NOTE (player.runningOnCompositorTooltip):
 # This string is displayed as a tooltip for the icon that indicates that the
 # animation is running on the compositor thread.
 player.runningOnCompositorTooltip=This animation is running on compositor thread
 
+# LOCALIZATION NOTE (player.allPropertiesOnCompositorTooltip):
+# This string is displayed as a tooltip for the icon that indicates that
+# all of animation is running on the compositor thread.
+player.allPropertiesOnCompositorTooltip=All animation properties are optimized
+
+# LOCALIZATION NOTE (player.somePropertiesOnCompositorTooltip):
+# This string is displayed as a tooltip for the icon that indicates that
+# all of animation is not running on the compositor thread.
+player.somePropertiesOnCompositorTooltip=Some animation properties are optimized
+
 # LOCALIZATION NOTE (timeline.rateSelectorTooltip):
 # This string is displayed in the timeline toolbar, as the tooltip of the
 # drop-down list that can be used to change the rate at which the animations
 # run.
 timeline.rateSelectorTooltip=Set the animations playback rates
 
 # LOCALIZATION NOTE (timeline.pauseResumeButtonTooltip):
 # This string is displayed in the timeline toolbar, as the tooltip of the
--- a/devtools/client/moz.build
+++ b/devtools/client/moz.build
@@ -10,16 +10,17 @@ DIRS += [
     'aboutdebugging',
     'animationinspector',
     'canvasdebugger',
     'commandline',
     'debugger',
     'dom',
     'eyedropper',
     'framework',
+    'fronts',
     'inspector',
     'jsonview',
     'locales',
     'memory',
     'netmonitor',
     'performance',
     'preferences',
     'projecteditor',
--- a/devtools/client/performance/modules/widgets/graphs.js
+++ b/devtools/client/performance/modules/widgets/graphs.js
@@ -13,17 +13,17 @@ const { Heritage } = require("resource:/
 const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
 const BarGraphWidget = require("devtools/client/shared/widgets/BarGraphWidget");
 const MountainGraphWidget = require("devtools/client/shared/widgets/MountainGraphWidget");
 const { CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
 
 const promise = require("promise");
 const EventEmitter = require("devtools/shared/event-emitter");
 
-const { colorUtils } = require("devtools/shared/css-color");
+const { colorUtils } = require("devtools/client/shared/css-color");
 const { getColor } = require("devtools/client/shared/theme");
 const ProfilerGlobal = require("devtools/client/performance/modules/global");
 const { MarkersOverview } = require("devtools/client/performance/modules/widgets/markers-overview");
 const { createTierGraphDataFromFrameNode } = require("devtools/client/performance/modules/logic/jit");
 
 /**
  * For line graphs
  */
--- a/devtools/client/performance/modules/widgets/markers-overview.js
+++ b/devtools/client/performance/modules/widgets/markers-overview.js
@@ -8,17 +8,17 @@
  * the timeline data. Regions inside it may be selected, determining which
  * markers are visible in the "waterfall".
  */
 
 const { Cc, Ci, Cu, Cr } = require("chrome");
 const { Heritage } = require("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 const { AbstractCanvasGraph } = require("devtools/client/shared/widgets/Graphs");
 
-const { colorUtils } = require("devtools/shared/css-color");
+const { colorUtils } = require("devtools/client/shared/css-color");
 const { getColor } = require("devtools/client/shared/theme");
 const ProfilerGlobal = require("devtools/client/performance/modules/global");
 const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
 const { TickUtils } = require("devtools/client/performance/modules/widgets/waterfall-ticks");
 const { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
 
 const OVERVIEW_HEADER_HEIGHT = 14; // px
 const OVERVIEW_ROW_HEIGHT = 11; // px
--- a/devtools/client/responsivedesign/test/head.js
+++ b/devtools/client/responsivedesign/test/head.js
@@ -123,18 +123,17 @@ var closeToolbox = Task.async(function*(
 /**
  * Wait for the toolbox frame to receive focus after it loads
  * @param {Toolbox} toolbox
  * @return a promise that resolves when focus has been received
  */
 function waitForToolboxFrameFocus(toolbox) {
   info("Making sure that the toolbox's frame is focused");
   let def = promise.defer();
-  let win = toolbox.frame.contentWindow;
-  waitForFocus(def.resolve, win);
+  waitForFocus(def.resolve, toolbox.win);
   return def.promise;
 }
 
 /**
  * Open the toolbox, with the inspector tool visible, and the sidebar that
  * corresponds to the given id selected
  * @return a promise that resolves when the inspector is ready and the sidebar
  * view is visible and ready
--- a/devtools/client/shared/components/test/mochitest/chrome.ini
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 support-files =
   head.js
 
 [test_HSplitBox_01.html]
+[test_reps_undefined.html]
 [test_frame_01.html]
 [test_tree_01.html]
 [test_tree_02.html]
 [test_tree_03.html]
 [test_tree_04.html]
 [test_tree_05.html]
 [test_tree_06.html]
 [test_tree_07.html]
--- a/devtools/client/shared/components/test/mochitest/head.js
+++ b/devtools/client/shared/components/test/mochitest/head.js
@@ -19,16 +19,20 @@ var { TargetFactory } = require("devtool
 var { Toolbox } = require("devtools/client/framework/toolbox");
 
 DevToolsUtils.testing = true;
 var { require: browserRequire } = BrowserLoader({
   baseURI: "resource://devtools/client/shared/",
   window: this
 });
 
+let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+let React = browserRequire("devtools/client/shared/vendor/react");
+var TestUtils = React.addons.TestUtils;
+
 var EXAMPLE_URL = "http://example.com/browser/browser/devtools/shared/test/";
 
 function forceRender(comp) {
   return setState(comp, {})
     .then(() => setState(comp, {}));
 }
 
 // All tests are asynchronous.
@@ -159,8 +163,25 @@ function checkFrameString({ frame, file,
   }
 
   if (column != null) {
     is(+$column.textContent, +column);
   } else {
     ok(!$column, "Should not have an element for `column`");
   }
 }
+
+function renderComponent(component, props) {
+  const el = React.createElement(component, props, {});
+  // By default, renderIntoDocument() won't work for stateless components, but
+  // it will work if the stateless component is wrapped in a stateful one.
+  // See https://github.com/facebook/react/issues/4839
+  const wrappedEl = React.DOM.span({}, [el]);
+  const renderedComponent = TestUtils.renderIntoDocument(wrappedEl);
+  return ReactDOM.findDOMNode(renderedComponent).children[0];
+}
+
+function shallowRenderComponent(component, props) {
+  const el = React.createElement(component, props);
+  const renderer = TestUtils.createRenderer();
+  renderer.render(el, {});
+  return renderer.getRenderOutput();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_undefined.html
@@ -0,0 +1,46 @@
+
+<!DOCTYPE HTML>
+<html>
+<!--
+Test undefined rep
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Rep test - undefined</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+  try {
+    let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+    let React = browserRequire("devtools/client/shared/vendor/react");
+    let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+    let { Undefined } = browserRequire("devtools/client/shared/components/reps/undefined");
+
+    let gripStub = {
+      "type": "undefined"
+    };
+
+    // Test that correct rep is chosen
+    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+    is(renderedRep.type, Undefined.rep, `Rep correctly selects ${Undefined.rep.displayName}`);
+
+    // Test rendering
+    const renderedComponent = renderComponent(Undefined.rep, {});
+    is(renderedComponent.className, "objectBox objectBox-undefined", "Undefined rep has expected class names");
+    is(renderedComponent.getAttribute("role"), "presentation", "Undefined rep has expected aria role");
+    is(renderedComponent.textContent, "undefined", "Undefined rep has expected text content");
+  } catch(e) {
+    ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+  } finally {
+    SimpleTest.finish();
+  }
+});
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/css-color-db.js
@@ -0,0 +1,162 @@
+/* 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";
+
+// /!\  Auto-generated from nsColorNameList.h.
+// This should be kept in sync with that list.
+// test_cssColorDatabase.js tries to enforce this.
+
+const cssColors = {
+  aliceblue: [240, 248, 255, 1],
+  antiquewhite: [250, 235, 215, 1],
+  aqua: [0, 255, 255, 1],
+  aquamarine: [127, 255, 212, 1],
+  azure: [240, 255, 255, 1],
+  beige: [245, 245, 220, 1],
+  bisque: [255, 228, 196, 1],
+  black: [0, 0, 0, 1],
+  blanchedalmond: [255, 235, 205, 1],
+  blue: [0, 0, 255, 1],
+  blueviolet: [138, 43, 226, 1],
+  brown: [165, 42, 42, 1],
+  burlywood: [222, 184, 135, 1],
+  cadetblue: [95, 158, 160, 1],
+  chartreuse: [127, 255, 0, 1],
+  chocolate: [210, 105, 30, 1],
+  coral: [255, 127, 80, 1],
+  cornflowerblue: [100, 149, 237, 1],
+  cornsilk: [255, 248, 220, 1],
+  crimson: [220, 20, 60, 1],
+  cyan: [0, 255, 255, 1],
+  darkblue: [0, 0, 139, 1],
+  darkcyan: [0, 139, 139, 1],
+  darkgoldenrod: [184, 134, 11, 1],
+  darkgray: [169, 169, 169, 1],
+  darkgreen: [0, 100, 0, 1],
+  darkgrey: [169, 169, 169, 1],
+  darkkhaki: [189, 183, 107, 1],
+  darkmagenta: [139, 0, 139, 1],
+  darkolivegreen: [85, 107, 47, 1],
+  darkorange: [255, 140, 0, 1],
+  darkorchid: [153, 50, 204, 1],
+  darkred: [139, 0, 0, 1],
+  darksalmon: [233, 150, 122, 1],
+  darkseagreen: [143, 188, 143, 1],
+  darkslateblue: [72, 61, 139, 1],
+  darkslategray: [47, 79, 79, 1],
+  darkslategrey: [47, 79, 79, 1],
+  darkturquoise: [0, 206, 209, 1],
+  darkviolet: [148, 0, 211, 1],
+  deeppink: [255, 20, 147, 1],
+  deepskyblue: [0, 191, 255, 1],
+  dimgray: [105, 105, 105, 1],
+  dimgrey: [105, 105, 105, 1],
+  dodgerblue: [30, 144, 255, 1],
+  firebrick: [178, 34, 34, 1],
+  floralwhite: [255, 250, 240, 1],
+  forestgreen: [34, 139, 34, 1],
+  fuchsia: [255, 0, 255, 1],
+  gainsboro: [220, 220, 220, 1],
+  ghostwhite: [248, 248, 255, 1],
+  gold: [255, 215, 0, 1],
+  goldenrod: [218, 165, 32, 1],
+  gray: [128, 128, 128, 1],
+  grey: [128, 128, 128, 1],
+  green: [0, 128, 0, 1],
+  greenyellow: [173, 255, 47, 1],
+  honeydew: [240, 255, 240, 1],
+  hotpink: [255, 105, 180, 1],
+  indianred: [205, 92, 92, 1],
+  indigo: [75, 0, 130, 1],
+  ivory: [255, 255, 240, 1],
+  khaki: [240, 230, 140, 1],
+  lavender: [230, 230, 250, 1],
+  lavenderblush: [255, 240, 245, 1],
+  lawngreen: [124, 252, 0, 1],
+  lemonchiffon: [255, 250, 205, 1],
+  lightblue: [173, 216, 230, 1],
+  lightcoral: [240, 128, 128, 1],
+  lightcyan: [224, 255, 255, 1],
+  lightgoldenrodyellow: [250, 250, 210, 1],
+  lightgray: [211, 211, 211, 1],
+  lightgreen: [144, 238, 144, 1],
+  lightgrey: [211, 211, 211, 1],
+  lightpink: [255, 182, 193, 1],
+  lightsalmon: [255, 160, 122, 1],
+  lightseagreen: [32, 178, 170, 1],
+  lightskyblue: [135, 206, 250, 1],
+  lightslategray: [119, 136, 153, 1],
+  lightslategrey: [119, 136, 153, 1],
+  lightsteelblue: [176, 196, 222, 1],
+  lightyellow: [255, 255, 224, 1],
+  lime: [0, 255, 0, 1],
+  limegreen: [50, 205, 50, 1],
+  linen: [250, 240, 230, 1],
+  magenta: [255, 0, 255, 1],
+  maroon: [128, 0, 0, 1],
+  mediumaquamarine: [102, 205, 170, 1],
+  mediumblue: [0, 0, 205, 1],
+  mediumorchid: [186, 85, 211, 1],
+  mediumpurple: [147, 112, 219, 1],
+  mediumseagreen: [60, 179, 113, 1],
+  mediumslateblue: [123, 104, 238, 1],
+  mediumspringgreen: [0, 250, 154, 1],
+  mediumturquoise: [72, 209, 204, 1],
+  mediumvioletred: [199, 21, 133, 1],
+  midnightblue: [25, 25, 112, 1],
+  mintcream: [245, 255, 250, 1],
+  mistyrose: [255, 228, 225, 1],
+  moccasin: [255, 228, 181, 1],
+  navajowhite: [255, 222, 173, 1],
+  navy: [0, 0, 128, 1],
+  oldlace: [253, 245, 230, 1],
+  olive: [128, 128, 0, 1],
+  olivedrab: [107, 142, 35, 1],
+  orange: [255, 165, 0, 1],
+  orangered: [255, 69, 0, 1],
+  orchid: [218, 112, 214, 1],
+  palegoldenrod: [238, 232, 170, 1],
+  palegreen: [152, 251, 152, 1],
+  paleturquoise: [175, 238, 238, 1],
+  palevioletred: [219, 112, 147, 1],
+  papayawhip: [255, 239, 213, 1],
+  peachpuff: [255, 218, 185, 1],
+  peru: [205, 133, 63, 1],
+  pink: [255, 192, 203, 1],
+  plum: [221, 160, 221, 1],
+  powderblue: [176, 224, 230, 1],
+  purple: [128, 0, 128, 1],
+  rebeccapurple: [102, 51, 153, 1],
+  red: [255, 0, 0, 1],
+  rosybrown: [188, 143, 143, 1],
+  royalblue: [65, 105, 225, 1],
+  saddlebrown: [139, 69, 19, 1],
+  salmon: [250, 128, 114, 1],
+  sandybrown: [244, 164, 96, 1],
+  seagreen: [46, 139, 87, 1],
+  seashell: [255, 245, 238, 1],
+  sienna: [160, 82, 45, 1],
+  silver: [192, 192, 192, 1],
+  skyblue: [135, 206, 235, 1],
+  slateblue: [106, 90, 205, 1],
+  slategray: [112, 128, 144, 1],
+  slategrey: [112, 128, 144, 1],
+  snow: [255, 250, 250, 1],
+  springgreen: [0, 255, 127, 1],
+  steelblue: [70, 130, 180, 1],
+  tan: [210, 180, 140, 1],
+  teal: [0, 128, 128, 1],
+  thistle: [216, 191, 216, 1],
+  tomato: [255, 99, 71, 1],
+  turquoise: [64, 224, 208, 1],
+  violet: [238, 130, 238, 1],
+  wheat: [245, 222, 179, 1],
+  white: [255, 255, 255, 1],
+  whitesmoke: [245, 245, 245, 1],
+  yellow: [255, 255, 0, 1],
+  yellowgreen: [154, 205, 50, 1],
+};
+
+exports.cssColors = cssColors;
rename from devtools/shared/css-color.js
rename to devtools/client/shared/css-color.js
--- a/devtools/shared/css-color.js
+++ b/devtools/client/shared/css-color.js
@@ -2,16 +2,18 @@
  * 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} = require("chrome");
 const Services = require("Services");
 
+const {cssColors} = require("devtools/client/shared/css-color-db");
+
 const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
 
 const SPECIALVALUES = new Set([
   "currentcolor",
   "initial",
   "inherit",
   "transparent",
   "unset"
@@ -53,17 +55,20 @@ const SPECIALVALUES = new Set([
 function CssColor(colorValue) {
   this.newColor(colorValue);
 }
 
 module.exports.colorUtils = {
   CssColor: CssColor,
   rgbToHsl: rgbToHsl,
   setAlpha: setAlpha,
-  classifyColor: classifyColor
+  classifyColor: classifyColor,
+  rgbToColorName: rgbToColorName,
+  colorToRGBA: colorToRGBA,
+  isValidCSSColor: isValidCSSColor,
 };
 
 /**
  * Values used in COLOR_UNIT_PREF
  */
 CssColor.COLORUNIT = {
   "authored": "authored",
   "hex": "hex",
@@ -113,17 +118,17 @@ CssColor.prototype = {
   get hasAlpha() {
     if (!this.valid) {
       return false;
     }
     return this._getRGBATuple().a !== 1;
   },
 
   get valid() {
-    return DOMUtils.isValidCSSColor(this.authored);
+    return isValidCSSColor(this.authored);
   },
 
   /**
    * Return true for all transparent values e.g. rgba(0, 0, 0, 0).
    */
   get transparent() {
     try {
       let tuple = this._getRGBATuple();
@@ -145,17 +150,17 @@ CssColor.prototype = {
 
     try {
       let tuple = this._getRGBATuple();
 
       if (tuple.a !== 1) {
         return this.rgb;
       }
       let {r, g, b} = tuple;
-      return DOMUtils.rgbToColorName(r, g, b);
+      return rgbToColorName(r, g, b);
     } catch (e) {
       return this.hex;
     }
   },
 
   get hex() {
     let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
     if (invalidOrSpecialValue !== false) {
@@ -464,11 +469,320 @@ function classifyColor(value) {
   } else if (value.startsWith("hsl(") || value.startsWith("hsla(")) {
     return CssColor.COLORUNIT.hsl;
   } else if (/^#[0-9a-f]+$/.exec(value)) {
     return CssColor.COLORUNIT.hex;
   }
   return CssColor.COLORUNIT.name;
 }
 
+// This holds a map from colors back to color names for use by
+// rgbToColorName.
+var cssRGBMap;
+
+/**
+ * Given a color, return its name, if it has one.  Throws an exception
+ * if the color does not have a name.
+ *
+ * @param {Number} r, g, b  The color components.
+ * @return {String} the name of the color
+ */
+function rgbToColorName(r, g, b) {
+  if (!cssRGBMap) {
+    cssRGBMap = {};
+    for (let name in cssColors) {
+      let key = JSON.stringify(cssColors[name]);
+      if (!(key in cssRGBMap)) {
+        cssRGBMap[key] = name;
+      }
+    }
+  }
+  let value = cssRGBMap[JSON.stringify([r, g, b, 1])];
+  if (!value) {
+    throw new Error("no such color");
+  }
+  return value;
+}
+
+// Originally from dom/tests/mochitest/ajax/mochikit/MochiKit/Color.js.
+function _hslValue(n1, n2, hue) {
+  if (hue > 6.0) {
+    hue -= 6.0;
+  } else if (hue < 0.0) {
+    hue += 6.0;
+  }
+  let val;
+  if (hue < 1.0) {
+    val = n1 + (n2 - n1) * hue;
+  } else if (hue < 3.0) {
+    val = n2;
+  } else if (hue < 4.0) {
+    val = n1 + (n2 - n1) * (4.0 - hue);
+  } else {
+    val = n1;
+  }
+  return val;
+}
+
+// Originally from dom/tests/mochitest/ajax/mochikit/MochiKit/Color.js.
+function hslToRGB([hue, saturation, lightness]) {
+  let red;
+  let green;
+  let blue;
+  if (saturation === 0) {
+    red = lightness;
+    green = lightness;
+    blue = lightness;
+  } else {
+    let m2;
+    if (lightness <= 0.5) {
+      m2 = lightness * (1.0 + saturation);
+    } else {
+      m2 = lightness + saturation - (lightness * saturation);
+    }
+    let m1 = (2.0 * lightness) - m2;
+    let f = _hslValue;
+    let h6 = hue * 6.0;
+    red = f(m1, m2, h6 + 2);
+    green = f(m1, m2, h6);
+    blue = f(m1, m2, h6 - 2);
+  }
+  return [red, green, blue];
+}
+
+/**
+ * A helper function to convert a hex string like "F0C" to a color.
+ *
+ * @param {String} name the color string
+ * @return {Object} an object of the form {r, g, b, a}; or null if the
+ *         name was not a valid color
+ */
+function hexToRGBA(name) {
+  let r, g, b;
+
+  if (name.length === 3) {
+    let val = parseInt(name, 16);
+    b = ((val & 15) << 4) + (val & 15);
+    val >>= 4;
+    g = ((val & 15) << 4) + (val & 15);
+    val >>= 4;
+    r = ((val & 15) << 4) + (val & 15);
+  } else if (name.length === 6) {
+    let val = parseInt(name, 16);
+    b = val & 255;
+    val >>= 8;
+    g = val & 255;
+    val >>= 8;
+    r = val & 255;
+  } else {
+    return null;
+  }
+
+  return {r, g, b, a: 1};
+}
+
+/**
+ * A helper function to clamp a value.
+ *
+ * @param {Number} value The value to clamp
+ * @param {Number} min The minimum value
+ * @param {Number} max The maximum value
+ * @return {Number} A value between min and max
+ */
+function clamp(value, min, max) {
+  if (value < min) {
+    value = min;
+  }
+  if (value > max) {
+    value = max;
+  }
+  return value;
+}
+
+/**
+ * A helper function to get a token from a lexer, skipping comments
+ * and whitespace.
+ *
+ * @param {CSSLexer} lexer The lexer
+ * @return {CSSToken} The next non-whitespace, non-comment token; or
+ * null at EOF.
+ */
+function getToken(lexer) {
+  while (true) {
+    let token = lexer.nextToken();
+    if (!token || (token.tokenType !== "comment" &&
+                   token.tokenType !== "whitespace")) {
+      return token;
+    }
+  }
+}
+
+/**
+ * A helper function to examine a token and ensure it is a comma.
+ * Then fetch and return the next token.  Returns null if the
+ * token was not a comma, or at EOF.
+ *
+ * @param {CSSLexer} lexer The lexer
+ * @param {CSSToken} token A token to be examined
+ * @return {CSSToken} The next non-whitespace, non-comment token; or
+ * null if token was not a comma, or at EOF.
+ */
+function requireComma(lexer, token) {
+  if (!token || token.tokenType !== "symbol" || token.text !== ",") {
+    return null;
+  }
+  return getToken(lexer);
+}
+
+/**
+ * A helper function to parse the first three arguments to hsl()
+ * or hsla().
+ *
+ * @param {CSSLexer} lexer The lexer
+ * @return {Array} An array of the form [r,g,b]; or null on error.
+ */
+function parseHsl(lexer) {
+  let vals = [];
+
+  let token = getToken(lexer);
+  if (!token || token.tokenType !== "number") {
+    return null;
+  }
+  let val = token.number % 60;
+  if (val < 0) {
+    val += 60;
+  }
+  vals.push(val / 60.0);
+
+  for (let i = 0; i < 2; ++i) {
+    token = requireComma(lexer, getToken(lexer));
+    if (!token || token.tokenType !== "percentage") {
+      return null;
+    }
+    vals.push(clamp(token.number, 0, 100));
+  }
+
+  return hslToRGB(vals).map((elt) => Math.trunc(elt * 255));
+}
+
+/**
+ * A helper function to parse the first three arguments to rgb()
+ * or rgba().
+ *
+ * @param {CSSLexer} lexer The lexer
+ * @return {Array} An array of the form [r,g,b]; or null on error.
+ */
+function parseRgb(lexer) {
+  let isPercentage = false;
+  let vals = [];
+  for (let i = 0; i < 3; ++i) {
+    let token = getToken(lexer);
+    if (i > 0) {
+      token = requireComma(lexer, token);
+    }
+    if (!token) {
+      return null;
+    }
+
+    /* Either all parameters are integers, or all are percentages, so
+       check the first one to see.  */
+    if (i === 0 && token.tokenType === "percentage") {
+      isPercentage = true;
+    }
+
+    if (isPercentage) {
+      if (token.tokenType !== "percentage") {
+        return null;
+      }
+      vals.push(Math.round(255 * clamp(token.number, 0, 100)));
+    } else {
+      if (token.tokenType !== "number" || !token.isInteger) {
+        return null;
+      }
+      vals.push(clamp(token.number, 0, 255));
+    }
+  }
+  return vals;
+}
+
+/**
+ * Convert a string representing a color to an object holding the
+ * color's components.  Any valid CSS color form can be passed in.
+ *
+ * @param {String} name the color
+ * @return {Object} an object of the form {r, g, b, a}; or null if the
+ *         name was not a valid color
+ */
+function colorToRGBA(name) {
+  name = name.trim().toLowerCase();
+
+  if (name in cssColors) {
+    let result = cssColors[name];
+    return {r: result[0], g: result[1], b: result[2], a: result[3]};
+  } else if (name === "transparent") {
+    return {r: 0, g: 0, b: 0, a: 0};
+  } else if (name === "currentcolor") {
+    return {r: 0, g: 0, b: 0, a: 1};
+  }
+
+  let lexer = DOMUtils.getCSSLexer(name);
+
+  let func = getToken(lexer);
+  if (!func) {
+    return null;
+  }
+
+  if (func.tokenType === "id" || func.tokenType === "hash") {
+    if (getToken(lexer) !== null) {
+      return null;
+    }
+    return hexToRGBA(func.text);
+  }
+
+  const expectedFunctions = ["rgba", "rgb", "hsla", "hsl"];
+  if (!func || func.tokenType !== "function" ||
+      !expectedFunctions.includes(func.text)) {
+    return null;
+  }
+
+  let hsl = func.text === "hsl" || func.text === "hsla";
+  let alpha = func.text === "rgba" || func.text === "hsla";
+
+  let vals = hsl ? parseHsl(lexer) : parseRgb(lexer);
+  if (!vals) {
+    return null;
+  }
+
+  if (alpha) {
+    let token = requireComma(lexer, getToken(lexer));
+    if (!token || token.tokenType !== "number") {
+      return null;
+    }
+    vals.push(clamp(token.number, 0, 1));
+  } else {
+    vals.push(1);
+  }
+
+  let parenToken = getToken(lexer);
+  if (!parenToken || parenToken.tokenType !== "symbol" ||
+      parenToken.text !== ")") {
+    return null;
+  }
+  if (getToken(lexer) !== null) {
+    return null;
+  }
+
+  return {r: vals[0], g: vals[1], b: vals[2], a: vals[3]};
+}
+
+/**
+ * Check whether a string names a valid CSS color.
+ *
+ * @param {String} name The string to check
+ * @return {Boolean} True if the string is a CSS color name.
+ */
+function isValidCSSColor(name) {
+  return colorToRGBA(name) !== null;
+}
+
 loader.lazyGetter(this, "DOMUtils", function () {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
--- a/devtools/client/shared/developer-toolbar.js
+++ b/devtools/client/shared/developer-toolbar.js
@@ -23,16 +23,17 @@ loader.lazyGetter(this, "prefBranch", fu
 });
 loader.lazyGetter(this, "toolboxStrings", function () {
   return Services.strings.createBundle("chrome://devtools/locale/toolbox.properties");
 });
 
 loader.lazyRequireGetter(this, "gcliInit", "devtools/shared/gcli/commands/index");
 loader.lazyRequireGetter(this, "util", "gcli/util/util");
 loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/shared/webconsole/utils", true);
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
 loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
 
 /**
  * A collection of utilities to help working with commands
  */
 var CommandUtils = {
   /**
    * Utility to ensure that things are loaded in the correct order
@@ -235,16 +236,19 @@ function DeveloperToolbar(aChromeWindow)
 
   this._doc = aChromeWindow.document;
 
   this._telemetry = new Telemetry();
   this._errorsCount = {};
   this._warningsCount = {};
   this._errorListeners = {};
 
+  this._onToolboxReady = this._onToolboxReady.bind(this);
+  this._onToolboxDestroyed = this._onToolboxDestroyed.bind(this);
+
   EventEmitter.decorate(this);
 }
 exports.DeveloperToolbar = DeveloperToolbar;
 
 /**
  * Inspector notifications dispatched through the nsIObserverService
  */
 const NOTIFICATIONS = {
@@ -491,16 +495,19 @@ DeveloperToolbar.prototype.show = functi
           this.onOutput.add(this.outputPanel._outputChanged, this.outputPanel);
 
           let tabbrowser = this._chromeWindow.gBrowser;
           tabbrowser.tabContainer.addEventListener("TabSelect", this, false);
           tabbrowser.tabContainer.addEventListener("TabClose", this, false);
           tabbrowser.addEventListener("load", this, true);
           tabbrowser.addEventListener("beforeunload", this, true);
 
+          gDevTools.on("toolbox-ready", this._onToolboxReady);
+          gDevTools.on("toolbox-destroyed", this._onToolboxDestroyed);
+
           this._initErrorsCount(tabbrowser.selectedTab);
 
           this._element.hidden = false;
 
           if (focus) {
             // If the toolbar was just inserted, the <textbox> may still have
             // its binding in process of being applied and not be focusable yet
             let waitForBinding = () => {
@@ -629,16 +636,19 @@ DeveloperToolbar.prototype.destroy = fun
   }
 
   let tabbrowser = this._chromeWindow.gBrowser;
   tabbrowser.tabContainer.removeEventListener("TabSelect", this, false);
   tabbrowser.tabContainer.removeEventListener("TabClose", this, false);
   tabbrowser.removeEventListener("load", this, true);
   tabbrowser.removeEventListener("beforeunload", this, true);
 
+  gDevTools.off("toolbox-ready", this._onToolboxReady);
+  gDevTools.off("toolbox-destroyed", this._onToolboxDestroyed);
+
   Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
 
   this.focusManager.removeMonitoredElement(this.outputPanel._frame);
   this.focusManager.removeMonitoredElement(this._element);
 
   this.focusManager.onVisibilityChange.remove(this.outputPanel._visibilityChanged,
                                               this.outputPanel);
   this.focusManager.onVisibilityChange.remove(this.tooltipPanel._visibilityChanged,
@@ -704,16 +714,26 @@ DeveloperToolbar.prototype.handleEvent =
     this._stopErrorsCount(ev.target);
   }
   else if (ev.type == "beforeunload") {
     this._onPageBeforeUnload(ev);
   }
 };
 
 /**
+ * Update toolbox toggle button when toolbox goes on and off
+ */
+DeveloperToolbar.prototype._onToolboxReady = function() {
+  this._errorCounterButton.setAttribute("checked", "true");
+}
+DeveloperToolbar.prototype._onToolboxDestroyed = function() {
+  this._errorCounterButton.setAttribute("checked", "false");
+}
+
+/**
  * Count a page error received for the currently selected tab. This
  * method counts the JavaScript exceptions received and CSS errors/warnings.
  *
  * @private
  * @param string tabId the ID of the tab from where the page error comes.
  * @param object pageError the page error object received from the
  * PageErrorListener.
  */
--- a/devtools/client/shared/moz.build
+++ b/devtools/client/shared/moz.build
@@ -13,16 +13,18 @@ DIRS += [
     'vendor',
     'widgets',
 ]
 
 DevToolsModules(
     'AppCacheUtils.jsm',
     'autocomplete-popup.js',
     'browser-loader.js',
+    'css-color-db.js',
+    'css-color.js',
     'css-parsing-utils.js',
     'css-reload.js',
     'Curl.jsm',
     'demangle.js',
     'developer-toolbar.js',
     'devices.js',
     'devtools-file-watcher.js',
     'DOMHelpers.jsm',
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 const {angleUtils} = require("devtools/shared/css-angle");
-const {colorUtils} = require("devtools/shared/css-color");
+const {colorUtils} = require("devtools/client/shared/css-color");
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/event-emitter");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 const BEZIER_KEYWORDS = ["linear", "ease-in-out", "ease-in", "ease-out",
                          "ease"];
 
@@ -202,43 +202,43 @@ OutputParser.prototype = {
             }
             ++parenDepth;
           } else {
             let functionText = this._collectFunctionText(token, text,
                                                          tokenStream);
 
             if (options.expectCubicBezier && token.text === "cubic-bezier") {
               this._appendCubicBezier(functionText, options);
-            } else if (colorOK() && DOMUtils.isValidCSSColor(functionText)) {
+            } else if (colorOK() && colorUtils.isValidCSSColor(functionText)) {
               this._appendColor(functionText, options);
             } else {
               this._appendTextNode(functionText);
             }
           }
           break;
         }
 
         case "ident":
           if (options.expectCubicBezier &&
               BEZIER_KEYWORDS.indexOf(token.text) >= 0) {
             this._appendCubicBezier(token.text, options);
-          } else if (colorOK() && DOMUtils.isValidCSSColor(token.text)) {
+          } else if (colorOK() && colorUtils.isValidCSSColor(token.text)) {
             this._appendColor(token.text, options);
           } else if (angleOK(token.text)) {
             this._appendAngle(token.text, options);
           } else {
             this._appendTextNode(text.substring(token.startOffset,
                                                 token.endOffset));
           }
           break;
 
         case "id":
         case "hash": {
           let original = text.substring(token.startOffset, token.endOffset);
-          if (colorOK() && DOMUtils.isValidCSSColor(original)) {
+          if (colorOK() && colorUtils.isValidCSSColor(original)) {
             this._appendColor(original, options);
           } else {
             this._appendTextNode(original);
           }
           break;
         }
         case "dimension":
           let value = text.substring(token.startOffset, token.endOffset);
--- a/devtools/client/shared/test/browser_css_color.js
+++ b/devtools/client/shared/test/browser_css_color.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_URI = "data:text/html;charset=utf-8,browser_css_color.js";
-var {colorUtils} = require("devtools/shared/css-color");
+var {colorUtils} = require("devtools/client/shared/css-color");
 var origColorUnit;
 
 add_task(function*() {
   yield addTab("about:blank");
   let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
   info("Creating a test canvas element to test colors");
   let canvas = createTestCanvas(doc);
rename from devtools/shared/tests/unit/test_cssColor.js
rename to devtools/client/shared/test/unit/test_cssColor.js
--- a/devtools/shared/tests/unit/test_cssColor.js
+++ b/devtools/client/shared/test/unit/test_cssColor.js
@@ -1,34 +1,62 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test classifyColor.
 
 "use strict";
 
-const {colorUtils} = require("devtools/shared/css-color");
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm");
+const {colorUtils} = require("devtools/client/shared/css-color");
+
+loader.lazyGetter(this, "DOMUtils", function () {
+  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
 
 const CLASSIFY_TESTS = [
   { input: "rgb(255,0,192)", output: "rgb" },
   { input: "RGB(255,0,192)", output: "rgb" },
+  { input: "RGB(100%,0%,83%)", output: "rgb" },
   { input: "rgba(255,0,192, 0.25)", output: "rgb" },
-  { input: "hsl(5, 5, 5)", output: "hsl" },
-  { input: "hsla(5, 5, 5, 0.25)", output: "hsl" },
-  { input: "hSlA(5, 5, 5, 0.25)", output: "hsl" },
+  { input: "hsl(5, 5%, 5%)", output: "hsl" },
+  { input: "hsla(5, 5%, 5%, 0.25)", output: "hsl" },
+  { input: "hSlA(5, 5%, 5%, 0.25)", output: "hsl" },
   { input: "#f0c", output: "hex" },
   { input: "#fe01cb", output: "hex" },
   { input: "#FE01CB", output: "hex" },
   { input: "blue", output: "name" },
   { input: "orange", output: "name" }
 ];
 
+function compareWithDomutils(input, isColor) {
+  let ours = colorUtils.colorToRGBA(input);
+  let platform = DOMUtils.colorToRGBA(input);
+  deepEqual(ours, platform, "color " + input + " matches DOMUtils");
+  if (isColor) {
+    ok(ours !== null, "'" + input + "' is a color");
+  } else {
+    ok(ours === null, "'" + input + "' is not a color");
+  }
+}
+
 function run_test() {
   for (let test of CLASSIFY_TESTS) {
     let result = colorUtils.classifyColor(test.input);
     equal(result, test.output, "test classifyColor(" + test.input + ")");
 
     let obj = new colorUtils.CssColor("purple");
     obj.setAuthoredUnitFromColor(test.input);
     equal(obj.colorUnit, test.output,
           "test setAuthoredUnitFromColor(" + test.input + ")");
+
+    // Check that our implementation matches DOMUtils.
+    compareWithDomutils(test.input, true);
+
+    // And check some obvious errors.
+    compareWithDomutils("mumble" + test.input, false);
+    compareWithDomutils(test.input + "trailingstuff", false);
   }
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColorDatabase.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that css-color-db matches platform.
+
+"use strict";
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm");
+
+loader.lazyGetter(this, "DOMUtils", function () {
+  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+const {colorUtils} = require("devtools/client/shared/css-color");
+const {cssColors} = require("devtools/client/shared/css-color-db");
+
+function isValid(colorName) {
+  ok(colorUtils.isValidCSSColor(colorName),
+     colorName + " is valid in database");
+  ok(DOMUtils.isValidCSSColor(colorName),
+     colorName + " is valid in DOMUtils");
+}
+
+function checkOne(colorName, checkName) {
+  let ours = colorUtils.colorToRGBA(colorName);
+  let fromDom = DOMUtils.colorToRGBA(colorName);
+  deepEqual(ours, fromDom, colorName + " agrees with DOMUtils");
+
+  isValid(colorName);
+
+  if (checkName) {
+    let {r, g, b} = ours;
+
+    // The color we got might not map back to the same name; but our
+    // implementation should agree with DOMUtils about which name is
+    // canonical.
+    let ourName = colorUtils.rgbToColorName(r, g, b);
+    let domName = DOMUtils.rgbToColorName(r, g, b);
+
+    equal(ourName, domName,
+          colorName + " canonical name agrees with DOMUtils");
+  }
+}
+
+function run_test() {
+  for (let name in cssColors) {
+    checkOne(name, true);
+  }
+  checkOne("transparent", false);
+
+  // Now check that platform didn't add a new name when we weren't
+  // looking.
+  let names = DOMUtils.getCSSValuesForProperty("background-color");
+  for (let name of names) {
+    if (name !== "hsl" && name !== "hsla" &&
+        name !== "rgb" && name !== "rgba" &&
+        name !== "inherit" && name !== "initial" && name !== "unset") {
+      checkOne(name, true);
+    }
+  }
+}
--- a/devtools/client/shared/test/unit/xpcshell.ini
+++ b/devtools/client/shared/test/unit/xpcshell.ini
@@ -4,16 +4,18 @@ head =
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_advanceValidate.js]
 [test_attribute-parsing-01.js]
 [test_attribute-parsing-02.js]
 [test_bezierCanvas.js]
+[test_cssColor.js]
+[test_cssColorDatabase.js]
 [test_cubicBezier.js]
 [test_escapeCSSComment.js]
 [test_parseDeclarations.js]
 [test_parsePseudoClassesAndAttributes.js]
 [test_parseSingleValue.js]
 [test_rewriteDeclarations.js]
 [test_source-utils.js]
 [test_suggestion-picker.js]
--- a/devtools/client/shared/widgets/Tooltip.js
+++ b/devtools/client/shared/widgets/Tooltip.js
@@ -7,17 +7,17 @@
 const {Cu, Ci} = require("chrome");
 const promise = require("promise");
 const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
 const {CubicBezierWidget} =
       require("devtools/client/shared/widgets/CubicBezierWidget");
 const {MdnDocsWidget} = require("devtools/client/shared/widgets/MdnDocsWidget");
 const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
 const EventEmitter = require("devtools/shared/event-emitter");
-const {colorUtils} = require("devtools/shared/css-color");
+const {colorUtils} = require("devtools/client/shared/css-color");
 const Heritage = require("sdk/core/heritage");
 const {Eyedropper} = require("devtools/client/eyedropper/eyedropper");
 const Editor = require("devtools/client/sourceeditor/editor");
 const Services = require("Services");
 
 loader.lazyRequireGetter(this, "beautify", "devtools/shared/jsbeautify/beautify");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
--- a/devtools/client/storage/panel.js
+++ b/devtools/client/storage/panel.js
@@ -4,17 +4,17 @@
  * 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 EventEmitter = require("devtools/shared/event-emitter");
 
 loader.lazyRequireGetter(this, "StorageFront",
-                        "devtools/server/actors/storage", true);
+                         "devtools/client/fronts/storage", true);
 loader.lazyRequireGetter(this, "StorageUI",
                          "devtools/client/storage/ui", true);
 
 var StoragePanel = this.StoragePanel =
 function StoragePanel(panelWin, toolbox) {
   EventEmitter.decorate(this);
 
   this._toolbox = toolbox;
--- a/devtools/client/storage/test/head.js
+++ b/devtools/client/storage/test/head.js
@@ -161,18 +161,17 @@ var openStoragePanel = Task.async(functi
  *
  * @param toolbox {Toolbox}
  *
  * @return a promise that resolves when focus has been received
  */
 function waitForToolboxFrameFocus(toolbox) {
   info("Making sure that the toolbox's frame is focused");
   let def = promise.defer();
-  let win = toolbox.frame.contentWindow;
-  waitForFocus(def.resolve, win);
+  waitForFocus(def.resolve, toolbox.win);
   return def.promise;
 }
 
 /**
  * Forces GC, CC and Shrinking GC to get rid of disconnected docshells and
  * windows.
  */
 function forceCollections() {
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -382,17 +382,17 @@ body {
 .animation-timeline .animation .name {
   position: absolute;
   color: var(--theme-selection-color);
   height: 100%;
   display: flex;
   align-items: center;
   padding: 0 2px;
   box-sizing: border-box;
-  --fast-track-icon-width: 12px;
+  --fast-track-icon-width: 15px;
   z-index: 1;
 }
 
 .animation-timeline .animation .name div {
   /* Flex items don't support text-overflow, so a child div is used */
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
@@ -402,23 +402,33 @@ body {
   width: calc(100% - var(--fast-track-icon-width));
 }
 
 .animation-timeline .fast-track .name::after {
   /* Animations running on the compositor have the fast-track background image*/
   content: "";
   display: block;
   position: absolute;
-  top: 0;
+  top: 1px;
   right: 0;
   height: 100%;
   width: var(--fast-track-icon-width);
   z-index: 1;
+}
 
-  background-image: url("images/animation-fast-track.svg");
+.animation-timeline .all-properties .name::after {
+  background-color: white;
+  clip-path: url(images/animation-fast-track.svg#thunderbolt);
+  background-repeat: no-repeat;
+  background-position: center;
+}
+
+.animation-timeline .some-properties .name::after {
+  background-color: var(--theme-content-color3);
+  clip-path: url(images/animation-fast-track.svg#thunderbolt);
   background-repeat: no-repeat;
   background-position: center;
 }
 
 .animation-timeline .animation .delay,
 .animation-timeline .animation .end-delay {
   position: absolute;
   height: 100%;
@@ -524,16 +534,42 @@ body {
   align-items: center;
 }
 
 .animation-timeline .animated-properties .name div {
   overflow: hidden;
   text-overflow: ellipsis;
 }
 
+.animated-properties.cssanimation {
+  --background-color: var(--theme-contrast-background);
+}
+
+.animated-properties.csstransition {
+  --background-color: var(--theme-highlight-blue);
+}
+
+.animated-properties.scriptanimation {
+  --background-color: var(--theme-graphs-green);
+}
+
+.animation-timeline .animated-properties .oncompositor::before {
+  content: "";
+  display: inline-block;
+  width: 17px;
+  height: 17px;
+  background-color: var(--background-color);
+  clip-path: url(images/animation-fast-track.svg#thunderbolt);
+  vertical-align: middle;
+}
+
+.animation-timeline .animated-properties .warning {
+  text-decoration: underline dotted;
+}
+
 .animation-timeline .animated-properties .frames {
   /* The frames list is absolutely positioned and the left and width properties
      are dynamically set from javascript to match the animation's startTime and
      duration */
   position: absolute;
   top: 0;
   height: 100%;
   /* Using flexbox to vertically center the frames */
--- a/devtools/client/themes/images/animation-fast-track.svg
+++ b/devtools/client/themes/images/animation-fast-track.svg
@@ -1,6 +1,8 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9 12" width="16" height="16">
-  <path d="M5.75 0l-1 5.5 2 .5-3.5 6 1-5-2-.5z" fill="#fff"/>
+<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0">
+  <clipPath id="thunderbolt" transform="scale(1.4)">
+    <path d="M5.75 0l-1 5.5 2 .5-3.5 6 1-5-2-.5z"/>
+  </clipPath>
 </svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/debugging-tabs.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
+  <path d="M17,12v2a1,1,0,0,1-1,1H2a1,1,0,0,1-1-1V12a1,1,0,0,1,1-1H1.142c2.3,0,2.536-1.773,2.874-4,0.351-2.316.083-4,3.13-4h3.707C13.917,3,13.647,4.684,14,7c0.34,2.228.582,4,2.89,4H16A1,1,0,0,1,17,12Z" fill="white"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/tabs-icon.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <circle fill="#a6a6a6" cx="8" cy="8" r="7" />
+  <path transform="translate(1 1)" fill="#fff" d="M5.31617536,1.74095137 C5.29841561,1.73995137 5.27868256,1.74095137 5.26190947,1.74795137 C5.25796286,1.74995137 5.2530296,1.75395137 5.24908299,1.75895137 C5.2550029,1.75895137 5.26190947,1.75895137 5.26684273,1.75795137 C5.28460248,1.75395137 5.29841561,1.74195137 5.31617536,1.74095137 L5.31617536,1.74095137 Z M5.33886837,2.59995137 C5.36156138,2.57095137 5.30729549,2.54695137 5.27670926,2.54895137 C5.28460248,2.51395137 5.32900184,2.49595137 5.31716201,2.45195137 C5.30630884,2.40595137 5.25105629,2.41495137 5.22145672,2.43995137 C5.1948171,2.46295137 5.18100396,2.50295137 5.15831095,2.52995137 C5.14548447,2.54495137 5.12180481,2.54995137 5.11292494,2.56795137 C5.10503172,2.58495137 5.11489824,2.61395137 5.11391159,2.63295137 C5.15041773,2.63795137 5.18889718,2.62695137 5.2155368,2.60095137 L5.23329655,2.59295137 C5.22934994,2.59595137 5.22737663,2.60295137 5.22540333,2.60695137 C5.24316307,2.62895137 5.32209528,2.62295137 5.33886837,2.59995137 L5.33886837,2.59995137 Z M5.37636117,1.37295137 C5.37438786,1.42695137 5.42668044,1.43295137 5.46515989,1.45395137 C5.45332006,1.48495137 5.410894,1.48395137 5.39116095,1.50895137 C5.36748129,1.53995137 5.410894,1.56695137 5.43260036,1.58095137 C5.47502642,1.60695137 5.45134676,1.63695137 5.44345354,1.67395137 C5.43161371,1.72595137 5.54310544,1.71195137 5.56777176,1.71095137 C5.61019782,1.70895137 5.67729019,1.71595137 5.71774294,1.69595137 C5.76115565,1.67195137 5.78384866,1.61895137 5.82923468,1.59295137 C5.86672748,1.57095137 5.92000671,1.55895137 5.96144612,1.57395137 C6.00485883,1.58895137 5.99992557,1.64495137 6.03544506,1.66895137 C6.07688447,1.69795137 6.12227048,1.70695137 6.15778997,1.66395137 C6.18048298,1.63695137 6.23080226,1.60295137 6.23277557,1.57395137 C6.23672218,1.52295137 6.25152196,1.48295137 6.30776116,1.47195137 C6.35314718,1.46295137 6.34328065,1.50695137 6.37485353,1.51495137 C6.44490586,1.53295137 6.47845205,1.31895137 6.55442429,1.38195137 C6.57218404,1.39695137 6.5771173,1.45495137 6.60770353,1.44995137 C6.63927641,1.44495137 6.64026306,1.39895137 6.67380925,1.39795137 C6.68466243,1.42895137 6.61559675,1.46695137 6.60671688,1.50095137 C6.64914294,1.46595137 6.66986264,1.47095137 6.71820861,1.46595137 C6.7310351,1.49895137 6.63631645,1.55295137 6.61165014,1.55795137 C6.5771173,1.56695137 6.5563976,1.54695137 6.52975798,1.56595137 C6.50903828,1.57995137 6.48042535,1.57895137 6.45575904,1.58095137 C6.4212262,1.58495137 6.35610713,1.63095137 6.35709379,1.66895137 C6.35709379,1.68395137 6.36893362,1.71795137 6.35610713,1.72995137 C6.3442673,1.74295137 6.31565438,1.73095137 6.31269442,1.71795137 C6.28309485,1.76195137 6.2446154,1.68495137 6.21994908,1.74695137 C6.25941518,1.75695137 6.29592133,1.79495137 6.34032069,1.80595137 C6.3837334,1.81695137 6.42714612,1.82795137 6.46957217,1.83995137 C6.54159781,1.86195137 6.64914294,1.77495137 6.70439548,1.73295137 C6.75668806,1.69395137 6.82279378,1.60595137 6.83660692,1.54295137 C6.85239336,1.47395137 6.92737895,1.39495137 6.91159251,1.32695137 C6.89777937,1.26295137 6.88791285,1.23295137 6.95993848,1.20995137 C6.99052471,1.19995137 7.06452365,1.18395137 7.07537683,1.14895137 C7.09116327,1.09695137 6.9283656,1.11095137 6.90369929,1.09895137 C6.82180713,1.06195137 6.78628764,1.02095137 6.69156899,1.05795137 C6.64223637,1.07695137 6.59389039,1.09295137 6.54258446,1.10695137 C6.51594484,1.11395137 6.48930523,1.11595137 6.47450544,1.13895137 C6.46858552,1.14795137 6.4606923,1.15495137 6.45082578,1.15995137 C6.40839972,1.17695137 6.4606923,1.09595137 6.46562556,1.09095137 C6.4794387,1.07495137 6.50213171,1.02595137 6.45773234,1.03695137 C6.39261328,1.05195137 6.34525395,1.15195137 6.27520162,1.15695137 C6.22192239,1.16095137 6.23869548,1.11395137 6.25250862,1.08695137 C6.27914824,1.03795137 6.20317599,1.03195137 6.1696298,1.03195137 C6.12227048,1.03195137 6.08675099,1.05895137 6.04136497,1.06395137 C5.99893892,1.06795137 5.94960629,1.07595137 5.90718023,1.07495137 C5.82232811,1.07195137 5.76608892,1.12195137 5.68222345,1.09395137 C5.59342472,1.06495137 5.49771943,1.13895137 5.41188066,1.14895137 C5.38326773,1.15295137 5.34182833,1.14695137 5.3299885,1.18095137 C5.32012197,1.20895137 5.3299885,1.25195137 5.35169485,1.27295137 L5.35860142,1.26695137 C5.33985502,1.28595137 5.33788172,1.31295137 5.31025545,1.32295137 C5.28361583,1.33195137 5.25697621,1.36695137 5.24316307,1.39095137 C5.2323099,1.40895137 5.20172367,1.48395137 5.2550029,1.44495137 C5.29348235,1.41595137 5.31518871,1.36195137 5.37636117,1.37295137 L5.37636117,1.37295137 Z M2.18355356,6.10795137 C2.09278153,6.04195137 1.88657115,6.02595137 1.91222411,5.87195137 C1.92801055,5.77795137 2.0247025,5.70495137 2.10264805,5.65895137 C2.20525992,5.59895137 2.31971161,5.59695137 2.43514996,5.60695137 C2.46277623,5.60995137 2.51506881,5.60495137 2.5298686,5.62695137 C2.53776182,5.63795137 2.55354826,5.64495137 2.56637474,5.64895137 C2.59696097,5.65795137 2.62853385,5.65895137 2.66010674,5.66495137 C2.70746606,5.67395137 2.74101224,5.71495137 2.78837156,5.68095137 C2.84263745,5.64295137 2.85151733,5.63495137 2.91762305,5.64295137 C2.9768222,5.64995137 3.01234169,5.60495137 3.06167432,5.60895137 C3.07746076,5.60995137 3.09127389,5.61295137 3.10311372,5.61795137 C3.10804699,5.60095137 3.11495355,5.58595137 3.12580673,5.58295137 C3.15047305,5.57595137 3.20473893,5.63595137 3.2303919,5.64095137 C3.29551097,5.65495137 3.29156436,5.60895137 3.29649762,5.56195137 C3.32905715,5.55595137 3.34484359,5.60095137 3.37444317,5.57295137 C3.37345652,5.58195137 3.37937643,5.59595137 3.37937643,5.60495137 C3.38529635,5.60895137 3.39220292,5.60895137 3.39812283,5.60395137 C3.40108279,5.59895137 3.40206944,5.59395137 3.40009614,5.58795137 C3.41588258,5.59295137 3.4237758,5.58195137 3.4257491,5.56295137 C3.43758893,5.56395137 3.45633533,5.55695137 3.46817516,5.55895137 C3.47705503,5.52495137 3.49678809,5.47995137 3.47212177,5.44895137 C3.47804169,5.44795137 3.48494825,5.44595137 3.49185482,5.44495137 C3.49185482,5.41095137 3.51454783,5.39595137 3.51553448,5.36895137 C3.48001499,5.36395137 3.44054889,5.36595137 3.40404275,5.36695137 C3.4257491,5.34695137 3.47804169,5.30295137 3.48297495,5.27595137 C3.49284148,5.22795137 3.43068237,5.19895137 3.43561563,5.14195137 C3.44153554,5.17195137 3.47508173,5.24095137 3.50665461,5.25095137 C3.57769359,5.27495137 3.55697389,5.20395137 3.56190715,5.16695137 C3.5796669,5.04995137 3.68425207,5.14695137 3.68622537,5.20795137 C3.7168116,5.13795137 3.79278385,5.21595137 3.75825101,5.27595137 C3.74147791,5.30495137 3.71878491,5.29395137 3.73950461,5.33195137 C3.7543044,5.35895137 3.77601075,5.35995137 3.80758363,5.35295137 C3.81547685,5.33695137 3.82238342,5.31895137 3.82238342,5.29995137 C3.87664931,5.28295137 3.9121688,5.34795137 3.88059592,5.38695137 C3.92104868,5.36495137 3.96248808,5.34395137 4.00590079,5.33295137 C3.98024783,5.24295137 3.95360821,5.15495137 3.9703813,5.05895137 C3.97432791,5.03795137 3.97728787,5.01395137 3.99307431,4.99795137 C4.01280736,4.97695137 3.98814105,4.98495137 3.98616774,4.97095137 C3.98024783,4.92895137 4.02464719,4.88595137 4.04142028,4.84795137 C3.99504762,4.83795137 4.03747367,4.74595137 4.0680599,4.72995137 C4.10160609,4.71295137 4.20027134,4.74095137 4.20717791,4.71395137 C4.22691096,4.72495137 4.24565736,4.74095137 4.26933702,4.74095137 C4.32360291,4.74195137 4.36010905,4.74295137 4.39760185,4.78695137 C4.41634825,4.80995137 4.44397452,4.86095137 4.47752071,4.86495137 C4.47653405,4.90295137 4.51994676,4.93095137 4.47456075,4.96295137 C4.43904126,4.98795137 4.38970863,4.98195137 4.37490884,5.02995137 C4.36504232,5.05995137 4.33642939,5.07395137 4.3798421,5.09495137 C4.3985885,5.10495137 4.42226816,5.10695137 4.44298787,5.10695137 C4.44792113,5.13595137 4.46272092,5.17495137 4.50021371,5.16995137 C4.573226,5.16095137 4.58901244,5.06895137 4.64722494,5.03795137 C4.74194358,4.98795137 4.7271438,5.20395137 4.80903596,5.14995137 C4.82876901,5.13695137 4.82876901,5.08195137 4.83863553,5.06095137 C4.85836858,5.01695137 4.88106159,4.97195137 4.90967452,4.93295137 C4.94618066,4.88295137 4.99156668,4.83095137 4.97578024,4.76595137 C4.96690036,4.72995137 4.89783469,4.71495137 4.8662618,4.68995137 C4.82876901,4.65895137 4.79226286,4.62595137 4.76956986,4.58295137 C4.75575672,4.55695137 4.7478635,4.54795137 4.76956986,4.53495137 C4.78239634,4.52795137 4.77844973,4.51395137 4.77351647,4.50395137 C4.74983681,4.45395137 4.68570439,4.36495137 4.77548977,4.33395137 C4.79522282,4.32695137 4.83666223,4.26295137 4.83962219,4.23795137 C4.84455545,4.19595137 4.78140969,4.15795137 4.81002261,4.11595137 C4.83074231,4.08495137 4.8830349,4.06495137 4.90967452,4.03395137 C4.922501,4.01895137 4.93730079,4.00595137 4.95703384,4.00195137 C4.95802049,3.98495137 4.9619671,3.96595137 4.97676689,3.95495137 C5.00044655,3.93695137 5.03793935,3.94595137 5.06556562,3.93695137 C5.11095163,3.92295137 5.13068468,3.87595137 5.16620418,3.84995137 C5.19580375,3.82795137 5.22934994,3.83595137 5.26092282,3.81995137 C5.27769591,3.81195137 5.28460248,3.79395137 5.30137557,3.78595137 C5.34281498,3.76595137 5.3901743,3.79795137 5.4089207,3.83295137 C5.45332006,3.91695137 5.5085726,4.04695137 5.63486413,4.01295137 C5.68617006,3.99895137 5.72464951,3.95695137 5.74043595,3.90895137 C5.75523574,3.86295137 5.73747599,3.82495137 5.74043595,3.77995137 C5.74438256,3.69995137 5.82232811,3.64895137 5.83120798,3.56995137 C5.77200883,3.57095137 5.80259506,3.53395137 5.78286201,3.49995137 C5.76115565,3.46195137 5.71182303,3.48995137 5.67926349,3.48395137 C5.71280968,3.40295137 5.71280968,3.37495137 5.63387748,3.33595137 C5.59934464,3.31895137 5.54211879,3.23895137 5.51547917,3.24195137 C5.53718553,3.21195137 5.58849146,3.26195137 5.6042779,3.27595137 C5.63881074,3.30895137 5.66939697,3.32395137 5.71774294,3.32795137 C5.70392981,3.30695137 5.69702324,3.26895137 5.70590311,3.24495137 C5.71478298,3.22295137 5.69307663,3.19995137 5.69504993,3.17195137 C5.75030248,3.24295137 5.7414226,3.32395137 5.77299548,3.40195137 C5.78582197,3.43495137 5.8183815,3.45695137 5.83219464,3.49095137 C5.84995438,3.53395137 5.83811455,3.53295137 5.87560735,3.55895137 C5.89830036,3.57495137 5.90619358,3.60295137 5.91014019,3.62795137 C5.91704675,3.67195137 5.9328332,3.65295137 5.95651286,3.67795137 C5.97032599,3.69295137 6.00584548,3.69495137 5.99893892,3.72595137 C5.99400565,3.74795137 5.97920586,3.76595137 5.97624591,3.78895137 C5.96736603,3.85495137 6.09661752,3.76495137 6.109444,3.75595137 C6.13707027,3.73495137 6.18245629,3.73095137 6.20416264,3.70595137 C6.22685565,3.67995137 6.22192239,3.64195137 6.24560205,3.61795137 C6.27520162,3.58695137 6.30381455,3.60795137 6.33933404,3.60195137 C6.38077345,3.59595137 6.41629294,3.56295137 6.44687917,3.53795137 C6.51199823,3.48295137 6.55343764,3.42295137 6.60770353,3.35995137 C6.58402387,3.36595137 6.50311836,3.42495137 6.4981851,3.36995137 C6.46759887,3.36995137 6.39655989,3.36495137 6.38570671,3.33095137 C6.37682684,3.30595137 6.37978679,3.27795137 6.37978679,3.25295137 C6.37880014,3.22595137 6.34624061,3.23495137 6.32453425,3.22095137 C6.28112154,3.19295137 6.25941518,3.14095137 6.21304252,3.11695137 C6.13904358,3.07795137 6.09464421,3.01495137 6.05024485,2.94795137 C6.02459188,2.90895137 5.93381985,2.82995137 5.94072642,2.78295137 C5.94467303,2.75195137 5.97032599,2.71895137 5.96835269,2.68795137 C5.96736603,2.65995137 5.94565968,2.64495137 5.94861964,2.61395137 C5.95157959,2.57795137 5.86475417,2.51495137 5.94072642,2.50795137 C5.96440608,2.50595137 5.96835269,2.47695137 5.9949923,2.46095137 C6.02459188,2.44295137 6.01768531,2.42695137 6.05024485,2.43595137 C6.10253743,2.45195137 6.13904358,2.39395137 6.17456307,2.36295137 C6.23573552,2.30895137 6.13805692,2.30795137 6.13312366,2.26695137 C6.1281904,2.22595137 6.10451074,2.19595137 6.09760417,2.14795137 C6.09365756,2.11295137 6.06109802,2.12695137 6.04235163,2.13595137 C6.01669866,2.14795137 5.99104569,2.12995137 5.96637938,2.12495137 C5.94368637,2.11995137 5.92493997,2.08195137 5.8973137,2.09395137 C5.876594,2.10395137 5.87758065,2.12895137 5.84798108,2.12595137 C5.82627472,2.12395137 5.81246159,2.10295137 5.79075523,2.09895137 C5.75720904,2.09495137 5.78680862,2.12695137 5.74931582,2.12995137 C5.72267621,2.13195137 5.63683743,2.09595137 5.63486413,2.12995137 C5.60822451,2.08395137 5.59737133,2.16195137 5.56875841,2.16995137 C5.53718553,2.17895137 5.50363934,2.17095137 5.47206646,2.18295137 C5.40300078,2.21095137 5.42569379,2.27995137 5.49179951,2.29095137 C5.54507875,2.29895137 5.47601307,2.33595137 5.49377282,2.37195137 C5.50955926,2.40395137 5.51449252,2.42595137 5.54902536,2.43895137 C5.60625121,2.45995137 5.66742366,2.47695137 5.64769061,2.55095137 C5.62203765,2.64295137 5.55790523,2.72995137 5.46811985,2.77195137 C5.38228108,2.81195137 5.35860142,2.70295137 5.29348235,2.67495137 C5.2530296,2.65795137 5.20764358,2.66395137 5.16521752,2.66895137 C5.15831095,2.67995137 5.22441667,2.70095137 5.23526985,2.71995137 C5.2550029,2.75895137 5.20073701,2.75395137 5.1967904,2.78395137 C5.19284379,2.80895137 5.16028426,2.82695137 5.17804401,2.85195137 C5.15929761,2.82895137 5.12279146,2.85995137 5.10996498,2.87395137 C5.09121858,2.89395137 5.09516519,2.90695137 5.10305841,2.93195137 C5.11884485,2.98295137 5.04188596,3.03595137 4.99649994,3.02995137 C4.95802049,3.02395137 4.92151435,3.02695137 4.8850082,3.00895137 C4.84159549,2.98795137 4.85639528,3.00095137 4.84751541,2.95195137 C4.83863553,2.90595137 4.77548977,2.88595137 4.81298257,2.82895137 C4.83962219,2.78695137 4.8267957,2.79095137 4.82186244,2.75095137 C4.81594253,2.70895137 4.83468892,2.70295137 4.86823511,2.69695137 C4.90474125,2.68995137 4.92052769,2.62495137 4.94223405,2.59395137 C4.94716731,2.58695137 4.96986032,2.52895137 4.93434083,2.54395137 C4.91460778,2.55295137 4.92940757,2.57795137 4.89882134,2.58195137 C4.87711498,2.58595137 4.85540863,2.57095137 4.83271562,2.57095137 C4.80706265,2.57095137 4.78042303,2.58395137 4.75674337,2.56795137 C4.7685832,2.55395137 4.85343532,2.48395137 4.78634295,2.46995137 C4.75970333,2.46395137 4.78140969,2.50795137 4.7458902,2.50195137 C4.73898363,2.53695137 4.69655757,2.53395137 4.67583787,2.55595137 C4.68471774,2.51895137 4.76266329,2.49095137 4.73701032,2.46095137 C4.79324952,2.41195137 4.80508935,2.40295137 4.7291171,2.37595137 C4.60973215,2.33395137 4.61861202,2.21095137 4.70050418,2.13695137 C4.77548977,2.06895137 4.89882134,1.98295137 4.97183363,2.09595137 C5.04977918,2.21695137 5.0991118,2.12895137 5.16324422,2.05095137 C5.14153786,2.04195137 5.16127091,2.03595137 5.15436434,2.00895137 C5.08332536,2.03795137 5.0201796,1.94595137 5.06852557,1.89095137 C5.09812515,1.85795137 5.14351117,1.86695137 5.18297727,1.85695137 C5.21751011,1.84795137 5.24908299,1.81395137 5.26388278,1.78195137 C5.2342832,1.78995137 5.23822981,1.77195137 5.24908299,1.75895137 C5.23132324,1.75695137 5.21159019,1.74895137 5.1967904,1.74395137 C5.15436434,1.72895137 5.1573243,1.69595137 5.11193829,1.68995137 C5.00439316,1.67395137 5.22441667,1.54995137 5.11687155,1.54995137 C5.08233871,1.54895137 5.05175248,1.49695137 5.02609952,1.50695137 C5.00833977,1.51395137 5.00340651,1.52795137 4.98170015,1.51895137 C4.96690036,1.51295137 4.94914062,1.49995137 4.93138087,1.50995137 C4.89290142,1.53395137 4.8850082,1.50495137 4.84751541,1.51595137 C4.81692918,1.52595137 4.80015608,1.55595137 4.76463659,1.54795137 C4.80015608,1.49995137 4.8435688,1.45995137 4.87514168,1.40995137 C4.89586138,1.37595137 4.92151435,1.34495137 4.95604719,1.32395137 C4.97479358,1.31295137 5.02807282,1.30195137 5.03103278,1.27595137 C5.03596604,1.23295137 5.00932642,1.23695137 4.97972685,1.25395137 C4.90276795,1.29995137 4.82284909,1.34895137 4.7478635,1.39795137 C4.70247748,1.42695137 4.66695799,1.45195137 4.6107188,1.44395137 C4.56730609,1.43695137 4.54954634,1.48495137 4.5150135,1.48095137 C4.49824041,1.41395137 4.12824571,1.65695137 4.08285969,1.67795137 C4.01083406,1.70995137 3.92992855,1.76495137 3.85296965,1.78395137 C3.82139677,1.79195137 3.75529105,1.86595137 3.75923766,1.78395137 C3.71977156,1.77895137 3.69017198,1.81895137 3.66353236,1.83895137 C3.62603957,1.86795137 3.5816402,1.88595137 3.54118745,1.91095137 C3.45436203,1.96695137 3.37246987,2.03395137 3.29156436,2.09695137 C3.21460546,2.15695137 3.13764656,2.22695137 3.05674105,2.28095137 C3.02911478,2.29995137 2.92748957,2.35195137 2.93044953,2.39095137 C3.00247516,2.40495137 3.24815165,2.09695137 3.31721732,2.17995137 C3.33497707,2.20095137 3.21263216,2.26295137 3.1928991,2.27495137 C3.17612601,2.28395137 3.15639296,2.28295137 3.13961987,2.29195137 C3.11791351,2.30495137 3.10410038,2.32695137 3.08338067,2.34095137 C3.02812813,2.37595137 2.98175546,2.42095137 2.94130271,2.47095137 C2.91268978,2.50795137 2.89197008,2.55595137 2.8603972,2.58995137 C2.86533046,2.55395137 2.85842389,2.52795137 2.85941055,2.49295137 C2.81895779,2.51895137 2.8021847,2.56295137 2.74594551,2.55095137 C2.69463957,2.53895137 2.65418682,2.59095137 2.61768068,2.61895137 C2.53282856,2.68395137 2.47560271,2.75595137 2.40456373,2.83195137 C2.36509763,2.87495137 2.32267157,2.90495137 2.29800525,2.95795137 C2.27136564,3.01495137 2.23387284,3.06595137 2.19934,3.11895137 C2.13323428,3.21595137 2.05726204,3.30495137 1.99214297,3.40195137 C1.85894488,3.60095137 1.7711328,3.82895137 1.66161437,4.04095137 C1.60537517,4.15095137 1.55110929,4.25895137 1.52841628,4.38195137 C1.50868323,4.48795137 1.50769657,4.59595137 1.50966988,4.70395137 C1.56985568,4.65695137 1.56689573,4.75495137 1.55110929,4.78395137 C1.52841628,4.82895137 1.5195364,4.87995137 1.51262984,4.92995137 C1.50276331,4.99495137 1.49092348,5.05995137 1.49092348,5.12595137 C1.49092348,5.18195137 1.47316374,5.23395137 1.47217708,5.28795137 C1.45145738,5.27195137 1.49585674,5.20395137 1.45639064,5.21395137 C1.43665759,5.21895137 1.43567094,5.24795137 1.43073768,5.26295137 C1.41495124,5.31495137 1.34489891,5.30995137 1.33404573,5.36895137 C1.32812581,5.40495137 1.3241792,5.42595137 1.30049954,5.45495137 C1.28175314,5.47695137 1.29951289,5.48695137 1.3034595,5.50895137 C1.31233937,5.56095137 1.245247,5.63295137 1.26300675,5.67495137 C1.27977984,5.71595137 1.26794001,5.76195137 1.28668641,5.80095137 C1.29655293,5.82095137 1.31924594,5.84695137 1.31036607,5.87195137 C1.26794001,5.87995137 1.3222059,5.97795137 1.32615251,6.00795137 C1.33207242,6.05695137 1.37548513,6.21095137 1.42284446,6.23295137 C1.48204361,6.32395137 1.56294912,6.45095137 1.66753428,6.49695137 C1.74153322,6.52895137 1.76817284,6.43295137 1.80961225,6.39295137 C1.86190483,6.34095137 1.92998386,6.30795137 1.99904954,6.28395137 C2.05726204,6.26295137 2.30096521,6.19195137 2.18355356,6.10795137 L2.18355356,6.10795137 Z M2.28616542,9.39295137 C2.29800525,9.37295137 2.28912538,9.32195137 2.26741903,9.30495137 C2.21512644,9.26095137 2.19440674,9.36495137 2.22795292,9.39595137 C2.24077941,9.42895137 2.27136564,9.41795137 2.28616542,9.39295137 L2.28616542,9.39295137 Z M2.50026902,6.36895137 C2.48546924,6.35595137 2.47461606,6.36395137 2.47362941,6.33695137 C2.47461606,6.31295137 2.47658936,6.26695137 2.44501648,6.29595137 C2.43613661,6.29895137 2.44797644,6.30395137 2.43514996,6.30895137 C2.42627008,6.31195137 2.41936352,6.30495137 2.4134436,6.30195137 C2.39667051,6.29495137 2.38680398,6.29395137 2.3739775,6.31195137 C2.36509763,6.32395137 2.36509763,6.33795137 2.35029784,6.34795137 L2.32464487,6.35695137 C2.315765,6.35995137 2.29011203,6.37795137 2.28912538,6.38795137 C2.28517877,6.40295137 2.30787178,6.41395137 2.32365822,6.41795137 C2.3364847,6.42695137 2.3532578,6.43495137 2.36608428,6.44395137 C2.37891076,6.45295137 2.39963047,6.46895137 2.41541691,6.47295137 C2.4509364,6.49295137 2.50618894,6.51495137 2.53381521,6.47295137 C2.54170843,6.45695137 2.54762835,6.44495137 2.53677517,6.43095137 C2.52690864,6.41595137 2.5111222,6.41195137 2.50618894,6.39995137 C2.50224233,6.38695137 2.51309551,6.37895137 2.50026902,6.36895137 L2.50026902,6.36895137 Z M7.24508107,7.12395137 C7.22633467,7.12495137 7.19278848,7.13695137 7.17798869,7.14995137 C7.14838912,7.17595137 7.21153488,7.19095137 7.23620119,7.19795137 C7.26382747,7.21395137 7.30329357,7.22195137 7.32993319,7.23795137 C7.35262619,7.25495137 7.36841263,7.27795137 7.3940656,7.28895137 C7.42563848,7.30395137 7.46905119,7.31095137 7.50358403,7.31995137 C7.51838382,7.32495137 7.54107683,7.32395137 7.56080988,7.32795137 C7.58251623,7.34095137 7.59238276,7.36095137 7.61014251,7.37495137 C7.64072873,7.40295137 7.68414145,7.40995137 7.7245942,7.40795137 C7.76307365,7.41195137 7.79168657,7.41895137 7.82621941,7.40995137 C7.86568551,7.39995137 7.89331178,7.41995137 7.92981793,7.41995137 C7.94461771,7.41995137 7.9594175,7.40795137 7.97323064,7.40895137 C7.99197704,7.40895137 7.99395034,7.41695137 8.00283021,7.43295137 C8.01861666,7.45595137 8.05906941,7.49095137 8.08768233,7.49195137 C8.10544208,7.49195137 8.11925521,7.48895137 8.134055,7.49395137 C8.15082809,7.50395137 8.15773466,7.50395137 8.16957449,7.51395137 C8.1902942,7.52295137 8.20805394,7.52895137 8.21693381,7.54495137 C8.23272026,7.57295137 8.2317336,7.60395137 8.25639992,7.62595137 C8.27317301,7.63895137 8.29093275,7.65295137 8.3086925,7.66595137 C8.32053233,7.67695137 8.31066581,7.67495137 8.32842555,7.67495137 C8.33829208,7.67695137 8.35703847,7.67695137 8.36986496,7.67295137 C8.41919758,7.66995137 8.39255797,7.59995137 8.37677153,7.57695137 C8.366905,7.55695137 8.35802513,7.54095137 8.36197174,7.52195137 C8.36493169,7.49895137 8.37578487,7.48295137 8.36098508,7.46395137 C8.35309186,7.45195137 8.34223869,7.44595137 8.33138551,7.43995137 C8.32546559,7.43195137 8.32250564,7.42395137 8.31559907,7.41195137 C8.30079928,7.39295137 8.27218636,7.38695137 8.25343996,7.36895137 C8.22186708,7.33695137 8.20509398,7.29095137 8.16464123,7.26095137 C8.14293487,7.24795137 8.12320182,7.25795137 8.09656221,7.24695137 C8.08570903,7.23995137 8.07978911,7.23295137 8.06400267,7.22795137 C8.04920288,7.22295137 8.0363764,7.22595137 8.02256327,7.22495137 C7.99395034,7.22295137 7.96928403,7.19795137 7.94165776,7.19995137 C7.91107153,7.20395137 7.90515161,7.23695137 7.88739187,7.25495137 C7.87160543,7.26795137 7.85384568,7.26795137 7.84792577,7.24695137 C7.84595246,7.21995137 7.85581899,7.20395137 7.86963212,7.18695137 C7.89133848,7.16395137 7.86963212,7.15095137 7.8410192,7.14895137 C7.80451305,7.14895137 7.79760649,7.17795137 7.7828067,7.20895137 C7.75912704,7.24195137 7.74432725,7.21895137 7.71078106,7.21395137 C7.68808806,7.21495137 7.67230162,7.22395137 7.65059526,7.21495137 C7.63579547,7.20995137 7.63283551,7.19795137 7.62198234,7.19095137 C7.60520924,7.18195137 7.59238276,7.18495137 7.58054293,7.19295137 C7.56376984,7.19695137 7.56376984,7.19695137 7.54699674,7.18795137 C7.53219696,7.18195137 7.52825034,7.16995137 7.50950395,7.16595137 C7.47990437,7.15995137 7.44931814,7.18495137 7.42465183,7.17795137 C7.41379865,7.17195137 7.40491878,7.15595137 7.39011899,7.15095137 C7.3733459,7.14095137 7.37630585,7.14995137 7.36545268,7.16195137 C7.34670628,7.17995137 7.32105331,7.18595137 7.30329357,7.17195137 C7.28060056,7.15595137 7.27862725,7.12895137 7.24508107,7.12395137 L7.24508107,7.12395137 Z M8.37183826,8.30595137 C8.3876247,8.30395137 8.39551792,8.28795137 8.40933106,8.28995137 C8.4251175,8.28695137 8.41722428,8.30295137 8.42807746,8.31295137 C8.43794398,8.32195137 8.44781051,8.32195137 8.45767703,8.32195137 C8.47543678,8.32495137 8.50996962,8.32695137 8.51687619,8.31095137 C8.52476941,8.28595137 8.48333,8.28095137 8.47247682,8.26095137 C8.4626103,8.23195137 8.4853033,8.20395137 8.49319652,8.17895137 C8.50503635,8.14495137 8.4626103,8.12995137 8.46655691,8.10295137 C8.46557025,8.07395137 8.4853033,8.06395137 8.47938339,8.03595137 C8.47445013,8.01495137 8.45669038,7.99195137 8.4438639,7.97695137 C8.43202407,7.96095137 8.40933106,7.94595137 8.41130436,7.92295137 C8.41327767,7.89895137 8.45669038,7.89995137 8.43597068,7.87095137 C8.42413085,7.84595137 8.39255797,7.85095137 8.36394504,7.84695137 C8.35407852,7.84695137 8.34421199,7.84795137 8.33434547,7.83795137 C8.32546559,7.82395137 8.3294122,7.81695137 8.3294122,7.80695137 C8.32349229,7.77995137 8.30277259,7.76995137 8.27909292,7.75895137 C8.2711997,7.75495137 8.25935987,7.74995137 8.25442661,7.73795137 C8.25048,7.72595137 8.26231983,7.72195137 8.25837322,7.71095137 C8.24554674,7.68495137 8.19818742,7.72095137 8.17845437,7.71195137 C8.16464123,7.70995137 8.16661454,7.69695137 8.15773466,7.68295137 L8.134055,7.67195137 C8.10149547,7.65695137 8.08866899,7.68395137 8.0945889,7.71095137 C8.10938869,7.77195137 8.15378805,7.81195137 8.14885479,7.87295137 C8.15181475,7.89795137 8.15576136,7.90995137 8.16464123,7.93295137 C8.17253445,7.96595137 8.18141432,7.98195137 8.16661454,8.01395137 C8.14293487,8.03195137 8.16464123,8.05395137 8.17253445,8.07695137 C8.17746771,8.10795137 8.18536093,8.13195137 8.18437428,8.16495137 C8.17845437,8.22495137 8.15970797,8.28395137 8.16464123,8.34495137 C8.16760119,8.36995137 8.16562788,8.39295137 8.17450776,8.41695137 C8.17845437,8.44795137 8.20312068,8.45895137 8.22877365,8.47595137 C8.25343996,8.49695137 8.36789165,8.56595137 8.33434547,8.48195137 C8.32447894,8.46295137 8.3086925,8.43595137 8.30375924,8.41395137 C8.29586602,8.39095137 8.32349229,8.37495137 8.32447894,8.35095137 C8.32842555,8.32395137 8.30770585,8.31495137 8.3461853,8.30795137 C8.35407852,8.30195137 8.36591835,8.30795137 8.37183826,8.30595137 L8.37183826,8.30595137 Z M7.1819353,1.09995137 C7.21252153,1.09295137 7.24310776,1.10195137 7.27172069,1.09095137 C7.28652047,1.08495137 7.33486645,1.06795137 7.33190649,1.04795137 C7.32697323,1.01095137 7.17009547,1.03495137 7.14444251,1.04595137 C7.13654929,1.06895137 7.16022895,1.08695137 7.18094865,1.09295137 C7.18094865,1.09495137 7.1819353,1.09795137 7.1819353,1.09995137 L7.1819353,1.09995137 Z M7.93573784,7.78795137 C7.92981793,7.77495137 7.93573784,7.76295137 7.93573784,7.74995137 C7.93277788,7.72895137 7.92685797,7.72295137 7.92981793,7.70095137 C7.93672449,7.68895137 7.93672449,7.66995137 7.93376454,7.65495137 C7.92784462,7.64295137 7.9179781,7.63295137 7.90909822,7.62395137 C7.90909822,7.61795137 7.90613827,7.60795137 7.8992317,7.60195137 C7.88739187,7.58995137 7.87456538,7.60795137 7.8617389,7.61395137 C7.85187238,7.62295137 7.83312598,7.62895137 7.83016602,7.63895137 C7.8202995,7.65395137 7.82621941,7.66595137 7.82621941,7.67895137 L7.82917937,7.69395137 C7.80747301,7.71595137 7.82819272,7.77395137 7.82523276,7.79595137 C7.82523276,7.82095137 7.78971327,7.89095137 7.83707259,7.86995137 C7.84989907,7.86395137 7.8597656,7.85495137 7.87160543,7.84895137 C7.88739187,7.83995137 7.90712492,7.83995137 7.92587132,7.83395137 C7.93179123,7.83395137 7.96632407,7.83095137 7.96632407,7.82495137 C7.96731072,7.81195137 7.9386978,7.80295137 7.93573784,7.78795137 L7.93573784,7.78795137 Z M7.0447906,9.05195137 C7.0842567,9.07095137 7.15332238,9.03295137 7.19081518,9.02095137 C7.2381745,9.00595137 7.31316009,8.95595137 7.36150607,8.98395137 C7.38123912,8.99495137 7.39110564,9.01795137 7.41182535,9.02695137 C7.43747831,9.03795137 7.46806454,9.02795137 7.49371751,9.02195137 C7.52035712,9.01595137 7.55094335,9.01195137 7.57560967,8.99995137 C7.59731602,8.98895137 7.61112916,8.97095137 7.62987556,8.95695137 C7.67822153,8.91995137 7.71966094,8.95495137 7.77294017,8.94695137 C7.8035264,8.94295137 7.83213933,8.92795137 7.8617389,8.91995137 C7.88344526,8.91495137 7.92192471,8.91495137 7.9386978,8.89895137 C7.9574442,8.88095137 7.94856432,8.84195137 7.94856432,8.81895137 C7.94757767,8.78795137 7.94955098,8.75595137 7.9386978,8.72695137 C7.91699144,8.66995137 7.83805924,8.60295137 7.9199514,8.55795137 C7.93573784,8.45795137 7.81931284,8.47495137 7.78576666,8.40295137 C7.7640603,8.35595137 7.75715373,8.31995137 7.69499462,8.31495137 C7.64270204,8.30995137 7.61112916,8.33795137 7.56574314,8.35595137 C7.51443721,8.37495137 7.47497111,8.35795137 7.43057174,8.33295137 C7.40393213,8.31795137 7.34769293,8.28295137 7.33683975,8.32895137 C7.32697323,8.36895137 7.36545268,8.40795137 7.3338798,8.44395137 C7.30625352,8.47595137 7.25790755,8.48995137 7.21844145,8.49895137 C7.13260268,8.51695137 7.06452365,8.58295137 7.00236454,8.63995137 L7.00927111,8.64595137 C6.9846048,8.64495137 6.94809865,8.71095137 6.947112,8.73095137 C6.95697853,8.73395137 6.9658584,8.73695137 6.97671158,8.73995137 C6.97572493,8.77395137 7.01420437,8.75095137 7.01716433,8.72695137 C7.02505755,8.72895137 7.03295077,8.73395137 7.04084399,8.73495137 C7.04775056,8.73695137 7.06255035,8.73595137 7.06847026,8.73895137 C7.08524336,8.74595137 7.08820331,8.76195137 7.10892302,8.76395137 C7.09708319,8.81595137 7.10793636,8.87095137 7.08327005,8.91995137 C7.06748361,8.94995137 6.98756476,9.02395137 7.0447906,9.05195137 L7.0447906,9.05195137 Z M7.4522781,1.35995137 C7.48187768,1.39195137 7.51838382,1.40095137 7.51147725,1.45095137 C7.54897005,1.45595137 7.57264971,1.46995137 7.59435606,1.43795137 C7.6081692,1.41795137 7.6288889,1.40195137 7.65158191,1.39395137 C7.67920818,1.38295137 7.79267322,1.38395137 7.78773996,1.43095137 C7.78478,1.45395137 7.77096687,1.47395137 7.76702026,1.49695137 C7.762087,1.52895137 7.79661983,1.50595137 7.81141962,1.51395137 C7.79464653,1.52595137 7.77392683,1.53295137 7.75320712,1.53795137 C7.762087,1.54395137 7.76800691,1.55195137 7.76899356,1.56195137 C7.7433406,1.56795137 7.73051411,1.63995137 7.6851281,1.65395137 C7.65750183,1.66295137 7.61704907,1.64395137 7.5894228,1.64095137 C7.55686327,1.63695137 7.53219696,1.62695137 7.49963742,1.62495137 C7.46806454,1.62295137 7.49371751,1.58095137 7.4542514,1.58895137 C7.44734484,1.61695137 7.46115797,1.68795137 7.46609124,1.71595137 C7.4710245,1.75095137 7.50062407,1.77095137 7.53515691,1.77695137 C7.58350289,1.78495137 7.6061959,1.80095137 7.6476353,1.82495137 C7.68019484,1.84295137 7.71670098,1.83195137 7.75222047,1.83495137 C7.77590013,1.83695137 7.79563318,1.84595137 7.81339293,1.86095137 C7.80944632,1.87195137 7.80056644,1.88995137 7.80648636,1.90195137 C7.81339293,1.91795137 7.86371221,1.89995137 7.87555204,1.89895137 C7.91107153,1.89495137 7.94461771,1.85595137 7.9781639,1.86095137 C7.99099038,1.86295137 8.05018954,1.88095137 8.04722958,1.89595137 C8.0156567,1.88295137 7.99493699,1.92195137 7.97027068,1.90095137 C7.94856432,1.88195137 7.89035183,1.89795137 7.92784462,1.92495137 C7.93080458,1.92795137 7.9406711,2.00295137 7.9406711,2.01095137 C7.93771115,2.03895137 7.88739187,2.06895137 7.89133848,2.08695137 C7.89824505,2.08795137 7.94363106,2.09095137 7.95349759,2.09995137 C7.95645755,2.08795137 7.94757767,2.08295137 7.97520394,2.07495137 C7.99592365,2.06895137 8.02058996,2.06695137 8.04130966,2.07695137 C8.04920288,2.11195137 8.02354992,2.14795137 8.07189589,2.13895137 C8.11728191,2.12995137 8.13701496,2.15995137 8.18437428,2.12895137 C8.21397386,2.11095137 8.24554674,2.11395137 8.2711997,2.13995137 C8.30573254,2.17395137 8.23568021,2.22095137 8.27613297,2.25395137 C8.29191941,2.26695137 8.30474589,2.30695137 8.32053233,2.31395137 C8.33138551,2.31895137 8.39058466,2.29795137 8.40143784,2.29295137 C8.42018424,2.32695137 8.43695733,2.27495137 8.45175712,2.27195137 C8.45767703,2.24995137 8.4853033,2.22495137 8.51194292,2.22195137 C8.55140902,2.21795137 8.55239568,2.22495137 8.5790353,2.24395137 C8.65698085,2.29795137 8.64612767,2.16595137 8.68658042,2.13195137 C8.75959271,2.07195137 8.79609885,2.01495137 8.84641813,1.93795137 C8.88588423,1.87595137 8.94113678,1.86095137 9.01118911,1.84995137 C9.06644165,1.84095137 9.15129377,1.82795137 9.17398677,1.76795137 C9.20062639,1.69795137 9.13649398,1.65995137 9.08025478,1.63895137 C9.01710902,1.61695137 8.94607004,1.59295137 8.97369631,1.51295137 C9.00625584,1.41995137 8.97764292,1.36595137 8.87897767,1.33595137 C8.67079398,1.27095137 8.48333,1.16195137 8.2711997,1.10195137 C8.08373572,1.04895137 7.89429844,1.02995137 7.70190119,1.01995137 C7.61606242,0.98995137 7.43451835,0.98695137 7.38222577,1.05995137 C7.34867958,1.10695137 7.39110564,1.14795137 7.38715903,1.19695137 C7.38222577,1.25595137 7.41083869,1.31595137 7.4522781,1.35995137 L7.4522781,1.35995137 Z M10.7269779,10.6309514 L10.7259912,10.6299514 C10.7289512,10.6349514 10.7269779,10.6439514 10.7279645,10.6509514 C10.766444,10.6509514 10.7832171,10.6859514 10.8246565,10.6729514 C10.8670825,10.6609514 10.8917488,10.6199514 10.8582027,10.5859514 C10.8286031,10.5569514 10.8029501,10.5319514 10.7595374,10.5399514 C10.7082315,10.5499514 10.7190846,10.5909514 10.7269779,10.6309514 L10.7269779,10.6309514 Z M12.0678387,9.29395137 C12.0658654,9.28495137 12.0638921,9.27695137 12.0619187,9.26795137 C12.021466,9.25595137 11.995813,9.29795137 11.9583202,9.26695137 C11.8862946,9.31595137 11.9632535,9.41295137 11.8448552,9.40695137 C11.8655749,9.43195137 11.8636016,9.45995137 11.8537351,9.48895137 C11.8389353,9.53395137 11.8270954,9.52995137 11.7965092,9.53595137 C11.7323768,9.54595137 11.7017906,9.50595137 11.6820575,9.45195137 C11.6189118,9.45395137 11.5320863,9.55195137 11.4827537,9.58295137 C11.4699272,9.58995137 11.4472342,9.61095137 11.4334211,9.61995137 C11.4225679,9.62595137 11.3959283,9.63895137 11.3821151,9.64695137 C11.348569,9.66395137 11.2765433,9.68695137 11.2725967,9.72495137 C11.2558236,9.72195137 11.2301707,9.73195137 11.2133976,9.72995137 C11.2074776,9.73795137 11.2074776,9.74695137 11.2133976,9.75595137 C11.2903565,9.76895137 11.3308092,9.74295137 11.3959283,9.71495137 C11.4640073,9.68395137 11.5370196,9.69095137 11.601152,9.66695137 C11.6317382,9.65595137 11.6327249,9.62195137 11.6830442,9.64195137 C11.7047505,9.65195137 11.7304035,9.68395137 11.7353368,9.70595137 C11.7452033,9.75595137 11.6929107,9.82995137 11.6406181,9.83295137 C11.6277916,9.80195137 11.646538,9.76995137 11.6524579,9.74495137 C11.5833923,9.72195137 11.4699272,9.81995137 11.4511808,9.87795137 C11.5222198,9.89295137 11.5518194,9.99695137 11.5133399,10.0539514 C11.5005135,10.0679514 11.4857137,10.0859514 11.4610474,10.0939514 C11.4205946,10.1059514 11.4018482,10.0689514 11.3623821,10.0979514 C11.3110762,10.1369514 11.3673154,10.2439514 11.3377158,10.3039514 C11.3150228,10.3499514 11.2765433,10.3669514 11.2439838,10.3989514 C11.2222774,10.4219514 11.209451,10.4469514 11.1798514,10.4669514 C11.1413719,10.4929514 11.0476399,10.5489514 11.0555332,10.6029514 C11.1403853,10.6319514 11.3160094,10.4839514 11.3890217,10.4349514 C11.4353944,10.4039514 11.4640073,10.3559514 11.5113666,10.3249514 C11.5646459,10.2919514 11.6346982,10.2749514 11.669231,10.2159514 C11.6889641,10.1819514 11.6731776,10.1519514 11.6850175,10.1179514 C11.6958707,10.0879514 11.7165904,10.0779514 11.7363234,10.0549514 C11.7728296,10.0109514 11.8063757,9.99695137 11.8478151,9.96095137 C11.8991211,9.91495137 11.8872812,9.84295137 11.9109609,9.78195137 C11.9316806,9.72895137 11.9721334,9.68795137 12.0007463,9.63795137 C12.0451457,9.55895137 12.1615707,9.37095137 12.112238,9.28195137 C12.1003982,9.29195137 12.0786918,9.28895137 12.0678387,9.29395137 L12.0678387,9.29395137 Z M13.0752109,6.73495137 C13.0495579,6.68695137 13.0880374,6.54895137 13.0880374,6.49195137 C13.0870507,6.38695137 13.0554778,6.30795137 13.0406781,6.20995137 C13.0317982,6.11795137 13.0189717,5.87395137 13.0525179,5.79095137 C13.0998772,5.67395137 12.8690005,5.47595137 12.856174,5.34895137 C12.8443342,5.23895137 12.7821751,5.13495137 12.6923897,5.07195137 C12.6558836,5.04495137 12.5769514,4.68195137 12.5305787,4.69895137 C12.5078857,4.70995137 12.555245,4.78995137 12.5522851,4.81495137 C12.5394586,4.90295137 12.4950592,4.81495137 12.4486865,4.83495137 C12.3628478,4.86995137 12.2720757,4.95295137 12.2612226,5.03795137 C12.2207698,5.35295137 11.9977863,5.02695137 12.0155461,5.01395137 C12.0648787,4.97595137 12.0826384,4.98795137 12.1408509,4.97995137 C12.2049834,4.95695137 12.1053315,4.91095137 12.20597,4.90095137 C12.1822904,4.83595137 12.2355696,4.81495137 12.2099166,4.76395137 C12.1714372,4.68895137 12.1438109,4.69795137 12.1822904,4.61695137 C12.1990634,4.57295137 12.0984249,4.43395137 12.0905317,4.38095137 C12.0826384,4.32895137 12.0816518,4.26095137 12.0747452,4.20295137 C12.0707986,4.16595137 12.1309844,4.13095137 12.1201312,4.10195137 C12.1181579,3.99895137 12.1408509,3.88795137 12.1043448,3.78795137 C12.0786918,3.71995137 12.0490923,3.62995137 12.0056796,3.57195137 C11.9908798,3.55195137 11.9445071,3.44895137 11.9395738,3.41995137 C11.927734,3.35595137 11.8991211,3.37995137 11.8636016,3.35495137 C11.8438685,3.32995137 11.7550698,3.24695137 11.7294168,3.23495137 C11.7057372,3.22395137 11.5340596,3.06695137 11.530113,3.05395137 C11.5153132,3.00895137 11.4186213,2.97395137 11.4294745,2.92495137 C11.4452609,2.85095137 11.1877446,2.65895137 11.115719,2.64595137 C11.0693463,2.63795137 11.2577969,2.86395137 11.2568103,2.85895137 C11.2597702,2.87195137 11.3781685,3.02295137 11.3781685,3.02295137 C11.4048082,3.03195137 11.4699272,3.21695137 11.4679539,3.24095137 C11.4610474,3.31095137 11.2804899,3.12595137 11.2666768,3.10095137 C11.1778781,2.99195137 11.0170537,2.90395137 10.9154285,2.83095137 C10.8434029,2.76395137 10.8789224,2.72595137 10.7555908,2.66895137 C10.7102048,2.64795137 10.5868732,2.54695137 10.5483938,2.54395137 C10.5020211,2.54195137 10.5553003,2.63995137 10.556287,2.65095137 C10.5631935,2.72095137 10.6391658,2.72595137 10.6845518,2.77195137 C10.7210579,2.80995137 10.7536175,2.85695137 10.7220446,2.89895137 C10.7210579,2.89895137 10.6648188,3.00295137 10.6618588,2.99395137 C10.6776452,3.03795137 10.80887,3.13495137 10.8414296,3.17095137 C10.8355096,3.16195137 11.0131071,3.39495137 11.0279069,3.27095137 C11.0338268,3.22595137 10.9835075,3.17195137 10.9904141,3.13295137 C10.9953474,3.10895137 11.1936645,3.35995137 11.2045177,3.38195137 C11.2528637,3.51495137 11.2489171,3.36195137 11.2992363,3.37795137 C11.3406757,3.39095137 11.4521675,3.52995137 11.3594221,3.53595137 C11.2183308,3.54495137 11.3850751,3.66795137 11.4245412,3.68695137 C11.5064334,3.72695137 11.5626726,3.81995137 11.6475247,3.85495137 C11.7807228,3.90895137 11.7530965,4.00495137 11.8201889,4.10295137 C11.8418952,4.13395137 11.4373677,4.10295137 11.4057948,4.12095137 C11.3525156,4.16295137 11.6090452,4.44995137 11.6100319,4.49295137 C11.6120052,4.58295137 11.6633111,4.64895137 11.6771243,4.73895137 C11.6850175,4.82195137 11.675151,4.93095137 11.7294168,4.99795137 C11.7738162,5.03895137 11.8152556,4.92995137 11.8853079,4.99495137 C11.9109609,5.00695137 11.9474671,5.03595137 11.9553603,5.05795137 C11.9790399,5.11995137 12.1132247,5.49895137 11.9524003,5.47095137 C11.8813613,5.45795137 11.9218141,5.76895137 11.9267473,5.81395137 C11.9484537,5.91195137 11.9879198,5.90395137 11.9622668,6.02795137 C11.9652268,6.13095137 11.882348,6.18295137 11.8231488,6.25695137 C11.7955226,6.29095137 11.7777628,6.33095137 11.7649363,6.37395137 C11.7323768,6.34195137 11.7165904,6.29095137 11.6712043,6.27395137 C11.6218717,6.25495137 11.5133399,6.31495137 11.4699272,6.33595137 C11.3653421,6.38895137 11.442301,6.48495137 11.4008615,6.56795137 C11.371262,6.62895137 11.2824632,6.65895137 11.2242507,6.68895137 C11.1541984,6.72495137 11.0604664,6.76295137 10.9914007,6.70495137 C10.9322016,6.65695137 10.9578546,6.55995137 10.8956954,6.51795137 C10.8256431,6.47095137 10.8187366,6.57595137 10.8029501,6.61795137 C10.7723639,6.69695137 10.6806052,6.72395137 10.7042849,6.82295137 C10.7141514,6.86395137 10.7348711,6.90095137 10.7427643,6.94195137 C10.7526308,6.99295137 10.7269779,7.03895137 10.7240179,7.08995137 C10.718098,7.17695137 10.80887,7.19695137 10.8325497,7.26795137 C10.8532694,7.33195137 10.831563,7.43095137 10.7605241,7.45495137 C10.6845518,7.48195137 10.6006863,7.41295137 10.5257007,7.40495137 C10.4507152,7.39695137 10.3550099,7.41795137 10.3411967,7.50395137 C10.3283702,7.57995137 10.4053291,7.64195137 10.3678363,7.71995137 C10.3520499,7.75295137 10.3244236,7.77895137 10.3046906,7.80895137 C10.2701577,7.85895137 10.2504247,7.91695137 10.2178652,7.96795137 C10.2563446,7.96895137 10.252398,7.94495137 10.2869308,7.95195137 C10.323437,7.95995137 10.3559965,7.92295137 10.3865827,7.91095137 C10.3925027,7.93495137 10.3895427,7.95995137 10.3925027,7.98395137 C10.4181556,7.99195137 10.4438086,7.98195137 10.4665016,7.97295137 C10.4694616,7.99395137 10.459595,8.01795137 10.4684749,8.03895137 C10.4753815,8.05695137 10.4961012,8.06295137 10.507941,8.07695137 C10.5385272,8.11395137 10.5010344,8.17495137 10.4793281,8.20695137 C10.417169,8.29895137 10.3106105,8.34995137 10.2415448,8.43595137 C10.1764257,8.51595137 10.1705058,8.61295137 10.1221599,8.69995137 C10.1053868,8.72995137 10.0886137,8.77095137 10.133013,8.78495137 C10.1428796,8.76895137 10.1576794,8.75595137 10.1783991,8.75595137 C10.2089853,8.75495137 10.1971455,8.77795137 10.2129319,8.79595137 C10.2770643,8.87795137 10.3451433,8.74295137 10.3727696,8.70395137 C10.4003959,8.66195137 10.5148476,8.59895137 10.5464205,8.66895137 C10.5710868,8.72195137 10.5424738,8.79695137 10.5178075,8.84495137 C10.5592469,8.86395137 10.5474071,8.89395137 10.5572736,8.92995137 C10.5701001,8.97995137 10.6154861,9.01195137 10.6154861,9.06695137 C10.6154861,9.13295137 10.4714349,9.26395137 10.5276741,9.31395137 C10.5977264,9.37595137 10.6806052,9.20395137 10.7082315,9.16695137 C10.7605241,9.09595137 10.879909,9.08595137 10.9095086,8.99895137 C10.9420681,8.89995137 10.9312149,8.84095137 11.0624397,8.83795137 C11.1176923,8.83695137 11.158145,8.80195137 11.2104376,8.79095137 C11.2676635,8.77995137 11.2933164,8.77395137 11.3298226,8.72995137 C11.3821151,8.66695137 11.4294745,8.74195137 11.4314478,8.79195137 C11.4334211,8.84295137 11.4107281,8.90695137 11.442301,8.95295137 C11.4807804,9.00895137 11.5232065,8.93495137 11.5626726,8.89895137 C11.558726,8.93695137 11.6090452,8.95895137 11.6386448,8.97095137 C11.6840308,8.93995137 11.7126437,8.88895137 11.7609897,8.86095137 C11.7836827,8.84795137 11.8093357,8.84295137 11.8349887,8.83895137 C11.8418952,8.87995137 11.8488018,8.92395137 11.8853079,8.94395137 C11.9376005,8.97395137 11.8734681,9.00295137 11.9425338,9.03495137 C12.0283726,9.06795137 12.0569855,9.15495137 12.0984249,9.22495137 C12.1181579,9.25695137 12.2977287,9.06195137 12.3667944,9.05495137 C12.5956978,9.02895137 12.7150827,8.72995137 12.7999348,8.55295137 C12.9222798,8.29995137 12.9775323,8.01895137 13.0091052,7.75795137 C13.0870507,7.59695137 13.1186236,7.30195137 13.0870507,7.11495137 C13.0683043,7.00095137 13.1334234,6.84295137 13.0752109,6.73495137 L13.0752109,6.73495137 Z M11.0032406,10.5319514 C11.0091605,10.5039514 11.0683596,10.3999514 11.0131071,10.3849514 C10.993374,10.3799514 10.976601,10.4099514 10.9588412,10.4149514 C10.9351615,10.4229514 10.9095086,10.4079514 10.8878022,10.4189514 C10.8680692,10.4299514 10.8493228,10.4619514 10.8374829,10.4799514 C10.8226832,10.5019514 10.8286031,10.5109514 10.8522827,10.5229514 C10.8759624,10.5359514 10.9065486,10.5419514 10.9203618,10.5679514 C10.9322016,10.5909514 10.9262817,10.6219514 10.9233217,10.6459514 C10.9233217,10.6449514 10.9272683,10.6409514 10.928255,10.6369514 C10.9322016,10.6359514 10.9391082,10.6349514 10.9430548,10.6359514 L10.9381215,10.6459514 C11.0012673,10.6559514 10.996334,10.5729514 11.0032406,10.5319514 L11.0032406,10.5319514 Z M11.7422433,9.28095137 C11.7442166,9.31095137 11.7767762,9.30795137 11.7984825,9.29995137 C11.8182156,9.29395137 11.8310421,9.27695137 11.8438685,9.26195137 C11.8616283,9.23895137 11.8724815,9.21595137 11.856695,9.18895137 C11.8409086,9.16095137 11.8310421,9.14095137 11.8231488,9.10795137 C11.8103223,9.11495137 11.7945359,9.12695137 11.7807228,9.13095137 C11.7669096,9.13595137 11.7649363,9.13195137 11.7491499,9.13095137 C11.7126437,9.12995137 11.720537,9.15795137 11.7047505,9.18095137 C11.691924,9.20095137 11.6633111,9.20895137 11.6741643,9.23495137 C11.6820575,9.25495137 11.7146171,9.27195137 11.7333635,9.28095137 L11.7382967,9.27495137 C11.7373101,9.27695137 11.7363234,9.27795137 11.7353368,9.27995137 C11.7373101,9.28095137 11.74027,9.28095137 11.7422433,9.28095137 L11.7422433,9.28095137 Z M8.18042767,11.4279514 C8.21693381,11.3629514 8.28205288,11.3219514 8.34026538,11.2769514 C8.41031771,11.2229514 8.47247682,11.1599514 8.52772936,11.0919514 C8.49516983,11.0839514 8.49319652,11.0529514 8.47247682,11.0329514 C8.44090394,11.0019514 8.39255797,11.0219514 8.3856514,10.9749514 C8.37874483,10.9329514 8.34421199,10.9239514 8.31066581,10.9069514 C8.23370691,10.8679514 8.20213403,10.7919514 8.13997492,10.7389514 C8.07189589,10.6789514 7.97915055,10.6989514 7.89627174,10.6829514 C7.82325945,10.6689514 7.74926051,10.5519514 7.67131496,10.6019514 C7.62198234,10.6329514 7.59928933,10.7119514 7.63283551,10.7609514 C7.65947513,10.7989514 7.70486115,10.8179514 7.72262089,10.8629514 C7.69598128,10.8879514 7.69006136,10.9039514 7.72262089,10.9269514 C7.76110034,10.9539514 7.83509928,10.9819514 7.81635289,11.0409514 C7.80648636,11.0729514 7.77984674,11.1039514 7.7453139,11.1099514 C7.72064759,11.1149514 7.66046178,11.1009514 7.67328827,11.1459514 C7.645662,11.0719514 7.56771645,11.1879514 7.52529039,11.1269514 C7.49075755,11.0779514 7.46905119,11.0339514 7.4147853,11.0009514 C7.34473297,10.9579514 7.44339823,10.9159514 7.4315584,10.8509514 C7.41379865,10.7559514 7.2983603,10.7819514 7.2569209,10.7119514 C7.23225458,10.6719514 7.26580077,10.6399514 7.28158721,10.6049514 C7.29737365,10.5689514 7.33979971,10.5979514 7.36249272,10.6079514 C7.43649166,10.6429514 7.54502344,10.6299514 7.60718255,10.5789514 C7.63579547,10.5549514 7.69894123,10.4439514 7.61902238,10.4439514 C7.56376984,10.4449514 7.52134378,10.4929514 7.46905119,10.4959514 C7.46115797,10.4329514 7.4315584,10.3259514 7.49865077,10.2839514 C7.55982323,10.2459514 7.68808806,10.2019514 7.63382217,10.1039514 C7.61408912,10.0699514 7.57955628,10.1259514 7.55094335,10.1009514 C7.53910352,10.0909514 7.5479834,10.0679514 7.55193001,10.0569514 C7.53318361,10.0399514 7.51542386,10.0189514 7.50555734,9.99495137 C7.46214463,9.88895137 7.59040945,9.80595137 7.53614357,9.69395137 C7.51345056,9.64695137 7.47497111,9.61895137 7.43254505,9.58995137 C7.39011899,9.55995137 7.38814568,9.52195137 7.37235924,9.47695137 C7.36446602,9.45195137 7.32302662,9.39295137 7.28750713,9.40795137 C7.2569209,9.41995137 7.24804102,9.47295137 7.22436136,9.49495137 C7.17108213,9.54695137 7.05860374,9.56695137 6.98756476,9.54995137 C6.93033891,9.53695137 6.93329887,9.51495137 6.9056726,9.47695137 C6.89679272,9.46295137 6.87705967,9.46195137 6.86225988,9.45595137 C6.83660692,9.44595137 6.83364696,9.42295137 6.82772704,9.39995137 C6.80404738,9.31295137 6.63236984,9.42095137 6.60573022,9.29895137 C6.59981031,9.27095137 6.60967683,9.22395137 6.56922408,9.21795137 C6.52383806,9.20995137 6.52186476,9.16595137 6.52186476,9.12895137 C6.52186476,9.09895137 6.52383806,9.05695137 6.49226518,9.03995137 C6.45181243,9.01795137 6.4419459,9.02795137 6.42911942,8.98195137 C6.41431963,8.92295137 6.37386688,8.98395137 6.34032069,8.97195137 C6.26928171,8.94395137 6.28210819,8.97895137 6.22488235,9.00895137 C6.12720374,9.06095137 6.11635057,8.81995137 6.08280438,8.77295137 C6.01768531,8.68295137 6.03445841,8.88395137 5.99005904,8.90895137 C5.94960629,8.93195137 5.90718023,8.87895137 5.89238044,8.84795137 C5.88350057,8.82995137 5.87856731,8.80995137 5.86771413,8.79195137 C5.85094103,8.76595137 5.82134146,8.75495137 5.80456837,8.72895137 C5.79075523,8.70595137 5.77003553,8.67895137 5.760169,8.65395137 C5.75128913,8.63195137 5.75326243,8.60395137 5.73648934,8.58595137 C5.71576964,8.56295137 5.7414226,8.52495137 5.75622239,8.49595137 C5.78187536,8.48595137 5.82035481,8.50595137 5.8391012,8.52295137 C5.88547387,8.56195137 5.9555262,8.73295137 6.03643171,8.70095137 C6.01965862,8.67895137 6.0305118,8.65195137 6.01867197,8.62795137 C6.00584548,8.60295137 5.98117917,8.58795137 5.96243277,8.56795137 C5.92099336,8.51995137 5.87560735,8.47195137 5.84798108,8.41395137 C5.82430142,8.36395137 5.81246159,8.31095137 5.76411561,8.27595137 C5.72464951,8.24695137 5.64670396,8.21895137 5.66347705,8.15695137 C5.66347705,8.15595137 5.66446371,8.15495137 5.66446371,8.15495137 C5.69702324,8.16195137 5.71971625,8.18595137 5.74339591,8.20695137 C5.77792875,8.23695137 5.82232811,8.25195137 5.86278087,8.27195137 C5.93677981,8.30795137 6.02261858,8.33295137 6.08576434,8.38795137 C6.12523044,8.42095137 6.10451074,8.49495137 6.15384336,8.53595137 C6.19034951,8.56595137 6.2446154,8.66695137 6.31170777,8.62695137 C6.33637408,8.61195137 6.34722726,8.58295137 6.37189357,8.56595137 C6.39853319,8.54695137 6.44293256,8.52995137 6.47351878,8.51595137 C6.49226518,8.50695137 6.52383806,8.50995137 6.53863785,8.49495137 C6.56231751,8.47195137 6.50607832,8.40595137 6.49325184,8.38895137 C6.44293256,8.32395137 6.39655989,8.25295137 6.32946751,8.20395137 C6.29493468,8.17895137 6.26138849,8.15195137 6.22093574,8.13495137 C6.19922938,8.12595137 6.16074993,8.12695137 6.15680332,8.09695137 C6.1676565,8.10395137 6.17357641,8.10195137 6.17554972,8.09095137 C6.17456307,8.07095137 6.14595014,8.06995137 6.13213701,8.06595137 C6.09859082,8.05695137 6.07589781,8.05695137 6.06307133,8.02895137 C6.04629824,7.99495137 5.98709908,7.99595137 5.9555262,7.98795137 C5.90816688,7.97595137 5.87067409,7.93995137 5.82528807,7.92195137 C5.77200883,7.90195137 5.73155608,7.92295137 5.67926349,7.93495137 C5.67038362,7.93695137 5.65262388,7.96795137 5.63585078,7.99395137 C5.59835799,7.98495137 5.55691858,7.98895137 5.5253457,8.01395137 C5.47798638,8.05095137 5.45036011,8.10695137 5.41286731,8.15295137 C5.39708087,8.17195137 5.37438786,8.19095137 5.35169485,8.18395137 C5.34774824,8.18195137 5.34972155,8.17695137 5.34676159,8.17495137 C5.37537451,7.96995137 5.39116095,7.76295137 5.37241456,7.80395137 C5.33492176,7.88395137 5.30729549,7.93995137 5.27868256,7.99795137 C5.23724316,7.97995137 5.18889718,7.97895137 5.17113744,8.02295137 C5.15239104,8.06995137 5.17705735,8.13095137 5.14745778,8.17195137 C5.14055121,8.18295137 5.12969803,8.18195137 5.11983151,8.18695137 C5.1178582,8.18195137 5.10799168,8.16895137 5.10897833,8.16795137 C5.10009846,8.18295137 5.0991118,8.18795137 5.09121858,8.20095137 C5.06161901,8.20195137 5.02511286,8.18995137 4.98860672,8.17795137 C4.98860672,8.17795137 4.98860672,8.17495137 4.98762007,8.17495137 C4.98663341,8.17595137 4.98663341,8.17595137 4.98564676,8.17695137 C4.94223405,8.16195137 4.89684803,8.14795137 4.85738193,8.16195137 C4.77844973,8.18995137 4.77548977,8.30295137 4.72517049,8.37195137 C4.6501849,8.47695137 4.456801,8.43195137 4.42325482,8.30695137 C4.45088109,8.27295137 4.47752071,8.23895137 4.50514698,8.20495137 C4.46272092,8.09695137 4.34925588,8.02195137 4.23480418,8.02495137 C4.20224465,8.02595137 4.16771181,8.03195137 4.13811223,8.01795137 C4.107526,8.00295137 4.09075291,7.97095137 4.06411329,7.95095137 C3.98123448,7.88895137 3.8707294,7.97095137 3.80166372,8.04795137 C3.68326541,8.06795137 3.57374698,8.13495137 3.49974804,8.23095137 C3.45238872,8.22695137 3.4050294,8.22295137 3.35865673,8.21895137 C3.386283,8.29495137 3.29452432,8.35695137 3.25308491,8.42695137 C3.20177898,8.51195137 3.2284186,8.61095137 3.27873787,8.70195137 C3.27281796,8.71595137 3.26887135,8.73095137 3.25604487,8.73695137 C3.19585906,8.76895137 3.2116455,8.78795137 3.22940525,8.85295137 C3.24519169,8.90895137 3.23927177,9.01395137 3.22644529,9.06995137 C3.21657877,9.11395137 3.17316605,9.21995137 3.11988682,9.19495137 C3.09226055,9.18095137 3.06266097,9.16995137 3.037008,9.19595137 C3.02516817,9.20695137 3.01727495,9.22095137 3.01332834,9.23595137 C2.9955686,9.23695137 2.97780885,9.23895137 2.96103576,9.24295137 C2.92748957,9.24995137 2.89197008,9.25795137 2.85941055,9.24395137 C2.82685101,9.22995137 2.7834383,9.20295137 2.74693216,9.21495137 C2.71634593,9.22495137 2.65616013,9.25195137 2.64333364,9.28395137 C2.63741373,9.29795137 2.65616013,9.33495137 2.65616013,9.35395137 C2.65517347,9.38795137 2.68181309,9.43895137 2.67095991,9.46995137 C2.6462936,9.45795137 2.60978746,9.45395137 2.59301436,9.42895137 C2.57722792,9.40795137 2.55354826,9.41295137 2.53578852,9.39095137 C2.53184191,9.42895137 2.51802877,9.48195137 2.47264275,9.49195137 C2.42923004,9.50195137 2.38680398,9.46695137 2.34240462,9.47895137 C2.22597962,9.50895137 2.41245695,9.65495137 2.43712326,9.68295137 C2.47856267,9.72995137 2.4923758,9.79195137 2.52296203,9.84495137 C2.55650822,9.90295137 2.6255739,9.92195137 2.66602665,9.97295137 C2.69957284,10.0159514 2.7064794,10.0739514 2.75482538,10.1059514 C2.80810462,10.1429514 2.85645059,10.1759514 2.87815695,10.2389514 C2.90084995,10.2169514 2.94820928,10.3209514 2.99655525,10.2379514 C3.02220822,10.1929514 3.06759423,10.1539514 3.09620716,10.2269514 C3.12087347,10.2899514 3.09620716,10.3299514 3.15047305,10.3849514 C3.19191245,10.4279514 3.18993915,10.4789514 3.11890016,10.4739514 C3.13073999,10.5059514 3.14948639,10.5379514 3.11890016,10.5669514 C3.10508703,10.5809514 3.06562093,10.6099514 3.09423385,10.6299514 C3.12679338,10.6149514 3.16132622,10.6059514 3.19388576,10.5909514 C3.22940525,10.5759514 3.26393809,10.5399514 3.30537749,10.5409514 C3.3073508,10.5539514 3.25012495,10.5919514 3.28564444,10.5949514 C3.31524402,10.5979514 3.35767008,10.5669514 3.38134974,10.5929514 C3.40798936,10.6209514 3.37444317,10.6639514 3.39022961,10.6949514 C3.40601605,10.7269514 3.45929529,10.7029514 3.48494825,10.7079514 C3.47409508,10.7359514 3.43265567,10.7309514 3.40996266,10.7419514 C3.46225525,10.8059514 3.39417622,10.8999514 3.31820398,10.9019514 C3.28169783,10.9019514 3.15244635,10.7529514 3.14652644,10.8489514 C3.14553978,10.8769514 3.15441966,10.9119514 3.16329953,10.9389514 C3.17513936,10.9739514 3.25999148,10.9589514 3.29057771,10.9719514 C3.33497707,10.9899514 3.386283,11.0329514 3.40404275,11.0779514 C3.42081584,11.1239514 3.45929529,11.1539514 3.47409508,11.1979514 C3.502708,11.2799514 3.58065355,11.2909514 3.66155906,11.3149514 C3.76910418,11.3469514 3.7168116,11.5139514 3.71089169,11.5939514 C3.70595842,11.6729514 3.81646351,11.6919514 3.86678279,11.7369514 C3.92302198,11.7859514 3.93190185,11.8809514 3.83915652,11.8889514 C3.79179719,11.8929514 3.71286499,11.8709514 3.69510524,11.9319514 C3.66945228,12.0179514 3.79969041,12.0089514 3.85691626,12.0279514 C3.88355588,12.0369514 3.99110101,12.0479514 4.00096753,12.0729514 C4.01576732,12.1119514 4.00392749,12.1649514 4.01774062,12.2059514 C4.05128681,12.3109514 4.14797876,12.3809514 4.24171075,12.4329514 C4.44298787,12.5459514 4.68175778,12.6169514 4.90276795,12.6799514 C5.02609952,12.7159514 5.15140439,12.7439514 5.27769591,12.7609514 C5.40004083,12.7769514 5.50758595,12.7669514 5.61217112,12.8349514 C5.68419676,12.8819514 5.72958277,12.8469514 5.80358171,12.8599514 C5.83515459,12.8659514 5.84896773,12.8949514 5.87264739,12.9119514 C5.89928701,12.9329514 5.92987324,12.9059514 5.95848616,12.9169514 C5.96341942,12.8979514 5.96144612,12.8799514 5.95256625,12.8619514 C6.00880544,12.8829514 6.07787112,12.9429514 6.13707027,12.8949514 C6.16666985,12.8709514 6.1864029,12.8379514 6.21698913,12.8149514 C6.25349527,12.8179514 6.28901476,12.8199514 6.3255209,12.8199514 C6.47845205,12.8199514 6.59882366,12.7499514 6.72116857,12.6679514 C6.85239336,12.5799514 7.01025776,12.5779514 7.16220225,12.5639514 C7.32302662,12.5479514 7.49371751,12.5269514 7.64072873,12.4569514 C7.76899356,12.3949514 7.8015531,12.2819514 7.83805924,12.1569514 C7.87752534,12.0209514 7.99592365,11.9659514 8.0738692,11.8559514 C8.16562788,11.7279514 8.10544208,11.5609514 8.18042767,11.4279514 L8.18042767,11.4279514 Z M2.2950453,9.62395137 C2.29011203,9.59195137 2.2782722,9.57195137 2.25360589,9.55295137 C2.25261924,9.55595137 2.25163259,9.55795137 2.25163259,9.56195137 C2.2180864,9.54495137 2.21413979,9.48095137 2.16875377,9.48595137 C2.13126098,9.42595137 2.03654233,9.45295137 2.00694276,9.50795137 C1.98720971,9.54495137 2.01088937,9.56195137 2.03259572,9.58895137 C2.06022199,9.62295137 2.05528873,9.64895137 2.06515526,9.68795137 C2.08982157,9.78895137 2.17368704,9.71995137 2.23288619,9.75895137 C2.2555792,9.77395137 2.26445907,9.81695137 2.29800525,9.80995137 C2.33549805,9.80095137 2.33352475,9.74495137 2.32365822,9.71995137 C2.30984509,9.68395137 2.29997856,9.66295137 2.2950453,9.62395137 L2.2950453,9.62395137 Z M3.08338067,10.8149514 C3.08930059,10.7979514 3.06266097,10.7789514 3.04490122,10.7799514 C3.02911478,10.7809514 3.014315,10.8029514 3.00839508,10.8149514 C2.98866203,10.8499514 3.01036839,10.8969514 3.0557544,10.8969514 C3.06660758,10.8779514 3.06266097,10.8409514 3.09127389,10.8379514 C3.08930059,10.8289514 3.08338067,10.8259514 3.07548745,10.8229514 L3.08338067,10.8149514 L3.08338067,10.8149514 Z M2.24472602,9.54595137 C2.24768598,9.54795137 2.25064593,9.54995137 2.25360589,9.55295137 C2.25656585,9.54795137 2.25952581,9.54395137 2.26149911,9.53795137 L2.24472602,9.54595137 L2.24472602,9.54595137 Z M11.4896603,10.9489514 C11.4728872,10.9649514 11.4778204,10.9829514 11.4699272,11.0019514 C11.4610474,11.0249514 11.4265145,11.0339514 11.4077681,11.0459514 C11.3781685,11.0649514 11.368302,11.1099514 11.3357425,11.1209514 C11.3219293,11.0999514 11.3012096,11.0409514 11.2725967,11.0919514 C11.2558236,11.1249514 11.2666768,11.1579514 11.2400372,11.1879514 C11.2133976,11.2159514 11.2153709,11.2499514 11.1966245,11.2809514 C11.1680115,11.3299514 11.1393986,11.3589514 11.0910527,11.3879514 C11.0525732,11.4109514 11.04468,11.4539514 11.0170537,11.4859514 C10.9874541,11.5209514 10.9420681,11.5339514 10.9016154,11.5519514 C10.8730024,11.5639514 10.8256431,11.5979514 10.7930836,11.5789514 C10.7496709,11.5519514 10.80887,11.5039514 10.8295897,11.4869514 C10.8493228,11.4709514 10.9430548,11.4139514 10.9213484,11.3819514 C10.9065486,11.3609514 10.8532694,11.3639514 10.831563,11.3659514 C10.7871637,11.3709514 10.7536175,11.4159514 10.7161247,11.4369514 C10.6736986,11.4609514 10.6371925,11.4809514 10.5908198,11.4969514 C10.5375406,11.5159514 10.533594,11.5659514 10.4911679,11.5959514 C10.4576217,11.6219514 10.414209,11.6419514 10.3707963,11.6419514 C10.3135705,11.6419514 10.3165304,11.5939514 10.2997573,11.5539514 C10.278051,11.5569514 10.2593046,11.5849514 10.2385849,11.5939514 C10.2050387,11.6079514 10.1833323,11.6239514 10.1981321,11.6609514 C10.2119452,11.6989514 10.0590141,11.7339514 10.0323745,11.7529514 C10.0264546,11.7339514 10.0560541,11.7139514 10.067894,11.7029514 C10.0205346,11.6989514 9.96725541,11.7399514 9.91890944,11.7469514 C9.87253677,11.7529514 9.81531092,11.7849514 9.80840435,11.8329514 C9.80347109,11.8709514 9.75315181,11.8699514 9.72157893,11.8829514 C9.66928635,11.9049514 9.6909927,11.9359514 9.68112618,11.9779514 C9.66139313,12.0569514 9.49662215,11.9969514 9.58048762,11.8919514 C9.61008719,11.8549514 9.65448656,11.8309514 9.68112618,11.7929514 C9.71171241,11.7489514 9.71664567,11.6939514 9.74032533,11.6469514 C9.68803274,11.6619514 9.64856664,11.6919514 9.60416728,11.7209514 C9.553848,11.7539514 9.51142194,11.7459514 9.45518275,11.7349514 C9.39006368,11.7209514 9.34467766,11.7539514 9.28449186,11.7689514 C9.24601241,11.7779514 9.16017364,11.7749514 9.15524038,11.8309514 C9.15228042,11.8669514 9.21443953,11.8739514 9.23515923,11.8949514 C9.26574546,11.9269514 9.29929165,11.9739514 9.32494461,12.0099514 C9.34566432,12.0379514 9.42262321,12.0769514 9.41768995,12.1129514 C9.40979673,12.1829514 9.32198465,12.1709514 9.27462533,12.1829514 C9.22726601,12.1949514 9.22035944,12.2379514 9.1858266,12.2639514 C9.1463605,12.2929514 9.09308127,12.2589514 9.04966855,12.2769514 C9.00526919,12.2939514 8.97369631,12.3339514 8.93521686,12.3599514 C8.87009779,12.4049514 8.82767173,12.3559514 8.76057936,12.3529514 C8.70631347,12.3509514 8.65698085,12.3759514 8.60567491,12.3869514 C8.55930224,12.3969514 8.50306305,12.4049514 8.46754356,12.4379514 C8.38959801,12.5079514 8.64020775,12.4889514 8.66388741,12.4869514 C8.65303424,12.5269514 8.64218106,12.5739514 8.60567491,12.5999514 C8.5602889,12.6329514 8.49615648,12.6249514 8.4438639,12.6369514 C8.40341114,12.6469514 8.34717195,12.6919514 8.4063711,12.7239514 C8.45965034,12.7509514 8.52772936,12.7369514 8.5810086,12.7179514 C8.64218106,12.6969514 8.6994069,12.6639514 8.76353932,12.6499514 C8.83063169,12.6349514 8.90068402,12.6419514 8.96777639,12.6299514 C9.03980203,12.6159514 9.10294779,12.5769514 9.17004016,12.5499514 C9.23417258,12.5239514 9.30126495,12.5139514 9.36934398,12.5119514 C9.35553084,12.5369514 9.28843847,12.5349514 9.2627855,12.5409514 C9.21246622,12.5509514 9.17793338,12.5949514 9.1256408,12.5919514 C9.06644165,12.5899514 9.07334822,12.6319514 9.03092216,12.6419514 C9.00329589,12.6489514 8.93817682,12.7129514 8.91745711,12.6729514 C8.90167067,12.6419514 8.87108445,12.6479514 8.86121792,12.6859514 C8.8533247,12.7139514 8.86911114,12.7239514 8.83063169,12.7249514 C8.80103212,12.7249514 8.78721898,12.7129514 8.76057936,12.7059514 C8.70730012,12.6919514 8.68362046,12.7469514 8.64612767,12.7609514 C8.59087513,12.7819514 8.53167597,12.7749514 8.47839674,12.8099514 C8.44781051,12.8299514 8.41426432,12.8359514 8.37775818,12.8469514 C8.31165246,12.8679514 8.24949335,12.8929514 8.18338763,12.9149514 C8.1320817,12.9329514 8.08077576,12.9549514 8.02552322,12.9559514 C8.00283021,12.9559514 7.91205818,12.9399514 7.89725839,12.9669514 C7.86963212,13.0169514 7.95448424,12.9979514 7.97224399,12.9879514 C8.02256327,12.9609514 8.08373572,12.9769514 8.13997492,12.9769514 C8.20904059,12.9769514 8.26626644,12.9629514 8.32842555,12.9309514 C8.34519864,12.9219514 8.45669038,12.8979514 8.4626103,12.9109514 C8.47247682,12.9169514 8.54647576,12.8899514 8.55930224,12.8869514 C8.61948805,12.8729514 8.67967385,12.8599514 8.73887301,12.8449514 C8.92140372,12.7979514 9.10097449,12.7269514 9.27857194,12.6659514 C9.6327802,12.5459514 9.95837554,12.3429514 10.2662111,12.1369514 C10.4053291,12.0439514 10.5187942,11.9219514 10.669752,11.8459514 C10.8216965,11.7699514 10.9578546,11.6689514 11.0969726,11.5739514 C11.2331306,11.4809514 11.3367291,11.3529514 11.4501942,11.2359514 C11.5646459,11.1169514 11.6613378,11.0049514 11.7116571,10.8469514 C11.6830442,10.8399514 11.6534446,10.8989514 11.6297649,10.9099514 C11.5902988,10.9289514 11.5212332,10.9189514 11.4896603,10.9489514 L11.4896603,10.9489514 Z M10.6391658,10.7879514 C10.6736986,10.7409514 10.6440991,10.6769514 10.5829266,10.7139514 C10.5602336,10.7269514 10.5631935,10.7529514 10.5454338,10.7689514 C10.5266874,10.7859514 10.5247141,10.7659514 10.5059677,10.7609514 C10.4793281,10.7549514 10.4359154,10.7909514 10.4270355,10.8149514 C10.3905294,10.8139514 10.3579698,10.8549514 10.3747429,10.8869514 C10.4230889,10.8689514 10.4526885,10.8239514 10.504981,10.8379514 C10.5464205,10.8489514 10.6125262,10.8229514 10.6391658,10.7879514 L10.6391658,10.7879514 Z" />
+</svg>
--- a/devtools/client/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js
@@ -18,17 +18,17 @@ add_task(function* () {
   let hud = yield openConsole();
 
   // On e10s, the exception is triggered in child process
   // and is ignored by test harness
   if (!Services.appinfo.browserTabsRemoteAutostart) {
     expectUncaughtException();
   }
 
-  content.location = TEST_URI2;
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
 
   yield waitForMessages({
     webconsole: hud,
     messages: [{
       text: "bug618078exception",
       category: CATEGORY_JS,
       severity: SEVERITY_ERROR,
     }],
--- a/devtools/client/webconsole/test/browser_webconsole_bug_646025_console_file_location.js
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_646025_console_file_location.js
@@ -14,17 +14,17 @@ const TEST_URI2 = "http://example.com/br
                  "webconsole/test/" +
                  "test-bug-646025-console-file-location.html";
 
 add_task(function* () {
   yield loadTab(TEST_URI);
 
   let hud = yield openConsole();
 
-  content.location = TEST_URI2;
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
 
   yield waitForMessages({
     webconsole: hud,
     messages: [{
       text: "message for level log",
       category: CATEGORY_WEBDEV,
       severity: SEVERITY_LOG,
       source: { url: "test-file-location.js", line: 6 },
--- a/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_02.js
+++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_02.js
@@ -102,17 +102,17 @@ function test() {
         is(attrs[i].value, data.attrs[i].value,
            "The correct node was highlighted");
       }
 
       info("Unhighlight the node by moving away from the markup view");
       let onNodeUnhighlight = toolbox.once("node-unhighlight");
       let btn = inspector.toolbox.doc.querySelector(".toolbox-dock-button");
       EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"},
-        inspector.toolbox.doc.defaultView);
+        inspector.toolbox.win);
       yield onNodeUnhighlight;
 
       info("Switching back to the console");
       yield toolbox.selectTool("webconsole");
     }
   }).then(finishTest);
 }
 
--- a/devtools/client/webconsole/test/browser_webconsole_output_regexp.js
+++ b/devtools/client/webconsole/test/browser_webconsole_output_regexp.js
@@ -5,33 +5,31 @@
 
 // Test the webconsole output for various types of objects.
 
 "use strict";
 
 const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
                  "test/test-console-output-regexp.html";
 
-var dateNow = Date.now();
-
 var inputTests = [
   // 0
   {
     input: "/foo/igym",
     output: "/foo/gimy",
     printOutput: "Error: source called",
     inspectable: true,
   },
 ];
 
 function test() {
   requestLongerTimeout(2);
-  Task.spawn(function*() {
+  Task.spawn(function* () {
     let {tab} = yield loadTab(TEST_URI);
     let hud = yield openConsole(tab);
     return checkOutputForInputs(hud, inputTests);
   }).then(finishUp);
 }
 
 function finishUp() {
-  inputTests = dateNow = null;
+  inputTests = null;
   finishTest();
 }
--- a/devtools/client/webconsole/test/browser_webconsole_split.js
+++ b/devtools/client/webconsole/test/browser_webconsole_split.js
@@ -36,17 +36,17 @@ function test() {
     info("About to check that panel responds to ESCAPE keyboard shortcut");
 
     toolbox.once("split-console", () => {
       ok(true, "Split console has been triggered via ESCAPE keypress");
       checkAllTools();
     });
 
     // Closes split console.
-    EventUtils.sendKey("ESCAPE", toolbox.frame.contentWindow);
+    EventUtils.sendKey("ESCAPE", toolbox.win);
   }
 
   function checkAllTools() {
     info("About to check split console with each panel individually.");
 
     Task.spawn(function*() {
       yield openAndCheckPanel("jsdebugger");
       yield openAndCheckPanel("inspector");
@@ -55,17 +55,17 @@ function test() {
       yield openAndCheckPanel("netmonitor");
 
       yield checkWebconsolePanelOpened();
       testBottomHost();
     });
   }
 
   function getCurrentUIState() {
-    let win = toolbox.doc.defaultView;
+    let win = toolbox.win;
     let deck = toolbox.doc.querySelector("#toolbox-deck");
     let webconsolePanel = toolbox.webconsolePanel;
     let splitter = toolbox.doc.querySelector("#toolbox-console-splitter");
 
     let containerHeight = parseFloat(win.getComputedStyle(deck.parentNode)
       .getPropertyValue("height"));
     let deckHeight = parseFloat(win.getComputedStyle(deck)
       .getPropertyValue("height"));
--- a/devtools/client/webconsole/test/browser_webconsole_split_escape_key.js
+++ b/devtools/client/webconsole/test/browser_webconsole_split_escape_key.js
@@ -44,40 +44,40 @@ function test() {
 
   function testCreateSplitConsoleAfterEscape() {
     let result = toolbox.once("webconsole-ready", () => {
       hud = toolbox.getPanel("webconsole").hud;
       jsterm = hud.jsterm;
       ok(toolbox.splitConsole, "Split console is created.");
     });
 
-    let contentWindow = toolbox.frame.contentWindow;
+    let contentWindow = toolbox.win;
     contentWindow.focus();
     EventUtils.sendKey("ESCAPE", contentWindow);
 
     return result;
   }
 
   function testHideSplitConsoleAfterEscape() {
     let result = toolbox.once("split-console", () => {
       ok(!toolbox.splitConsole, "Split console is hidden.");
     });
-    EventUtils.sendKey("ESCAPE", toolbox.frame.contentWindow);
+    EventUtils.sendKey("ESCAPE", toolbox.win);
 
     return result;
   }
 
   function testHideVariablesViewAfterEscape() {
     let result = jsterm.once("sidebar-closed", () => {
       ok(!hud.ui.jsterm.sidebar,
         "Variables view is hidden.");
       ok(toolbox.splitConsole,
         "Split console is open after hiding the variables view.");
     });
-    EventUtils.sendKey("ESCAPE", toolbox.frame.contentWindow);
+    EventUtils.sendKey("ESCAPE", toolbox.win);
 
     return result;
   }
 
   function testHideAutoCompletePopupAfterEscape() {
     let deferred = promise.defer();
     let popup = jsterm.autocompletePopup;
 
@@ -86,17 +86,17 @@ function test() {
       ok(!popup.isOpen,
         "Auto complete popup is hidden.");
       ok(toolbox.splitConsole,
         "Split console is open after hiding the autocomplete popup.");
 
       deferred.resolve();
     }, false);
 
-    EventUtils.sendKey("ESCAPE", toolbox.frame.contentWindow);
+    EventUtils.sendKey("ESCAPE", toolbox.win);
 
     return deferred.promise;
   }
 
   function testCancelPropertyEditorAfterEscape() {
     EventUtils.sendKey("ESCAPE", variablesView.window);
     ok(hud.ui.jsterm.sidebar,
       "Variables view is open after canceling property editor.");
--- a/devtools/client/webconsole/test/browser_webconsole_split_persist.js
+++ b/devtools/client/webconsole/test/browser_webconsole_split_persist.js
@@ -99,17 +99,17 @@ function test() {
 
   function isCommandButtonChecked() {
     return toolbox.doc.querySelector("#command-button-splitconsole")
       .hasAttribute("checked");
   }
 
   function toggleSplitConsoleWithEscape() {
     let onceSplitConsole = toolbox.once("split-console");
-    let contentWindow = toolbox.frame.contentWindow;
+    let contentWindow = toolbox.win;
     contentWindow.focus();
     EventUtils.sendKey("ESCAPE", contentWindow);
     return onceSplitConsole;
   }
 
   function finish() {
     toolbox = TEST_URI = null;
     Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
--- a/devtools/server/actors/animation.js
+++ b/devtools/server/actors/animation.js
@@ -244,16 +244,27 @@ var AnimationPlayerActor = ActorClass({
    * Get the animation iterationStart from this player, in ratio.
    * That is offset of starting position of the animation.
    * @return {Number}
    */
   getIterationStart: function() {
     return this.player.effect.getComputedTiming().iterationStart;
   },
 
+  getPropertiesCompositorStatus: function() {
+    let properties = this.player.effect.getProperties();
+    return properties.map(prop => {
+      return {
+        property: prop.property,
+        runningOnCompositor: prop.runningOnCompositor,
+        warning: prop.warning
+      };
+    });
+  },
+
   /**
    * Return the current start of the Animation.
    * @return {Object}
    */
   getState: function() {
     // Remember the startTime each time getState is called, it may be useful
     // when animations get paused. As in, when an animation gets paused, its
     // startTime goes back to null, but the front-end might still be interested
@@ -277,17 +288,20 @@ var AnimationPlayerActor = ActorClass({
       name: this.getName(),
       duration: this.getDuration(),
       delay: this.getDelay(),
       endDelay: this.getEndDelay(),
       iterationCount: this.getIterationCount(),
       iterationStart: this.getIterationStart(),
       // animation is hitting the fast path or not. Returns false whenever the
       // animation is paused as it is taken off the compositor then.
-      isRunningOnCompositor: this.player.isRunningOnCompositor,
+      isRunningOnCompositor:
+        this.getPropertiesCompositorStatus()
+            .some(propState => propState.runningOnCompositor),
+      propertyState: this.getPropertiesCompositorStatus(),
       // The document timeline's currentTime is being sent along too. This is
       // not strictly related to the node's animationPlayer, but is useful to
       // know the current time of the animation with respect to the document's.
       documentCurrentTime: this.node.ownerDocument.timeline.currentTime
     };
   },
 
   /**
@@ -507,16 +521,17 @@ var AnimationPlayerFront = FrontClass(An
       playbackRate: this._form.playbackRate,
       name: this._form.name,
       duration: this._form.duration,
       delay: this._form.delay,
       endDelay: this._form.endDelay,
       iterationCount: this._form.iterationCount,
       iterationStart: this._form.iterationStart,
       isRunningOnCompositor: this._form.isRunningOnCompositor,
+      propertyState: this._form.propertyState,
       documentCurrentTime: this._form.documentCurrentTime
     };
   },
 
   /**
    * Executed when the AnimationPlayerActor emits a "changed" event. Used to
    * update the local knowledge of the state.
    */
--- a/devtools/server/actors/inspector.js
+++ b/devtools/server/actors/inspector.js
@@ -2240,17 +2240,17 @@ var WalkerActor = protocol.ActorClass({
           nodes = this._multiFrameQuerySelectorAll("[id]");
         } else {
           nodes = this._multiFrameQuerySelectorAll(query);
         }
         for (let node of nodes) {
           sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
         }
         for (let [id, count] of sugs.ids) {
-          if (id.startsWith(completing)) {
+          if (id.startsWith(completing) && id !== "") {
             result.push(["#" + CSS.escape(id), count, selectorState]);
           }
         }
         break;
 
       case "tag":
         if (!query) {
           nodes = this._multiFrameQuerySelectorAll("*");
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -2,23 +2,23 @@
  * 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} = require("chrome");
 const events = require("sdk/event/core");
 const protocol = require("devtools/server/protocol");
-const {Arg, method, RetVal, types} = protocol;
 const {LongStringActor} = require("devtools/server/actors/string");
 const {DebuggerServer} = require("devtools/server/main");
 const Services = require("Services");
 const promise = require("promise");
 const {isWindowIncluded} = require("devtools/shared/layout/utils");
 const {setTimeout, clearTimeout} = require("sdk/timers");
+const specs = require("devtools/shared/specs/storage");
 
 loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
 loader.lazyImporter(this, "Sqlite", "resource://gre/modules/Sqlite.jsm");
 loader.lazyImporter(this, "Task", "resource://gre/modules/Task.jsm", "Task");
 
 var gTrackedMessageManager = new Map();
 
 // Maximum number of cookies/local storage key-value-pairs that can be sent
@@ -44,107 +44,31 @@ var illegalFileNameCharacters = [
   "]"
 ].join("");
 var ILLEGAL_CHAR_REGEX = new RegExp(illegalFileNameCharacters, "g");
 
 // Holder for all the registered storage actors.
 var storageTypePool = new Map();
 
 /**
- * Gets an accumulated list of all storage actors registered to be used to
- * create a RetVal to define the return type of StorageActor.listStores method.
- */
-function getRegisteredTypes() {
-  let registeredTypes = {};
-  for (let store of storageTypePool.keys()) {
-    registeredTypes[store] = store;
-  }
-  return registeredTypes;
-}
-
-/**
  * An async method equivalent to setTimeout but using Promises
  *
  * @param {number} time
  *        The wait time in milliseconds.
  */
 function sleep(time) {
   let deferred = promise.defer();
 
   setTimeout(() => {
     deferred.resolve(null);
   }, time);
 
   return deferred.promise;
 }
 
-// Cookies store object
-types.addDictType("cookieobject", {
-  name: "string",
-  value: "longstring",
-  path: "nullable:string",
-  host: "string",
-  isDomain: "boolean",
-  isSecure: "boolean",
-  isHttpOnly: "boolean",
-  creationTime: "number",
-  lastAccessed: "number",
-  expires: "number"
-});
-
-// Array of cookie store objects
-types.addDictType("cookiestoreobject", {
-  total: "number",
-  offset: "number",
-  data: "array:nullable:cookieobject"
-});
-
-// Local Storage / Session Storage store object
-types.addDictType("storageobject", {
-  name: "string",
-  value: "longstring"
-});
-
-// Array of Local Storage / Session Storage store objects
-types.addDictType("storagestoreobject", {
-  total: "number",
-  offset: "number",
-  data: "array:nullable:storageobject"
-});
-
-// Indexed DB store object
-// This is a union on idb object, db metadata object and object store metadata
-// object
-types.addDictType("idbobject", {
-  name: "nullable:string",
-  db: "nullable:string",
-  objectStore: "nullable:string",
-  origin: "nullable:string",
-  version: "nullable:number",
-  objectStores: "nullable:number",
-  keyPath: "nullable:string",
-  autoIncrement: "nullable:boolean",
-  indexes: "nullable:string",
-  value: "nullable:longstring"
-});
-
-// Array of Indexed DB store objects
-types.addDictType("idbstoreobject", {
-  total: "number",
-  offset: "number",
-  data: "array:nullable:idbobject"
-});
-
-// Update notification object
-types.addDictType("storeUpdateObject", {
-  changed: "nullable:json",
-  deleted: "nullable:json",
-  added: "nullable:json"
-});
-
 // Helper methods to create a storage actor.
 var StorageActors = {};
 
 /**
  * Creates a default object with the common methods required by all storage
  * actors.
  *
  * This default object is missing a couple of required methods that should be
@@ -158,20 +82,18 @@ var StorageActors = {};
  *                     so that it can be transferred over wire.
  *   - populateStoresForHost : Given a host, populate the map of all store
  *                             objects for it
  *
  * @param {string} typeName
  *        The typeName of the actor.
  * @param {string} observationTopic
  *        The topic which this actor listens to via Notification Observers.
- * @param {string} storeObjectType
- *        The RetVal type of the store object of this actor.
  */
-StorageActors.defaults = function(typeName, observationTopic, storeObjectType) {
+StorageActors.defaults = function(typeName, observationTopic) {
   return {
     typeName: typeName,
 
     get conn() {
       return this.storageActor.conn;
     },
 
     /**
@@ -328,17 +250,17 @@ StorageActors.defaults = function(typeNa
      *
      * @return {object} An object containing following properties:
      *          - offset - The actual offset of the returned array. This might
      *                     be different from the requested offset if that was
      *                     invalid
      *          - total - The total number of entries possible.
      *          - data - The requested values.
      */
-    getStoreObjects: method(Task.async(function* (host, names, options = {}) {
+    getStoreObjects: Task.async(function* (host, names, options = {}) {
       let offset = options.offset || 0;
       let size = options.size || MAX_STORE_OBJECT_COUNT;
       if (size > MAX_STORE_OBJECT_COUNT) {
         size = MAX_STORE_OBJECT_COUNT;
       }
       let sortOn = options.sortOn || "name";
 
       let toReturn = {
@@ -404,23 +326,16 @@ StorageActors.defaults = function(typeNa
           toReturn.data = obj.sort((a, b) => {
             return a[sortOn] - b[sortOn];
           }).slice(offset, offset + size)
             .map(object => this.toStoreObject(object));
         }
       }
 
       return toReturn;
-    }), {
-      request: {
-        host: Arg(0),
-        names: Arg(1, "nullable:array:string"),
-        options: Arg(2, "nullable:json")
-      },
-      response: RetVal(storeObjectType)
     })
   };
 };
 
 /**
  * Creates an actor and its corresponding front and registers it to the Storage
  * Actor.
  *
@@ -428,54 +343,39 @@ StorageActors.defaults = function(typeNa
  *
  * @param {object} options
  *        Options required by StorageActors.defaults method which are :
  *         - typeName {string}
  *                    The typeName of the actor.
  *         - observationTopic {string}
  *                            The topic which this actor listens to via
  *                            Notification Observers.
- *         - storeObjectType {string}
- *                           The RetVal type of the store object of this actor.
  * @param {object} overrides
  *        All the methods which you want to be different from the ones in
  *        StorageActors.defaults method plus the required ones described there.
  */
 StorageActors.createActor = function(options = {}, overrides = {}) {
   let actorObject = StorageActors.defaults(
     options.typeName,
-    options.observationTopic || null,
-    options.storeObjectType
+    options.observationTopic || null
   );
   for (let key in overrides) {
     actorObject[key] = overrides[key];
   }
 
-  let actor = protocol.ActorClass(actorObject);
-  protocol.FrontClass(actor, {
-    form: function(form, detail) {
-      if (detail === "actorid") {
-        this.actorID = form;
-        return null;
-      }
-
-      this.actorID = form.actor;
-      this.hosts = form.hosts;
-      return null;
-    }
-  });
+  let actorSpec = specs.childSpecs[options.typeName];
+  let actor = protocol.ActorClassWithSpec(actorSpec, actorObject);
   storageTypePool.set(actorObject.typeName, actor);
 };
 
 /**
  * The Cookies actor and front.
  */
 StorageActors.createActor({
-  typeName: "cookies",
-  storeObjectType: "cookiestoreobject"
+  typeName: "cookies"
 }, {
   initialize: function(storageActor) {
     protocol.Actor.prototype.initialize.call(this, null);
 
     this.storageActor = storageActor;
 
     this.maybeSetupChildProcess();
     this.populateStoresForHosts();
@@ -647,66 +547,44 @@ StorageActors.createActor({
   },
 
   /**
    * This method marks the table as editable.
    *
    * @return {Array}
    *         An array of column header ids.
    */
-  getEditableFields: method(Task.async(function* () {
+  getEditableFields: Task.async(function* () {
     return [
       "name",
       "path",
       "host",
       "expires",
       "value",
       "isSecure",
       "isHttpOnly"
     ];
-  }), {
-    request: {},
-    response: {
-      value: RetVal("json")
-    }
   }),
 
   /**
    * Pass the editItem command from the content to the chrome process.
    *
    * @param {Object} data
    *        See editCookie() for format details.
    */
-  editItem: method(Task.async(function* (data) {
+  editItem: Task.async(function* (data) {
     this.editCookie(data);
-  }), {
-    request: {
-      data: Arg(0, "json"),
-    },
-    response: {}
   }),
 
-  removeItem: method(Task.async(function* (host, name) {
+  removeItem: Task.async(function* (host, name) {
     this.removeCookie(host, name);
-  }), {
-    request: {
-      host: Arg(0, "string"),
-      name: Arg(1, "string"),
-    },
-    response: {}
   }),
 
-  removeAll: method(Task.async(function* (host, domain) {
+  removeAll: Task.async(function* (host, domain) {
     this.removeAllCookies(host, domain);
-  }), {
-    request: {
-      host: Arg(0, "string"),
-      domain: Arg(1, "nullable:string")
-    },
-    response: {}
   }),
 
   maybeSetupChildProcess: function() {
     cookieHelpers.onCookieChanged = this.onCookieChanged.bind(this);
 
     if (!DebuggerServer.isInChildProcess) {
       this.getCookiesFromHost =
         cookieHelpers.getCookiesFromHost.bind(cookieHelpers);
@@ -1124,47 +1002,47 @@ function getObjectForLocalOrSessionStora
     },
 
     /**
      * This method marks the fields as editable.
      *
      * @return {Array}
      *         An array of field ids.
      */
-    getEditableFields: method(Task.async(function* () {
+    getEditableFields: Task.async(function* () {
       return [
         "name",
         "value"
       ];
-    }), {
-      request: {},
-      response: {
-        value: RetVal("json")
-      }
     }),
 
     /**
      * Edit localStorage or sessionStorage fields.
      *
      * @param {Object} data
      *        See editCookie() for format details.
      */
-    editItem: method(Task.async(function* ({host, field, oldValue, items}) {
+    editItem: Task.async(function* ({host, field, oldValue, items}) {
       let storage = this.hostVsStores.get(host);
 
       if (field === "name") {
         storage.removeItem(oldValue);
       }
 
       storage.setItem(items.name, items.value);
-    }), {
-      request: {
-        data: Arg(0, "json"),
-      },
-      response: {}
+    }),
+
+    removeItem: Task.async(function* (host, name) {
+      let storage = this.hostVsStores.get(host);
+      storage.removeItem(name);
+    }),
+
+    removeAll: Task.async(function* (host) {
+      let storage = this.hostVsStores.get(host);
+      storage.clear();
     }),
 
     observe: function(subject, topic, data) {
       if (topic != "dom-storage2-changed" || data != type) {
         return null;
       }
 
       let host = this.getSchemaAndHost(subject.url);
@@ -1202,73 +1080,37 @@ function getObjectForLocalOrSessionStora
         return null;
       }
 
       return {
         name: item.name,
         value: new LongStringActor(this.conn, item.value || "")
       };
     },
-
-    removeItem: method(Task.async(function* (host, name) {
-      let storage = this.hostVsStores.get(host);
-      storage.removeItem(name);
-    }), {
-      request: {
-        host: Arg(0),
-        name: Arg(1),
-      },
-      response: {}
-    }),
-
-    removeAll: method(Task.async(function* (host) {
-      let storage = this.hostVsStores.get(host);
-      storage.clear();
-    }), {
-      request: {
-        host: Arg(0)
-      },
-      response: {}
-    }),
   };
 }
 
 /**
  * The Local Storage actor and front.
  */
 StorageActors.createActor({
   typeName: "localStorage",
-  observationTopic: "dom-storage2-changed",
-  storeObjectType: "storagestoreobject"
+  observationTopic: "dom-storage2-changed"
 }, getObjectForLocalOrSessionStorage("localStorage"));
 
 /**
  * The Session Storage actor and front.
  */
 StorageActors.createActor({
   typeName: "sessionStorage",
-  observationTopic: "dom-storage2-changed",
-  storeObjectType: "storagestoreobject"
+  observationTopic: "dom-storage2-changed"
 }, getObjectForLocalOrSessionStorage("sessionStorage"));
 
-types.addDictType("cacheobject", {
-  "url": "string",
-  "status": "string"
-});
-
-// Array of Cache store objects
-types.addDictType("cachestoreobject", {
-  total: "number",
-  offset: "number",
-  data: "array:nullable:cacheobject"
-});
-
 StorageActors.createActor({
-  typeName: "Cache",
-  storeObjectType: "cachestoreobject"
+  typeName: "Cache"
 }, {
   getCachesForHost: Task.async(function* (host) {
     let uri = Services.io.newURI(host, null, null);
     let principal =
       Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
 
     // The first argument tells if you want to get |content| cache or |chrome|
     // cache.
@@ -1487,18 +1329,17 @@ DatabaseMetadata.prototype = {
       origin: this._origin,
       version: this._version,
       objectStores: this._objectStores.size
     };
   }
 };
 
 StorageActors.createActor({
-  typeName: "indexedDB",
-  storeObjectType: "idbstoreobject"
+  typeName: "indexedDB"
 }, {
   initialize: function(storageActor) {
     protocol.Actor.prototype.initialize.call(this, null);
 
     this.storageActor = storageActor;
 
     this.maybeSetupChildProcess();
 
@@ -1517,36 +1358,30 @@ StorageActors.createActor({
 
     events.off(this.storageActor, "window-ready", this.onWindowReady);
     events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed);
   },
 
   /**
    * Remove an indexedDB database from given host with a given name.
    */
-  removeDatabase: method(Task.async(function* (host, name) {
+  removeDatabase: Task.async(function* (host, name) {
     let win = this.storageActor.getWindowFromHost(host);
     if (win) {
       let principal = win.document.nodePrincipal;
       let result = yield this.removeDB(host, principal, name);
       if (!result.error) {
         if (this.hostVsStores.has(host)) {
           this.hostVsStores.get(host).delete(name);
         }
         this.storageActor.update("deleted", "indexedDB", {
           [host]: [ JSON.stringify([name]) ]
         });
       }
     }
-  }), {
-    request: {
-      host: Arg(0, "string"),
-      name: Arg(1, "string"),
-    },
-    response: {}
   }),
 
   getHostName: function(location) {
     if (!location.host) {
       return location.href;
     }
     return location.protocol + "//" + location.host;
   },
@@ -2143,54 +1978,31 @@ exports.setupParentProcessForIndexedDB =
     mm.removeMessageListener("storage:storage-indexedDB-request-parent",
                              indexedDBHelpers.handleChildRequest);
   }
 };
 
 /**
  * The main Storage Actor.
  */
-var StorageActor = exports.StorageActor = protocol.ActorClass({
+let StorageActor = protocol.ActorClassWithSpec(specs.storageSpec, {
   typeName: "storage",
 
   get window() {
     return this.parentActor.window;
   },
 
   get document() {
     return this.parentActor.window.document;
   },
 
   get windows() {
     return this.childWindowPool;
   },
 
-  /**
-   * List of event notifications that the server can send to the client.
-   *
-   *  - stores-update : When any store object in any storage type changes.
-   *  - stores-cleared : When all the store objects are removed.
-   *  - stores-reloaded : When all stores are reloaded. This generally mean that
-   *                      we should refetch everything again.
-   */
-  events: {
-    "stores-update": {
-      type: "storesUpdate",
-      data: Arg(0, "storeUpdateObject")
-    },
-    "stores-cleared": {
-      type: "storesCleared",
-      data: Arg(0, "json")
-    },
-    "stores-reloaded": {
-      type: "storesReloaded",
-      data: Arg(0, "json")
-    }
-  },
-
   initialize: function(conn, tabActor) {
     protocol.Actor.prototype.initialize.call(this, null);
 
     this.conn = conn;
     this.parentActor = tabActor;
 
     this.childActorPool = new Map();
     this.childWindowPool = new Set();
@@ -2354,29 +2166,27 @@ var StorageActor = exports.StorageActor 
    * Lists the available hosts for all the registered storage types.
    *
    * @returns {object} An object containing with the following structure:
    *  - <storageType> : [{
    *      actor: <actorId>,
    *      host: <hostname>
    *    }]
    */
-  listStores: method(Task.async(function* () {
+  listStores: Task.async(function* () {
     let toReturn = {};
 
     for (let [name, value] of this.childActorPool) {
       if (value.preListStores) {
         yield value.preListStores();
       }
       toReturn[name] = value;
     }
 
     return toReturn;
-  }), {
-    response: RetVal(types.addDictType("storelist", getRegisteredTypes()))
   }),
 
   /**
    * This method is called by the registered storage types so as to tell the
    * Storage Actor that there are some changes in the stores. Storage Actor then
    * notifies the client front about these changes at regular (BATCH_DELAY)
    * interval.
    *
@@ -2493,18 +2303,9 @@ var StorageActor = exports.StorageActor 
           delete this.boundUpdate[action][storeType][host];
         }
       }
     }
     return null;
   }
 });
 
-/**
- * Front for the Storage Actor.
- */
-exports.StorageFront = protocol.FrontClass(StorageActor, {
-  initialize: function(client, tabForm) {
-    protocol.Front.prototype.initialize.call(this, client);
-    this.actorID = tabForm.storageActor;
-    this.manage(this);
-  }
-});
+exports.StorageActor = StorageActor;
--- a/devtools/server/actors/stylesheets.js
+++ b/devtools/server/actors/stylesheets.js
@@ -9,47 +9,50 @@ var Services = require("Services");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 const promise = require("promise");
 const events = require("sdk/event/core");
+const {OriginalSourceFront} = require("devtools/client/fronts/stylesheets");
 const protocol = require("devtools/server/protocol");
 const {Arg, Option, method, RetVal, types} = protocol;
 const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
 const {fetch} = require("devtools/shared/DevToolsUtils");
 const {listenOnce} = require("devtools/shared/async-utils");
+const {originalSourceSpec} = require("devtools/shared/specs/stylesheets");
 const {SourceMapConsumer} = require("source-map");
 
 loader.lazyGetter(this, "CssLogic", () => require("devtools/shared/inspector/css-logic").CssLogic);
 
 const {
   getIndentationFromPrefs,
   getIndentationFromString
 } = require("devtools/shared/indentation");
 
+XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
+  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
 var TRANSITION_CLASS = "moz-styleeditor-transitioning";
 var TRANSITION_DURATION_MS = 500;
 var TRANSITION_BUFFER_MS = 1000;
 var TRANSITION_RULE_SELECTOR =
 ".moz-styleeditor-transitioning:root, .moz-styleeditor-transitioning:root *";
 var TRANSITION_RULE = TRANSITION_RULE_SELECTOR + " {\
 transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \
 transition-delay: 0ms !important;\
 transition-timing-function: ease-out !important;\
 transition-property: all !important;\
 }";
 
 var LOAD_ERROR = "error-load";
 
-types.addActorType("stylesheet");
-types.addActorType("originalsource");
-
 // The possible kinds of style-applied events.
 // UPDATE_PRESERVING_RULES means that the update is guaranteed to
 // preserve the number and order of rules on the style sheet.
 // UPDATE_GENERAL covers any other kind of change to the style sheet.
 const UPDATE_PRESERVING_RULES = 0;
 exports.UPDATE_PRESERVING_RULES = UPDATE_PRESERVING_RULES;
 const UPDATE_GENERAL = 1;
 exports.UPDATE_GENERAL = UPDATE_GENERAL;
@@ -57,222 +60,67 @@ exports.UPDATE_GENERAL = UPDATE_GENERAL;
 // If the user edits a style sheet, we stash a copy of the edited text
 // here, keyed by the style sheet.  This way, if the tools are closed
 // and then reopened, the edited text will be available.  A weak map
 // is used so that navigation by the user will eventually cause the
 // edited text to be collected.
 let modifiedStyleSheets = new WeakMap();
 
 /**
- * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the
- * stylesheets of a document.
+ * Actor representing an original source of a style sheet that was specified
+ * in a source map.
  */
-var StyleSheetsActor = exports.StyleSheetsActor = protocol.ActorClass({
-  typeName: "stylesheets",
-
-  /**
-   * The window we work with, taken from the parent actor.
-   */
-  get window() {
-    return this.parentActor.window;
-  },
-
-  /**
-   * The current content document of the window we work with.
-   */
-  get document() {
-    return this.window.document;
-  },
-
-  form: function()
-  {
-    return { actor: this.actorID };
-  },
-
-  initialize: function (conn, tabActor) {
+var OriginalSourceActor = protocol.ActorClassWithSpec(originalSourceSpec, {
+  initialize: function(aUrl, aSourceMap, aParentActor) {
     protocol.Actor.prototype.initialize.call(this, null);
 
-    this.parentActor = tabActor;
+    this.url = aUrl;
+    this.sourceMap = aSourceMap;
+    this.parentActor = aParentActor;
+    this.conn = this.parentActor.conn;
+
+    this.text = null;
   },
 
-  /**
-   * Protocol method for getting a list of StyleSheetActors representing
-   * all the style sheets in this document.
-   */
-  getStyleSheets: method(Task.async(function* () {
-    // Iframe document can change during load (bug 1171919). Track their windows
-    // instead.
-    let windows = [this.window];
-    let actors = [];
+  form: function() {
+    return {
+      actor: this.actorID, // actorID is set when it's added to a pool
+      url: this.url,
+      relatedStyleSheet: this.parentActor.form()
+    };
+  },
 
-    for (let win of windows) {
-      let sheets = yield this._addStyleSheets(win);
-      actors = actors.concat(sheets);
-
-      // Recursively handle style sheets of the documents in iframes.
-      for (let iframe of win.document.querySelectorAll("iframe, browser, frame")) {
-        if (iframe.contentDocument && iframe.contentWindow) {
-          // Sometimes, iframes don't have any document, like the
-          // one that are over deeply nested (bug 285395)
-          windows.push(iframe.contentWindow);
-        }
-      }
+  _getText: function() {
+    if (this.text) {
+      return promise.resolve(this.text);
     }
-    return actors;
-  }), {
-    request: {},
-    response: { styleSheets: RetVal("array:stylesheet") }
-  }),
-
-  /**
-   * Check if we should be showing this stylesheet.
-   *
-   * @param {Document} doc
-   *        Document for which we're checking
-   * @param {DOMCSSStyleSheet} sheet
-   *        Stylesheet we're interested in
-   *
-   * @return boolean
-   *         Whether the stylesheet should be listed.
-   */
-  _shouldListSheet: function(doc, sheet) {
-    // Special case about:PreferenceStyleSheet, as it is generated on the
-    // fly and the URI is not registered with the about: handler.
-    // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37
-    if (sheet.href && sheet.href.toLowerCase() == "about:preferencestylesheet") {
-      return false;
+    let content = this.sourceMap.sourceContentFor(this.url);
+    if (content) {
+      this.text = content;
+      return promise.resolve(content);
     }
-
-    return true;
+    let options = {
+      policy: Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET,
+      window: this.window
+    };
+    return fetch(this.url, options).then(({content}) => {
+      this.text = content;
+      return content;
+    });
   },
 
   /**
-   * Add all the stylesheets for the document in this window to the map and
-   * create an actor for each one if not already created.
-   *
-   * @param {Window} win
-   *        Window for which to add stylesheets
-   *
-   * @return {Promise}
-   *         Promise that resolves to an array of StyleSheetActors
-   */
-  _addStyleSheets: function(win)
-  {
-    return Task.spawn(function*() {
-      let doc = win.document;
-      // readyState can be uninitialized if an iframe has just been created but
-      // it has not started to load yet.
-      if (doc.readyState === "loading" || doc.readyState === "uninitialized") {
-        // Wait for the document to load first.
-        yield listenOnce(win, "DOMContentLoaded", true);
-
-        // Make sure we have the actual document for this window. If the
-        // readyState was initially uninitialized, the initial dummy document
-        // was replaced with the actual document (bug 1171919).
-        doc = win.document;
-      }
-
-      let isChrome = Services.scriptSecurityManager.isSystemPrincipal(doc.nodePrincipal);
-      let styleSheets = isChrome ? DOMUtils.getAllStyleSheets(doc) : doc.styleSheets;
-      let actors = [];
-      for (let i = 0; i < styleSheets.length; i++) {
-        let sheet = styleSheets[i];
-        if (!this._shouldListSheet(doc, sheet)) {
-          continue;
-        }
-
-        let actor = this.parentActor.createStyleSheetActor(sheet);
-        actors.push(actor);
-
-        // Get all sheets, including imported ones
-        let imports = yield this._getImported(doc, actor);
-        actors = actors.concat(imports);
-      }
-      return actors;
-    }.bind(this));
-  },
-
-  /**
-   * Get all the stylesheets @imported from a stylesheet.
-   *
-   * @param  {Document} doc
-   *         The document including the stylesheet
-   * @param  {DOMStyleSheet} styleSheet
-   *         Style sheet to search
-   * @return {Promise}
-   *         A promise that resolves with an array of StyleSheetActors
+   * Protocol method to get the text of this source.
    */
-  _getImported: function(doc, styleSheet) {
-    return Task.spawn(function*() {
-      let rules = yield styleSheet.getCSSRules();
-      let imported = [];
-
-      for (let i = 0; i < rules.length; i++) {
-        let rule = rules[i];
-        if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
-          // Associated styleSheet may be null if it has already been seen due
-          // to duplicate @imports for the same URL.
-          if (!rule.styleSheet || !this._shouldListSheet(doc, rule.styleSheet)) {
-            continue;
-          }
-          let actor = this.parentActor.createStyleSheetActor(rule.styleSheet);
-          imported.push(actor);
-
-          // recurse imports in this stylesheet as well
-          let children = yield this._getImported(doc, actor);
-          imported = imported.concat(children);
-        }
-        else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
-          // @import rules must precede all others except @charset
-          break;
-        }
-      }
-
-      return imported;
-    }.bind(this));
-  },
-
-
-  /**
-   * Create a new style sheet in the document with the given text.
-   * Return an actor for it.
-   *
-   * @param  {object} request
-   *         Debugging protocol request object, with 'text property'
-   * @return {object}
-   *         Object with 'styelSheet' property for form on new actor.
-   */
-  addStyleSheet: method(function(text) {
-    let parent = this.document.documentElement;
-    let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
-    style.setAttribute("type", "text/css");
-
-    if (text) {
-      style.appendChild(this.document.createTextNode(text));
-    }
-    parent.appendChild(style);
-
-    let actor = this.parentActor.createStyleSheetActor(style.sheet);
-    return actor;
-  }, {
-    request: { text: Arg(0, "string") },
-    response: { styleSheet: RetVal("stylesheet") }
-  })
-});
-
-/**
- * The corresponding Front object for the StyleSheetsActor.
- */
-var StyleSheetsFront = protocol.FrontClass(StyleSheetsActor, {
-  initialize: function(client, tabForm) {
-    protocol.Front.prototype.initialize.call(this, client);
-    this.actorID = tabForm.styleSheetsActor;
-    this.manage(this);
+  getText: function() {
+    return this._getText().then((text) => {
+      return new LongStringActor(this.conn, text || "");
+    });
   }
-});
+})
 
 /**
  * A MediaRuleActor lives on the server and provides access to properties
  * of a DOM @media rule and emits events when it changes.
  */
 var MediaRuleActor = protocol.ActorClass({
   typeName: "mediarule",
 
@@ -345,17 +193,17 @@ var MediaRuleActor = protocol.ActorClass
   },
 
   _matchesChange: function() {
     events.emit(this, "matches-change", this.matches);
   }
 });
 
 /**
- * Cooresponding client-side front for a MediaRuleActor.
+ * Corresponding client-side front for a MediaRuleActor.
  */
 var MediaRuleFront = protocol.FrontClass(MediaRuleActor, {
   initialize: function(client, form) {
     protocol.Front.prototype.initialize.call(this, client, form);
 
     this._onMatchesChange = this._onMatchesChange.bind(this);
     events.on(this, "matches-change", this._onMatchesChange);
   },
@@ -388,16 +236,18 @@ var MediaRuleFront = protocol.FrontClass
   get column() {
     return this._form.column || -1;
   },
   get parentStyleSheet() {
     return this.conn.getActor(this._form.parentStyleSheet);
   }
 });
 
+types.addActorType("stylesheet");
+
 /**
  * A StyleSheetActor represents a stylesheet on the server.
  */
 var StyleSheetActor = protocol.ActorClass({
   typeName: "stylesheet",
 
   events: {
     "property-change" : {
@@ -1011,16 +861,18 @@ var StyleSheetActor = protocol.ActorClas
     if (rule.selectorText == TRANSITION_RULE_SELECTOR) {
       this.rawSheet.deleteRule(index);
     }
 
     events.emit(this, "style-applied", kind, this);
   }
 })
 
+exports.StyleSheetActor = StyleSheetActor;
+
 /**
  * StyleSheetFront is the client-side counterpart to a StyleSheetActor.
  */
 var StyleSheetFront = protocol.FrontClass(StyleSheetActor, {
   initialize: function(conn, form) {
     protocol.Front.prototype.initialize.call(this, conn, form);
 
     this._onPropertyChange = this._onPropertyChange.bind(this);
@@ -1086,114 +938,230 @@ var StyleSheetFront = protocol.FrontClas
 
       let {indentUnit, indentWithTabs} = getIndentationFromString(source);
 
       return indentWithTabs ? "\t" : " ".repeat(indentUnit);
     }.bind(this));
   }
 });
 
+exports.StyleSheetFront = StyleSheetFront;
+
 /**
- * Actor representing an original source of a style sheet that was specified
- * in a source map.
+ * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the
+ * stylesheets of a document.
  */
-var OriginalSourceActor = protocol.ActorClass({
-  typeName: "originalsource",
+var StyleSheetsActor = protocol.ActorClass({
+  typeName: "stylesheets",
+
+  /**
+   * The window we work with, taken from the parent actor.
+   */
+  get window() {
+    return this.parentActor.window;
+  },
 
-  initialize: function(aUrl, aSourceMap, aParentActor) {
+  /**
+   * The current content document of the window we work with.
+   */
+  get document() {
+    return this.window.document;
+  },
+
+  form: function()
+  {
+    return { actor: this.actorID };
+  },
+
+  initialize: function (conn, tabActor) {
     protocol.Actor.prototype.initialize.call(this, null);
 
-    this.url = aUrl;
-    this.sourceMap = aSourceMap;
-    this.parentActor = aParentActor;
-    this.conn = this.parentActor.conn;
-
-    this.text = null;
+    this.parentActor = tabActor;
   },
 
-  form: function() {
-    return {
-      actor: this.actorID, // actorID is set when it's added to a pool
-      url: this.url,
-      relatedStyleSheet: this.parentActor.form()
-    };
-  },
+  /**
+   * Protocol method for getting a list of StyleSheetActors representing
+   * all the style sheets in this document.
+   */
+  getStyleSheets: method(Task.async(function* () {
+    // Iframe document can change during load (bug 1171919). Track their windows
+    // instead.
+    let windows = [this.window];
+    let actors = [];
 
-  _getText: function() {
-    if (this.text) {
-      return promise.resolve(this.text);
+    for (let win of windows) {
+      let sheets = yield this._addStyleSheets(win);
+      actors = actors.concat(sheets);
+
+      // Recursively handle style sheets of the documents in iframes.
+      for (let iframe of win.document.querySelectorAll("iframe, browser, frame")) {
+        if (iframe.contentDocument && iframe.contentWindow) {
+          // Sometimes, iframes don't have any document, like the
+          // one that are over deeply nested (bug 285395)
+          windows.push(iframe.contentWindow);
+        }
+      }
     }
-    let content = this.sourceMap.sourceContentFor(this.url);
-    if (content) {
-      this.text = content;
-      return promise.resolve(content);
+    return actors;
+  }), {
+    request: {},
+    response: { styleSheets: RetVal("array:stylesheet") }
+  }),
+
+  /**
+   * Check if we should be showing this stylesheet.
+   *
+   * @param {Document} doc
+   *        Document for which we're checking
+   * @param {DOMCSSStyleSheet} sheet
+   *        Stylesheet we're interested in
+   *
+   * @return boolean
+   *         Whether the stylesheet should be listed.
+   */
+  _shouldListSheet: function(doc, sheet) {
+    // Special case about:PreferenceStyleSheet, as it is generated on the
+    // fly and the URI is not registered with the about: handler.
+    // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37
+    if (sheet.href && sheet.href.toLowerCase() == "about:preferencestylesheet") {
+      return false;
     }
-    let options = {
-      policy: Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET,
-      window: this.window
-    };
-    return fetch(this.url, options).then(({content}) => {
-      this.text = content;
-      return content;
-    });
+
+    return true;
   },
 
   /**
-   * Protocol method to get the text of this source.
+   * Add all the stylesheets for the document in this window to the map and
+   * create an actor for each one if not already created.
+   *
+   * @param {Window} win
+   *        Window for which to add stylesheets
+   *
+   * @return {Promise}
+   *         Promise that resolves to an array of StyleSheetActors
+   */
+  _addStyleSheets: function(win)
+  {
+    return Task.spawn(function*() {
+      let doc = win.document;
+      // readyState can be uninitialized if an iframe has just been created but
+      // it has not started to load yet.
+      if (doc.readyState === "loading" || doc.readyState === "uninitialized") {
+        // Wait for the document to load first.
+        yield listenOnce(win, "DOMContentLoaded", true);
+
+        // Make sure we have the actual document for this window. If the
+        // readyState was initially uninitialized, the initial dummy document
+        // was replaced with the actual document (bug 1171919).
+        doc = win.document;
+      }
+
+      let isChrome = Services.scriptSecurityManager.isSystemPrincipal(doc.nodePrincipal);
+      let styleSheets = isChrome ? DOMUtils.getAllStyleSheets(doc) : doc.styleSheets;
+      let actors = [];
+      for (let i = 0; i < styleSheets.length; i++) {
+        let sheet = styleSheets[i];
+        if (!this._shouldListSheet(doc, sheet)) {
+          continue;
+        }
+
+        let actor = this.parentActor.createStyleSheetActor(sheet);
+        actors.push(actor);
+
+        // Get all sheets, including imported ones
+        let imports = yield this._getImported(doc, actor);
+        actors = actors.concat(imports);
+      }
+      return actors;
+    }.bind(this));
+  },
+
+  /**
+   * Get all the stylesheets @imported from a stylesheet.
+   *
+   * @param  {Document} doc
+   *         The document including the stylesheet
+   * @param  {DOMStyleSheet} styleSheet
+   *         Style sheet to search
+   * @return {Promise}
+   *         A promise that resolves with an array of StyleSheetActors
    */
-  getText: method(function() {
-    return this._getText().then((text) => {
-      return new LongStringActor(this.conn, text || "");
-    });
+  _getImported: function(doc, styleSheet) {
+    return Task.spawn(function*() {
+      let rules = yield styleSheet.getCSSRules();
+      let imported = [];
+
+      for (let i = 0; i < rules.length; i++) {
+        let rule = rules[i];
+        if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
+          // Associated styleSheet may be null if it has already been seen due
+          // to duplicate @imports for the same URL.
+          if (!rule.styleSheet || !this._shouldListSheet(doc, rule.styleSheet)) {
+            continue;
+          }
+          let actor = this.parentActor.createStyleSheetActor(rule.styleSheet);
+          imported.push(actor);
+
+          // recurse imports in this stylesheet as well
+          let children = yield this._getImported(doc, actor);
+          imported = imported.concat(children);
+        }
+        else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
+          // @import rules must precede all others except @charset
+          break;
+        }
+      }
+
+      return imported;
+    }.bind(this));
+  },
+
+
+  /**
+   * Create a new style sheet in the document with the given text.
+   * Return an actor for it.
+   *
+   * @param  {object} request
+   *         Debugging protocol request object, with 'text property'
+   * @return {object}
+   *         Object with 'styelSheet' property for form on new actor.
+   */
+  addStyleSheet: method(function(text) {
+    let parent = this.document.documentElement;
+    let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
+    style.setAttribute("type", "text/css");
+
+    if (text) {
+      style.appendChild(this.document.createTextNode(text));
+    }
+    parent.appendChild(style);
+
+    let actor = this.parentActor.createStyleSheetActor(style.sheet);
+    return actor;
   }, {
-    response: {
-      text: RetVal("longstring")
-    }
+    request: { text: Arg(0, "string") },
+    response: { styleSheet: RetVal("stylesheet") }
   })
-})
+});
+
+exports.StyleSheetsActor = StyleSheetsActor;
 
 /**
- * The client-side counterpart for an OriginalSourceActor.
+ * The corresponding Front object for the StyleSheetsActor.
  */
-var OriginalSourceFront = protocol.FrontClass(OriginalSourceActor, {
-  initialize: function(client, form) {
-    protocol.Front.prototype.initialize.call(this, client, form);
-
-    this.isOriginalSource = true;
-  },
-
-  form: function(form, detail) {
-    if (detail === "actorid") {
-      this.actorID = form;
-      return;
-    }
-    this.actorID = form.actor;
-    this._form = form;
-  },
-
-  get href() {
-    return this._form.url;
-  },
-  get url() {
-    return this._form.url;
+var StyleSheetsFront = protocol.FrontClass(StyleSheetsActor, {
+  initialize: function(client, tabForm) {
+    protocol.Front.prototype.initialize.call(this, client);
+    this.actorID = tabForm.styleSheetsActor;
+    this.manage(this);
   }
 });
 
-
-XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
-  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
-});
-
-exports.StyleSheetsActor = StyleSheetsActor;
 exports.StyleSheetsFront = StyleSheetsFront;
 
-exports.StyleSheetActor = StyleSheetActor;
-exports.StyleSheetFront = StyleSheetFront;
-
-
 /**
  * Normalize multiple relative paths towards the base paths on the right.
  */
 function normalize(...aURLs) {
   let base = Services.io.newURI(aURLs.pop(), null, null);
   let url;
   while ((url = aURLs.pop())) {
     base = Services.io.newURI(url, null, base);
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -208,16 +208,25 @@ function createRootActor(connection) {
  * linked browser's content window objects do).
  *
  * However, while we could thus assume that each tab stays with the XUL window
  * it belonged to when it was created, I'm not sure this is behavior one should
  * rely upon. When a XUL window is closed, we take the less efficient, more
  * conservative approach of simply searching the entire table for actors that
  * belong to the closing XUL window, rather than trying to somehow track which
  * XUL window each tab belongs to.
+ *
+ * - Title changes:
+ *
+ * For tabs living in the child process, we listen for DOMTitleChange message
+ * via the top-level window's message manager. Doing this also allows listening
+ * for title changes on Fennec.
+ * But as these messages aren't sent for tabs loaded in the parent process,
+ * we also listen for TabAttrModified event, which is fired only on Firefox
+ * desktop.
  */
 function BrowserTabList(connection) {
   this._connection = connection;
 
   /*
    * The XUL document of a tabbed browser window has "tab" elements, whose
    * 'linkedBrowser' JavaScript properties are "browser" elements; those
    * browsers' 'contentWindow' properties are wrappers on the tabs' content
@@ -287,18 +296,29 @@ BrowserTabList.prototype._getBrowsers = 
     // browser.contentWindow as the debuggee global.
     for (let browser of this._getChildren(win)) {
       yield browser;
     }
   }
 };
 
 BrowserTabList.prototype._getChildren = function (window) {
-  let children = window.gBrowser ? window.gBrowser.browsers : [];
-  return children ? children : [];
+  if (!window.gBrowser) {
+    return [];
+  }
+  let { gBrowser } = window;
+  if (!gBrowser.browsers) {
+    return [];
+  }
+  return gBrowser.browsers.filter(browser => {
+    // Filter tabs that are closing. listTabs calls made right after TabClose
+    // events still list tabs in process of being closed.
+    let tab = gBrowser.getTabForBrowser(browser);
+    return !tab.closing;
+  });
 };
 
 BrowserTabList.prototype._isRemoteBrowser = function (browser) {
   return browser.getAttribute("remote") == "true";
 };
 
 BrowserTabList.prototype.getList = function () {
   let topXULWindow = Services.wm.getMostRecentWindow(
@@ -453,38 +473,50 @@ BrowserTabList.prototype._handleActorClo
  * Make sure we are listening or not listening for activity elsewhere in
  * the browser, as appropriate. Other than setting up newly created XUL
  * windows, all listener / observer connection and disconnection should
  * happen here.
  */
 BrowserTabList.prototype._checkListening = function () {
   /*
    * If we have an onListChanged handler that we haven't sent an announcement
-   * to since the last iteration, we need to watch for tab creation.
+   * to since the last iteration, we need to watch for tab creation as well as
+   * change of the currently selected tab and tab title changes of tabs in
+   * parent process via TabAttrModified (tabs oop uses DOMTitleChanges).
    *
    * Oddly, we don't need to watch for 'close' events here. If our actor list
    * is empty, then either it was empty the last time we iterated, and no
    * close events are possible, or it was not empty the last time we
    * iterated, but all the actors have since been closed, and we must have
    * sent a notification already when they closed.
    */
   this._listenForEventsIf(this._onListChanged && this._mustNotify,
-                          "_listeningForTabOpen", ["TabOpen", "TabSelect"]);
+                          "_listeningForTabOpen",
+                          ["TabOpen", "TabSelect", "TabAttrModified"]);
 
   /* If we have live actors, we need to be ready to mark them dead. */
   this._listenForEventsIf(this._actorByBrowser.size > 0,
-                          "_listeningForTabClose", ["TabClose"]);
+                          "_listeningForTabClose",
+                          ["TabClose", "TabRemotenessChange"]);
 
   /*
    * We must listen to the window mediator in either case, since that's the
    * only way to find out about tabs that come and go when top-level windows
    * are opened and closed.
    */
   this._listenToMediatorIf((this._onListChanged && this._mustNotify) ||
                            (this._actorByBrowser.size > 0));
+
+  /*
+   * We also listen for title changed from the child process.
+   * This allows listening for title changes from Fennec and OOP tabs in Fx.
+   */
+  this._listenForMessagesIf(this._onListChanged && this._mustNotify,
+                            "_listeningForTitleChange",
+                            ["DOMTitleChanged"]);
 };
 
 /*
  * Add or remove event listeners for all XUL windows.
  *
  * @param shouldListen boolean
  *    True if we should add event handlers; false if we should remove them.
  * @param guard string
@@ -501,36 +533,107 @@ BrowserTabList.prototype._listenForEvent
         for (let name of eventNames) {
           win[op](name, this, false);
         }
       }
       this[guard] = shouldListen;
     }
   };
 
+/*
+ * Add or remove message listeners for all XUL windows.
+ *
+ * @param aShouldListen boolean
+ *    True if we should add message listeners; false if we should remove them.
+ * @param aGuard string
+ *    The name of a guard property of 'this', indicating whether we're
+ *    already listening for those messages.
+ * @param aMessageNames array of strings
+ *    An array of message names.
+ */
+BrowserTabList.prototype._listenForMessagesIf = function(aShouldListen, aGuard, aMessageNames) {
+  if (!aShouldListen !== !this[aGuard]) {
+    let op = aShouldListen ? "addMessageListener" : "removeMessageListener";
+    for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
+      for (let name of aMessageNames) {
+        win.messageManager[op](name, this);
+      }
+    }
+    this[aGuard] = aShouldListen;
+  }
+};
+
+/**
+ * Implement nsIMessageListener.
+ */
+BrowserTabList.prototype.receiveMessage = DevToolsUtils.makeInfallible(function(message) {
+  let browser = message.target;
+  switch (message.name) {
+    case "DOMTitleChanged": {
+      let actor = this._actorByBrowser.get(browser);
+      if (actor) {
+        this._notifyListChanged();
+        this._checkListening();
+      }
+      break;
+    }
+  }
+});
+
 /**
  * Implement nsIDOMEventListener.
  */
 BrowserTabList.prototype.handleEvent =
 DevToolsUtils.makeInfallible(function (event) {
+  let browser = event.target.linkedBrowser;
   switch (event.type) {
     case "TabOpen":
-    case "TabSelect":
-      // Don't create a new actor; iterate will take care of that.
-      // Just notify.
+    case "TabSelect": {
+      /* Don't create a new actor; iterate will take care of that. Just notify. */
       this._notifyListChanged();
       this._checkListening();
       break;
-    case "TabClose":
-      let browser = event.target.linkedBrowser;
+    }
+    case "TabClose": {
       let actor = this._actorByBrowser.get(browser);
       if (actor) {
         this._handleActorClose(actor, browser);
       }
       break;
+    }
+    case "TabRemotenessChange": {
+      // We have to remove the cached actor as we have to create a new instance
+      // based on BrowserTabActor or RemoteBrowserTabActor.
+      let actor = this._actorByBrowser.get(browser);
+      if (actor) {
+        this._actorByBrowser.delete(browser);
+        // Don't create a new actor; iterate will take care of that. Just notify.
+        this._notifyListChanged();
+        this._checkListening();
+      }
+      break;
+    }
+    case "TabAttrModified": {
+      // Remote <browser> title changes are handled via DOMTitleChange message
+      // TabAttrModified is only here for browsers in parent process which
+      // don't send this message.
+      if (browser.isRemoteBrowser) {
+        break;
+      }
+      let actor = this._actorByBrowser.get(browser);
+      if (actor) {
+        // TabAttrModified is fired in various cases, here only care about title
+        // changes
+        if (event.detail.changed.includes("label")) {
+          this._notifyListChanged();
+          this._checkListening();
+        }
+      }
+      break;
+    }
   }
 }, "BrowserTabList.prototype.handleEvent");
 
 /*
  * If |shouldListen| is true, ensure we've registered a listener with the
  * window mediator. Otherwise, ensure we haven't registered a listener.
  */
 BrowserTabList.prototype._listenToMediatorIf = function (shouldListen) {
@@ -561,19 +664,24 @@ DevToolsUtils.makeInfallible(function (w
     if (appShellDOMWindowType(window) !== DebuggerServer.chromeWindowType) {
       return;
     }
 
     // Listen for future tab activity.
     if (this._listeningForTabOpen) {
       window.addEventListener("TabOpen", this, false);
       window.addEventListener("TabSelect", this, false);
+      window.addEventListener("TabAttrModified", this, false);
     }
     if (this._listeningForTabClose) {
       window.addEventListener("TabClose", this, false);
+      window.addEventListener("TabRemotenessChange", this, false);
+    }
+    if (this._listeningForTitleChange) {
+      window.messageManager.addMessageListener("DOMTitleChanged", this);
     }
 
     // As explained above, we will not receive a TabOpen event for this
     // document's initial tab, so we must notify our client of the new tab
     // this will have.
     this._notifyListChanged();
   });
 
@@ -959,19 +1067,20 @@ TabActor.prototype = {
                "form() shouldn't be called on exited browser actor.");
     assert(this.actorID,
                "tab should have an actorID.");
 
     let response = {
       actor: this.actorID
     };
 
-    // On xpcshell we are using tabactor even if there is no valid document.
-    // Actors like chrome debugger can work.
-    if (this.window) {
+    // We may try to access window while the document is closing, then
+    // accessing window throws. Also on xpcshell we are using tabactor even if
+    // there is no valid document.
+    if (this.docShell && !this.docShell.isBeingDestroyed()) {
       response.title = this.title;
       response.url = this.url;
       let windowUtils = this.window
         .QueryInterface(Ci.nsIInterfaceRequestor)
         .getInterface(Ci.nsIDOMWindowUtils);
       response.outerWindowID = windowUtils.outerWindowID;
     }
 
--- a/devtools/server/tests/browser/browser_storage_dynamic_windows.js
+++ b/devtools/server/tests/browser/browser_storage_dynamic_windows.js
@@ -1,15 +1,15 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const {StorageFront} = require("devtools/server/actors/storage");
+const {StorageFront} = require("devtools/client/fronts/storage");
 Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js", this);
 
 const beforeReload = {
   cookies: {
     "test1.example.org": ["c1", "cs2", "c3", "uc1"],
     "sectest1.example.org": ["uc1", "cs2"]
   },
   localStorage: {
--- a/devtools/server/tests/browser/browser_storage_listings.js
+++ b/devtools/server/tests/browser/browser_storage_listings.js
@@ -1,15 +1,15 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const {StorageFront} = require("devtools/server/actors/storage");
+const {StorageFront} = require("devtools/client/fronts/storage");
 Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js", this);
 
 const storeMap = {
   cookies: {
     "test1.example.org": [
       {
         name: "c1",
         value: "foobar",
--- a/devtools/server/tests/browser/browser_storage_updates.js
+++ b/devtools/server/tests/browser/browser_storage_updates.js
@@ -1,15 +1,15 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const {StorageFront} = require("devtools/server/actors/storage");
+const {StorageFront} = require("devtools/client/fronts/storage");
 const beforeReload = {
   cookies: ["test1.example.org", "sectest1.example.org"],
   localStorage: ["http://test1.example.org", "http://sectest1.example.org"],
   sessionStorage: ["http://test1.example.org", "http://sectest1.example.org"],
 };
 
 const TESTS = [
   // index 0
--- a/devtools/shared/Parser.jsm
+++ b/devtools/shared/Parser.jsm
@@ -31,17 +31,17 @@ Parser.prototype = {
    * Gets a collection of parser methods for a specified source.
    *
    * @param string source
    *        The source text content.
    * @param string url [optional]
    *        The source url. The AST nodes will be cached, so you can use this
    *        identifier to avoid parsing the whole source again.
    */
-  get: function(source, url = "") {
+  get(source, url = "") {
     // Try to use the cached AST nodes, to avoid useless parsing operations.
     if (this._cache.has(url)) {
       return this._cache.get(url);
     }
 
     // The source may not necessarily be JS, in which case we need to extract
     // all the scripts. Fastest/easiest way is with a regular expression.
     // Don't worry, the rules of using a <script> tag are really strict,
@@ -102,27 +102,27 @@ Parser.prototype = {
     }
 
     return pool;
   },
 
   /**
    * Clears all the parsed sources from cache.
    */
-  clearCache: function() {
+  clearCache() {
     this._cache.clear();
   },
 
   /**
    * Clears the AST for a particular source.
    *
    * @param String url
    *        The URL of the source that is being cleared.
    */
-  clearSource: function(url) {
+  clearSource(url) {
     this._cache.delete(url);
   },
 
   _cache: null,
   errors: null
 };
 
 /**
@@ -138,33 +138,33 @@ function SyntaxTreesPool(syntaxTrees, ur
   this._url = url;
   this._cache = new Map();
 }
 
 SyntaxTreesPool.prototype = {
   /**
    * @see SyntaxTree.prototype.getIdentifierAt
    */
-  getIdentifierAt: function({ line, column, scriptIndex, ignoreLiterals }) {
+  getIdentifierAt({ line, column, scriptIndex, ignoreLiterals }) {
     return this._call("getIdentifierAt",
       scriptIndex, line, column, ignoreLiterals)[0];
   },
 
   /**
    * @see SyntaxTree.prototype.getNamedFunctionDefinitions
    */
-  getNamedFunctionDefinitions: function(substring) {
+  getNamedFunctionDefinitions(substring) {
     return this._call("getNamedFunctionDefinitions", -1, substring);
   },
 
   /**
    * @return SyntaxTree
    *         The last tree in this._trees
    */
-  getLastSyntaxTree: function() {
+  getLastSyntaxTree() {
     return this._trees[this._trees.length - 1];
   },
 
   /**
    * Gets the total number of scripts in the parent source.
    * @return number
    */
   get scriptCount() {
@@ -175,17 +175,17 @@ SyntaxTreesPool.prototype = {
    * Finds the start and length of the script containing the specified offset
    * relative to its parent source.
    *
    * @param number atOffset
    *        The offset relative to the parent source.
    * @return object
    *         The offset and length relative to the enclosing script.
    */
-  getScriptInfo: function(atOffset) {
+  getScriptInfo(atOffset) {
     let info = { start: -1, length: -1, index: -1 };
 
     for (let { offset, length } of this._trees) {
       info.index++;
       if (offset <= atOffset && offset + length >= atOffset) {
         info.start = offset;
         info.length = length;
         return info;
@@ -205,17 +205,17 @@ SyntaxTreesPool.prototype = {
    *        The syntax tree for which to handle the request. If the tree at
    *        the specified index isn't found, the accumulated results for all
    *        syntax trees are returned.
    * @param any params
    *        Any kind params to pass to the request function.
    * @return array
    *         The results given by all known syntax trees.
    */
-  _call: function(functionName, syntaxTreeIndex, ...params) {
+  _call(functionName, syntaxTreeIndex, ...params) {
     let results = [];
     let requestId = [functionName, syntaxTreeIndex, params].toSource();
 
     if (this._cache.has(requestId)) {
       return this._cache.get(requestId);
     }
 
     let requestedTree = this._trees[syntaxTreeIndex];
@@ -274,52 +274,52 @@ SyntaxTree.prototype = {
    * @param number column
    *        The column in the source.
    * @param boolean ignoreLiterals
    *        Specifies if alone literals should be ignored.
    * @return object
    *         An object containing identifier information as { name, location,
    *         evalString } properties, or null if nothing is found.
    */
-  getIdentifierAt: function(line, column, ignoreLiterals) {
+  getIdentifierAt(line, column, ignoreLiterals) {
     let info = null;
 
     SyntaxTreeVisitor.walk(this.AST, {
       /**
        * Callback invoked for each identifier node.
        * @param Node node
        */
-      onIdentifier: function(node) {
+      onIdentifier(node) {
         if (ParserHelpers.nodeContainsPoint(node, line, column)) {
           info = {
             name: node.name,
             location: ParserHelpers.getNodeLocation(node),
             evalString: ParserHelpers.getIdentifierEvalString(node)
           };
 
           // Abruptly halt walking the syntax tree.
           SyntaxTreeVisitor.break = true;
         }
       },
 
       /**
        * Callback invoked for each literal node.
        * @param Node node
        */
-      onLiteral: function(node) {
+      onLiteral(node) {
         if (!ignoreLiterals) {
           this.onIdentifier(node);
         }
       },
 
       /**
        * Callback invoked for each 'this' node.
        * @param Node node
        */
-      onThisExpression: function(node) {
+      onThisExpression(node) {
         this.onIdentifier(node);
       }
     });
 
     return info;
   },
 
   /**
@@ -328,44 +328,44 @@ SyntaxTree.prototype = {
    *
    * @param string substring
    *        The string to be contained in the function name (or inferred name).
    *        Can be an empty string to match all functions.
    * @return array
    *         All the matching function declarations and expressions, as
    *         { functionName, functionLocation ... } object hashes.
    */
-  getNamedFunctionDefinitions: function(substring) {
+  getNamedFunctionDefinitions(substring) {
     let lowerCaseToken = substring.toLowerCase();
     let store = [];
 
     function includesToken(name) {
       return name && name.toLowerCase().includes(lowerCaseToken);
     }
 
     SyntaxTreeVisitor.walk(this.AST, {
       /**
        * Callback invoked for each function declaration node.
        * @param Node node
        */
-      onFunctionDeclaration: function(node) {
+      onFunctionDeclaration(node) {
         let functionName = node.id.name;
         if (includesToken(functionName)) {
           store.push({
             functionName: functionName,
             functionLocation: ParserHelpers.getNodeLocation(node)
           });
         }
       },
 
       /**
        * Callback invoked for each function expression node.
        * @param Node node
        */
-      onFunctionExpression: function(node) {
+      onFunctionExpression(node) {
         // Function expressions don't necessarily have a name.
         let functionName = node.id ? node.id.name : "";
         let functionLocation = ParserHelpers.getNodeLocation(node);
 
         // Infer the function's name from an enclosing syntax tree node.
         let inferredInfo = ParserHelpers.inferFunctionExpressionInfo(node);
         let inferredName = inferredInfo.name;
         let inferredChain = inferredInfo.chain;
@@ -386,17 +386,17 @@ SyntaxTree.prototype = {
           });
         }
       },
 
       /**
        * Callback invoked for each arrow expression node.
        * @param Node node
        */
-      onArrowFunctionExpression: function(node) {
+      onArrowFunctionExpression(node) {
         // Infer the function's name from an enclosing syntax tree node.
         let inferredInfo = ParserHelpers.inferFunctionExpressionInfo(node);
         let inferredName = inferredInfo.name;
         let inferredChain = inferredInfo.chain;
         let inferredLocation = inferredInfo.loc;
 
         // Current node may be part of a larger assignment expression stack.
         if (node._parent.type == "AssignmentExpression") {
@@ -431,17 +431,17 @@ var ParserHelpers = {
    * location property directly attached, or the location information
    * is incorrect, in which cases it's accessible via the parent.
    *
    * @param Node node
    *        The node who's location needs to be retrieved.
    * @return object
    *         An object containing { line, column } information.
    */
-  getNodeLocation: function(node) {
+  getNodeLocation(node) {
     if (node.type != "Identifier") {
       return node.loc;
     }
     // Work around the fact that some identifier nodes don't have the
     // correct location attached.
     let { loc: parentLocation, type: parentType } = node._parent;
     let { loc: nodeLocation } = node;
     if (!nodeLocation) {
@@ -495,51 +495,51 @@ var ParserHelpers = {
    *
    * @param Node node
    *        The node's bounds used as reference.
    * @param number line
    *        The line number to check.
    * @return boolean
    *         True if the line and column is contained in the node's bounds.
    */
-  nodeContainsLine: function(node, line) {
+  nodeContainsLine(node, line) {
     let { start: s, end: e } = this.getNodeLocation(node);
     return s.line <= line && e.line >= line;
   },
 
   /**
    * Checks if a node's bounds contains a specified line and column.
    *
    * @param Node node
    *        The node's bounds used as reference.
    * @param number line
    *        The line number to check.
    * @param number column
    *        The column number to check.
    * @return boolean
    *         True if the line and column is contained in the node's bounds.
    */
-  nodeContainsPoint: function(node, line, column) {
+  nodeContainsPoint(node, line, column) {
     let { start: s, end: e } = this.getNodeLocation(node);
     return s.line == line && e.line == line &&
            s.column <= column && e.column >= column;
   },
 
   /**
    * Try to infer a function expression's name & other details based on the
    * enclosing VariableDeclarator, AssignmentExpression or ObjectExpression.
    *
    * @param Node node
    *        The function expression node to get the name for.
    * @return object
    *         The inferred function name, or empty string can't infer the name,
    *         along with the chain (a generic "context", like a prototype chain)
    *         and location if available.
    */
-  inferFunctionExpressionInfo: function(node) {
+  inferFunctionExpressionInfo(node) {
     let parent = node._parent;
 
     // A function expression may be defined in a variable declarator,
     // e.g. var foo = function(){}, in which case it is possible to infer
     // the variable name.
     if (parent.type == "VariableDeclarator") {
       return {
         name: parent.id.name,
@@ -594,26 +594,27 @@ var ParserHelpers = {
    * "{ foo: bar }" object expression, the returned node is the "foo"
    * identifier.
    *
    * @param Node node
    *        The value node in an object expression.
    * @return object
    *         The key identifier node in the object expression.
    */
-  _getObjectExpressionPropertyKeyForValue: function(node) {
+  _getObjectExpressionPropertyKeyForValue(node) {
     let parent = node._parent;
     if (parent.type != "ObjectExpression") {
       return null;
     }
     for (let property of parent.properties) {
       if (property.value == node) {
         return property.key;
       }
     }
+    return null;
   },
 
   /**
    * Gets an object expression's property chain to its parent
    * variable declarator or assignment expression, if available.
    *
    * Used for inferring function expression information and retrieving
    * an identifier evaluation string.
@@ -624,17 +625,17 @@ var ParserHelpers = {
    *
    * @param Node node
    *        The object expression node to begin the scan from.
    * @param array aStore [optional]
    *        The chain to store the nodes into.
    * @return array
    *         The chain to the parent variable declarator, as strings.
    */
-  _getObjectExpressionPropertyChain: function(node, aStore = []) {
+  _getObjectExpressionPropertyChain(node, aStore = []) {
     switch (node.type) {
       case "ObjectExpression":
         this._getObjectExpressionPropertyChain(node._parent, aStore);
         let propertyKey = this._getObjectExpressionPropertyKeyForValue(node);
         if (propertyKey) {
           aStore.push(propertyKey.name);
         }
         break;
@@ -672,17 +673,17 @@ var ParserHelpers = {
    *
    * @param Node node
    *        The member expression node to begin the scan from.
    * @param array store [optional]
    *        The chain to store the nodes into.
    * @return array
    *         The full member chain, as strings.
    */
-  _getMemberExpressionPropertyChain: function(node, store = []) {
+  _getMemberExpressionPropertyChain(node, store = []) {
     switch (node.type) {
       case "MemberExpression":
         this._getMemberExpressionPropertyChain(node.object, store);
         this._getMemberExpressionPropertyChain(node.property, store);
         break;
       case "ThisExpression":
         store.push("this");
         break;
@@ -698,17 +699,17 @@ var ParserHelpers = {
    * current value for the respective identifier.
    *
    * @param Node node
    *        The leaf node (e.g. Identifier, Literal) to begin the scan from.
    * @return string
    *         The corresponding evaluation string, or empty string if
    *         the specified leaf node can't be used.
    */
-  getIdentifierEvalString: function(node) {
+  getIdentifierEvalString(node) {
     switch (node._parent.type) {
       case "ObjectExpression":
         // If the identifier is the actual property value, it can be used
         // directly as an evaluation string. Otherwise, construct the property
         // access chain, since the value might have changed.
         if (!this._getObjectExpressionPropertyKeyForValue(node)) {
           let propertyChain =
             this._getObjectExpressionPropertyChain(node._parent);
@@ -751,32 +752,32 @@ var SyntaxTreeVisitor = {
    * Walks a syntax tree.
    *
    * @param object tree
    *        The AST nodes generated by the reflection API
    * @param object callbacks
    *        A map of all the callbacks to invoke when passing through certain
    *        types of noes (e.g: onFunctionDeclaration, onBlockStatement etc.).
    */
-  walk: function(tree, callbacks) {
+  walk(tree, callbacks) {
     this.break = false;
     this[tree.type](tree, callbacks);
   },
 
   /**
    * Filters all the nodes in this syntax tree based on a predicate.
    *
    * @param object tree
    *        The AST nodes generated by the reflection API
    * @param function predicate
    *        The predicate ran on each node.
    * @return array
    *         An array of nodes validating the predicate.
    */
-  filter: function(tree, predicate) {
+  filter(tree, predicate) {
     let store = [];
     this.walk(tree, {
       onNode: e => {
         if (predicate(e)) {
           store.push(e);
         }
       }
     });
@@ -792,31 +793,31 @@ var SyntaxTreeVisitor = {
   /**
    * A complete program source tree.
    *
    * interface Program <: Node {
    *   type: "Program";
    *   body: [ Statement ];
    * }
    */
-  Program: function(node, callbacks) {
+  Program(node, callbacks) {
     if (callbacks.onProgram) {
       callbacks.onProgram(node);
     }
     for (let statement of node.body) {
       this[statement.type](statement, node, callbacks);
     }
   },
 
   /**
    * Any statement.
    *
    * interface Statement <: Node { }
    */
-  Statement: function(node, parent, callbacks) {
+  Statement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -829,17 +830,17 @@ var SyntaxTreeVisitor = {
 
   /**
    * An empty statement, i.e., a solitary semicolon.
    *
    * interface EmptyStatement <: Statement {
    *   type: "EmptyStatement";
    * }
    */
-  EmptyStatement: function(node, parent, callbacks) {
+  EmptyStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -853,17 +854,17 @@ var SyntaxTreeVisitor = {
   /**
    * A block statement, i.e., a sequence of statements surrounded by braces.
    *
    * interface BlockStatement <: Statement {
    *   type: "BlockStatement";
    *   body: [ Statement ];
    * }
    */
-  BlockStatement: function(node, parent, callbacks) {
+  BlockStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -881,17 +882,17 @@ var SyntaxTreeVisitor = {
    * An expression statement, i.e., a statement consisting of a single
    * expression.
    *
    * interface ExpressionStatement <: Statement {
    *   type: "ExpressionStatement";
    *   expression: Expression;
    * }
    */
-  ExpressionStatement: function(node, parent, callbacks) {
+  ExpressionStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -908,17 +909,17 @@ var SyntaxTreeVisitor = {
    *
    * interface IfStatement <: Statement {
    *   type: "IfStatement";
    *   test: Expression;
    *   consequent: Statement;
    *   alternate: Statement | null;
    * }
    */
-  IfStatement: function(node, parent, callbacks) {
+  IfStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -938,17 +939,17 @@ var SyntaxTreeVisitor = {
    * A labeled statement, i.e., a statement prefixed by a break/continue label.
    *
    * interface LabeledStatement <: Statement {
    *   type: "LabeledStatement";
    *   label: Identifier;
    *   body: Statement;
    * }
    */
-  LabeledStatement: function(node, parent, callbacks) {
+  LabeledStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -964,17 +965,17 @@ var SyntaxTreeVisitor = {
   /**
    * A break statement.
    *
    * interface BreakStatement <: Statement {
    *   type: "BreakStatement";
    *   label: Identifier | null;
    * }
    */
-  BreakStatement: function(node, parent, callbacks) {
+  BreakStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -991,17 +992,17 @@ var SyntaxTreeVisitor = {
   /**
    * A continue statement.
    *
    * interface ContinueStatement <: Statement {
    *   type: "ContinueStatement";
    *   label: Identifier | null;
    * }
    */
-  ContinueStatement: function(node, parent, callbacks) {
+  ContinueStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1019,17 +1020,17 @@ var SyntaxTreeVisitor = {
    * A with statement.
    *
    * interface WithStatement <: Statement {
    *   type: "WithStatement";
    *   object: Expression;
    *   body: Statement;
    * }
    */
-  WithStatement: function(node, parent, callbacks) {
+  WithStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1049,17 +1050,17 @@ var SyntaxTreeVisitor = {
    *
    * interface SwitchStatement <: Statement {
    *   type: "SwitchStatement";
    *   discriminant: Expression;
    *   cases: [ SwitchCase ];
    *   lexical: boolean;
    * }
    */
-  SwitchStatement: function(node, parent, callbacks) {
+  SwitchStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1077,17 +1078,17 @@ var SyntaxTreeVisitor = {
   /**
    * A return statement.
    *
    * interface ReturnStatement <: Statement {
    *   type: "ReturnStatement";
    *   argument: Expression | null;
    * }
    */
-  ReturnStatement: function(node, parent, callbacks) {
+  ReturnStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1104,17 +1105,17 @@ var SyntaxTreeVisitor = {
   /**
    * A throw statement.
    *
    * interface ThrowStatement <: Statement {
    *   type: "ThrowStatement";
    *   argument: Expression;
    * }
    */
-  ThrowStatement: function(node, parent, callbacks) {
+  ThrowStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1132,17 +1133,17 @@ var SyntaxTreeVisitor = {
    * interface TryStatement <: Statement {
    *   type: "TryStatement";
    *   block: BlockStatement;
    *   handler: CatchClause | null;
    *   guardedHandlers: [ CatchClause ];
    *   finalizer: BlockStatement | null;
    * }
    */
-  TryStatement: function(node, parent, callbacks) {
+  TryStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1167,17 +1168,17 @@ var SyntaxTreeVisitor = {
    * A while statement.
    *
    * interface WhileStatement <: Statement {
    *   type: "WhileStatement";
    *   test: Expression;
    *   body: Statement;
    * }
    */
-  WhileStatement: function(node, parent, callbacks) {
+  WhileStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1194,17 +1195,17 @@ var SyntaxTreeVisitor = {
    * A do/while statement.
    *
    * interface DoWhileStatement <: Statement {
    *   type: "DoWhileStatement";
    *   body: Statement;
    *   test: Expression;
    * }
    */
-  DoWhileStatement: function(node, parent, callbacks) {
+  DoWhileStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1223,17 +1224,17 @@ var SyntaxTreeVisitor = {
    * interface ForStatement <: Statement {
    *   type: "ForStatement";
    *   init: VariableDeclaration | Expression | null;
    *   test: Expression | null;
    *   update: Expression | null;
    *   body: Statement;
    * }
    */
-  ForStatement: function(node, parent, callbacks) {
+  ForStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1260,17 +1261,17 @@ var SyntaxTreeVisitor = {
    * interface ForInStatement <: Statement {
    *   type: "ForInStatement";
    *   left: VariableDeclaration | Expression;
    *   right: Expression;
    *   body: Statement;
    *   each: boolean;
    * }
    */
-  ForInStatement: function(node, parent, callbacks) {
+  ForInStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1289,17 +1290,17 @@ var SyntaxTreeVisitor = {
    *
    * interface ForOfStatement <: Statement {
    *   type: "ForOfStatement";
    *   left: VariableDeclaration | Expression;
    *   right: Expression;
    *   body: Statement;
    * }
    */
-  ForOfStatement: function(node, parent, callbacks) {
+  ForOfStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1317,17 +1318,17 @@ var SyntaxTreeVisitor = {
    * A let statement.
    *
    * interface LetStatement <: Statement {
    *   type: "LetStatement";
    *   head: [ { id: Pattern, init: Expression | null } ];
    *   body: Statement;
    * }
    */
-  LetStatement: function(node, parent, callbacks) {
+  LetStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1347,17 +1348,17 @@ var SyntaxTreeVisitor = {
 
   /**
    * A debugger statement.
    *
    * interface DebuggerStatement <: Statement {
    *   type: "DebuggerStatement";
    * }
    */
-  DebuggerStatement: function(node, parent, callbacks) {
+  DebuggerStatement(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1370,17 +1371,17 @@ var SyntaxTreeVisitor = {
 
   /**
    * Any declaration node. Note that declarations are considered statements;
    * this is because declarations can appear in any statement context in the
    * language recognized by the SpiderMonkey parser.
    *
    * interface Declaration <: Statement { }
    */
-  Declaration: function(node, parent, callbacks) {
+  Declaration(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1400,17 +1401,17 @@ var SyntaxTreeVisitor = {
    *   params: [ Pattern ];
    *   defaults: [ Expression ];
    *   rest: Identifier | null;
    *   body: BlockStatement | Expression;
    *   generator: boolean;
    *   expression: boolean;
    * }
    */
-  FunctionDeclaration: function(node, parent, callbacks) {
+  FunctionDeclaration(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1438,17 +1439,17 @@ var SyntaxTreeVisitor = {
    * A variable declaration, via one of var, let, or const.
    *
    * interface VariableDeclaration <: Declaration {
    *   type: "VariableDeclaration";
    *   declarations: [ VariableDeclarator ];
    *   kind: "var" | "let" | "const";
    * }
    */
-  VariableDeclaration: function(node, parent, callbacks) {
+  VariableDeclaration(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1466,17 +1467,17 @@ var SyntaxTreeVisitor = {
    * A variable declarator.
    *
    * interface VariableDeclarator <: Node {
    *   type: "VariableDeclarator";
    *   id: Pattern;
    *   init: Expression | null;
    * }
    */
-  VariableDeclarator: function(node, parent, callbacks) {
+  VariableDeclarator(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1492,17 +1493,17 @@ var SyntaxTreeVisitor = {
   },
 
   /**
    * Any expression node. Since the left-hand side of an assignment may be any
    * expression in general, an expression can also be a pattern.
    *
    * interface Expression <: Node, Pattern { }
    */
-  Expression: function(node, parent, callbacks) {
+  Expression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1515,17 +1516,17 @@ var SyntaxTreeVisitor = {
 
   /**
    * A this expression.
    *
    * interface ThisExpression <: Expression {
    *   type: "ThisExpression";
    * }
    */
-  ThisExpression: function(node, parent, callbacks) {
+  ThisExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1539,17 +1540,17 @@ var SyntaxTreeVisitor = {
   /**
    * An array expression.
    *
    * interface ArrayExpression <: Expression {
    *   type: "ArrayExpression";
    *   elements: [ Expression | null ];
    * }
    */
-  ArrayExpression: function(node, parent, callbacks) {
+  ArrayExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1568,17 +1569,17 @@ var SyntaxTreeVisitor = {
   /**
    * A spread expression.
    *
    * interface SpreadExpression <: Expression {
    *   type: "SpreadExpression";
    *   expression: Expression;
    * }
    */
-  SpreadExpression: function(node, parent, callbacks) {
+  SpreadExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1593,22 +1594,22 @@ var SyntaxTreeVisitor = {
   /**
    * An object expression. A literal property in an object expression can have
    * either a string or number as its value. Ordinary property initializers
    * have a kind value "init"; getters and setters have the kind values "get"
    * and "set", respectively.
    *
    * interface ObjectExpression <: Expression {
    *   type: "ObjectExpression";
-   *   properties: [ { key: Literal | Identifier,
+   *   properties: [ { key: Literal | Identifier | ComputedName,
    *                   value: Expression,
    *                   kind: "init" | "get" | "set" } ];
    * }
    */
-  ObjectExpression: function(node, parent, callbacks) {
+  ObjectExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1619,30 +1620,55 @@ var SyntaxTreeVisitor = {
     }
     for (let { key, value } of node.properties) {
       this[key.type](key, node, callbacks);
       this[value.type](value, node, callbacks);
     }
   },
 
   /**
+   * A computed property name in object expression, like in { [a]: b }
+   *
+   * interface ComputedName <: Node {
+   *   type: "ComputedName";
+   *   name: Expression;
+   * }
+   */
+  ComputedName(node, parent, callbacks) {
+    node._parent = parent;
+
+    if (this.break) {
+      return;
+    }
+    if (callbacks.onNode) {
+      if (callbacks.onNode(node, parent) === false) {
+        return;
+      }
+    }
+    if (callbacks.onComputedName) {
+      callbacks.onComputedName(node);
+    }
+    this[node.name.type](node.name, node, callbacks);
+  },
+
+  /**
    * A function expression.
    *
    * interface FunctionExpression <: Function, Expression {
    *   type: "FunctionExpression";
    *   id: Identifier | null;
    *   params: [ Pattern ];
    *   defaults: [ Expression ];
    *   rest: Identifier | null;
    *   body: BlockStatement | Expression;
    *   generator: boolean;
    *   expression: boolean;
    * }
    */
-  FunctionExpression: function(node, parent, callbacks) {
+  FunctionExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1676,17 +1702,17 @@ var SyntaxTreeVisitor = {
    *   params: [ Pattern ];
    *   defaults: [ Expression ];
    *   rest: Identifier | null;
    *   body: BlockStatement | Expression;
    *   generator: boolean;
    *   expression: boolean;
    * }
    */
-  ArrowFunctionExpression: function(node, parent, callbacks) {
+  ArrowFunctionExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1712,17 +1738,17 @@ var SyntaxTreeVisitor = {
   /**
    * A sequence expression, i.e., a comma-separated sequence of expressions.
    *
    * interface SequenceExpression <: Expression {
    *   type: "SequenceExpression";
    *   expressions: [ Expression ];
    * }
    */
-  SequenceExpression: function(node, parent, callbacks) {
+  SequenceExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1741,17 +1767,17 @@ var SyntaxTreeVisitor = {
    *
    * interface UnaryExpression <: Expression {
    *   type: "UnaryExpression";
    *   operator: UnaryOperator;
    *   prefix: boolean;
    *   argument: Expression;
    * }
    */
-  UnaryExpression: function(node, parent, callbacks) {
+  UnaryExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1768,17 +1794,17 @@ var SyntaxTreeVisitor = {
    *
    * interface BinaryExpression <: Expression {
    *   type: "BinaryExpression";
    *   operator: BinaryOperator;
    *   left: Expression;
    *   right: Expression;
    * }
    */
-  BinaryExpression: function(node, parent, callbacks) {
+  BinaryExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1796,17 +1822,17 @@ var SyntaxTreeVisitor = {
    *
    * interface AssignmentExpression <: Expression {
    *   type: "AssignmentExpression";
    *   operator: AssignmentOperator;
    *   left: Expression;
    *   right: Expression;
    * }
    */
-  AssignmentExpression: function(node, parent, callbacks) {
+  AssignmentExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1824,17 +1850,17 @@ var SyntaxTreeVisitor = {
    *
    * interface UpdateExpression <: Expression {
    *   type: "UpdateExpression";
    *   operator: UpdateOperator;
    *   argument: Expression;
    *   prefix: boolean;
    * }
    */
-  UpdateExpression: function(node, parent, callbacks) {
+  UpdateExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1851,17 +1877,17 @@ var SyntaxTreeVisitor = {
    *
    * interface LogicalExpression <: Expression {
    *   type: "LogicalExpression";
    *   operator: LogicalOperator;
    *   left: Expression;
    *   right: Expression;
    * }
    */
-  LogicalExpression: function(node, parent, callbacks) {
+  LogicalExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1879,17 +1905,17 @@ var SyntaxTreeVisitor = {
    *
    * interface ConditionalExpression <: Expression {
    *   type: "ConditionalExpression";
    *   test: Expression;
    *   alternate: Expression;
    *   consequent: Expression;
    * }
    */
-  ConditionalExpression: function(node, parent, callbacks) {
+  ConditionalExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1907,17 +1933,17 @@ var SyntaxTreeVisitor = {
    * A new expression.
    *
    * interface NewExpression <: Expression {
    *   type: "NewExpression";
    *   callee: Expression;
    *   arguments: [ Expression | null ];
    * }
    */
-  NewExpression: function(node, parent, callbacks) {
+  NewExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1938,17 +1964,17 @@ var SyntaxTreeVisitor = {
    * A function or method call expression.
    *
    * interface CallExpression <: Expression {
    *   type: "CallExpression";
    *   callee: Expression;
    *   arguments: [ Expression | null ];
    * }
    */
-  CallExpression: function(node, parent, callbacks) {
+  CallExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -1976,17 +2002,17 @@ var SyntaxTreeVisitor = {
    *
    * interface MemberExpression <: Expression {
    *   type: "MemberExpression";
    *   object: Expression;
    *   property: Identifier | Expression;
    *   computed: boolean;
    * }
    */
-  MemberExpression: function(node, parent, callbacks) {
+  MemberExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2001,17 +2027,17 @@ var SyntaxTreeVisitor = {
 
   /**
    * A yield expression.
    *
    * interface YieldExpression <: Expression {
    *   argument: Expression | null;
    * }
    */
-  YieldExpression: function(node, parent, callbacks) {
+  YieldExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2031,17 +2057,17 @@ var SyntaxTreeVisitor = {
    * final if clause, if present.
    *
    * interface ComprehensionExpression <: Expression {
    *   body: Expression;
    *   blocks: [ ComprehensionBlock ];
    *   filter: Expression | null;
    * }
    */
-  ComprehensionExpression: function(node, parent, callbacks) {
+  ComprehensionExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2065,17 +2091,17 @@ var SyntaxTreeVisitor = {
    * filter expression corresponds to the final if clause, if present.
    *
    * interface GeneratorExpression <: Expression {
    *   body: Expression;
    *   blocks: [ ComprehensionBlock ];
    *   filter: Expression | null;
    * }
    */
-  GeneratorExpression: function(node, parent, callbacks) {
+  GeneratorExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2096,17 +2122,17 @@ var SyntaxTreeVisitor = {
   /**
    * A graph expression, aka "sharp literal," such as #1={ self: #1# }.
    *
    * interface GraphExpression <: Expression {
    *   index: uint32;
    *   expression: Literal;
    * }
    */
-  GraphExpression: function(node, parent, callbacks) {
+  GraphExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2120,17 +2146,17 @@ var SyntaxTreeVisitor = {
 
   /**
    * A graph index expression, aka "sharp variable," such as #1#.
    *
    * interface GraphIndexExpression <: Expression {
    *   index: uint32;
    * }
    */
-  GraphIndexExpression: function(node, parent, callbacks) {
+  GraphIndexExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2145,17 +2171,17 @@ var SyntaxTreeVisitor = {
    * A let expression.
    *
    * interface LetExpression <: Expression {
    *   type: "LetExpression";
    *   head: [ { id: Pattern, init: Expression | null } ];
    *   body: Expression;
    * }
    */
-  LetExpression: function(node, parent, callbacks) {
+  LetExpression(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2173,17 +2199,17 @@ var SyntaxTreeVisitor = {
     this[node.body.type](node.body, node, callbacks);
   },
 
   /**
    * Any pattern.
    *
    * interface Pattern <: Node { }
    */
-  Pattern: function(node, parent, callbacks) {
+  Pattern(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2198,17 +2224,17 @@ var SyntaxTreeVisitor = {
    * An object-destructuring pattern. A literal property in an object pattern
    * can have either a string or number as its value.
    *
    * interface ObjectPattern <: Pattern {
    *   type: "ObjectPattern";
    *   properties: [ { key: Literal | Identifier, value: Pattern } ];
    * }
    */
-  ObjectPattern: function(node, parent, callbacks) {
+  ObjectPattern(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2226,17 +2252,17 @@ var SyntaxTreeVisitor = {
   /**
    * An array-destructuring pattern.
    *
    * interface ArrayPattern <: Pattern {
    *   type: "ArrayPattern";
    *   elements: [ Pattern | null ];
    * }
    */
-  ArrayPattern: function(node, parent, callbacks) {
+  ArrayPattern(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2257,17 +2283,17 @@ var SyntaxTreeVisitor = {
    * the body of a switch statement.
    *
    * interface SwitchCase <: Node {
    *   type: "SwitchCase";
    *   test: Expression | null;
    *   consequent: [ Statement ];
    * }
    */
-  SwitchCase: function(node, parent, callbacks) {
+  SwitchCase(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2290,17 +2316,17 @@ var SyntaxTreeVisitor = {
    *
    * interface CatchClause <: Node {
    *   type: "CatchClause";
    *   param: Pattern;
    *   guard: Expression | null;
    *   body: BlockStatement;
    * }
    */
-  CatchClause: function(node, parent, callbacks) {
+  CatchClause(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2320,17 +2346,17 @@ var SyntaxTreeVisitor = {
    * A for or for each block in an array comprehension or generator expression.
    *
    * interface ComprehensionBlock <: Node {
    *   left: Pattern;
    *   right: Expression;
    *   each: boolean;
    * }
    */
-  ComprehensionBlock: function(node, parent, callbacks) {
+  ComprehensionBlock(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2347,17 +2373,17 @@ var SyntaxTreeVisitor = {
    * An identifier. Note that an identifier may be an expression or a
    * destructuring pattern.
    *
    * interface Identifier <: Node, Expression, Pattern {
    *   type: "Identifier";
    *   name: string;
    * }
    */
-  Identifier: function(node, parent, callbacks) {
+  Identifier(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2371,17 +2397,17 @@ var SyntaxTreeVisitor = {
   /**
    * A literal token. Note that a literal can be an expression.
    *
    * interface Literal <: Node, Expression {
    *   type: "Literal";
    *   value: string | boolean | null | number | RegExp;
    * }
    */
-  Literal: function(node, parent, callbacks) {
+  Literal(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
@@ -2395,17 +2421,17 @@ var SyntaxTreeVisitor = {
   /**
    * A template string literal.
    *
    * interface TemplateLiteral <: Node {
    *   type: "TemplateLiteral";
    *   elements: [ Expression ];
    * }
    */
-  TemplateLiteral: function(node, parent, callbacks) {
+  TemplateLiteral(node, parent, callbacks) {
     node._parent = parent;
 
     if (this.break) {
       return;
     }
     if (callbacks.onNode) {
       if (callbacks.onNode(node, parent) === false) {
         return;
--- a/devtools/shared/moz.build
+++ b/devtools/shared/moz.build
@@ -18,16 +18,17 @@ DIRS += [
     'layout',
     'locales',
     'performance',
     'pretty-fast',
     'qrcode',
     'security',
     'sourcemap',
     'shims',
+    'specs',
     'touch',
     'transport',
     'webconsole',
     'worker',
 ]
 
 BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 MOCHITEST_CHROME_MANIFESTS += ['tests/mochitest/chrome.ini']
@@ -35,17 +36,16 @@ XPCSHELL_TESTS_MANIFESTS += ['tests/unit
 
 JAR_MANIFESTS += ['jar.mn']
 
 DevToolsModules(
     'async-storage.js',
     'async-utils.js',
     'content-observer.js',
     'css-angle.js',
-    'css-color.js',
     'deprecated-sync-thenables.js',
     'DevToolsUtils.js',
     'event-emitter.js',
     'event-parsers.js',
     'indentation.js',
     'Loader.jsm',
     'Parser.jsm',
     'path.js',
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'storage.js',
+    'stylesheets.js'
+)
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/storage.js
@@ -0,0 +1,235 @@
+/* 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 protocol = require("devtools/server/protocol");
+const { Arg, RetVal, types } = protocol;
+
+let childSpecs = {};
+
+function createStorageSpec(options) {
+  // common methods for all storage types
+  let methods = {
+    getStoreObjects: {
+      request: {
+        host: Arg(0),
+        names: Arg(1, "nullable:array:string"),
+        options: Arg(2, "nullable:json")
+      },
+      response: RetVal(options.storeObjectType)
+    }
+  };
+
+  // extra methods specific for storage type
+  Object.assign(methods, options.methods);
+
+  childSpecs[options.typeName] = protocol.generateActorSpec({
+    typeName: options.typeName,
+    methods
+  });
+}
+
+// Cookies store object
+types.addDictType("cookieobject", {
+  name: "string",
+  value: "longstring",
+  path: "nullable:string",
+  host: "string",
+  isDomain: "boolean",
+  isSecure: "boolean",
+  isHttpOnly: "boolean",
+  creationTime: "number",
+  lastAccessed: "number",
+  expires: "number"
+});
+
+// Array of cookie store objects
+types.addDictType("cookiestoreobject", {
+  total: "number",
+  offset: "number",
+  data: "array:nullable:cookieobject"
+});
+
+// Common methods for edit/remove
+const editRemoveMethods = {
+  getEditableFields: {
+    request: {},
+    response: {
+      value: RetVal("json")
+    }
+  },
+  editItem: {
+    request: {
+      data: Arg(0, "json"),
+    },
+    response: {}
+  },
+  removeItem: {
+    request: {
+      host: Arg(0, "string"),
+      name: Arg(1, "string"),
+    },
+    response: {}
+  },
+};
+
+// Cookies actor spec
+createStorageSpec({
+  typeName: "cookies",
+  storeObjectType: "cookiestoreobject",
+  methods: Object.assign({},
+    editRemoveMethods,
+    {
+      removeAll: {
+        request: {
+          host: Arg(0, "string"),
+          domain: Arg(1, "nullable:string")
+        },
+        response: {}
+      }
+    }
+  )
+});
+
+// Local Storage / Session Storage store object
+types.addDictType("storageobject", {
+  name: "string",
+  value: "longstring"
+});
+
+// Common methods for local/session storage
+const storageMethods = Object.assign({},
+  editRemoveMethods,
+  {
+    removeAll: {
+      request: {
+        host: Arg(0, "string")
+      },
+      response: {}
+    }
+  }
+);
+
+// Array of Local Storage / Session Storage store objects
+types.addDictType("storagestoreobject", {
+  total: "number",
+  offset: "number",
+  data: "array:nullable:storageobject"
+});
+
+createStorageSpec({
+  typeName: "localStorage",
+  storeObjectType: "storagestoreobject",
+  methods: storageMethods
+});
+
+createStorageSpec({
+  typeName: "sessionStorage",
+  storeObjectType: "storagestoreobject",
+  methods: storageMethods
+});
+
+types.addDictType("cacheobject", {
+  "url": "string",
+  "status": "string"
+});
+
+// Array of Cache store objects
+types.addDictType("cachestoreobject", {
+  total: "number",
+  offset: "number",
+  data: "array:nullable:cacheobject"
+});
+
+// Cache storage spec
+createStorageSpec({
+  typeName: "Cache",
+  storeObjectType: "cachestoreobject"
+});
+
+// Indexed DB store object
+// This is a union on idb object, db metadata object and object store metadata
+// object
+types.addDictType("idbobject", {
+  name: "nullable:string",
+  db: "nullable:string",
+  objectStore: "nullable:string",
+  origin: "nullable:string",
+  version: "nullable:number",
+  objectStores: "nullable:number",
+  keyPath: "nullable:string",
+  autoIncrement: "nullable:boolean",
+  indexes: "nullable:string",
+  value: "nullable:longstring"
+});
+
+// Array of Indexed DB store objects
+types.addDictType("idbstoreobject", {
+  total: "number",
+  offset: "number",
+  data: "array:nullable:idbobject"
+});
+
+createStorageSpec({
+  typeName: "indexedDB",
+  storeObjectType: "idbstoreobject",
+  methods: {
+    removeDatabase: {
+      request: {
+        host: Arg(0, "string"),
+        name: Arg(1, "string"),
+      },
+      response: {}
+    }
+  }
+});
+
+// Update notification object
+types.addDictType("storeUpdateObject", {
+  changed: "nullable:json",
+  deleted: "nullable:json",
+  added: "nullable:json"
+});
+
+// Generate a type definition for an object with actors for all storage types.
+types.addDictType("storelist", Object.keys(childSpecs).reduce((obj, type) => {
+  obj[type] = type;
+  return obj;
+}, {}));
+
+exports.childSpecs = childSpecs;
+
+exports.storageSpec = protocol.generateActorSpec({
+  typeName: "storage",
+
+  /**
+   * List of event notifications that the server can send to the client.
+   *
+   *  - stores-update : When any store object in any storage type changes.
+   *  - stores-cleared : When all the store objects are removed.
+   *  - stores-reloaded : When all stores are reloaded. This generally mean that
+   *                      we should refetch everything again.
+   */
+  events: {
+    "stores-update": {
+      type: "storesUpdate",
+      data: Arg(0, "storeUpdateObject")
+    },
+    "stores-cleared": {
+      type: "storesCleared",
+      data: Arg(0, "json")
+    },
+    "stores-reloaded": {
+      type: "storesReloaded",
+      data: Arg(0, "json")
+    }
+  },
+
+  methods: {
+    listStores: {
+      request: {},
+      response: RetVal("storelist")
+    },
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/stylesheets.js
@@ -0,0 +1,20 @@
+/* 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 { RetVal, generateActorSpec } = require("devtools/server/protocol.js");
+
+const originalSourceSpec = generateActorSpec({
+  typeName: "originalsource",
+
+  methods: {
+    getText: {
+      response: {
+        text: RetVal("longstring")
+      }
+    }
+  }
+});
+
+exports.originalSourceSpec = originalSourceSpec;
--- a/devtools/shared/tests/unit/test_independent_loaders.js
+++ b/devtools/shared/tests/unit/test_independent_loaders.js
@@ -5,16 +5,16 @@
  * Ensure that each instance of the Dev Tools loader contains its own loader
  * instance, and also returns unique objects.  This ensures there is no sharing
  * in place between loaders.
  */
 function run_test() {
   let loader1 = new DevToolsLoader();
   let loader2 = new DevToolsLoader();
 
-  let color1 = loader1.require("devtools/shared/css-color");
-  let color2 = loader2.require("devtools/shared/css-color");
+  let indent1 = loader1.require("devtools/shared/indentation");
+  let indent2 = loader2.require("devtools/shared/indentation");
 
-  do_check_true(color1 !== color2);
+  do_check_true(indent1 !== indent2);
 
   do_check_true(loader1._provider !== loader2._provider);
   do_check_true(loader1._provider.loader !== loader2._provider.loader);
 }
--- a/devtools/shared/tests/unit/test_invisible_loader.js
+++ b/devtools/shared/tests/unit/test_invisible_loader.js
@@ -11,17 +11,17 @@ addDebuggerToGlobal(this);
 function run_test() {
   visible_loader();
   invisible_loader();
 }
 
 function visible_loader() {
   let loader = new DevToolsLoader();
   loader.invisibleToDebugger = false;
-  loader.require("devtools/shared/css-color");
+  loader.require("devtools/shared/indentation");
 
   let dbg = new Debugger();
   let sandbox = loader._provider.loader.sharedGlobalSandbox;
 
   try {
     dbg.addDebuggee(sandbox);
     do_check_true(true);
   } catch(e) {
@@ -32,17 +32,17 @@ function visible_loader() {
   // Which is required to support unhandled promises rejection in mochitests
   const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
   do_check_eq(loader.require("promise"), promise);
 }
 
 function invisible_loader() {
   let loader = new DevToolsLoader();
   loader.invisibleToDebugger = true;
-  loader.require("devtools/shared/css-color");
+  loader.require("devtools/shared/indentation");
 
   let dbg = new Debugger();
   let sandbox = loader._provider.loader.sharedGlobalSandbox;
 
   try {
     dbg.addDebuggee(sandbox);
     do_throw("debugger added invisible value");
   } catch(e) {
--- a/devtools/shared/tests/unit/test_require.js
+++ b/devtools/shared/tests/unit/test_require.js
@@ -4,17 +4,17 @@
 // Test require
 
 // Ensure that DevtoolsLoader.require doesn't spawn multiple
 // loader/modules when early cached
 function testBug1091706() {
   let loader = new DevToolsLoader();
   let require = loader.require;
 
-  let color1 = require("devtools/shared/css-color");
-  let color2 = require("devtools/shared/css-color");
+  let indent1 = require("devtools/shared/indentation");
+  let indent2 = require("devtools/shared/indentation");
 
-  do_check_true(color1 === color2);
+  do_check_true(indent1 === indent2);
 }
 
 function run_test() {
   testBug1091706();
 }
--- a/devtools/shared/tests/unit/xpcshell.ini
+++ b/devtools/shared/tests/unit/xpcshell.ini
@@ -17,14 +17,13 @@ support-files =
 [test_independent_loaders.js]
 [test_invisible_loader.js]
 [test_isSet.js]
 [test_safeErrorString.js]
 [test_defineLazyPrototypeGetter.js]
 [test_async-utils.js]
 [test_console_filtering.js]
 [test_cssAngle.js]
-[test_cssColor.js]
 [test_prettifyCSS.js]
 [test_require_lazy.js]
 [test_require.js]
 [test_stack.js]
 [test_executeSoon.js]
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -4,17 +4,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // Must #include ImageLogging.h before any IPDL-generated files or other files
 // that #include prlog.h
 #include "ImageLogging.h"
 
 #include "RasterImage.h"
 
-#include "base/histogram.h"
 #include "gfxPlatform.h"
 #include "nsComponentManagerUtils.h"
 #include "nsError.h"
 #include "Decoder.h"
 #include "prenv.h"
 #include "prsystem.h"
 #include "ImageContainer.h"
 #include "ImageRegion.h"
@@ -91,30 +90,39 @@ RasterImage::RasterImage(ImageURL* aURI 
   mSyncLoad(false),
   mDiscardable(false),
   mHasSourceData(false),
   mHasBeenDecoded(false),
   mPendingAnimation(false),
   mAnimationFinished(false),
   mWantFullDecode(false)
 {
-  Telemetry::GetHistogramById(Telemetry::IMAGE_DECODE_COUNT)->Add(0);
 }
 
 //******************************************************************************
 RasterImage::~RasterImage()
 {
   // Make sure our SourceBuffer is marked as complete. This will ensure that any
   // outstanding decoders terminate.
   if (!mSourceBuffer->IsComplete()) {
     mSourceBuffer->Complete(NS_ERROR_ABORT);
   }
 
   // Release all frames from the surface cache.
   SurfaceCache::RemoveImage(ImageKey(this));
+
+  // Record Telemetry.
+  Telemetry::Accumulate(Telemetry::IMAGE_DECODE_COUNT, mDecodeCount);
+
+  if (mDecodeCount > sMaxDecodeCount) {
+    sMaxDecodeCount = mDecodeCount;
+    // Clear out any previously collected data first.
+    Telemetry::ClearHistogram(Telemetry::IMAGE_MAX_DECODE_COUNT);
+    Telemetry::Accumulate(Telemetry::IMAGE_MAX_DECODE_COUNT, sMaxDecodeCount);
+  }
 }
 
 nsresult
 RasterImage::Init(const char* aMimeType,
                   uint32_t aFlags)
 {
   // We don't support re-initialization
   if (mInitialized) {
@@ -1322,34 +1330,17 @@ RasterImage::Decode(const IntSize& aSize
     SurfaceCache::InsertPlaceholder(ImageKey(this),
                                     RasterSurfaceKey(aSize,
                                                      decoder->GetSurfaceFlags(),
                                                      /* aFrameNum = */ 0));
   if (outcome != InsertOutcome::SUCCESS) {
     return NS_ERROR_FAILURE;
   }
 
-  // Report telemetry.
-  Telemetry::GetHistogramById(Telemetry::IMAGE_DECODE_COUNT)
-    ->Subtract(mDecodeCount);
   mDecodeCount++;
-  Telemetry::GetHistogramById(Telemetry::IMAGE_DECODE_COUNT)
-    ->Add(mDecodeCount);
-
-  if (mDecodeCount > sMaxDecodeCount) {
-    // Don't subtract out 0 from the histogram, because that causes its count
-    // to go negative, which is not kosher.
-    if (sMaxDecodeCount > 0) {
-      Telemetry::GetHistogramById(Telemetry::IMAGE_MAX_DECODE_COUNT)
-        ->Subtract(sMaxDecodeCount);
-    }
-    sMaxDecodeCount = mDecodeCount;
-    Telemetry::GetHistogramById(Telemetry::IMAGE_MAX_DECODE_COUNT)
-      ->Add(sMaxDecodeCount);
-  }
 
   // We're ready to decode; start the decoder.
   LaunchDecoder(decoder, this, aFlags, mHasSourceData);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 RasterImage::DecodeMetadata(uint32_t aFlags)
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionCtx.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionCtx.cpp
@@ -1,15 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "CSFLog.h"
 
-#include "base/histogram.h"
 #include "PeerConnectionImpl.h"
 #include "PeerConnectionCtx.h"
 #include "runnable_utils.h"
 #include "prcvar.h"
 
 #include "mozilla/Telemetry.h"
 #include "browser_logging/WebRtcLog.h"
 
@@ -351,19 +350,16 @@ PeerConnectionCtx::EverySecondTelemetryC
   }
 }
 #endif
 
 nsresult PeerConnectionCtx::Initialize() {
   initGMP();
 
 #if !defined(MOZILLA_EXTERNAL_LINKAGE)
-  mConnectionCounter = 0;
-  Telemetry::GetHistogramById(Telemetry::WEBRTC_CALL_COUNT)->Add(0);
-
   mTelemetryTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
   MOZ_ASSERT(mTelemetryTimer);
   nsresult rv = mTelemetryTimer->SetTarget(gMainThread);
   NS_ENSURE_SUCCESS(rv, rv);
   mTelemetryTimer->InitWithFuncCallback(EverySecondTelemetryCallback_m, this, 1000,
                                         nsITimer::TYPE_REPEATING_PRECISE_CAN_SKIP);
 
   if (XRE_IsContentProcess()) {
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionCtx.h
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionCtx.h
@@ -75,19 +75,16 @@ class PeerConnectionCtx {
   nsresult Cleanup();
 
   void initGMP();
 
   static void
   EverySecondTelemetryCallback_m(nsITimer* timer, void *);
 
 #if !defined(MOZILLA_EXTERNAL_LINKAGE)
-  // Telemetry Peer conection counter
-  int mConnectionCounter;
-
   nsCOMPtr<nsITimer> mTelemetryTimer;
 
 public:
   // TODO(jib): If we ever enable move semantics on std::map...
   //std::map<nsString,nsAutoPtr<mozilla::dom::RTCStatsReportInternal>> mLastReports;
   nsTArray<nsAutoPtr<mozilla::dom::RTCStatsReportInternal>> mLastReports;
 private:
 #endif
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
@@ -4,17 +4,16 @@
 
 #include <cstdlib>
 #include <cerrno>
 #include <deque>
 #include <set>
 #include <sstream>
 #include <vector>
 
-#include "base/histogram.h"
 #include "CSFLog.h"
 #include "timecard.h"
 
 #include "jsapi.h"
 #include "nspr.h"
 #include "nss.h"
 #include "pk11pub.h"
 
@@ -3895,23 +3894,18 @@ PeerConnectionImpl::startCallTelem() {
   if (!mStartTime.IsNull()) {
     return;
   }
 
   // Start time for calls
   mStartTime = TimeStamp::Now();
 
   // Increment session call counter
-  // If we want to track Loop calls independently here, we need two mConnectionCounters
-  int &cnt = PeerConnectionCtx::GetInstance()->mConnectionCounter;
-  if (cnt > 0) {
-    Telemetry::GetHistogramById(Telemetry::WEBRTC_CALL_COUNT)->Subtract(cnt);
-  }
-  cnt++;
-  Telemetry::GetHistogramById(Telemetry::WEBRTC_CALL_COUNT)->Add(cnt);
+  // If we want to track Loop calls independently here, we need two histograms.
+  Telemetry::Accumulate(Telemetry::WEBRTC_CALL_COUNT_2, 1);
 }
 #endif
 
 NS_IMETHODIMP
 PeerConnectionImpl::GetLocalStreams(nsTArray<RefPtr<DOMMediaStream > >& result)
 {
   PC_AUTO_ENTER_API_CALL_NO_CHECK();
 #if !defined(MOZILLA_EXTERNAL_LINKAGE)
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -759,17 +759,16 @@ sync_thirdparty_java_files = [
     'org/mozilla/apache/commons/codec/net/URLCodec.java',
     'org/mozilla/apache/commons/codec/net/Utils.java',
     'org/mozilla/apache/commons/codec/StringDecoder.java',
     'org/mozilla/apache/commons/codec/StringEncoder.java',
     'org/mozilla/apache/commons/codec/StringEncoderComparator.java',
 ]
 
 sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozilla/gecko/' + x for x in [
-    'background/BackgroundService.java',
     'background/common/EditorBranch.java',
     'background/common/GlobalConstants.java',
     'background/common/log/Logger.java',
     'background/common/log/writers/AndroidLevelCachingLogWriter.java',
     'background/common/log/writers/AndroidLogWriter.java',
     'background/common/log/writers/LevelFilteringLogWriter.java',
     'background/common/log/writers/LogWriter.java',
     'background/common/log/writers/PrintLogWriter.java',
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -25,16 +25,17 @@ import org.mozilla.gecko.db.BrowserContr
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.dlc.DownloadContentService;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
+import org.mozilla.gecko.feeds.ContentNotificationsDelegate;
 import org.mozilla.gecko.feeds.FeedService;
 import org.mozilla.gecko.feeds.action.CheckForUpdatesAction;
 import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.home.BrowserSearch;
@@ -72,18 +73,18 @@ import org.mozilla.gecko.search.SearchEn
 import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
 import org.mozilla.gecko.tabqueue.TabQueueHelper;
 import org.mozilla.gecko.tabqueue.TabQueuePrompt;
 import org.mozilla.gecko.tabs.TabHistoryController;
 import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
 import org.mozilla.gecko.tabs.TabHistoryFragment;
 import org.mozilla.gecko.tabs.TabHistoryPage;
 import org.mozilla.gecko.tabs.TabsPanel;
-import org.mozilla.gecko.telemetry.TelemetryConstants;
-import org.mozilla.gecko.telemetry.TelemetryUploadService;
+import org.mozilla.gecko.telemetry.TelemetryDispatcher;
+import org.mozilla.gecko.telemetry.UploadTelemetryCorePingCallback;
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.toolbar.ToolbarProgressView;
 import org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.Clipboard;
@@ -162,17 +163,16 @@ import android.animation.Animator;
 import android.animation.ObjectAnimator;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.lang.ref.WeakReference;
 import java.lang.reflect.Method;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLEncoder;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashSet;
@@ -306,22 +306,25 @@ public class BrowserApp extends GeckoApp
     private boolean mHideWebContentOnAnimationEnd;
 
     private final DynamicToolbar mDynamicToolbar = new DynamicToolbar();
 
     private final List<BrowserAppDelegate> delegates = Collections.unmodifiableList(Arrays.asList(
             (BrowserAppDelegate) new AddToHomeScreenPromotion(),
             (BrowserAppDelegate) new ScreenshotDelegate(),
             (BrowserAppDelegate) new BookmarkStateChangeDelegate(),
-            (BrowserAppDelegate) new ReaderViewBookmarkPromotion()
+            (BrowserAppDelegate) new ReaderViewBookmarkPromotion(),
+            (BrowserAppDelegate) new ContentNotificationsDelegate()
     ));
 
     @NonNull
     private SearchEngineManager searchEngineManager; // Contains reference to Context - DO NOT LEAK!
 
+    private TelemetryDispatcher mTelemetryDispatcher;
+
     @Override
     public View onCreateView(final String name, final Context context, final AttributeSet attrs) {
         final View view;
         if (BrowserToolbar.class.getName().equals(name)) {
             view = BrowserToolbar.create(context, attrs);
         } else if (TabsPanel.TabsLayout.class.getName().equals(name)) {
             view = TabsPanel.createTabsLayout(context, attrs);
         } else {
@@ -681,33 +684,36 @@ public class BrowserApp extends GeckoApp
             "Menu:Add",
             "Menu:Remove",
             "Sanitize:ClearHistory",
             "Sanitize:ClearSyncedTabs",
             "Settings:Show",
             "Telemetry:Gather",
             "Updater:Launch");
 
+        final GeckoProfile profile = getProfile();
+
         // We want to upload the telemetry core ping as soon after startup as possible. It relies on the
         // Distribution being initialized. If you move this initialization, ensure it plays well with telemetry.
         final Distribution distribution = Distribution.init(this);
-        distribution.addOnDistributionReadyCallback(new DistributionStoreCallback(this, getProfile().getName()));
+        distribution.addOnDistributionReadyCallback(new DistributionStoreCallback(this, profile.getName()));
 
         searchEngineManager = new SearchEngineManager(this, distribution);
+        mTelemetryDispatcher = new TelemetryDispatcher(profile.getDir().getAbsolutePath());
 
         // Init suggested sites engine in BrowserDB.
         final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution);
-        final BrowserDB db = getProfile().getDB();
+        final BrowserDB db = profile.getDB();
         db.setSuggestedSites(suggestedSites);
 
         JavaAddonManager.getInstance().init(appContext);
         mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
         mOrderedBroadcastHelper = new OrderedBroadcastHelper(appContext);
-        mReadingListHelper = new ReadingListHelper(appContext, getProfile());
-        mAccountsHelper = new AccountsHelper(appContext, getProfile());
+        mReadingListHelper = new ReadingListHelper(appContext, profile);
+        mAccountsHelper = new AccountsHelper(appContext, profile);
 
         final AdjustHelperInterface adjustHelper = AdjustConstants.getAdjustHelper();
         adjustHelper.onCreate(this, AdjustConstants.MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN);
 
         // Adjust stores enabled state so this is only necessary because users may have set
         // their data preferences before this feature was implemented and we need to respect
         // those before upload can occur in Adjust.onResume.
         final SharedPreferences prefs = GeckoSharedPrefs.forApp(this);
@@ -978,26 +984,16 @@ public class BrowserApp extends GeckoApp
         }
     }
 
     private void openMultipleTabsFromIntent(final Intent intent) {
         final List<String> urls = intent.getStringArrayListExtra("urls");
         if (urls != null) {
             openUrls(urls);
         }
-
-        // Launched from a "content notification"
-        if (intent.hasExtra(CheckForUpdatesAction.EXTRA_CONTENT_NOTIFICATION)) {
-            Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(this));
-
-            Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "content_update");
-            Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "content_update");
-
-            Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(this));
-        }
     }
 
     @Override
     public void onResume() {
         super.onResume();
 
         // Needed for Adjust to get accurate session measurements
         AdjustConstants.getAdjustHelper().onResume();
@@ -1076,17 +1072,17 @@ public class BrowserApp extends GeckoApp
 
         // We don't upload in onCreate because that's only called when the Activity needs to be instantiated
         // and it's possible the system will never free the Activity from memory.
         //
         // We don't upload in onResume/onPause because that will be called each time the Activity is obscured,
         // including by our own Activities/dialogs, and there is no reason to upload each time we're unobscured.
         //
         // So we're left with onStart/onStop.
-        searchEngineManager.getEngine(new UploadTelemetryCallback(BrowserApp.this));
+        searchEngineManager.getEngine(new UploadTelemetryCorePingCallback(BrowserApp.this));
 
         for (final BrowserAppDelegate delegate : delegates) {
             delegate.onStart(this);
         }
     }
 
     @Override
     public void onStop() {
@@ -3705,16 +3701,20 @@ public class BrowserApp extends GeckoApp
             });
         }
 
         // Custom intent action for opening multiple URLs at once
         if (isViewMultipleAction) {
             openMultipleTabsFromIntent(intent);
         }
 
+        for (final BrowserAppDelegate delegate : delegates) {
+            delegate.onNewIntent(this, intent);
+        }
+
         if (!mInitialized || !Intent.ACTION_MAIN.equals(action)) {
             return;
         }
 
         // Check to see how many times the app has been launched.
         final String keyName = getPackageName() + ".feedback_launch_count";
         final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
 
@@ -3732,17 +3732,17 @@ public class BrowserApp extends GeckoApp
                     GeckoAppShell.notifyObservers("Feedback:Show", null);
                 }
             }
         } finally {
             StrictMode.setThreadPolicy(savedPolicy);
         }
     }
 
-    private void openUrls(List<String> urls) {
+    public void openUrls(List<String> urls) {
         try {
             JSONArray array = new JSONArray();
             for (String url : urls) {
                 array.put(url);
             }
 
             JSONObject object = new JSONObject();
             object.put("urls", array);
@@ -3861,16 +3861,20 @@ public class BrowserApp extends GeckoApp
     @Override
     public void onEditSuggestion(String suggestion) {
         mBrowserToolbar.onEditSuggestion(suggestion);
     }
 
     @Override
     public int getLayout() { return R.layout.gecko_app; }
 
+    public TelemetryDispatcher getTelemetryDispatcher() {
+        return mTelemetryDispatcher;
+    }
+
     // For use from tests only.
     @RobocopTarget
     public ReadingListHelper getReadingListHelper() {
         return mReadingListHelper;
     }
 
     /**
      * Launch UI that lets the user update Firefox.
@@ -3938,72 +3942,16 @@ public class BrowserApp extends GeckoApp
 
         mActionBarFlipper.showPrevious();
 
         // Only slide the urlbar out if it was hidden when the action mode started
         // Don't animate hiding it so that there's no flash as we switch back to url mode
         mDynamicToolbar.setTemporarilyVisible(false, VisibilityTransition.IMMEDIATE);
     }
 
-    @WorkerThread // synchronous SharedPrefs write.
-    private static void uploadTelemetry(final Context context, final GeckoProfile profile,
-            final org.mozilla.gecko.search.SearchEngine defaultEngine) {
-        if (!TelemetryUploadService.isUploadEnabledByProfileConfig(context, profile)) {
-            return;
-        }
-
-        final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(context, profile.getName());
-        final int seq = sharedPrefs.getInt(TelemetryConstants.PREF_SEQ_COUNT, 1);
-
-        // We store synchronously before sending the Intent to ensure this sequence number will not be re-used.
-        sharedPrefs.edit().putInt(TelemetryConstants.PREF_SEQ_COUNT, seq + 1).commit();
-
-        final Intent i = new Intent(TelemetryConstants.ACTION_UPLOAD_CORE);
-        i.setClass(context, TelemetryUploadService.class);
-        i.putExtra(TelemetryConstants.EXTRA_DEFAULT_SEARCH_ENGINE, (defaultEngine == null) ? null : defaultEngine.getIdentifier());
-        i.putExtra(TelemetryConstants.EXTRA_DOC_ID, UUID.randomUUID().toString());
-        i.putExtra(TelemetryConstants.EXTRA_PROFILE_NAME, profile.getName());
-        i.putExtra(TelemetryConstants.EXTRA_PROFILE_PATH, profile.getDir().getAbsolutePath());
-        i.putExtra(TelemetryConstants.EXTRA_SEQ, seq);
-        context.startService(i);
-    }
-
-    private static class UploadTelemetryCallback implements SearchEngineManager.SearchEngineCallback {
-        private final WeakReference<BrowserApp> activityWeakReference;
-
-        public UploadTelemetryCallback(final BrowserApp activity) {
-            this.activityWeakReference = new WeakReference<>(activity);
-        }
-
-        // May be called from any thread.
-        @Override
-        public void execute(final org.mozilla.gecko.search.SearchEngine engine) {
-            // Don't waste resources queueing to the background thread if we don't have a reference.
-            if (this.activityWeakReference.get() == null) {
-                return;
-            }
-
-            // The containing method can be called from onStart: queue this work so that
-            // the first launch of the activity doesn't trigger profile init too early.
-            //
-            // Additionally, uploadTelemetry must be called from a worker thread.
-            ThreadUtils.postToBackgroundThread(new Runnable() {
-                @WorkerThread
-                @Override
-                public void run() {
-                    final BrowserApp activity = activityWeakReference.get();
-                    if (activity == null) {
-                        return;
-                    }
-                    uploadTelemetry(activity, activity.getProfile(), engine);
-                }
-            });
-        }
-    }
-
     public static interface TabStripInterface {
         public void refresh();
         void setOnTabChangedListener(OnTabAddedOrRemovedListener listener);
         interface OnTabAddedOrRemovedListener {
             void onTabChanged();
         }
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserAppDelegate.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserAppDelegate.java
@@ -46,16 +46,21 @@ public abstract class BrowserAppDelegate
     public void onStop(BrowserApp browserApp) {}
 
     /**
      * The final call before the BrowserApp activity is destroyed.
      */
     public void onDestroy(BrowserApp browserApp) {}
 
     /**
+     * Called when BrowserApp already exists and a new Intent to re-launch it was fired.
+     */
+    public void onNewIntent(BrowserApp browserApp, Intent intent) {}
+
+    /**
      * Called when the tabs tray is opened.
      */
     public void onTabsTrayShown(BrowserApp browserApp, TabsPanel tabsPanel) {}
 
     /**
      * Called when the tabs tray is closed.
      */
     public void onTabsTrayHidden(BrowserApp browserApp, TabsPanel tabsPanel) {}
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
@@ -3,16 +3,18 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.db;
 
 import org.mozilla.gecko.AppConstants;
 
 import android.net.Uri;
+import android.support.annotation.NonNull;
+
 import org.mozilla.gecko.annotation.RobocopTarget;
 
 @RobocopTarget
 public class BrowserContract {
     public static final String AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.browser";
     public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
 
     public static final String PASSWORDS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.passwords";
@@ -52,29 +54,85 @@ public class BrowserContract {
     public static final String PARAM_DATASET_ID = "dataset_id";
     public static final String PARAM_GROUP_BY = "group_by";
 
     static public enum ExpirePriority {
         NORMAL,
         AGGRESSIVE
     }
 
-    static public String getFrecencySortOrder(boolean includesBookmarks, boolean asc) {
-        final String age = "(" + Combined.DATE_LAST_VISITED + " - " + System.currentTimeMillis() + ") / 86400000";
-
-        StringBuilder order = new StringBuilder(Combined.VISITS + " * MAX(1, 100 * 225 / (" + age + "*" + age + " + 225)) ");
+    /**
+     * Produces a SQL expression used for sorting results of the "combined" view by frecency.
+     * Combines remote and local frecency calculations, weighting local visits much heavier.
+     *
+     * @param includesBookmarks When URL is bookmarked, should we give it bonus frecency points?
+     * @param ascending Indicates if sorting order ascending
+     * @return Combined frecency sorting expression
+     */
+    static public String getCombinedFrecencySortOrder(boolean includesBookmarks, boolean ascending) {
+        final long now = System.currentTimeMillis();
+        StringBuilder order = new StringBuilder(getRemoteFrecencySQL(now) + " + " + getLocalFrecencySQL(now));
 
         if (includesBookmarks) {
             order.insert(0, "(CASE WHEN " + Combined.BOOKMARK_ID + " > -1 THEN 100 ELSE 0 END) + ");
         }
 
-        order.append(asc ? " ASC" : " DESC");
+        order.append(ascending ? " ASC" : " DESC");
         return order.toString();
     }
 
+    /**
+     * See Bug 1265525 for details (explanation + graphs) on how Remote frecency compares to Local frecency for different
+     * combinations of visits count and age.
+     *
+     * @param now Base time in milliseconds for age calculation
+     * @return remote frecency SQL calculation
+     */
+    static public String getRemoteFrecencySQL(final long now) {
+        return getFrecencyCalculation(now, 1, 110, Combined.REMOTE_VISITS_COUNT, Combined.REMOTE_DATE_LAST_VISITED);
+    }
+
+    /**
+     * Local frecency SQL calculation. Note higher scale factor and squared visit count which achieve
+     * visits generated locally being much preferred over remote visits.
+     * See Bug 1265525 for details (explanation + comparison graphs).
+     *
+     * @param now Base time in milliseconds for age calculation
+     * @return local frecency SQL calculation
+     */
+    static public String getLocalFrecencySQL(final long now) {
+        String visitCountExpr = "(" + Combined.LOCAL_VISITS_COUNT + " + 2)";
+        visitCountExpr = visitCountExpr + " * " + visitCountExpr;
+
+        return getFrecencyCalculation(now, 2, 225, visitCountExpr, Combined.LOCAL_DATE_LAST_VISITED);
+    }
+
+    /**
+     * Our version of frecency is computed by scaling the number of visits by a multiplier
+     * that approximates Gaussian decay, based on how long ago the entry was last visited.
+     * Since we're limited by the math we can do with sqlite, we're calculating this
+     * approximation using the Cauchy distribution: multiplier = scale_const / (age^2 + scale_const).
+     * For example, with 15 as our scale parameter, we get a scale constant 15^2 = 225. Then:
+     * frecencyScore = numVisits * max(1, 100 * 225 / (age*age + 225)). (See bug 704977)
+     *
+     * @param now Base time in milliseconds for age calculation
+     * @param minFrecency Minimum allowed frecency value
+     * @param multiplier Scale constant
+     * @param visitCountExpr Expression which will produce a visit count
+     * @param lastVisitExpr Expression which will produce "last-visited" timestamp
+     * @return Frecency SQL calculation
+     */
+    static public String getFrecencyCalculation(final long now, final int minFrecency, final int multiplier, @NonNull  final String visitCountExpr, @NonNull final String lastVisitExpr) {
+        final long nowInMicroseconds = now * 1000;
+        final long microsecondsPerDay = 86400000000L;
+        final String ageExpr = "(" + nowInMicroseconds + " - " + lastVisitExpr + ") / " + microsecondsPerDay;
+
+        return visitCountExpr + " * MAX(" + minFrecency + ", 100 * " + multiplier + " / (" + ageExpr + " * " + ageExpr + " + " + multiplier + "))";
+    }
+
     @RobocopTarget
     public interface CommonColumns {
         public static final String _ID = "_id";
     }
 
     @RobocopTarget
     public interface DateSyncColumns {
         public static final String DATE_CREATED = "created";
@@ -240,16 +298,22 @@ public class BrowserContract {
         public static final String VIEW_NAME = "combined";
 
         public static final String VIEW_WITH_FAVICONS = "combined_with_favicons";
 
         public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "combined");
 
         public static final String BOOKMARK_ID = "bookmark_id";
         public static final String HISTORY_ID = "history_id";
+
+        public static final String REMOTE_VISITS_COUNT = "remoteVisitCount";
+        public static final String REMOTE_DATE_LAST_VISITED = "remoteDateLastVisited";
+
+        public static final String LOCAL_VISITS_COUNT = "localVisitCount";
+        public static final String LOCAL_DATE_LAST_VISITED = "localDateLastVisited";
     }
 
     public static final class Schema {
         private Schema() {}
         public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "schema");
 
         public static final String VERSION = "version";
     }
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
@@ -53,17 +53,17 @@ import android.util.Log;
 
 
 // public for robocop testing
 public final class BrowserDatabaseHelper extends SQLiteOpenHelper {
     private static final String LOGTAG = "GeckoBrowserDBHelper";
 
     // Replace the Bug number below with your Bug that is conducting a DB upgrade, as to force a merge conflict with any
     // other patches that require a DB upgrade.
-    public static final int DATABASE_VERSION = 32; // Bug 1046709
+    public static final int DATABASE_VERSION = 33; // Bug 1265525
     public static final String DATABASE_NAME = "browser.db";
 
     final protected Context mContext;
 
     static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME;
     static final String TABLE_HISTORY = History.TABLE_NAME;
     static final String TABLE_VISITS = Visits.TABLE_NAME;
     static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
@@ -381,16 +381,143 @@ public final class BrowserDatabaseHelper
                 " SELECT " + qualifyColumn(VIEW_COMBINED, "*") + ", " +
                     qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Combined.FAVICON_URL + ", " +
                     qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Combined.FAVICON +
                 " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS +
                     " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID));
 
     }
 
+    private void createCombinedViewOn33(final SQLiteDatabase db) {
+        /*
+        Builds on top of v19 combined view, and adds the following aggregates:
+        - Combined.LOCAL_DATE_LAST_VISITED - last date visited for all local visits
+        - Combined.REMOTE_DATE_LAST_VISITED - last date visited for all remote visits
+        - Combined.LOCAL_VISITS_COUNT - total number of local visits
+        - Combined.REMOTE_VISITS_COUNT - total number of remote visits
+
+        Any code written prior to v33 referencing columns by index directly remains intact
+        (yet must die a fiery death), as new columns were added to the end of the list.
+
+        The rows in the ensuing view are, in order:
+            Combined.BOOKMARK_ID
+            Combined.HISTORY_ID
+            Combined._ID (always 0)
+            Combined.URL
+            Combined.TITLE
+            Combined.VISITS
+            Combined.DISPLAY
+            Combined.DATE_LAST_VISITED
+            Combined.FAVICON_ID
+            Combined.LOCAL_DATE_LAST_VISITED
+            Combined.REMOTE_DATE_LAST_VISITED
+            Combined.LOCAL_VISITS_COUNT
+            Combined.REMOTE_VISITS_COUNT
+         */
+        db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" +
+
+                // Bookmarks without history.
+                " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + "," +
+                "-1 AS " + Combined.HISTORY_ID + "," +
+                "0 AS " + Combined._ID + "," +
+                qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
+                qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
+                "-1 AS " + Combined.VISITS + ", " +
+                "-1 AS " + Combined.DATE_LAST_VISITED + "," +
+                qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID + "," +
+                "0 AS " + Combined.LOCAL_DATE_LAST_VISITED + ", " +
+                "0 AS " + Combined.REMOTE_DATE_LAST_VISITED + ", " +
+                "0 AS " + Combined.LOCAL_VISITS_COUNT + ", " +
+                "0 AS " + Combined.REMOTE_VISITS_COUNT +
+                " FROM " + TABLE_BOOKMARKS +
+                " WHERE " +
+                qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
+                // Ignore pinned bookmarks.
+                qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT)  + " <> " + Bookmarks.FIXED_PINNED_LIST_ID + " AND " +
+                qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED)  + " = 0 AND " +
+                qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
+                " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
+                " UNION ALL" +
+
+                // History with and without bookmark.
+                " SELECT " +
+                "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) +
+
+                // Give pinned bookmarks a NULL ID so that they're not treated as bookmarks. We can't
+                // completely ignore them here because they're joined with history entries we care about.
+                " WHEN 0 THEN " +
+                "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) +
+                " WHEN " + Bookmarks.FIXED_PINNED_LIST_ID + " THEN " +
+                "NULL " +
+                "ELSE " +
+                qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) +
+                " END " +
+                "ELSE " +
+                "NULL " +
+                "END AS " + Combined.BOOKMARK_ID + "," +
+                qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + "," +
+                "0 AS " + Combined._ID + "," +
+                qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + "," +
+
+                // Prioritize bookmark titles over history titles, since the user may have
+                // customized the title for a bookmark.
+                "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
+                qualifyColumn(TABLE_HISTORY, History.TITLE) +
+                ") AS " + Combined.TITLE + "," +
+                qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + "," +
+                qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED + "," +
+                qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " AS " + Combined.FAVICON_ID + "," +
+
+                // Figure out "last visited" days using MAX values for visit timestamps.
+                // We use CASE statements here to separate local from remote visits.
+                "COALESCE(MAX(CASE " + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + " " +
+                    "WHEN 1 THEN " + qualifyColumn(TABLE_VISITS, Visits.DATE_VISITED) + " " +
+                    "ELSE 0 END" +
+                "), 0) AS " + Combined.LOCAL_DATE_LAST_VISITED + ", " +
+
+                "COALESCE(MAX(CASE " + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + " " +
+                    "WHEN 0 THEN " + qualifyColumn(TABLE_VISITS, Visits.DATE_VISITED) + " " +
+                    "ELSE 0 END" +
+                "), 0) AS " + Combined.REMOTE_DATE_LAST_VISITED + ", " +
+
+                // Sum up visit counts for local and remote visit types. Again, use CASE to separate the two.
+                "COALESCE(SUM(" + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + "), 0) AS " + Combined.LOCAL_VISITS_COUNT + ", " +
+                "COALESCE(SUM(CASE " + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + " WHEN 0 THEN 1 ELSE 0 END), 0) AS " + Combined.REMOTE_VISITS_COUNT +
+
+                // We need to JOIN on Visits in order to compute visit counts
+                " FROM " + TABLE_HISTORY + " " +
+                "LEFT OUTER JOIN " + TABLE_VISITS +
+                " ON " + qualifyColumn(TABLE_HISTORY, History.GUID) + " = " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " " +
+
+                // We really shouldn't be selecting deleted bookmarks, but oh well.
+                "LEFT OUTER JOIN " + TABLE_BOOKMARKS +
+                " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
+                " WHERE " +
+                qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND " +
+                "(" +
+                // The left outer join didn't match...
+                qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
+
+                // ... or it's a bookmark. This is less efficient than filtering prior
+                // to the join if you have lots of folders.
+                qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK +
+
+                ") GROUP BY " + qualifyColumn(TABLE_HISTORY, History.GUID)
+        );
+
+        debug("Creating " + VIEW_COMBINED_WITH_FAVICONS + " view");
+
+        db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED_WITH_FAVICONS + " AS" +
+                " SELECT " + qualifyColumn(VIEW_COMBINED, "*") + ", " +
+                qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Combined.FAVICON_URL + ", " +
+                qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Combined.FAVICON +
+                " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS +
+                " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID));
+    }
+
     private void createLoginsTable(SQLiteDatabase db, final String tableName) {
         debug("Creating logins.db: " + db.getPath());
         debug("Creating " + tableName + " table");
 
         // Table for each login.
         db.execSQL("CREATE TABLE " + tableName + "(" +
                 BrowserContract.Logins._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                 BrowserContract.Logins.HOSTNAME + " TEXT NOT NULL," +
@@ -457,17 +584,16 @@ public final class BrowserDatabaseHelper
         createClientsTable(db);
         createLocalClient(db);
         createTabsTable(db, TABLE_TABS);
         createTabsTableIndices(db, TABLE_TABS);
 
 
         createBookmarksWithFaviconsView(db);
         createHistoryWithFaviconsView(db);
-        createCombinedViewOn19(db);
 
         createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
             R.string.bookmarks_folder_places, 0);
 
         createOrUpdateAllSpecialFolders(db);
         createSearchHistoryTable(db);
         createUrlAnnotationsTable(db);
         createNumbersTable(db);
@@ -475,16 +601,17 @@ public final class BrowserDatabaseHelper
         createDeletedLoginsTable(db, TABLE_DELETED_LOGINS);
         createDisabledHostsTable(db, TABLE_DISABLED_HOSTS);
         createLoginsTable(db, TABLE_LOGINS);
         createLoginsTableIndices(db, TABLE_LOGINS);
 
         createBookmarksWithAnnotationsView(db);
 
         createVisitsTable(db);
+        createCombinedViewOn33(db);
     }
 
     /**
      * Copies the tabs and clients tables out of the given tabs.db file and into the destinationDB.
      *
      * @param tabsDBFile Path to existing tabs.db.
      * @param destinationDB The destination database.
      */
@@ -1625,16 +1752,27 @@ public final class BrowserDatabaseHelper
             } while (cursor.moveToNext());
         } catch (Exception e) {
             Log.e(LOGTAG, "Error while synthesizing visits for history record", e);
         } finally {
             cursor.close();
         }
     }
 
+    private void upgradeDatabaseFrom32to33(final SQLiteDatabase db) {
+        createV33CombinedView(db);
+    }
+
+    private void createV33CombinedView(final SQLiteDatabase db) {
+        db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
+        db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS);
+
+        createCombinedViewOn33(db);
+    }
+
     private void createV19CombinedView(SQLiteDatabase db) {
         db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
         db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS);
 
         createCombinedViewOn19(db);
     }
 
     @Override
@@ -1730,16 +1868,20 @@ public final class BrowserDatabaseHelper
 
                 case 31:
                     upgradeDatabaseFrom30to31(db);
                     break;
 
                 case 32:
                     upgradeDatabaseFrom31to32(db);
                     break;
+
+                case 33:
+                    upgradeDatabaseFrom32to33(db);
+                    break;
             }
         }
 
         for (Table table : BrowserProvider.sTables) {
             table.onUpgrade(db, oldVersion, newVersion);
         }
 
         // Delete the obsolete favicon database after all other upgrades complete.
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
@@ -243,16 +243,20 @@ public class BrowserProvider extends Sha
         map.put(Combined.HISTORY_ID, Combined.HISTORY_ID);
         map.put(Combined.URL, Combined.URL);
         map.put(Combined.TITLE, Combined.TITLE);
         map.put(Combined.VISITS, Combined.VISITS);
         map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED);
         map.put(Combined.FAVICON, Combined.FAVICON);
         map.put(Combined.FAVICON_ID, Combined.FAVICON_ID);
         map.put(Combined.FAVICON_URL, Combined.FAVICON_URL);
+        map.put(Combined.LOCAL_DATE_LAST_VISITED, Combined.LOCAL_DATE_LAST_VISITED);
+        map.put(Combined.REMOTE_DATE_LAST_VISITED, Combined.REMOTE_DATE_LAST_VISITED);
+        map.put(Combined.LOCAL_VISITS_COUNT, Combined.LOCAL_VISITS_COUNT);
+        map.put(Combined.REMOTE_VISITS_COUNT, Combined.REMOTE_VISITS_COUNT);
         COMBINED_PROJECTION_MAP = Collections.unmodifiableMap(map);
 
         // Schema
         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "schema", SCHEMA);
 
         map = new HashMap<String, String>();
         map.put(Schema.VERSION, Schema.VERSION);
         SCHEMA_PROJECTION_MAP = Collections.unmodifiableMap(map);
@@ -305,59 +309,58 @@ public class BrowserProvider extends Sha
     }
 
     /**
      * Remove enough history items to bring the database count below <code>retain</code>,
      * removing no items with a modified time after <code>keepAfter</code>.
      *
      * Provide <code>keepAfter</code> less than or equal to zero to skip that check.
      *
-     * Items will be removed according to an approximate frecency calculation.
+     * Items will be removed according to last visited date.
      */
     private void expireHistory(final SQLiteDatabase db, final int retain, final long keepAfter) {
         Log.d(LOGTAG, "Expiring history.");
         final long rows = DatabaseUtils.queryNumEntries(db, TABLE_HISTORY);
 
         if (retain >= rows) {
             debug("Not expiring history: only have " + rows + " rows.");
             return;
         }
 
-        final String sortOrder = BrowserContract.getFrecencySortOrder(false, true);
         final long toRemove = rows - retain;
         debug("Expiring at most " + toRemove + " rows earlier than " + keepAfter + ".");
 
         final String sql;
         if (keepAfter > 0) {
             sql = "DELETE FROM " + TABLE_HISTORY + " " +
                   "WHERE MAX(" + History.DATE_LAST_VISITED + ", " + History.DATE_MODIFIED + ") < " + keepAfter + " " +
                   " AND " + History._ID + " IN ( SELECT " +
                     History._ID + " FROM " + TABLE_HISTORY + " " +
-                    "ORDER BY " + sortOrder + " LIMIT " + toRemove +
+                    "ORDER BY " + History.DATE_LAST_VISITED + " ASC LIMIT " + toRemove +
                   ")";
         } else {
             sql = "DELETE FROM " + TABLE_HISTORY + " WHERE " + History._ID + " " +
                   "IN ( SELECT " + History._ID + " FROM " + TABLE_HISTORY + " " +
-                  "ORDER BY " + sortOrder + " LIMIT " + toRemove + ")";
+                  "ORDER BY " + History.DATE_LAST_VISITED + " ASC LIMIT " + toRemove + ")";
         }
         trace("Deleting using query: " + sql);
 
         beginWrite(db);
         db.execSQL(sql);
     }
 
     /**
      * Remove any thumbnails that for sites that aren't likely to be ever shown.
      * Items will be removed according to a frecency calculation and only if they are not pinned
      *
      * Call this method within a transaction.
      */
     private void expireThumbnails(final SQLiteDatabase db) {
         Log.d(LOGTAG, "Expiring thumbnails.");
-        final String sortOrder = BrowserContract.getFrecencySortOrder(true, false);
+        final String sortOrder = BrowserContract.getCombinedFrecencySortOrder(true, false);
         final String sql = "DELETE FROM " + TABLE_THUMBNAILS +
                            " WHERE " + Thumbnails.URL + " NOT IN ( " +
                              " SELECT " + Combined.URL +
                              " FROM " + Combined.VIEW_NAME +
                              " ORDER BY " + sortOrder +
                              " LIMIT " + DEFAULT_EXPIRY_THUMBNAIL_COUNT +
                            ") AND " + Thumbnails.URL + " NOT IN ( " +
                              " SELECT " + Bookmarks.URL +
@@ -893,17 +896,17 @@ public class BrowserProvider extends Sha
                        Combined.BOOKMARK_ID + ", " +
                        Combined.HISTORY_ID + ", " +
                        Bookmarks.URL + ", " +
                        Bookmarks.TITLE + ", " +
                        Combined.HISTORY_ID + ", " +
                        TopSites.TYPE_TOP + " AS " + TopSites.TYPE +
                        " FROM " + Combined.VIEW_NAME +
                        " WHERE " + ignoreForTopSitesWhereClause +
-                       " ORDER BY " + BrowserContract.getFrecencySortOrder(true, false) +
+                       " ORDER BY " + BrowserContract.getCombinedFrecencySortOrder(true, false) +
                        " LIMIT " + totalLimit,
 
                        ignoreForTopSitesArgs);
 
             if (hasProcessedAnySuggestedSites) {
                 db.execSQL("INSERT INTO " + TABLE_TOPSITES +
                            // We need to LIMIT _after_ selecting the relevant suggested sites, which requires us to
                            // use an additional internal subquery, since we cannot LIMIT a subquery that is part of UNION ALL.
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
@@ -564,24 +564,20 @@ public class LocalBrowserDB implements B
             }
         }
 
         if (urlFilter != null) {
             selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " NOT LIKE ?)");
             selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { urlFilter.toString() });
         }
 
-        // Our version of frecency is computed by scaling the number of visits by a multiplier
-        // that approximates Gaussian decay, based on how long ago the entry was last visited.
-        // Since we're limited by the math we can do with sqlite, we're calculating this
-        // approximation using the Cauchy distribution: multiplier = 15^2 / (age^2 + 15^2).
-        // Using 15 as our scale parameter, we get a constant 15^2 = 225. Following this math,
-        // frecencyScore = numVisits * max(1, 100 * 225 / (age*age + 225)). (See bug 704977)
-        // We also give bookmarks an extra bonus boost by adding 100 points to their frecency score.
-        final String sortOrder = BrowserContract.getFrecencySortOrder(true, false);
+        // Order by combined remote+local frecency score.
+        // Local visits are preferred, so they will by far outweigh remote visits.
+        // Bookmarked history items get extra frecency points.
+        final String sortOrder = BrowserContract.getCombinedFrecencySortOrder(true, false);
 
         return cr.query(combinedUriWithLimit(limit),
                         projection,
                         selection,
                         selectionArgs,
                         sortOrder);
     }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/ContentNotificationsDelegate.java
@@ -0,0 +1,80 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.feeds;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationManagerCompat;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.BrowserAppDelegate;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+
+import java.util.List;
+
+/**
+ * BrowserAppDelegate implementation that takes care of handling intents from content notifications.
+ */
+public class ContentNotificationsDelegate extends BrowserAppDelegate {
+    // The application is opened from a content notification
+    public static final String ACTION_CONTENT_NOTIFICATION = AppConstants.ANDROID_PACKAGE_NAME + ".action.CONTENT_NOTIFICATION";
+
+    public static final String EXTRA_READ_BUTTON = "read_button";
+    public static final String EXTRA_URLS = "urls";
+
+    private static final String TELEMETRY_EXTRA_CONTENT_UPDATE = "content_update";
+    private static final String TELEMETRY_EXTRA_READ_NOW_BUTTON = TELEMETRY_EXTRA_CONTENT_UPDATE + "_read_now";
+
+    @Override
+    public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
+        final Intent intent = browserApp.getIntent();
+
+        if (savedInstanceState != null) {
+            // This activity is getting restored: We do not want to handle the URLs in the Intent again. The browser
+            // will take care of restoring the tabs we already created.
+            return;
+        }
+
+        if (intent != null && ACTION_CONTENT_NOTIFICATION.equals(intent.getAction())) {
+            openURLsFromIntent(browserApp, intent);
+        }
+    }
+
+    @Override
+    public void onNewIntent(BrowserApp browserApp, Intent intent) {
+        if (intent != null && ACTION_CONTENT_NOTIFICATION.equals(intent.getAction())) {
+            openURLsFromIntent(browserApp, intent);
+        }
+    }
+
+    private void openURLsFromIntent(BrowserApp browserApp, final Intent intent) {
+        final List<String> urls = intent.getStringArrayListExtra(EXTRA_URLS);
+        if (urls != null) {
+            browserApp.openUrls(urls);
+        }
+
+        Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(browserApp));
+
+        Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, TELEMETRY_EXTRA_CONTENT_UPDATE);
+
+        if (intent.getBooleanExtra(EXTRA_READ_BUTTON, false)) {
+            // "READ NOW" button in notification was clicked
+            Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, TELEMETRY_EXTRA_READ_NOW_BUTTON);
+
+            // Android's "auto cancel" won't remove the notification when an action button is pressed. So we do it ourselves here.
+            NotificationManagerCompat.from(browserApp).cancel(R.id.websiteContentNotification);
+        } else {
+            // Notification was clicked
+            Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, TELEMETRY_EXTRA_CONTENT_UPDATE);
+        }
+
+        Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(browserApp));
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckForUpdatesAction.java
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckForUpdatesAction.java
@@ -16,46 +16,47 @@ import android.net.Uri;
 import android.support.v4.app.NotificationCompat;
 import android.support.v4.app.NotificationManagerCompat;
 import android.support.v4.content.ContextCompat;
 import android.text.format.DateFormat;
 
 import org.json.JSONException;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.BrowserAppDelegate;
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.UrlAnnotations;
+import org.mozilla.gecko.feeds.ContentNotificationsDelegate;
 import org.mozilla.gecko.feeds.FeedFetcher;
 import org.mozilla.gecko.feeds.FeedService;
 import org.mozilla.gecko.feeds.parser.Feed;
 import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.util.StringUtils;
 
+import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 
 /**
  * CheckForUpdatesAction: Check if feeds we subscribed to have new content available.
  */
 public class CheckForUpdatesAction extends FeedAction {
     /**
      * This extra will be added to Intents fired by the notification.
      */
     public static final String EXTRA_CONTENT_NOTIFICATION = "content-notification";
 
-    private static final String LOGTAG = "FeedCheckAction";
-
-    private Context context;
+    private final Context context;
 
     public CheckForUpdatesAction(Context context) {
         this.context = context;
     }
 
     @Override
     public void perform(BrowserDB browserDB, Intent intent) {
         final UrlAnnotations urlAnnotations = browserDB.getUrlAnnotations();
@@ -158,87 +159,113 @@ public class CheckForUpdatesAction exten
         Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
         Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.NOTIFICATION, "content_update");
         Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
     }
 
     private void showNotificationForSingleUpdate(Feed feed) {
         final String date = DateFormat.getMediumDateFormat(context).format(new Date(feed.getLastItem().getTimestamp()));
 
-        NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle()
+        final NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle()
                 .bigText(feed.getLastItem().getTitle())
                 .setBigContentTitle(feed.getTitle())
                 .setSummaryText(context.getString(R.string.content_notification_updated_on, date));
 
-        Intent intent = new Intent(Intent.ACTION_VIEW);
-        intent.setComponent(new ComponentName(context, BrowserApp.class));
-        intent.setData(Uri.parse(feed.getLastItem().getURL()));
-        intent.putExtra(EXTRA_CONTENT_NOTIFICATION, true);
+        final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, createOpenIntent(feed), PendingIntent.FLAG_UPDATE_CURRENT);
 
-        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
-
-        Notification notification = new NotificationCompat.Builder(context)
+        final Notification notification = new NotificationCompat.Builder(context)
                 .setSmallIcon(R.drawable.ic_status_logo)
                 .setContentTitle(feed.getTitle())
                 .setContentText(feed.getLastItem().getTitle())
                 .setStyle(style)
                 .setColor(ContextCompat.getColor(context, R.color.fennec_ui_orange))
                 .setContentIntent(pendingIntent)
                 .setAutoCancel(true)
+                .addAction(createOpenAction(feed))
                 .addAction(createNotificationSettingsAction())
                 .build();
 
         NotificationManagerCompat.from(context).notify(R.id.websiteContentNotification, notification);
     }
 
     private void showNotificationForMultipleUpdates(List<Feed> feeds) {
-        final ArrayList<String> urls = new ArrayList<>();
-
         final NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
         for (Feed feed : feeds) {
-            final String url = feed.getLastItem().getURL();
-
-            inboxStyle.addLine(StringUtils.stripScheme(url, StringUtils.UrlFlags.STRIP_HTTPS));
-            urls.add(url);
+            inboxStyle.addLine(StringUtils.stripScheme(feed.getLastItem().getURL(), StringUtils.UrlFlags.STRIP_HTTPS));
         }
         inboxStyle.setSummaryText(context.getString(R.string.content_notification_summary));
 
-        Intent intent = new Intent(context, BrowserApp.class);
-        intent.setAction(BrowserApp.ACTION_VIEW_MULTIPLE);
-        intent.putStringArrayListExtra("urls", urls);
-        intent.putExtra(EXTRA_CONTENT_NOTIFICATION, true);
-
-        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+        final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, createOpenIntent(feeds), PendingIntent.FLAG_UPDATE_CURRENT);
 
         Notification notification = new NotificationCompat.Builder(context)
                 .setSmallIcon(R.drawable.ic_status_logo)
                 .setContentTitle(context.getString(R.string.content_notification_title_plural, feeds.size()))
                 .setContentText(context.getString(R.string.content_notification_summary))
                 .setStyle(inboxStyle)
                 .setColor(ContextCompat.getColor(context, R.color.fennec_ui_orange))
                 .setContentIntent(pendingIntent)
                 .setAutoCancel(true)
+                .addAction(createOpenAction(feeds))
                 .setNumber(feeds.size())
                 .addAction(createNotificationSettingsAction())
                 .build();
 
         NotificationManagerCompat.from(context).notify(R.id.websiteContentNotification, notification);
     }
 
+    private Intent createOpenIntent(Feed feed) {
+        final List<Feed> feeds = new ArrayList<>();
+        feeds.add(feed);
+
+        return createOpenIntent(feeds);
+    }
+
+    private Intent createOpenIntent(List<Feed> feeds) {
+        final ArrayList<String> urls = new ArrayList<>();
+        for (Feed feed : feeds) {
+            urls.add(feed.getLastItem().getURL());
+        }
+
+        final Intent intent = new Intent(context, BrowserApp.class);
+        intent.setAction(ContentNotificationsDelegate.ACTION_CONTENT_NOTIFICATION);
+        intent.putStringArrayListExtra(ContentNotificationsDelegate.EXTRA_URLS, urls);
+
+        return intent;
+    }
+
+    private NotificationCompat.Action createOpenAction(Feed feed) {
+        final List<Feed> feeds = new ArrayList<>();
+        feeds.add(feed);
+
+        return createOpenAction(feeds);
+    }
+
+    private NotificationCompat.Action createOpenAction(List<Feed> feeds) {
+        Intent intent = createOpenIntent(feeds);
+        intent.putExtra(ContentNotificationsDelegate.EXTRA_READ_BUTTON, true);
+
+        PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+        return new NotificationCompat.Action(
+                R.drawable.open_in_browser,
+                context.getString(R.string.content_notification_action_read_now),
+                pendingIntent);
+    }
+
     private NotificationCompat.Action createNotificationSettingsAction() {
         final Intent intent = new Intent(GeckoApp.ACTION_LAUNCH_SETTINGS);
         intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
         intent.putExtra(EXTRA_CONTENT_NOTIFICATION, true);
 
         GeckoPreferences.setResourceToOpen(intent, "preferences_notifications");
 
         PendingIntent settingsIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
 
         return new NotificationCompat.Action(
-                R.drawable.firefox_settings_alert,
+                R.drawable.settings_notifications,
                 context.getString(R.string.content_notification_action_settings),
                 settingsIntent);
     }
 
     private FeedFetcher.FeedResponse fetchFeed(FeedSubscription subscription) {
         return FeedFetcher.fetchAndParseFeedIfModified(
                 subscription.getFeedUrl(),
                 subscription.getETag(),
--- a/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java
@@ -91,17 +91,17 @@ public class ClientsAdapter extends Recy
             case HIDDEN_DEVICES:
                 view = inflater.inflate(R.layout.home_remote_tabs_hidden_devices, parent, false);
                 return new CombinedHistoryItem.BasicItem(view);
         }
         return null;
     }
 
     @Override
-    public void onBindViewHolder (CombinedHistoryItem holder, final int position){
+    public void onBindViewHolder(CombinedHistoryItem holder, final int position) {
        final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
 
         switch (itemType) {
             case CLIENT:
                 final CombinedHistoryItem.ClientItem clientItem = (CombinedHistoryItem.ClientItem) holder;
                 final String clientGuid = adapterList.get(position).first;
                 final RemoteClient client = visibleClients.get(clientGuid);
                 clientItem.bind(context, client, sState.isClientCollapsed(clientGuid));
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
@@ -127,17 +127,17 @@ public class CombinedHistoryAdapter exte
      *
      * @param type ItemType of the item
      * @param position position in the adapter
      * @return position of the item in the data structure
      */
     private int transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType type, int position) {
         if (type == CombinedHistoryItem.ItemType.SECTION_HEADER) {
             return position;
-        } else if (type == CombinedHistoryItem.ItemType.HISTORY){
+        } else if (type == CombinedHistoryItem.ItemType.HISTORY) {
             return position - getHeadersBefore(position) - CombinedHistoryPanel.NUM_SMART_FOLDERS;
         } else {
             return position;
         }
     }
 
     private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
         if (position == SYNCED_DEVICES_SMARTFOLDER_INDEX) {
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
@@ -366,17 +366,17 @@ public class CombinedHistoryPanel extend
                 break;
 
             case CHILD:
                 showEmptyClientsView = mClientsAdapter.getItemCount() == 1;
                 break;
         }
 
         final boolean showEmptyView = showEmptyClientsView || showEmptyHistoryView;
-        mRecyclerView.setOverScrollMode(showEmptyView? View.OVER_SCROLL_NEVER : View.OVER_SCROLL_IF_CONTENT_SCROLLS);
+        mRecyclerView.setOverScrollMode(showEmptyView ? View.OVER_SCROLL_NEVER : View.OVER_SCROLL_IF_CONTENT_SCROLLS);
 
         mClientsEmptyView.setVisibility(showEmptyClientsView ? View.VISIBLE : View.GONE);
         mHistoryEmptyView.setVisibility(showEmptyHistoryView ? View.VISIBLE : View.GONE);
     }
 
     /**
      * Make Span that is clickable, and underlined
      * between the string markers <code>FORMAT_S1</code> and
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java
@@ -10,18 +10,11 @@ public class TelemetryConstants {
 
     // Change these two values to enable upload in developer builds.
     public static final boolean UPLOAD_ENABLED = AppConstants.MOZILLA_OFFICIAL; // Disabled for developer builds.
     public static final String DEFAULT_SERVER_URL = "https://incoming.telemetry.mozilla.org";
 
     public static final String USER_AGENT =
             "Firefox-Android-Telemetry/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")";
 
-    public static final String ACTION_UPLOAD_CORE = "uploadCore";
-    public static final String EXTRA_DEFAULT_SEARCH_ENGINE = "defaultSearchEngine";
-    public static final String EXTRA_DOC_ID = "docId";
-    public static final String EXTRA_PROFILE_NAME = "geckoProfileName";
-    public static final String EXTRA_PROFILE_PATH = "geckoProfilePath";
-    public static final String EXTRA_SEQ = "seq";
-
     public static final String PREF_SERVER_URL = "telemetry-serverUrl";
     public static final String PREF_SEQ_COUNT = "telemetry-seqCount";
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java
@@ -0,0 +1,113 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.mozilla.gecko.telemetry;
+
+import android.content.Context;
+import android.util.Log;
+import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder;
+import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadScheduler;
+import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadAllPingsImmediatelyScheduler;
+import org.mozilla.gecko.telemetry.stores.TelemetryJSONFilePingStore;
+import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * The entry-point for Java-based telemetry. This class handles:
+ *  * Initializing the Stores & Schedulers.
+ *  * Queueing upload requests for a given ping.
+ *
+ * The full architecture is:
+ *
+ * Fennec -(PingBuilder)-> Dispatcher -2-> Scheduler -> UploadService
+ *                             | 1                            |
+ *                           Store <--------------------------
+ *
+ * The store acts as a single store of truth and contains a list of all
+ * pings waiting to be uploaded. The dispatcher will queue a ping to upload
+ * by writing it to the store. Later, the UploadService will try to upload
+ * this queued ping by reading directly from the store.
+ *
+ * To implement a new ping type, you should:
+ *   1) Implement a {@link org.mozilla.gecko.telemetry.pingbuilders.TelemetryPingBuilder} for your ping type.
+ *   2) Re-use a ping store in .../stores/ or implement a new one: {@link TelemetryPingStore}. The
+ * type of store may be affected by robustness requirements (e.g. do you have data in addition to
+ * pings that need to be atomically updated when a ping is stored?) and performance requirements.
+ *   3) Re-use an upload scheduler in .../schedulers/ or implement a new one: {@link TelemetryUploadScheduler}.
+ *   4) Initialize your Store & (if new) Scheduler in the constructor of this class
+ *   5) Add a queuePingForUpload method for your PingBuilder class (see
+ * {@link #queuePingForUpload(Context, TelemetryCorePingBuilder)})
+ *   6) In Fennec, where you want to store a ping and attempt upload, create a PingBuilder and
+ * pass it to the new queuePingForUpload method.
+ */
+public class TelemetryDispatcher {
+    private static final String LOGTAG = "Gecko" + TelemetryDispatcher.class.getSimpleName();
+
+    private static final String STORE_CONTAINER_DIR_NAME = "telemetry_java";
+    private static final String CORE_STORE_DIR_NAME = "core";
+
+    private final TelemetryJSONFilePingStore coreStore;
+
+    private final TelemetryUploadAllPingsImmediatelyScheduler uploadAllPingsImmediatelyScheduler;
+
+    public TelemetryDispatcher(final String profilePath) {
+        final String storePath = profilePath + File.separator + STORE_CONTAINER_DIR_NAME;
+
+        // There are measurements in the core ping (e.g. seq #) that would ideally be atomically updated
+        // when the ping is stored. However, for simplicity, we use the json store and accept the possible
+        // loss of data (see bug 1243585 comment 16+ for more).
+        coreStore = new TelemetryJSONFilePingStore(new File(storePath, CORE_STORE_DIR_NAME));
+
+        uploadAllPingsImmediatelyScheduler = new TelemetryUploadAllPingsImmediatelyScheduler();
+    }
+
+    private void queuePingForUpload(final Context context, final TelemetryPing ping, final TelemetryPingStore store,
+            final TelemetryUploadScheduler scheduler) {
+        final QueuePingRunnable runnable = new QueuePingRunnable(context, ping, store, scheduler);
+        ThreadUtils.postToBackgroundThread(runnable); // TODO: Investigate how busy this thread is. See if we want another.
+    }
+
+    /**
+     * Queues the given ping for upload and potentially schedules upload. This method can be called from any thread.
+     */
+    public void queuePingForUpload(final Context context, final TelemetryCorePingBuilder pingBuilder) {
+        final TelemetryPing ping = pingBuilder.build();
+        queuePingForUpload(context, ping, coreStore, uploadAllPingsImmediatelyScheduler);
+    }
+
+    private static class QueuePingRunnable implements Runnable {
+        private final Context applicationContext;
+        private final TelemetryPing ping;
+        private final TelemetryPingStore store;
+        private final TelemetryUploadScheduler scheduler;
+
+        public QueuePingRunnable(final Context context, final TelemetryPing ping, final TelemetryPingStore store,
+                final TelemetryUploadScheduler scheduler) {
+            this.applicationContext = context.getApplicationContext();
+            this.ping = ping;
+            this.store = store;
+            this.scheduler = scheduler;
+        }
+
+        @Override
+        public void run() {
+            // We block while storing the ping so the scheduled upload is guaranteed to have the newly-stored value.
+            try {
+                store.storePing(ping);
+            } catch (final IOException e) {
+                // Don't log exception to avoid leaking profile path.
+                Log.e(LOGTAG, "Unable to write ping to disk. Continuing with upload attempt");
+            }
+
+            if (scheduler.isReadyToUpload(store)) {
+                scheduler.scheduleUpload(applicationContext, store);
+            }
+        }
+    }
+}
rename from mobile/android/base/java/org/mozilla/gecko/telemetry/pings/TelemetryPing.java
rename to mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pings/TelemetryPing.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java
@@ -1,27 +1,35 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-package org.mozilla.gecko.telemetry.pings;
+package org.mozilla.gecko.telemetry;
 
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 
 /**
  * Container for telemetry data and the data necessary to upload it.
  *
+ * The unique ID is used by a Store to manipulate its internal pings. Some ping
+ * payloads already contain a unique ID (e.g. sequence number in core ping) and
+ * this field can mirror that value.
+ *
  * If you want to create one of these, consider extending
- * {@link TelemetryPingBuilder} or one of its descendants.
+ * {@link org.mozilla.gecko.telemetry.pingbuilders.TelemetryPingBuilder}
+ * or one of its descendants.
  */
 public class TelemetryPing {
-    private final String url;
+    private final String urlPath;
     private final ExtendedJSONObject payload;
+    private final int uniqueID;
 
-    public TelemetryPing(final String url, final ExtendedJSONObject payload) {
-        this.url = url;
+    public TelemetryPing(final String urlPath, final ExtendedJSONObject payload, final int uniqueID) {
+        this.urlPath = urlPath;
         this.payload = payload;
+        this.uniqueID = uniqueID;
     }
 
-    public String getURL() { return url; }
+    public String getURLPath() { return urlPath; }
     public ExtendedJSONObject getPayload() { return payload; }
+    public int getUniqueID() { return uniqueID; }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
@@ -1,104 +1,164 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.telemetry;
 
+import android.app.IntentService;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-import android.text.TextUtils;
 import android.util.Log;
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.background.BackgroundService;
-import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.preferences.GeckoPreferences;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.net.Resource;
-import org.mozilla.gecko.telemetry.pings.TelemetryCorePingBuilder;
-import org.mozilla.gecko.telemetry.pings.TelemetryPing;
+import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
+import org.mozilla.gecko.util.NetworkUtils;
 import org.mozilla.gecko.util.StringUtils;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 /**
- * The service that handles uploading telemetry payloads to the server.
- *
- * Note that we'll fail to upload if the network is off or background uploads are disabled but the caller is still
- * expected to increment the sequence number.
+ * The service that handles retrieving a list of telemetry pings to upload from the given
+ * {@link TelemetryPingStore}, uploading those payloads to the associated server, and reporting
+ * back to the Store which uploads were a success.
  */
-public class TelemetryUploadService extends BackgroundService {
+public class TelemetryUploadService extends IntentService {
     private static final String LOGTAG = StringUtils.safeSubstring("Gecko" + TelemetryUploadService.class.getSimpleName(), 0, 23);
     private static final String WORKER_THREAD_NAME = LOGTAG + "Worker";
 
-    private static final int MILLIS_IN_DAY = 1000 * 60 * 60 * 24;
+    public static final String ACTION_UPLOAD = "upload";
+    public static final String EXTRA_STORE = "store";
 
     public TelemetryUploadService() {
         super(WORKER_THREAD_NAME);
 
-        // Intent redelivery can fail hard (e.g. we OOM as we try to upload, the Intent gets redelivered, repeat) so for
-        // simplicity, we avoid it for now. In the unlikely event that Android kills our upload service, we'll thus fail
-        // to upload the document with a specific sequence number. Furthermore, we never attempt to re-upload it.
-        //
-        // We'll fix this issue in bug 1243585.
+        // Intent redelivery can fail hard (e.g. we OOM as we try to upload, the Intent gets redelivered, repeat)
+        // so for simplicity, we avoid it. We expect the upload service to eventually get called again by the caller.
         setIntentRedelivery(false);
     }
 
     /**
-     * Handles a core ping with the mandatory extras:
-     *   EXTRA_DOC_ID: a unique document ID.
-     *   EXTRA_SEQ: a sequence number for this upload.
-     *   EXTRA_PROFILE_NAME: the gecko profile name.
-     *   EXTRA_PROFILE_PATH: the gecko profile path.
-     *
-     * Note that for a given doc ID, seq should always be identical because these are the tools the server uses to
-     * de-duplicate documents. In order to maintain this consistency, we receive the doc ID and seq from the Intent and
-     * rely on the caller to update the values. The Service can be killed at any time so we can't ensure seq could be
-     * incremented properly if we tried to do so in the Service.
+     * Handles a ping with the mandatory extras:
+     *   * EXTRA_STORE: A {@link TelemetryPingStore} where the pings to upload are located
      */
     @Override
     public void onHandleIntent(final Intent intent) {
         Log.d(LOGTAG, "Service started");
 
+        if (!isReadyToUpload(this, intent)) {
+            return;
+        }
+
+        final TelemetryPingStore store = intent.getParcelableExtra(EXTRA_STORE);
+        final boolean wereAllUploadsSuccessful = uploadPendingPingsFromStore(this, store);
+        store.maybePrunePings();
+        Log.d(LOGTAG, "Service finished: upload and prune attempts completed");
+
+        if (!wereAllUploadsSuccessful) {
+            // If we had an upload failure, we should stop the IntentService and drop any
+            // pending Intents in the queue so we don't waste resources (e.g. battery)
+            // trying to upload when there's likely to be another connection failure.
+            Log.d(LOGTAG, "Clearing Intent queue due to connection failures");
+            stopSelf();
+        }
+    }
+
+    /**
+     * @return true if all pings were uploaded successfully, false otherwise.
+     */
+    private static boolean uploadPendingPingsFromStore(final Context context, final TelemetryPingStore store) {
+        final List<TelemetryPing> pingsToUpload = store.getAllPings();
+        if (pingsToUpload.isEmpty()) {
+            return true;
+        }
+
+        final String serverSchemeHostPort = getServerSchemeHostPort(context);
+        final HashSet<Integer> successfulUploadIDs = new HashSet<>(pingsToUpload.size()); // used for side effects.
+        final PingResultDelegate delegate = new PingResultDelegate(successfulUploadIDs);
+        for (final TelemetryPing ping : pingsToUpload) {
+            // TODO: It'd be great to re-use the same HTTP connection for each upload request.
+            delegate.setPingID(ping.getUniqueID());
+            final String url = serverSchemeHostPort + "/" + ping.getURLPath();
+            uploadPayload(url, ping.getPayload(), delegate);
+
+            // There are minimal gains in trying to upload if we already failed one attempt.
+            if (delegate.hadConnectionError()) {
+                break;
+            }
+        }
+
+        final boolean wereAllUploadsSuccessful = !delegate.hadConnectionError();
+        if (wereAllUploadsSuccessful) {
+            // We don't log individual successful uploads to avoid log spam.
+            Log.d(LOGTAG, "Telemetry upload success!");
+        }
+        store.onUploadAttemptComplete(successfulUploadIDs);
+        return wereAllUploadsSuccessful;
+    }
+
+    private static String getServerSchemeHostPort(final Context context) {
+        // TODO (bug 1241685): Sync this preference with the gecko preference or a Java-based pref.
+        // We previously had this synced with profile prefs with no way to change it in the client, so
+        // we don't have to worry about losing data by switching it to app prefs, which is more convenient for now.
+        final SharedPreferences sharedPrefs = GeckoSharedPrefs.forApp(context);
+        return sharedPrefs.getString(TelemetryConstants.PREF_SERVER_URL, TelemetryConstants.DEFAULT_SERVER_URL);
+    }
+
+    private static void uploadPayload(final String url, final ExtendedJSONObject payload, final ResultDelegate delegate) {
+        final BaseResource resource;
+        try {
+            resource = new BaseResource(url);
+        } catch (final URISyntaxException e) {
+            Log.w(LOGTAG, "URISyntaxException for server URL when creating BaseResource: returning.");
+            return;
+        }
+
+        delegate.setResource(resource);
+        resource.delegate = delegate;
+        resource.setShouldCompressUploadedEntity(true);
+        resource.setShouldChunkUploadsHint(false); // Telemetry servers don't support chunking.
+
+        // We're in a background thread so we don't have any reason to do this asynchronously.
+        // If we tried, onStartCommand would return and IntentService might stop itself before we finish.
+        resource.postBlocking(payload);
+    }
+
+    private static boolean isReadyToUpload(final Context context, final Intent intent) {
         // Sanity check: is upload enabled? Generally, the caller should check this before starting the service.
         // Since we don't have the profile here, we rely on the caller to check the enabled state for the profile.
-        if (!isUploadEnabledByAppConfig(this)) {
+        if (!isUploadEnabledByAppConfig(context)) {
             Log.w(LOGTAG, "Upload is not available by configuration; returning");
-            return;
+            return false;
         }
 
         if (!isIntentValid(intent)) {
             Log.w(LOGTAG, "Received invalid Intent; returning");
-            return;
-        }
-
-        if (!TelemetryConstants.ACTION_UPLOAD_CORE.equals(intent.getAction())) {
-            Log.w(LOGTAG, "Unknown action: " + intent.getAction() + ". Returning");
-            return;
+            return false;
         }
 
-        final String defaultSearchEngine = intent.getStringExtra(TelemetryConstants.EXTRA_DEFAULT_SEARCH_ENGINE);
-        final String docId = intent.getStringExtra(TelemetryConstants.EXTRA_DOC_ID);
-        final int seq = intent.getIntExtra(TelemetryConstants.EXTRA_SEQ, -1);
+        if (!ACTION_UPLOAD.equals(intent.getAction())) {
+            Log.w(LOGTAG, "Unknown action: " + intent.getAction() + ". Returning");
+            return false;
+        }
 
-        final String profileName = intent.getStringExtra(TelemetryConstants.EXTRA_PROFILE_NAME);
-        final String profilePath = intent.getStringExtra(TelemetryConstants.EXTRA_PROFILE_PATH);
-
-        uploadCorePing(docId, seq, profileName, profilePath, defaultSearchEngine);
+        return true;
     }
 
     /**
      * Determines if the telemetry upload feature is enabled via the application configuration. Prefer to use
      * {@link #isUploadEnabledByProfileConfig(Context, GeckoProfile)} if the profile is available as it takes into
      * account more information.
      *
      * Note that this method logs debug statements when upload is disabled.
@@ -109,17 +169,17 @@ public class TelemetryUploadService exte
             return false;
         }
 
         if (!GeckoPreferences.getBooleanPref(context, GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true)) {
             Log.d(LOGTAG, "Telemetry upload opt-out");
             return false;
         }
 
-        if (!backgroundDataIsEnabled(context)) {
+        if (!NetworkUtils.isBackgroundDataEnabled(context)) {
             Log.d(LOGTAG, "Background data is disabled");
             return false;
         }
 
         return true;
     }
 
     /**
@@ -132,155 +192,118 @@ public class TelemetryUploadService exte
         if (profile.inGuestMode()) {
             Log.d(LOGTAG, "Profile is in guest mode");
             return false;
         }
 
         return isUploadEnabledByAppConfig(context);
     }
 
-    private boolean isIntentValid(final Intent intent) {
+    private static boolean isIntentValid(final Intent intent) {
         // Intent can be null. Bug 1025937.
         if (intent == null) {
             Log.d(LOGTAG, "Received null intent");
             return false;
         }
 
-        if (intent.getStringExtra(TelemetryConstants.EXTRA_DOC_ID) == null) {
-            Log.d(LOGTAG, "Received invalid doc ID in Intent");
-            return false;
-        }
-
-        if (!intent.hasExtra(TelemetryConstants.EXTRA_SEQ)) {
-            Log.d(LOGTAG, "Received Intent without sequence number");
-            return false;
-        }
-
-        if (intent.getStringExtra(TelemetryConstants.EXTRA_PROFILE_NAME) == null) {
-            Log.d(LOGTAG, "Received invalid profile name in Intent");
-            return false;
-        }
-
-        // GeckoProfile can use the name to get the path so this isn't strictly necessary.
-        // However, getting the path requires parsing an ini file so we optimize by including it here.
-        if (intent.getStringExtra(TelemetryConstants.EXTRA_PROFILE_PATH) == null) {
-            Log.d(LOGTAG, "Received invalid profile path in Intent");
+        if (intent.getParcelableExtra(EXTRA_STORE) == null) {
+            Log.d(LOGTAG, "Received invalid store in Intent");
             return false;
         }
 
         return true;
     }
 
-    @WorkerThread
-    private void uploadCorePing(@NonNull final String docId, final int seq, @NonNull final String profileName,
-                @NonNull final String profilePath, @Nullable final String defaultSearchEngine) {
-        final GeckoProfile profile = GeckoProfile.get(this, profileName, profilePath);
-        final String clientId;
-        try {
-            clientId = profile.getClientId();
-        } catch (final IOException e) {
-            Log.w(LOGTAG, "Unable to get client ID to generate core ping: returning.", e);
-            return;
-        }
+    /**
+     * Logs on success & failure and appends the set ID to the given Set on success.
+     *
+     * Note: you *must* set the ping ID before attempting upload or we'll throw!
+     *
+     * We use mutation on the set ID and the successful upload array to avoid object allocation.
+     */
+    private static class PingResultDelegate extends ResultDelegate {
+        // We persist pings and don't need to worry about losing data so we keep these
+        // durations sho