Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 13 Jun 2017 12:11:42 +0200
changeset 412427 0d7234fcd2fa0d58d35afcf422047098331a0599
parent 412426 410f73774f02fadf83ef3704f3f6ef2361799641 (current diff)
parent 412337 a8f8e440d627d686fa8898483aa9c5da928a8fa4 (diff)
child 412428 54ce8da73fbb29ba56359808822af307e0a7ca41
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone56.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
browser/locales/en-US/chrome/browser/browser.dtd
services/sync/tests/unit/test_collection_inc_get.js
--- a/addon-sdk/source/lib/dev/debuggee.js
+++ b/addon-sdk/source/lib/dev/debuggee.js
@@ -6,18 +6,17 @@
 
 module.metadata = {
   "stability": "experimental"
 };
 
 const { Cu } = require("chrome");
 const { Class } = require("../sdk/core/heritage");
 const { MessagePort, MessageChannel } = require("../sdk/messaging");
-const { require: devtoolsRequire } = Cu.import("resource://devtools/shared/Loader.jsm", {});
-const { DebuggerServer } = devtoolsRequire("devtools/server/main");
+const { DevToolsShim } = Cu.import("chrome://devtools-shim/content/DevToolsShim.jsm", {});
 
 const outputs = new WeakMap();
 const inputs = new WeakMap();
 const targets = new WeakMap();
 const transports = new WeakMap();
 
 const inputFor = port => inputs.get(port);
 const outputFor = port => outputs.get(port);
@@ -44,22 +43,18 @@ const Debuggee = Class({
     }
     outputFor(this).close();
   },
   start: function() {
     const target = targets.get(this);
     if (target.isLocalTab) {
       // Since a remote protocol connection will be made, let's start the
       // DebuggerServer here, once and for all tools.
-      if (!DebuggerServer.initialized) {
-        DebuggerServer.init();
-        DebuggerServer.addBrowserActors();
-      }
-
-      transports.set(this, DebuggerServer.connectPipe());
+      let transport = DevToolsShim.connectDebuggerServer();
+      transports.set(this, transport);
     }
     // TODO: Implement support for remote connections (See Bug 980421)
     else {
       throw Error("Remote targets are not yet supported");
     }
 
     // pipe messages send to the debuggee to an actual
     // server via remote debugging protocol transport.
--- a/addon-sdk/source/lib/dev/toolbox.js
+++ b/addon-sdk/source/lib/dev/toolbox.js
@@ -10,17 +10,17 @@ module.metadata = {
 
 const { Cu, Cc, Ci } = require("chrome");
 const { Class } = require("../sdk/core/heritage");
 const { Disposable, setup } = require("../sdk/core/disposable");
 const { contract, validate } = require("../sdk/util/contract");
 const { each, pairs, values } = require("../sdk/util/sequence");
 const { onEnable, onDisable } = require("../dev/theme/hooks");
 
-const { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
+const { DevToolsShim } = Cu.import("chrome://devtools-shim/content/DevToolsShim.jsm", {});
 
 // This is temporary workaround to allow loading of the developer tools client - volcan
 // into a toolbox panel, this hack won't be necessary as soon as devtools patch will be
 // shipped in nightly, after which it can be removed. Bug 1038517
 const registerSDKURI = () => {
   const ioService = Cc['@mozilla.org/network/io-service;1']
                       .getService(Ci.nsIIOService);
   const resourceHandler = ioService.getProtocolHandler("resource")
@@ -41,17 +41,17 @@ const Tool = Class({
     this.panels = panels;
     this.themes = themes;
 
     each(([key, Panel]) => {
       const { url, label, tooltip, icon, invertIconForLightTheme,
               invertIconForDarkTheme } = validate(Panel.prototype);
       const { id } = Panel.prototype;
 
-      gDevTools.registerTool({
+      DevToolsShim.registerTool({
         id: id,
         url: "about:blank",
         label: label,
         tooltip: tooltip,
         icon: icon,
         invertIconForLightTheme: invertIconForLightTheme,
         invertIconForDarkTheme: invertIconForDarkTheme,
         isTargetSupported: target => target.isLocalTab,
@@ -65,37 +65,37 @@ const Tool = Class({
         }
       });
     }, pairs(panels));
 
     each(([key, theme]) => {
       validate(theme);
       setup(theme);
 
-      gDevTools.registerTheme({
+      DevToolsShim.registerTheme({
         id: theme.id,
         label: theme.label,
         stylesheets: theme.getStyles(),
         classList: theme.getClassList(),
         onApply: (window, oldTheme) => {
           onEnable(theme, { window: window,
                             oldTheme: oldTheme });
         },
         onUnapply: (window, newTheme) => {
           onDisable(theme, { window: window,
                             newTheme: newTheme });
         }
       });
     }, pairs(themes));
   },
   dispose: function() {
-    each(Panel => gDevTools.unregisterTool(Panel.prototype.id),
+    each(Panel => DevToolsShim.unregisterTool(Panel.prototype.id),
          values(this.panels));
 
-    each(Theme => gDevTools.unregisterTheme(Theme.prototype.id),
+    each(Theme => DevToolsShim.unregisterTheme(Theme.prototype.id),
          values(this.themes));
   }
 });
 
 validate.define(Tool, contract({
   panels: {
     is: ["object", "undefined"]
   },
--- a/addon-sdk/source/lib/dev/utils.js
+++ b/addon-sdk/source/lib/dev/utils.js
@@ -1,40 +1,39 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { Cu } = require("chrome");
-const { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
-const { devtools } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { DevToolsShim } = Cu.import("chrome://devtools-shim/content/DevToolsShim.jsm", {});
 
 const { getActiveTab } = require("../sdk/tabs/utils");
 const { getMostRecentBrowserWindow } = require("../sdk/window/utils");
 
 const targetFor = target => {
   target = target || getActiveTab(getMostRecentBrowserWindow());
-  return devtools.TargetFactory.forTab(target);
+  return DevToolsShim.getTargetForTab(target);
 };
 
 const getId = id => ((id.prototype && id.prototype.id) || id.id || id);
 
 const getCurrentPanel = toolbox => toolbox.getCurrentPanel();
 exports.getCurrentPanel = getCurrentPanel;
 
 const openToolbox = (id, tab) => {
   id = getId(id);
-  return gDevTools.showToolbox(targetFor(tab), id);
+  return DevToolsShim.showToolbox(targetFor(tab), id);
 };
 exports.openToolbox = openToolbox;
 
-const closeToolbox = tab => gDevTools.closeToolbox(targetFor(tab));
+const closeToolbox = tab => DevToolsShim.closeToolbox(targetFor(tab));
 exports.closeToolbox = closeToolbox;
 
-const getToolbox = tab => gDevTools.getToolbox(targetFor(tab));
+const getToolbox = tab => DevToolsShim.getToolbox(targetFor(tab));
 exports.getToolbox = getToolbox;
 
 const openToolboxPanel = (id, tab) => {
   id = getId(id);
-  return gDevTools.showToolbox(targetFor(tab), id).then(getCurrentPanel);
+  return DevToolsShim.showToolbox(targetFor(tab), id).then(getCurrentPanel);
 };
 exports.openToolboxPanel = openToolboxPanel;
--- a/addon-sdk/source/lib/sdk/addon/runner.js
+++ b/addon-sdk/source/lib/sdk/addon/runner.js
@@ -14,19 +14,19 @@ const { exit, env, staticArgs } = requir
 const { when: unload } = require('../system/unload');
 const globals = require('../system/globals');
 const { get } = require('../preferences/service');
 const { preferences } = metadata;
 
 const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyGetter(this, "BrowserToolboxProcess", function () {
-  return Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {}).
-         BrowserToolboxProcess;
+XPCOMUtils.defineLazyGetter(this, "DevToolsShim", function () {
+  return Cu.import("chrome://devtools-shim/content/DevToolsShim.jsm", {}).
+         DevToolsShim;
 });
 
 // Initializes default preferences
 function setDefaultPrefs(prefsURI) {
   const prefs = Cc['@mozilla.org/preferences-service;1'].
                 getService(Ci.nsIPrefService).
                 QueryInterface(Ci.nsIPrefBranch2);
   const branch = prefs.getDefaultBranch('');
@@ -151,17 +151,17 @@ function run(options) {
         staticArgs: staticArgs
       }, {
         print: function print(_) { dump(_ + '\n') },
         quit: exit
       });
     }
 
     if (get("extensions." + id + ".sdk.debug.show", false)) {
-      BrowserToolboxProcess.init({ addonID: id });
+      DevToolsShim.initBrowserToolboxProcessForAddon(id);
     }
   } catch (error) {
     console.exception(error);
     throw error;
   }
 }
 exports.startup = startup;
 
--- a/addon-sdk/source/test/addons/page-mod-debugger-post/main.js
+++ b/addon-sdk/source/test/addons/page-mod-debugger-post/main.js
@@ -7,40 +7,32 @@ const { Cu } = require('chrome');
 const { PageMod } = require('sdk/page-mod');
 const tabs = require('sdk/tabs');
 const { closeTab } = require('sdk/tabs/utils');
 const promise = require('sdk/core/promise')
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { data } = require('sdk/self');
 const { set } = require('sdk/preferences/service');
 
-const { require: devtoolsRequire } = Cu.import("resource://devtools/shared/Loader.jsm", {});
-const { DebuggerServer } = devtoolsRequire("devtools/server/main");
-const { DebuggerClient } = devtoolsRequire("devtools/shared/client/main");
+const { DevToolsShim } = Cu.import("chrome://devtools-shim/content/DevToolsShim.jsm", {});
 
 var gClient;
 var ok;
 var testName = 'testDebugger';
 var iframeURL = 'data:text/html;charset=utf-8,' + testName;
 var TAB_URL = 'data:text/html;charset=utf-8,' + encodeURIComponent('<iframe src="' + iframeURL + '" />');
 TAB_URL = data.url('index.html');
 var mod;
 
 exports.testDebugger = function(assert, done) {
   ok = assert.ok.bind(assert);
   assert.pass('starting test');
   set('devtools.debugger.log', true);
 
-  if (!DebuggerServer.initialized) {
-    DebuggerServer.init();
-    DebuggerServer.addBrowserActors();
-  }
-
-  let transport = DebuggerServer.connectPipe();
-  gClient = new DebuggerClient(transport);
+  gClient = DevToolsShim.createDebuggerClient();
   gClient.connect((aType, aTraits) => {
     tabs.open({
       url: TAB_URL,
       onLoad: function(tab) {
         assert.pass('tab loaded');
 
         attachTabActorForUrl(gClient, TAB_URL).
           then(_ => { assert.pass('attachTabActorForUrl called'); return _; }).
--- a/addon-sdk/source/test/addons/page-mod-debugger-pre/main.js
+++ b/addon-sdk/source/test/addons/page-mod-debugger-pre/main.js
@@ -7,19 +7,17 @@ const { Cu } = require('chrome');
 const { PageMod } = require('sdk/page-mod');
 const tabs = require('sdk/tabs');
 const { closeTab } = require('sdk/tabs/utils');
 const promise = require('sdk/core/promise')
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { data } = require('sdk/self');
 const { set } = require('sdk/preferences/service');
 
-const { require: devtoolsRequire } = Cu.import("resource://devtools/shared/Loader.jsm", {});
-const { DebuggerServer } = devtoolsRequire("devtools/server/main");
-const { DebuggerClient } = devtoolsRequire("devtools/shared/client/main");
+const { DevToolsShim } = Cu.import("chrome://devtools-shim/content/DevToolsShim.jsm", {});
 
 var gClient;
 var ok;
 var testName = 'testDebugger';
 var iframeURL = 'data:text/html;charset=utf-8,' + testName;
 var TAB_URL = 'data:text/html;charset=utf-8,' + encodeURIComponent('<iframe src="' + iframeURL + '" />');
 TAB_URL = data.url('index.html');
 var mod;
@@ -31,23 +29,17 @@ exports.testDebugger = function(assert, 
 
   mod = PageMod({
     include: TAB_URL,
     attachTo: ['existing', 'top', 'frame'],
     contentScriptFile: data.url('script.js'),
   });
   ok(true, 'PageMod was created');
 
-  if (!DebuggerServer.initialized) {
-    DebuggerServer.init();
-    DebuggerServer.addBrowserActors();
-  }
-
-  let transport = DebuggerServer.connectPipe();
-  gClient = new DebuggerClient(transport);
+  gClient = DevToolsShim.createDebuggerClient();
   gClient.connect((aType, aTraits) => {
     tabs.open({
       url: TAB_URL,
       onLoad: function(tab) {
         assert.pass('tab loaded');
 
         attachTabActorForUrl(gClient, TAB_URL).
           then(_ => { assert.pass('attachTabActorForUrl called'); return _; }).
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1212,18 +1212,16 @@ pref("services.sync.prefs.sync.spellchec
 pref("services.sync.prefs.sync.xpinstall.whitelist.required", true);
 
 // A preference that controls whether we should show the icon for a remote tab.
 // This pref has no UI but exists because some people may be concerned that
 // fetching these icons to show remote tabs may leak information about that
 // user's tabs and bookmarks. Note this pref is also synced.
 pref("services.sync.syncedTabs.showRemoteIcons", true);
 
-pref("services.sync.sendTabToDevice.enabled", true);
-
 // Developer edition preferences
 #ifdef MOZ_DEV_EDITION
 sticky_pref("lightweightThemes.selectedThemeID", "firefox-compact-dark@mozilla.org");
 #else
 sticky_pref("lightweightThemes.selectedThemeID", "");
 #endif
 
 // Whether the character encoding menu is under the main Firefox button. This
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -35,18 +35,22 @@ var gSync = {
     delete this.syncStrings;
     // XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
     //        but for now just make it work
     return this.syncStrings = Services.strings.createBundle(
       "chrome://weave/locale/sync.properties"
     );
   },
 
-  get sendTabToDeviceEnabled() {
-    return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled");
+  get syncReady() {
+    return Cc["@mozilla.org/weave/service;1"].getService().wrappedJSObject.ready;
+  },
+
+  get isSignedIn() {
+    return UIState.get().status == UIState.STATUS_SIGNED_IN;
   },
 
   get remoteClients() {
     return Weave.Service.clientsEngine.remoteClients
            .sort((a, b) => a.name.localeCompare(b.name));
   },
 
   _generateNodeGetters(usePhoton) {
@@ -292,50 +296,50 @@ var gSync = {
     for (let i = devicesPopup.childNodes.length - 1; i >= 0; --i) {
       let child = devicesPopup.childNodes[i];
       if (child.classList.contains("sync-menuitem")) {
         child.remove();
       }
     }
 
     const fragment = document.createDocumentFragment();
+    if (this.syncReady) {
+      const onTargetDeviceCommand = (event) => {
+        let clients = event.target.getAttribute("clientId") ?
+          [event.target.getAttribute("clientId")] :
+          this.remoteClients.map(client => client.id);
 
-    const onTargetDeviceCommand = (event) => {
-      let clients = event.target.getAttribute("clientId") ?
-        [event.target.getAttribute("clientId")] :
-        this.remoteClients.map(client => client.id);
-
-      clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
-      gPageActionButton.panel.hidePopup();
-    }
+        clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
+        gPageActionButton.panel.hidePopup();
+      }
 
-    function addTargetDevice(clientId, name, clientType) {
-      const targetDevice = createDeviceNodeFn(clientId, name, clientType);
-      targetDevice.addEventListener("command", onTargetDeviceCommand, true);
-      targetDevice.classList.add("sync-menuitem", "sendtab-target");
-      targetDevice.setAttribute("clientId", clientId);
-      targetDevice.setAttribute("clientType", clientType);
-      targetDevice.setAttribute("label", name);
-      fragment.appendChild(targetDevice);
-    }
+      function addTargetDevice(clientId, name, clientType) {
+        const targetDevice = createDeviceNodeFn(clientId, name, clientType);
+        targetDevice.addEventListener("command", onTargetDeviceCommand, true);
+        targetDevice.classList.add("sync-menuitem", "sendtab-target");
+        targetDevice.setAttribute("clientId", clientId);
+        targetDevice.setAttribute("clientType", clientType);
+        targetDevice.setAttribute("label", name);
+        fragment.appendChild(targetDevice);
+      }
 
-    const clients = this.remoteClients;
-    for (let client of clients) {
-      addTargetDevice(client.id, client.name, client.type);
-    }
+      const clients = this.remoteClients;
+      for (let client of clients) {
+        addTargetDevice(client.id, client.name, client.type);
+      }
 
-    // "All devices" menu item
-    if (clients.length > 1) {
-      const separator = createDeviceNodeFn();
-      separator.classList.add("sync-menuitem");
-      fragment.appendChild(separator);
-      const allDevicesLabel = this.fxaStrings.GetStringFromName("sendTabToAllDevices.menuitem");
-      addTargetDevice("", allDevicesLabel, "");
+      // "All devices" menu item
+      if (clients.length > 1) {
+        const separator = createDeviceNodeFn();
+        separator.classList.add("sync-menuitem");
+        fragment.appendChild(separator);
+        const allDevicesLabel = this.fxaStrings.GetStringFromName("sendTabToAllDevices.menuitem");
+        addTargetDevice("", allDevicesLabel, "");
+      }
     }
-
     devicesPopup.appendChild(fragment);
   },
 
   isSendableURI(aURISpec) {
     if (!aURISpec) {
       return false;
     }
     // Disallow sending tabs with more than 65535 characters.
@@ -351,34 +355,29 @@ var gSync = {
       // The preference has been removed, or is an invalid regexp, so we log an
       // error and treat it as a valid URI -- and the more problematic case is
       // the length, which we've already addressed.
       Cu.reportError(`Failed to build url filter regexp for send tab: ${e}`);
       return true;
     }
   },
 
+  // "Send Tab to Device" menu item
   updateTabContextMenu(aPopupMenu, aTargetTab) {
-    if (!this.sendTabToDeviceEnabled || !this.weaveService.ready) {
-      return;
-    }
-
-    const targetURI = aTargetTab.linkedBrowser.currentURI.spec;
-    const showSendTab = this.remoteClients.length > 0 && this.isSendableURI(targetURI);
+    const show = this.syncReady &&
+                 this.remoteClients.length > 0 &&
+                 this.isSendableURI(aTargetTab.linkedBrowser.currentURI.spec);
 
     ["context_sendTabToDevice", "context_sendTabToDevice_separator"]
-    .forEach(id => { document.getElementById(id).hidden = !showSendTab });
+    .forEach(id => document.getElementById(id).hidden = !show);
   },
 
+  // "Send Page to Device" and "Send Link to Device" menu items
   initPageContextMenu(contextMenu) {
-    if (!this.sendTabToDeviceEnabled || !this.weaveService.ready) {
-      return;
-    }
-
-    const remoteClientPresent = this.remoteClients.length > 0;
+    const remoteClientPresent = this.syncReady && this.remoteClients.length > 0;
     // showSendLink and showSendPage are mutually exclusive
     let showSendLink = remoteClientPresent
                        && (contextMenu.onSaveableLink || contextMenu.onPlainTextLink);
     const showSendPage = !showSendLink && remoteClientPresent
                          && !(contextMenu.isContentSelected ||
                               contextMenu.onImage || contextMenu.onCanvas ||
                               contextMenu.onVideo || contextMenu.onAudio ||
                               contextMenu.onLink || contextMenu.onTextInput)
@@ -562,14 +561,8 @@ var gSync = {
     }
   },
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference
   ])
 };
-
-XPCOMUtils.defineLazyGetter(gSync, "weaveService", function() {
-  return Components.classes["@mozilla.org/weave/service;1"]
-                   .getService(Components.interfaces.nsISupports)
-                   .wrappedJSObject;
-});
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1320,15 +1320,15 @@ toolbarpaletteitem[place="palette"][hidd
 
 .dragfeedback-tab {
   -moz-appearance: none;
   opacity: 0.65;
   -moz-window-shadow: none;
 }
 
 /* Page action menu */
-#page-action-sendToDeviceView-body[signedin] > #page-action-sendToDevice-fxa-button,
-#page-action-sendToDeviceView-body:not([signedin]) > #page-action-no-devices-button,
-#page-action-sendToDeviceView-body[hasdevices] > #page-action-no-devices-button {
+#page-action-sendToDeviceView-body:not([state="notsignedin"]) > #page-action-sendToDevice-fxa-button,
+#page-action-sendToDeviceView-body:not([state="nodevice"]) > #page-action-no-devices-button,
+#page-action-sendToDeviceView-body:not([state="notready"]) > #page-action-sync-not-ready-button {
   display: none;
 }
 
 %include theme-vars.inc.css
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7842,47 +7842,65 @@ var gPageActionButton = {
   },
 
   emailLink() {
     this.panel.hidePopup();
     MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);
   },
 
   showSendToDeviceView(subviewButton) {
+    this.setupSendToDeviceView();
+    PanelUI.showSubView("page-action-sendToDeviceView", subviewButton);
+  },
+
+  setupSendToDeviceView() {
     let browser = gBrowser.selectedBrowser;
     let url = browser.currentURI.spec;
     let title = browser.contentTitle;
     let body = this.sendToDeviceBody;
 
+    // This is on top because it also clears the device list between state changes.
     gSync.populateSendTabToDevicesMenu(body, url, title, (clientId, name, clientType) => {
       if (!name) {
         return document.createElement("toolbarseparator");
       }
       let item = document.createElement("toolbarbutton");
       item.classList.add("page-action-sendToDevice-device", "subviewbutton");
       if (clientId) {
         item.classList.add("subviewbutton-iconic");
       }
       item.setAttribute("tooltiptext", name);
       return item;
     });
 
-    if (gSync.remoteClients.length) {
-      body.setAttribute("hasdevices", "true");
-    } else {
-      body.removeAttribute("hasdevices");
-    }
-
-    if (UIState.get().status == UIState.STATUS_SIGNED_IN) {
-      body.setAttribute("signedin", "true");
-    } else {
-      body.removeAttribute("signedin");
-    }
-
-    PanelUI.showSubView("page-action-sendToDeviceView", subviewButton);
+    if (!gSync.isSignedIn) {
+      // Could be unconfigured or unverified
+      body.setAttribute("state", "notsignedin");
+      return;
+    }
+
+    // In the first ~10 sec after startup, Sync may not be loaded and the list
+    // of devices will be empty.
+    if (!gSync.syncReady) {
+      body.setAttribute("state", "notready");
+      // Force a background Sync
+      Services.tm.dispatchToMainThread(() => {
+        Weave.Service.sync([]);  // [] = clients engine only
+        if (!window.closed && gSync.syncReady) {
+          this.setupSendToDeviceView();
+        }
+      });
+      return;
+    }
+    if (!gSync.remoteClients.length) {
+      body.setAttribute("state", "nodevice");
+      return;
+    }
+
+    body.setAttribute("state", "signedin");
   },
 
   fxaButtonClicked() {
     this.panel.hidePopup();
     gSync.openPrefs();
   },
 };
 
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -97,19 +97,19 @@
                 tbattr="tabbrowser-multiple"
                 oncommand="gBrowser.replaceTabWithWindow(TabContextMenu.contextTab);"/>
 #ifdef E10S_TESTING_ONLY
       <menuitem id="context_openNonRemoteWindow" label="Open in new non-e10s window"
                 tbattr="tabbrowser-remote"
                 hidden="true"
                 oncommand="gBrowser.openNonRemoteWindow(TabContextMenu.contextTab);"/>
 #endif
-      <menuseparator id="context_sendTabToDevice_separator" hidden="true"/>
+      <menuseparator id="context_sendTabToDevice_separator"/>
       <menu id="context_sendTabToDevice" label="&sendTabToDevice.label;"
-            accesskey="&sendTabToDevice.accesskey;" hidden="true">
+            accesskey="&sendTabToDevice.accesskey;">
         <menupopup id="context_sendTabToDevicePopupMenu"
                    onpopupshowing="gSync.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/>
       </menu>
       <menuseparator/>
       <menuitem id="context_reloadAllTabs" label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;"
                 tbattr="tabbrowser-multiple-visible"
                 oncommand="gBrowser.reloadAllTabs();"/>
       <menuitem id="context_bookmarkAllTabs"
@@ -489,16 +489,20 @@
                            class="subviewbutton subviewbutton-iconic"
                            label="&syncBrand.fxAccount.label;"
                            shortcut="&sendToDevice.fxaRequired.label;"
                            oncommand="gPageActionButton.fxaButtonClicked();"/>
             <toolbarbutton id="page-action-no-devices-button"
                            class="subviewbutton"
                            label="&sendToDevice.noDevices.label;"
                            disabled="true"/>
+            <toolbarbutton id="page-action-sync-not-ready-button"
+                           class="subviewbutton"
+                           label="&sendToDevice.syncNotReady.label;"
+                           disabled="true"/>
           </vbox>
         </panelview>
       </photonpanelmultiview>
     </panel>
 
     <!-- Bookmarks and history tooltip -->
     <tooltip id="bhTooltip"/>
 
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -68,17 +68,18 @@
       </popupnotificationcontent>
     </popupnotification>
 
     <popupnotification id="addon-install-confirmation-notification" hidden="true">
       <popupnotificationcontent id="addon-install-confirmation-content" orient="vertical"/>
     </popupnotification>
 
     <popupnotification id="addon-webext-permissions-notification" hidden="true">
-      <popupnotificationcontent orient="vertical">
+      <popupnotificationcontent class="addon-webext-perm-notification-content" orient="vertical">
+        <description id="addon-webext-perm-header" class="addon-webext-perm-header"/>
         <description id="addon-webext-perm-text" class="addon-webext-perm-text"/>
         <label id="addon-webext-perm-intro" class="addon-webext-perm-text"/>
         <html:ul id="addon-webext-perm-list" class="addon-webext-perm-list"/>
       </popupnotificationcontent>
     </popupnotification>
 
     <popupnotification id="addon-installed-notification" hidden="true">
       <popupnotificationcontent class="addon-installed-notification-content" orient="vertical">
--- a/browser/base/content/test/general/browser_contextmenu.js
+++ b/browser/base/content/test/general/browser_contextmenu.js
@@ -901,94 +901,16 @@ add_task(async function test_input_spell
      "context-delete",      false,
      "---",                 null,
      "context-selectall",   true,
     ]
   );
   */
 });
 
-const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
-
-add_task(async function test_plaintext_sendpagetodevice() {
-  if (!gSync.sendTabToDeviceEnabled) {
-    return;
-  }
-  await ensureSyncReady();
-  const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
-
-  let plainTextItemsWithSendPage =
-                    ["context-navigation",   null,
-                      ["context-back",         false,
-                        "context-forward",      false,
-                        "context-reload",       true,
-                        "context-bookmarkpage", true], null,
-                    "---",                  null,
-                    "context-savepage",     true,
-                    ...(hasPocket ? ["context-pocket", true] : []),
-                    "---",                  null,
-                    "context-sendpagetodevice", true,
-                      ["*Foo", true,
-                       "*Bar", true,
-                       "---", null,
-                       "*All Devices", true], null,
-                    "---",                  null,
-                    "context-viewbgimage",  false,
-                    "context-selectall",    true,
-                    "---",                  null,
-                    "context-viewsource",   true,
-                    "context-viewinfo",     true
-                   ];
-  await test_contextmenu("#test-text", plainTextItemsWithSendPage, {
-      maybeScreenshotsPresent: true,
-      async onContextMenuShown() {
-        await openMenuItemSubmenu("context-sendpagetodevice");
-      }
-    });
-
-  restoreRemoteClients(oldGetter);
-});
-
-add_task(async function test_link_sendlinktodevice() {
-  if (!gSync.sendTabToDeviceEnabled) {
-    return;
-  }
-  await ensureSyncReady();
-  const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
-
-  await test_contextmenu("#test-link",
-    ["context-openlinkintab", true,
-     ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
-     // We need a blank entry here because the containers submenu is
-     // dynamically generated with no ids.
-     ...(hasContainers ? ["", null] : []),
-     "context-openlink",      true,
-     "context-openlinkprivate", true,
-     "---",                   null,
-     "context-bookmarklink",  true,
-     "context-savelink",      true,
-     ...(hasPocket ? ["context-savelinktopocket", true] : []),
-     "context-copylink",      true,
-     "context-searchselect",  true,
-     "---",                  null,
-     "context-sendlinktodevice", true,
-      ["*Foo", true,
-       "*Bar", true,
-       "---", null,
-       "*All Devices", true], null,
-    ],
-    {
-      async onContextMenuShown() {
-        await openMenuItemSubmenu("context-sendlinktodevice");
-      }
-    });
-
-  restoreRemoteClients(oldGetter);
-});
-
 add_task(async function test_svg_link() {
   await test_contextmenu("#svg-with-link > a",
     ["context-openlinkintab", true,
      ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
      // We need a blank entry here because the containers submenu is
      // dynamically generated with no ids.
      ...(hasContainers ? ["", null] : []),
      "context-openlink",      true,
@@ -1057,15 +979,8 @@ async function selectText(selector) {
     let div = doc.createRange();
     let element = doc.querySelector(contentSelector);
     Assert.ok(element, "Found element to select text from");
     div.setStartBefore(element);
     div.setEndAfter(element);
     win.getSelection().addRange(div);
   });
 }
-
-function ensureSyncReady() {
-  let service = Cc["@mozilla.org/weave/service;1"]
-                  .getService(Components.interfaces.nsISupports)
-                  .wrappedJSObject;
-  return service.whenLoaded();
-}
--- a/browser/base/content/test/general/browser_visibleTabs_contextMenu.js
+++ b/browser/base/content/test/general/browser_visibleTabs_contextMenu.js
@@ -11,38 +11,16 @@ add_task(async function test() {
   let testTab = BrowserTestUtils.addTab(gBrowser);
   is(gBrowser.visibleTabs.length, 2, "there are now two visible tabs");
 
   // Check the context menu with two tabs
   updateTabContextMenu(origTab);
   is(document.getElementById("context_closeTab").disabled, false, "Close Tab is enabled");
   is(document.getElementById("context_reloadAllTabs").disabled, false, "Reload All Tabs is enabled");
 
-
-  if (gSync.sendTabToDeviceEnabled) {
-    const origIsSendableURI = gSync.isSendableURI;
-    gSync.isSendableURI = () => true;
-    // Check the send tab to device menu item
-    await ensureSyncReady();
-    const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
-    await updateTabContextMenu(origTab, async function() {
-      await openMenuItemSubmenu("context_sendTabToDevice");
-    });
-    is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
-    let targets = document.getElementById("context_sendTabToDevicePopupMenu").childNodes;
-    is(targets[0].getAttribute("label"), "Foo", "Foo target is present");
-    is(targets[1].getAttribute("label"), "Bar", "Bar target is present");
-    is(targets[3].getAttribute("label"), "All Devices", "All Devices target is present");
-    gSync.isSendableURI = () => false;
-    updateTabContextMenu(origTab);
-    is(document.getElementById("context_sendTabToDevice").hidden, true, "Send tab to device is hidden");
-    restoreRemoteClients(oldGetter);
-    gSync.isSendableURI = origIsSendableURI;
-  }
-
   // Hide the original tab.
   gBrowser.selectedTab = testTab;
   gBrowser.showOnlyTheseTabs([testTab]);
   is(gBrowser.visibleTabs.length, 1, "now there is only one visible tab");
 
   // Check the context menu with one tab.
   updateTabContextMenu(testTab);
   is(document.getElementById("context_closeTab").disabled, false, "Close Tab is enabled when more than one tab exists");
@@ -72,15 +50,8 @@ add_task(async function test() {
   // Close Tabs To The End should now be enabled
   updateTabContextMenu(origTab);
   is(document.getElementById("context_closeTabsToTheEnd").disabled, false, "Close Tabs To The End is enabled");
 
   gBrowser.removeTab(testTab);
   gBrowser.removeTab(pinned);
 });
 
-function ensureSyncReady() {
-  let service = Cc["@mozilla.org/weave/service;1"]
-                  .getService(Components.interfaces.nsISupports)
-                  .wrappedJSObject;
-  return service.whenLoaded();
-}
-
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -826,31 +826,8 @@ function getCertExceptionDialog(aLocatio
 
       if (childDoc.location.href == aLocation) {
         return childDoc;
       }
     }
   }
   return undefined;
 }
-
-function setupRemoteClientsFixture(fixture) {
-  let oldRemoteClientsGetter =
-    Object.getOwnPropertyDescriptor(gSync, "remoteClients").get;
-
-  Object.defineProperty(gSync, "remoteClients", {
-    get() { return fixture; }
-  });
-  return oldRemoteClientsGetter;
-}
-
-function restoreRemoteClients(getter) {
-  Object.defineProperty(gSync, "remoteClients", {
-    get: getter
-  });
-}
-
-async function openMenuItemSubmenu(id) {
-  let menuPopup = document.getElementById(id).menupopup;
-  let menuPopupPromise = BrowserTestUtils.waitForEvent(menuPopup, "popupshown");
-  menuPopup.showPopup();
-  await menuPopupPromise;
-}
--- a/browser/base/content/test/sync/browser.ini
+++ b/browser/base/content/test/sync/browser.ini
@@ -1,9 +1,15 @@
+[DEFAULT]
+support-files =
+  head.js
+
 [browser_sync.js]
+[browser_contextmenu_sendtab.js]
+[browser_contextmenu_sendpage.js]
 [browser_fxa_web_channel.js]
 support-files=
   browser_fxa_web_channel.html
 [browser_fxa_badge.js]
 [browser_aboutAccounts.js]
 skip-if = os == "linux" # Bug 958026
 support-files =
   content_aboutAccounts.js
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/browser_contextmenu_sendpage.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
+
+const origRemoteClients = mockReturn(gSync, "remoteClients", remoteClientsFixture);
+const origSyncReady = mockReturn(gSync, "syncReady", true);
+const origIsSendableURI = mockReturn(gSync, "isSendableURI", true);
+
+add_task(async function setup() {
+  await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+});
+
+add_task(async function test_page_contextmenu() {
+  await updateContentContextMenu("#moztext", "context-sendpagetodevice");
+  is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown");
+  is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled");
+  let devices = document.getElementById("context-sendpagetodevice-popup").childNodes;
+  is(devices[0].getAttribute("label"), "Foo", "Foo target is present");
+  is(devices[1].getAttribute("label"), "Bar", "Bar target is present");
+  is(devices[3].getAttribute("label"), "All Devices", "All Devices target is present");
+});
+
+add_task(async function test_page_contextmenu_notsendable() {
+  const isSendableURIMock = mockReturn(gSync, "isSendableURI", false);
+
+  await updateContentContextMenu("#moztext");
+  is(document.getElementById("context-sendpagetodevice").hidden, true, "Send tab to device is hidden");
+  is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled");
+
+  isSendableURIMock.restore();
+});
+
+add_task(async function test_page_contextmenu_sendtab_no_remote_clients() {
+  let remoteClientsMock = mockReturn(gSync, "remoteClients", []);
+
+  await updateContentContextMenu("#moztext");
+  is(document.getElementById("context-sendpagetodevice").hidden, true, "Send tab to device is hidden");
+  is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled");
+
+  remoteClientsMock.restore();
+});
+
+add_task(async function test_page_contextmenu_sync_not_ready() {
+  const syncReadyMock = mockReturn(gSync, "syncReady", false);
+
+  await updateContentContextMenu("#moztext");
+  is(document.getElementById("context-sendpagetodevice").hidden, true, "Send tab to device is hidden");
+  is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled");
+
+  syncReadyMock.restore();
+});
+
+// We are not going to bother testing the states of context-sendlinktodevice since they use
+// the exact same code.
+// However, browser_contextmenu.js contains tests that verify the menu item is present.
+
+add_task(async function cleanup() {
+  gBrowser.removeCurrentTab();
+  origSyncReady.restore();
+  origRemoteClients.restore();
+  origIsSendableURI.restore();
+});
+
+async function updateContentContextMenu(selector, openSubmenuId = null) {
+  let contextMenu = document.getElementById("contentAreaContextMenu");
+  is(contextMenu.state, "closed", "checking if popup is closed");
+
+  let awaitPopupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+  await BrowserTestUtils.synthesizeMouse(selector, 0, 0, {
+      type: "contextmenu",
+      button: 2,
+      shiftkey: false,
+      centered: true
+    },
+    gBrowser.selectedBrowser);
+  await awaitPopupShown;
+
+  if (openSubmenuId) {
+    let menuPopup = document.getElementById(openSubmenuId).menupopup;
+    let menuPopupPromise = BrowserTestUtils.waitForEvent(menuPopup, "popupshown");
+    menuPopup.showPopup();
+    await menuPopupPromise;
+  }
+
+  let awaitPopupHidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+
+  contextMenu.hidePopup();
+  await awaitPopupHidden;
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/browser_contextmenu_sendtab.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const chrome_base = "chrome://mochitests/content/browser/browser/base/content/test/general/";
+Services.scriptloader.loadSubScript(chrome_base + "head.js", this);
+/* import-globals-from ../general/head.js */
+
+const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
+
+const origRemoteClients = mockReturn(gSync, "remoteClients", remoteClientsFixture);
+const origSyncReady = mockReturn(gSync, "syncReady", true);
+const origIsSendableURI = mockReturn(gSync, "isSendableURI", true);
+let [testTab] = gBrowser.visibleTabs;
+
+add_task(async function setup() {
+  is(gBrowser.visibleTabs.length, 1, "there is one visible tab");
+});
+
+add_task(async function test_tab_contextmenu() {
+  await updateTabContextMenu(testTab, openSendTabTargetsSubmenu);
+  is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
+  is(document.getElementById("context_sendTabToDevice").disabled, false, "Send tab to device is enabled");
+  let devices = document.getElementById("context_sendTabToDevicePopupMenu").childNodes;
+  is(devices[0].getAttribute("label"), "Foo", "Foo target is present");
+  is(devices[1].getAttribute("label"), "Bar", "Bar target is present");
+  is(devices[3].getAttribute("label"), "All Devices", "All Devices target is present");
+});
+
+add_task(async function test_tab_contextmenu_only_one_remote_device() {
+  const remoteClientsMock = mockReturn(gSync, "remoteClients", [{ id: 1, name: "Foo"}]);
+
+  await updateTabContextMenu(testTab, openSendTabTargetsSubmenu);
+  is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
+  is(document.getElementById("context_sendTabToDevice").disabled, false, "Send tab to device is enabled");
+  let devices = document.getElementById("context_sendTabToDevicePopupMenu").childNodes;
+  is(devices.length, 1, "There should not be any separator or All Devices item");
+  is(devices[0].getAttribute("label"), "Foo", "Foo target is present");
+
+  remoteClientsMock.restore();
+});
+
+add_task(async function test_tab_contextmenu_not_sendable() {
+  const isSendableURIMock = mockReturn(gSync, "isSendableURI", false);
+
+  updateTabContextMenu(testTab);
+  is(document.getElementById("context_sendTabToDevice").hidden, true, "Send tab to device is hidden");
+  is(document.getElementById("context_sendTabToDevice").disabled, false, "Send tab to device is enabled");
+
+  isSendableURIMock.restore();
+});
+
+add_task(async function test_tab_contextmenu_no_remote_clients() {
+  let remoteClientsMock = mockReturn(gSync, "remoteClients", []);
+
+  updateTabContextMenu(testTab);
+  is(document.getElementById("context_sendTabToDevice").hidden, true, "Send tab to device is hidden");
+  is(document.getElementById("context_sendTabToDevice").disabled, false, "Send tab to device is enabled");
+
+  remoteClientsMock.restore();
+});
+
+add_task(async function test_tab_contextmenu_sync_not_ready() {
+  const syncReadyMock = mockReturn(gSync, "syncReady", false);
+
+  updateTabContextMenu(testTab);
+  is(document.getElementById("context_sendTabToDevice").hidden, true, "Send tab to device is hidden");
+  is(document.getElementById("context_sendTabToDevice").disabled, false, "Send tab to device is enabled");
+
+  syncReadyMock.restore();
+});
+
+add_task(async function cleanup() {
+  origSyncReady.restore();
+  origRemoteClients.restore();
+  origIsSendableURI.restore();
+});
+
+async function openSendTabTargetsSubmenu() {
+  let menuPopup = document.getElementById("context_sendTabToDevice").menupopup;
+  let menuPopupPromise = BrowserTestUtils.waitForEvent(menuPopup, "popupshown");
+  menuPopup.showPopup();
+  await menuPopupPromise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/head.js
@@ -0,0 +1,24 @@
+// Mocks a getter or a function
+// This is basically sinon.js (our in-tree version doesn't do getters :/) (see bug 1369855)
+function mockReturn(obj, symbol, fixture) {
+  let getter = Object.getOwnPropertyDescriptor(obj, symbol).get;
+  if (getter) {
+    Object.defineProperty(obj, symbol, {
+      get() { return fixture; }
+    });
+    return {
+      restore() {
+        Object.defineProperty(obj, symbol, {
+          get: getter
+        });
+      }
+    }
+  }
+  let func = obj[symbol];
+  obj[symbol] = () => fixture;
+  return {
+    restore() {
+      obj[symbol] = func;
+    }
+  }
+}
--- a/browser/base/content/test/urlbar/browser_page_action_menu.js
+++ b/browser/base/content/test/urlbar/browser_page_action_menu.js
@@ -1,12 +1,18 @@
 "use strict";
 
 let gPanel = document.getElementById("page-action-panel");
 
+const mockRemoteClients = [
+  { id: "0", name: "foo", type: "mobile" },
+  { id: "1", name: "bar", type: "desktop" },
+  { id: "2", name: "baz", type: "mobile" },
+];
+
 add_task(async function bookmark() {
   // Open a unique page.
   let url = "http://example.com/browser_page_action_menu";
   await BrowserTestUtils.withNewTab(url, async () => {
     // Open the panel.
     await promisePanelOpen();
 
     // The bookmark button should read "Bookmark This Page" and not be starred.
@@ -123,16 +129,122 @@ add_task(async function sendToDevice_non
       document.getElementById("page-action-send-to-device-button");
     Assert.ok(sendToDeviceButton.disabled);
     let hiddenPromise = promisePanelHidden();
     gPanel.hidePopup();
     await hiddenPromise;
   });
 });
 
+add_task(async function sendToDevice_syncNotReady() {
+  // Open a tab that's sendable.
+  await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+    let syncReadyMock = mockReturn(gSync, "syncReady", false);
+    let signedInMock = mockReturn(gSync, "isSignedIn", true);
+
+    let remoteClientsMock;
+    let origSync = Weave.Service.sync;
+    Weave.Service.sync = () => {
+      mockReturn(gSync, "syncReady", true);
+      remoteClientsMock = mockReturn(gSync, "remoteClients", mockRemoteClients);
+    };
+
+    let origSetupSendToDeviceView = gPageActionButton.setupSendToDeviceView;
+    gPageActionButton.setupSendToDeviceView = () => {
+      this.numCall++ || (this.numCall = 1);
+      origSetupSendToDeviceView.call(gPageActionButton);
+      testSendTabToDeviceMenu(this.numCall);
+    }
+
+    let cleanUp = () => {
+      Weave.Service.sync = origSync;
+      gPageActionButton.setupSendToDeviceView = origSetupSendToDeviceView;
+      signedInMock.restore();
+      syncReadyMock.restore();
+      remoteClientsMock.restore();
+    };
+    registerCleanupFunction(cleanUp);
+
+    // Open the panel.
+    await promisePanelOpen();
+    let sendToDeviceButton =
+      document.getElementById("page-action-send-to-device-button");
+    Assert.ok(!sendToDeviceButton.disabled);
+
+    // Click Send to Device.
+    let viewPromise = promiseViewShown();
+    EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
+    let view = await viewPromise;
+    Assert.equal(view.id, "page-action-sendToDeviceView");
+
+    function testSendTabToDeviceMenu(numCall) {
+      if (numCall == 1) {
+        // The Fxa button should be shown.
+        checkSendToDeviceItems([
+          {
+            id: "page-action-sendToDevice-fxa-button",
+            display: "none",
+          },
+          {
+            id: "page-action-no-devices-button",
+            display: "none",
+            disabled: true,
+          },
+          {
+            id: "page-action-sync-not-ready-button",
+            disabled: true,
+          },
+        ]);
+      } else if (numCall == 2) {
+        // The devices should be shown in the subview.
+        let expectedItems = [
+          {
+            id: "page-action-sendToDevice-fxa-button",
+            display: "none",
+          },
+          {
+            id: "page-action-no-devices-button",
+            display: "none",
+            disabled: true,
+          },
+          {
+            id: "page-action-sync-not-ready-button",
+            display: "none",
+            disabled: true,
+          },
+        ];
+        for (let client of mockRemoteClients) {
+          expectedItems.push({
+            attrs: {
+              clientId: client.id,
+              label: client.name,
+              clientType: client.type,
+            },
+          });
+        }
+        expectedItems.push(
+          null,
+          {
+            label: "All Devices",
+          }
+        );
+        checkSendToDeviceItems(expectedItems);
+      } else {
+        ok(false, "This should never happen");
+      }
+    }
+
+    // Done, hide the panel.
+    let hiddenPromise = promisePanelHidden();
+    gPanel.hidePopup();
+    await hiddenPromise;
+    cleanUp();
+  });
+});
+
 add_task(async function sendToDevice_notSignedIn() {
   // Open a tab that's sendable.
   await BrowserTestUtils.withNewTab("http://example.com/", async () => {
     await promiseSyncReady();
 
     // Open the panel.
     await promisePanelOpen();
     let sendToDeviceButton =
@@ -150,16 +262,21 @@ add_task(async function sendToDevice_not
       {
         id: "page-action-sendToDevice-fxa-button",
       },
       {
         id: "page-action-no-devices-button",
         display: "none",
         disabled: true,
       },
+      {
+        id: "page-action-sync-not-ready-button",
+        display: "none",
+        disabled: true,
+      },
     ]);
 
     // Click the Fxa button.
     let body = view.firstChild;
     let fxaButton = body.childNodes[0];
     Assert.equal(fxaButton.id, "page-action-sendToDevice-fxa-button");
     let prefsTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
     let hiddenPromise = promisePanelHidden();
@@ -201,16 +318,21 @@ add_task(async function sendToDevice_noD
       {
         id: "page-action-sendToDevice-fxa-button",
         display: "none",
       },
       {
         id: "page-action-no-devices-button",
         disabled: true,
       },
+      {
+        id: "page-action-sync-not-ready-button",
+        display: "none",
+        disabled: true,
+      },
     ]);
 
     // Done, hide the panel.
     let hiddenPromise = promisePanelHidden();
     gPanel.hidePopup();
     await hiddenPromise;
 
     await UIState.reset();
@@ -219,30 +341,19 @@ add_task(async function sendToDevice_noD
 
 add_task(async function sendToDevice_devices() {
   // Open a tab that's sendable.
   await BrowserTestUtils.withNewTab("http://example.com/", async () => {
     await promiseSyncReady();
     UIState._internal._state = { status: UIState.STATUS_SIGNED_IN };
 
     // Set up mock remote clients.
-    let mockRemoteClients = [
-      { id: "0", name: "foo", type: "mobile" },
-      { id: "1", name: "bar", type: "desktop" },
-      { id: "2", name: "baz", type: "mobile" },
-    ];
-    let originalGetter =
-      Object.getOwnPropertyDescriptor(gSync, "remoteClients").get;
-    Object.defineProperty(gSync, "remoteClients", {
-      get() { return mockRemoteClients; }
-    });
+    let remoteClientsMock = mockReturn(gSync, "remoteClients", mockRemoteClients);
     let cleanUp = () => {
-      Object.defineProperty(gSync, "remoteClients", {
-        get: originalGetter
-      });
+      remoteClientsMock.restore();
     };
     registerCleanupFunction(cleanUp);
 
     // Open the panel.
     await promisePanelOpen();
     let sendToDeviceButton =
       document.getElementById("page-action-send-to-device-button");
     Assert.ok(!sendToDeviceButton.disabled);
@@ -259,16 +370,21 @@ add_task(async function sendToDevice_dev
         id: "page-action-sendToDevice-fxa-button",
         display: "none",
       },
       {
         id: "page-action-no-devices-button",
         display: "none",
         disabled: true,
       },
+      {
+        id: "page-action-sync-not-ready-button",
+        display: "none",
+        disabled: true,
+      },
     ];
     for (let client of mockRemoteClients) {
       expectedItems.push({
         attrs: {
           clientId: client.id,
           label: client.name,
           clientType: client.type,
         },
@@ -376,8 +492,32 @@ function checkSendToDeviceItems(expected
     if ("attrs" in expected) {
       for (let name in expected.attrs) {
         Assert.ok(actual.hasAttribute(name));
         Assert.equal(actual.getAttribute(name), expected.attrs[name]);
       }
     }
   }
 }
+
+// Copied from test/sync/head.js (see bug 1369855)
+function mockReturn(obj, symbol, fixture) {
+  let getter = Object.getOwnPropertyDescriptor(obj, symbol).get;
+  if (getter) {
+    Object.defineProperty(obj, symbol, {
+      get() { return fixture; }
+    });
+    return {
+      restore() {
+        Object.defineProperty(obj, symbol, {
+          get: getter
+        });
+      }
+    }
+  }
+  let func = obj[symbol];
+  obj[symbol] = () => fixture;
+  return {
+    restore() {
+      obj[symbol] = func;
+    }
+  }
+}
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -18,16 +18,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource:///modules/ExtensionPopups.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
                                    "@mozilla.org/inspector/dom-utils;1",
                                    "inIDOMUtils");
 
 Cu.import("resource://gre/modules/EventEmitter.jsm");
 
+XPCOMUtils.defineLazyPreferenceGetter(this, "gPhotonStructure", "browser.photon.structure.enabled");
+
 var {
   DefaultWeakMap,
 } = ExtensionUtils;
 
 Cu.import("resource://gre/modules/ExtensionParent.jsm");
 
 var {
   IconDetails,
@@ -47,17 +49,17 @@ function isAncestorOrSelf(target, node) 
 }
 
 // WeakMap[Extension -> BrowserAction]
 const browserActionMap = new WeakMap();
 
 XPCOMUtils.defineLazyGetter(this, "browserAreas", () => {
   return {
     "navbar": CustomizableUI.AREA_NAVBAR,
-    "menupanel": CustomizableUI.AREA_PANEL,
+    "menupanel": gPhotonStructure ? CustomizableUI.AREA_FIXED_OVERFLOW_PANEL : CustomizableUI.AREA_PANEL,
     "tabstrip": CustomizableUI.AREA_TABSTRIP,
     "personaltoolbar": CustomizableUI.AREA_BOOKMARKS,
   };
 });
 
 this.browserAction = class extends ExtensionAPI {
   static for(extension) {
     return browserActionMap.get(extension);
@@ -213,17 +215,21 @@ this.browserAction = class extends Exten
       return;
     }
 
     // Popups are shown only if a popup URL is defined; otherwise
     // a "click" event is dispatched. This is done for compatibility with the
     // Google Chrome onClicked extension API.
     if (this.getProperty(tab, "popup")) {
       if (this.widget.areaType == CustomizableUI.TYPE_MENU_PANEL) {
-        await window.PanelUI.show();
+        if (gPhotonStructure) {
+          await window.document.getElementById("nav-bar").overflowable.show();
+        } else {
+          await window.PanelUI.show();
+        }
       }
 
       let event = new window.CustomEvent("command", {bubbles: true, cancelable: true});
       widget.node.dispatchEvent(event);
     } else {
       this.emit("click");
     }
   }
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -56,17 +56,17 @@
         "id": "Tab",
         "type": "object",
         "properties": {
           "id": {"type": "integer", "minimum": -1, "optional": true, "description": "The ID of the tab. Tab IDs are unique within a browser session. Under some circumstances a Tab may not be assigned an ID, for example when querying foreign tabs using the $(ref:sessions) API, in which case a session ID may be present. Tab ID can also be set to $(ref:tabs.TAB_ID_NONE) for apps and devtools windows."},
           "index": {"type": "integer", "minimum": -1, "description": "The zero-based index of the tab within its window."},
           "windowId": {"type": "integer", "minimum": 0, "description": "The ID of the window the tab is contained within."},
           "openerTabId": {"unsupported": true, "type": "integer", "minimum": 0, "optional": true, "description": "The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists."},
           "selected": {"type": "boolean", "description": "Whether the tab is selected.", "deprecated": "Please use $(ref:tabs.Tab.highlighted).", "unsupported": true},
-          "highlighted": {"type": "boolean", "description": "Whether the tab is highlighted."},
+          "highlighted": {"type": "boolean", "description": "Whether the tab is highlighted. Works as an alias of active"},
           "active": {"type": "boolean", "description": "Whether the tab is active in its window. (Does not necessarily mean the window is focused.)"},
           "pinned": {"type": "boolean", "description": "Whether the tab is pinned."},
           "audible": {"type": "boolean", "optional": true, "description": "Whether the tab has produced sound over the past couple of seconds (but it might not be heard if also muted). Equivalent to whether the speaker audio indicator is showing."},
           "mutedInfo": {"$ref": "MutedInfo", "optional": true, "description": "Current tab muted state and the reason for the last state change."},
           "url": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "title": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "favIconUrl": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
           "status": {"type": "string", "optional": true, "description": "Either <em>loading</em> or <em>complete</em>."},
@@ -469,17 +469,17 @@
               "muted": {
                 "type": "boolean",
                 "optional": true,
                 "description": "Whether the tabs are muted."
               },
               "highlighted": {
                 "type": "boolean",
                 "optional": true,
-                "description": "Whether the tabs are highlighted."
+                "description": "Whether the tabs are highlighted.  Works as an alias of active."
               },
               "currentWindow": {
                 "type": "boolean",
                 "optional": true,
                 "description": "Whether the tabs are in the $(topic:current-window)[current window]."
               },
               "lastFocusedWindow": {
                 "type": "boolean",
@@ -543,16 +543,17 @@
           }
         ]
       },
       {
         "name": "highlight",
         "type": "function",
         "description": "Highlights the given tabs.",
         "async": "callback",
+        "unsupported": "true",
         "parameters": [
           {
             "type": "object",
             "name": "highlightInfo",
             "properties": {
                "windowId": {
                  "type": "integer",
                  "optional": true,
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_area.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_area.js
@@ -1,15 +1,15 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 var browserAreas = {
   "navbar": CustomizableUI.AREA_NAVBAR,
-  "menupanel": CustomizableUI.AREA_PANEL,
+  "menupanel": getCustomizableUIPanelID(),
   "tabstrip": CustomizableUI.AREA_TABSTRIP,
   "personaltoolbar": CustomizableUI.AREA_BOOKMARKS,
 };
 
 async function testInArea(area) {
   let manifest = {
     "browser_action": {
       "browser_style": true,
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
@@ -290,34 +290,36 @@ add_task(async function testDetailsObjec
       await SpecialPowers.popPrefEnv();
     }
 
     if (!test.menuResolutions) {
       continue;
     }
 
 
-    // Test icon sizes in the menu panel.
-    CustomizableUI.addWidgetToArea(browserActionWidget.id,
-                                   CustomizableUI.AREA_PANEL);
+    if (!gPhotonStructure) {
+      // Test icon sizes in the menu panel.
+      CustomizableUI.addWidgetToArea(browserActionWidget.id,
+                                     CustomizableUI.AREA_PANEL);
 
-    await showBrowserAction(extension);
-    browserActionButton = browserActionWidget.forWindow(window).node;
+      await showBrowserAction(extension);
+      browserActionButton = browserActionWidget.forWindow(window).node;
 
-    for (let resolution of Object.keys(test.menuResolutions)) {
-      await SpecialPowers.pushPrefEnv({set: [[RESOLUTION_PREF, resolution]]});
+      for (let resolution of Object.keys(test.menuResolutions)) {
+        await SpecialPowers.pushPrefEnv({set: [[RESOLUTION_PREF, resolution]]});
 
-      is(window.devicePixelRatio, +resolution, "window has the required resolution");
+        is(window.devicePixelRatio, +resolution, "window has the required resolution");
 
-      let imageURL = test.menuResolutions[resolution];
-      is(getListStyleImage(browserActionButton), imageURL, `browser action has the correct menu image at ${resolution}x resolution`);
+        let imageURL = test.menuResolutions[resolution];
+        is(getListStyleImage(browserActionButton), imageURL, `browser action has the correct menu image at ${resolution}x resolution`);
 
-      await SpecialPowers.popPrefEnv();
-    }
+        await SpecialPowers.popPrefEnv();
+      }
 
-    await closeBrowserAction(extension);
+      await closeBrowserAction(extension);
 
-    CustomizableUI.addWidgetToArea(browserActionWidget.id,
-                                   CustomizableUI.AREA_NAVBAR);
+      CustomizableUI.addWidgetToArea(browserActionWidget.id,
+                                     CustomizableUI.AREA_NAVBAR);
+    }
   }
 
   await extension.unload();
 });
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js
@@ -228,10 +228,10 @@ async function testInArea(area) {
   is(view, null, "browserAction view removed from document");
 }
 
 add_task(async function testBrowserActionInToolbar() {
   await testInArea(CustomizableUI.AREA_NAVBAR);
 });
 
 add_task(async function testBrowserActionInPanel() {
-  await testInArea(CustomizableUI.AREA_PANEL);
+  await testInArea(getCustomizableUIPanelID());
 });
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js
@@ -143,21 +143,21 @@ async function testPopupSize(standardsMo
        `Window width should return to approximately its original value (${win.innerWidth} ~= ${innerWidth})`);
 
     await closeBrowserAction(extension, browserWin);
   }
 
 
   // Test the PanelUI panel for a menu panel button.
   let widget = getBrowserActionWidget(extension);
-  CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_PANEL);
+  CustomizableUI.addWidgetToArea(widget.id, getCustomizableUIPanelID());
 
   let browser = await openPanel(extension, browserWin);
 
-  let {panel} = browserWin.PanelUI;
+  let panel = gPhotonStructure ? browserWin.PanelUI.overflowPanel : browserWin.PanelUI.panel;
   let origPanelRect = panel.getBoundingClientRect();
 
   // Check that the panel is still positioned as expected.
   let checkPanelPosition = () => {
     is(panel.getAttribute("side"), arrowSide, "Panel arrow is positioned as expected");
 
     let panelRect = panel.getBoundingClientRect();
     if (arrowSide == "top") {
@@ -271,17 +271,17 @@ add_task(async function testBrowserActio
   await testPopupSize(false);
 });
 
 // Test that we still make reasonable maximum size calculations when the window
 // is close enough to the bottom of the screen that the menu panel opens above,
 // rather than below, its button.
 add_task(async function testBrowserActionMenuResizeBottomArrow() {
   const WIDTH = 800;
-  const HEIGHT = 300;
+  const HEIGHT = 80;
 
   let left = screen.availLeft + screen.availWidth - WIDTH;
   let top = screen.availTop + screen.availHeight - HEIGHT;
 
   let win = await BrowserTestUtils.openNewBrowserWindow();
 
   win.resizeTo(WIDTH, HEIGHT);
 
--- a/browser/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js
+++ b/browser/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js
@@ -106,17 +106,17 @@ add_task(async function test_execute_bro
 
 add_task(async function test_execute_browser_action_without_popup() {
   await testExecuteBrowserActionWithOptions();
 });
 
 add_task(async function test_execute_browser_action_in_hamburger_menu_with_popup() {
   await testExecuteBrowserActionWithOptions({
     withPopup: true,
-    inArea: CustomizableUI.AREA_PANEL,
+    inArea: getCustomizableUIPanelID(),
   });
 });
 
 add_task(async function test_execute_browser_action_in_hamburger_menu_without_popup() {
   await testExecuteBrowserActionWithOptions({
-    inArea: CustomizableUI.AREA_PANEL,
+    inArea: getCustomizableUIPanelID(),
   });
 });
--- a/browser/components/extensions/test/browser/browser_ext_popup_background.js
+++ b/browser/components/extensions/test/browser/browser_ext_popup_background.js
@@ -107,17 +107,17 @@ add_task(async function testPopupBackgro
     await testPanel(browser, true);
     await closeBrowserAction(extension);
   }
 
   {
     info("Test menu panel browserAction popup");
 
     let widget = getBrowserActionWidget(extension);
-    CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_PANEL);
+    CustomizableUI.addWidgetToArea(widget.id, getCustomizableUIPanelID());
 
     clickBrowserAction(extension);
     let browser = await awaitExtensionPanel(extension);
     await testPanel(browser, false);
     await closeBrowserAction(extension);
   }
 
   {
--- a/browser/components/extensions/test/browser/browser_ext_popup_corners.js
+++ b/browser/components/extensions/test/browser/browser_ext_popup_corners.js
@@ -72,17 +72,17 @@ add_task(async function testPopupBorderR
     await testPanel(browser);
     await closeBrowserAction(extension);
   }
 
   {
     info("Test menu panel browserAction popup");
 
     let widget = getBrowserActionWidget(extension);
-    CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_PANEL);
+    CustomizableUI.addWidgetToArea(widget.id, getCustomizableUIPanelID());
 
     clickBrowserAction(extension);
     let browser = await awaitExtensionPanel(extension);
     await testPanel(browser, false);
     await closeBrowserAction(extension);
   }
 
   {
--- a/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js
+++ b/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js
@@ -46,17 +46,17 @@ add_task(async function testStandaloneBr
 });
 
 add_task(async function testMenuPanelBrowserAction() {
   let extension = getExtension();
   await extension.startup();
   await extension.awaitMessage("pageAction ready");
 
   let widget = getBrowserActionWidget(extension);
-  CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_PANEL);
+  CustomizableUI.addWidgetToArea(widget.id, getCustomizableUIPanelID());
 
   clickBrowserAction(extension);
   let browser = await awaitExtensionPanel(extension);
   let panel = getPanelForNode(browser);
 
   await extension.unload();
 
   is(panel.state, "closed", "Panel should be closed");
--- a/browser/components/extensions/test/browser/browser_ext_themes_icons.js
+++ b/browser/components/extensions/test/browser/browser_ext_themes_icons.js
@@ -158,27 +158,29 @@ async function runTestWithIcons(icons) {
   }
 
   let extension = ExtensionTestUtils.loadExtension({manifest, files});
 
   await extension.startup();
 
   checkButtons(icons, ICON_INFO, "toolbar");
 
-  for (let button of ICON_INFO) {
-    if (button[2]) {
-      CustomizableUI.addWidgetToArea(button[2], CustomizableUI.AREA_PANEL);
+  if (!gPhotonStructure) {
+    for (let button of ICON_INFO) {
+      if (button[2]) {
+        CustomizableUI.addWidgetToArea(button[2], CustomizableUI.AREA_PANEL);
+      }
     }
-  }
 
-  await PanelUI.show();
+    await PanelUI.show();
 
-  checkButtons(icons, ICON_INFO, "panel");
+    checkButtons(icons, ICON_INFO, "panel");
 
-  await PanelUI.hide();
+    await PanelUI.hide();
+  }
 
   await extension.unload();
 
   for (let button of ICON_INFO) {
     verifyButtonWithoutCustomStyling(button[1],
       `The ${button[1]} should not have it's icon customized when the theme is unloaded`);
   }
 }
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -13,17 +13,17 @@
  *          openExtensionContextMenu closeExtensionContextMenu
  *          openActionContextMenu openSubmenu closeActionContextMenu
  *          openTabContextMenu closeTabContextMenu
  *          imageBuffer imageBufferFromDataURI
  *          getListStyleImage getPanelForNode
  *          awaitExtensionPanel awaitPopupResize
  *          promiseContentDimensions alterContent
  *          promisePrefChangeObserved openContextMenuInFrame
- *          promiseAnimationFrame
+ *          promiseAnimationFrame getCustomizableUIPanelID
  */
 
 const {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm", {});
 const {CustomizableUI} = Cu.import("resource:///modules/CustomizableUI.jsm", {});
 
 // We run tests under two different configurations, from browser.ini and
 // browser-remote.ini. When running from browser-remote.ini, the tests are
 // copied to the sub-directory "test-oop-extensions", which we detect here, and
@@ -196,37 +196,49 @@ var awaitExtensionPanel = async function
     promisePopupShown(getPanelForNode(browser)),
 
     awaitLoad && awaitBrowserLoaded(browser, awaitLoad),
   ]);
 
   return browser;
 };
 
+function getCustomizableUIPanelID() {
+  return gPhotonStructure ? CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
+                          : CustomizableUI.AREA_PANEL;
+}
+
 function getBrowserActionWidget(extension) {
   return CustomizableUI.getWidget(makeWidgetId(extension.id) + "-browser-action");
 }
 
 function getBrowserActionPopup(extension, win = window) {
   let group = getBrowserActionWidget(extension);
 
   if (group.areaType == CustomizableUI.TYPE_TOOLBAR) {
     return win.document.getElementById("customizationui-widget-panel");
   }
-  return win.PanelUI.panel;
+  return gPhotonStructure ? win.PanelUI.overflowPanel : win.PanelUI.panel;
 }
 
 var showBrowserAction = async function(extension, win = window) {
   let group = getBrowserActionWidget(extension);
   let widget = group.forWindow(win);
 
   if (group.areaType == CustomizableUI.TYPE_TOOLBAR) {
     ok(!widget.overflowed, "Expect widget not to be overflowed");
   } else if (group.areaType == CustomizableUI.TYPE_MENU_PANEL) {
-    await win.PanelUI.show();
+    // Show the right panel. After Photon is turned on permanently, this
+    // can be re-simplified. This is unfortunately easier than getting
+    // and using the panel (area) ID out of CustomizableUI for the widget.
+    if (gPhotonStructure) {
+      await win.document.getElementById("nav-bar").overflowable.show();
+    } else {
+      await win.PanelUI.show();
+    }
   }
 };
 
 var clickBrowserAction = async function(extension, win = window) {
   await promiseAnimationFrame(win);
   await showBrowserAction(extension, win);
 
   let widget = getBrowserActionWidget(extension).forWindow(win);
--- a/browser/components/extensions/test/mochitest/test_ext_all_apis.html
+++ b/browser/components/extensions/test/mochitest/test_ext_all_apis.html
@@ -26,17 +26,16 @@ let expectedBackgroundApisTargetSpecific
   "tabs.create",
   "tabs.detectLanguage",
   "tabs.duplicate",
   "tabs.executeScript",
   "tabs.get",
   "tabs.getCurrent",
   "tabs.getZoom",
   "tabs.getZoomSettings",
-  "tabs.highlight",
   "tabs.insertCSS",
   "tabs.move",
   "tabs.onActivated",
   "tabs.onAttached",
   "tabs.onCreated",
   "tabs.onDetached",
   "tabs.onHighlighted",
   "tabs.onMoved",
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -963,8 +963,9 @@ you can use these alternative items. Oth
 <!ENTITY updateRestart.panelUI.label2 "Restart to update &brandShorterName;">
 
 <!ENTITY pageActionButton.tooltip "Page actions">
 
 <!ENTITY sendToDevice.label "Send to Device…">
 <!ENTITY sendToDevice.viewTitle "Send to Device">
 <!ENTITY sendToDevice.fxaRequired.label "Required">
 <!ENTITY sendToDevice.noDevices.label "No Devices Available">
+<!ENTITY sendToDevice.syncNotReady.label "Syncing Devices…">
--- a/browser/modules/ExtensionsUI.jsm
+++ b/browser/modules/ExtensionsUI.jsm
@@ -372,20 +372,18 @@ this.ExtensionsUI = {
     }
 
     return result;
   },
 
   showPermissionsPrompt(browser, strings, icon, histkey) {
     function eventCallback(topic) {
       let doc = this.browser.ownerDocument;
-      if (topic == "shown") {
-        doc.getElementById("addon-webext-permissions-notification")
-           .description.innerHTML = strings.header;
-      } else if (topic == "showing") {
+      if (topic == "showing") {
+        doc.getElementById("addon-webext-perm-header").innerHTML = strings.header;
         let textEl = doc.getElementById("addon-webext-perm-text");
         textEl.innerHTML = strings.text;
         textEl.hidden = !strings.text;
 
         let listIntroEl = doc.getElementById("addon-webext-perm-intro");
         listIntroEl.value = strings.listIntro;
         listIntroEl.hidden = (strings.msgs.length == 0);
 
@@ -432,22 +430,17 @@ this.ExtensionsUI = {
             if (histkey) {
               this.histogram.add(histkey + "Rejected");
             }
             resolve(false);
           },
         },
       ];
 
-      // Get the text value of strings.header to pre-populate the header. This will get
-      // overwritten with the HTML version later.
-      let escapeHeader = browser.ownerDocument.createElement("div");
-      escapeHeader.innerHTML = strings.header;
-      win.PopupNotifications.show(browser, "addon-webext-permissions",
-                                  escapeHeader.textContent,
+      win.PopupNotifications.show(browser, "addon-webext-permissions", "",
                                   "addons-notification-icon",
                                   action, secondaryActions, popupOptions);
     });
   },
 
   showInstallNotification(target, addon) {
     let win = target.ownerGlobal;
     let popups = win.PopupNotifications;
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -490,29 +490,32 @@ html|*.addon-webext-perm-list {
   margin-block-end: 0;
   padding-inline-start: 10px;
 }
 
 .addon-webext-perm-text {
   margin-inline-start: 0;
 }
 
-.popup-notification-description[popupid="addon-webext-permissions"] {
-  margin: 0;
-  padding: 0;
-}
-
+.popup-notification-description[popupid="addon-webext-permissions"],
 .popup-notification-description[popupid="addon-installed"] {
   display: none;
 }
 
+.addon-webext-perm-notification-content,
 .addon-installed-notification-content {
   margin-top: 0;
 }
 
+#addon-webext-perm-header {
+  /* Align the text more closely with the icon by clearing some top line height. */
+  margin-top: -1px;
+  margin-inline-start: 0;
+}
+
 #addon-installed-notification-header {
   /* Align the text more closely with the icon by clearing some top line height. */
   margin-top: -1px;
 }
 
 .addon-webext-name {
   display: inline;
   font-weight: bold;
--- a/browser/themes/linux/downloads/indicator.css
+++ b/browser/themes/linux/downloads/indicator.css
@@ -17,17 +17,21 @@
   position: relative;
   /* The selected tab may overlap #downloads-indicator-notification */
   z-index: 5;
 }
 
 /*** Main indicator icon ***/
 
 #downloads-button {
+%ifdef MOZ_PHOTON_THEME
+  --downloads-indicator-image: url("chrome://browser/skin/download-arrow-with-bar.svg");
+%else
   --downloads-indicator-image: url("chrome://browser/skin/download.svg");
+%endif
 }
 
 #downloads-button[cui-areatype="toolbar"] > #downloads-indicator-anchor > #downloads-indicator-icon {
   background: var(--downloads-indicator-image) center no-repeat;
   -moz-context-properties: fill;
   fill: var(--toolbarbutton-icon-fill);
   width: 16px;
   height: 16px;
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1981,29 +1981,32 @@ html|*.addon-webext-perm-list {
   margin-block-end: 0;
   padding-inline-start: 10px;
 }
 
 .addon-webext-perm-text {
   margin-inline-start: 0;
 }
 
-.popup-notification-description[popupid="addon-webext-permissions"] {
-  margin: 0;
-  padding: 0;
-}
-
+.popup-notification-description[popupid="addon-webext-permissions"],
 .popup-notification-description[popupid="addon-installed"] {
   display: none;
 }
 
+.addon-webext-perm-notification-content,
 .addon-installed-notification-content {
   margin-top: 0;
 }
 
+#addon-webext-perm-header {
+  /* Align the text more closely with the icon by clearing some top line height. */
+  margin-top: -1px;
+  margin-inline-start: 0;
+}
+
 #addon-installed-notification-header {
   /* Align the text more closely with the icon by clearing some top line height. */
   margin-top: -1px;
 }
 
 .addon-webext-name {
   display: inline;
   font-weight: bold;
--- a/browser/themes/osx/downloads/indicator.css
+++ b/browser/themes/osx/downloads/indicator.css
@@ -22,17 +22,21 @@
   position: relative;
   /* The selected tab may overlap #downloads-indicator-notification */
   z-index: 5;
 }
 
 /*** Main indicator icon ***/
 
 #downloads-button {
+%ifdef MOZ_PHOTON_THEME
+  --downloads-indicator-image: url("chrome://browser/skin/download-arrow-with-bar.svg");
+%else
   --downloads-indicator-image: url("chrome://browser/skin/download.svg");
+%endif
 }
 
 #downloads-indicator-icon {
   background: var(--downloads-indicator-image) center no-repeat;
   -moz-context-properties: fill;
   fill: var(--toolbarbutton-icon-fill);
   background-size: 16px;
 }
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/icons/download-arrow-with-bar.svg
@@ -0,0 +1,6 @@
+<!-- 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">
+    <path fill="context-fill" d="M13 14H3a1 1 0 0 0 0 2h10a1 1 0 0 0 0-2zm-5.707-1.275a1 1 0 0 0 1.414 0l5-5a1 1 0 0 0-1.414-1.413L9 9.605V1.019a1 1 0 0 0-2 0v8.586L3.707 6.312a1 1 0 0 0-1.414 1.413l5 5z"/>
+</svg>
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -113,17 +113,21 @@
   skin/classic/browser/characterEncoding.svg          (../shared/icons/characterEncoding.svg)
   skin/classic/browser/chevron.svg                    (../shared/icons/chevron.svg)
   skin/classic/browser/check.svg                      (../shared/icons/check.svg)
   skin/classic/browser/containers.svg                 (../shared/icons/containers.svg)
   skin/classic/browser/customize.svg                  (../shared/icons/customize.svg)
   skin/classic/browser/developer.svg                  (../shared/icons/developer.svg)
   skin/classic/browser/device-mobile.svg              (../shared/icons/device-mobile.svg)
   skin/classic/browser/device-desktop.svg             (../shared/icons/device-desktop.svg)
+#ifdef MOZ_PHOTON_THEME
+  skin/classic/browser/download-arrow-with-bar.svg    (../shared/icons/download-arrow-with-bar.svg)
+#else
   skin/classic/browser/download.svg                   (../shared/icons/download.svg)
+#endif
   skin/classic/browser/edit-copy.svg                  (../shared/icons/edit-copy.svg)
   skin/classic/browser/edit-cut.svg                   (../shared/icons/edit-cut.svg)
   skin/classic/browser/edit-paste.svg                 (../shared/icons/edit-paste.svg)
   skin/classic/browser/email-link.svg                 (../shared/icons/email-link.svg)
   skin/classic/browser/feed.svg                       (../shared/icons/feed.svg)
   skin/classic/browser/find.svg                       (../shared/icons/find.svg)
   skin/classic/browser/forget.svg                     (../shared/icons/forget.svg)
   skin/classic/browser/forward.svg                    (../shared/icons/forward.svg)
--- a/browser/themes/shared/menuPanel.svg
+++ b/browser/themes/shared/menuPanel.svg
@@ -26,17 +26,21 @@
   <path id="new-window" d="M510,29H482a1,1,0,0,1-1-1V4a1,1,0,0,1,1-1h28a1,1,0,0,1,1,1V28A1,1,0,0,1,510,29ZM499,5.015a0.991,0.991,0,1,0,1,.99A0.995,0.995,0,0,0,499,5.015Zm3,0a0.991,0.991,0,1,0,1,.99A0.995,0.995,0,0,0,502,5.015ZM507,5h-2a1,1,0,0,0,0,2h2A1,1,0,0,0,507,5Zm1,5a1,1,0,0,0-1-1H485a1,1,0,0,0-1,1V25a1,1,0,0,0,1,1h22a1,1,0,0,0,1-1V10Z"/>
   <path id="encoding" d="M474,30H454a4,4,0,0,1-4-4V6a4,4,0,0,1,4-4h20a4,4,0,0,1,4,4V26A4,4,0,0,1,474,30Zm-1-19a6,6,0,0,0-6-6h-8a6,6,0,0,0-6,6v8a6,6,0,0,0,6,6h8c3.314,0,6-.686,6-4V11Zm-5.953,6.863a7.6,7.6,0,0,0,1.655-.171,7.822,7.822,0,0,0,1.587-.552v1.445a8.416,8.416,0,0,1-1.567.532,8.014,8.014,0,0,1-1.714.161A4.231,4.231,0,0,1,462.964,17a4.931,4.931,0,0,1-1.753,1.758,4.724,4.724,0,0,1-2.271.518,3.547,3.547,0,0,1-2.5-.83,3,3,0,0,1-.9-2.325,2.846,2.846,0,0,1,1.211-2.447,6.7,6.7,0,0,1,3.692-.952l1.8-.059V12a2.632,2.632,0,0,0-.566-1.86,2.271,2.271,0,0,0-1.729-.6,6.575,6.575,0,0,0-3,.82l-0.508-1.24a7.934,7.934,0,0,1,3.623-.918,4.438,4.438,0,0,1,2.076.425,2.656,2.656,0,0,1,1.206,1.353A3.647,3.647,0,0,1,464.7,8.653a3.833,3.833,0,0,1,1.909-.469,3.787,3.787,0,0,1,3.008,1.3,5.1,5.1,0,0,1,1.133,3.472V14H463.9Q463.98,17.863,467.047,17.863ZM462.2,13.819l-1.543.068a5.31,5.31,0,0,0-2.617.611,1.837,1.837,0,0,0-.8,1.646,1.673,1.673,0,0,0,.522,1.363,2.092,2.092,0,0,0,1.382.435,3.013,3.013,0,0,0,2.237-.825,3.16,3.16,0,0,0,.82-2.329V13.819Zm6.808-1.114a3.81,3.81,0,0,0-.625-2.344,2.124,2.124,0,0,0-1.8-.82,2.3,2.3,0,0,0-1.861.811,4.028,4.028,0,0,0-.786,2.354h5.069Z"/>
   <path id="share" d="M433.425,19.753l-0.658.08,0-.08L443.07,4.742l-13.5,15.01,0.154,0.45-0.494.06,0.547,0.094,3.651,10.653L426.77,20.562l-8.777,1.067L445,1V25.188Zm0,1.231,4.938,2.986-4.938,7.04-0.6-10.129Z"/>
   <path id="feed" d="M412.68,29.958l-3.1.031a1.516,1.516,0,0,1-1.538-1.516s0.687-7.114-6.308-14.356c-5.1-6.065-14.151-6.358-14.151-6.358a1.517,1.517,0,0,1-1.6-1.451l0.031-2.833a1.463,1.463,0,0,1,1.538-1.451s12.628,0.807,19.264,8.856c6.554,6.143,7.213,17.593,7.213,17.593A1.337,1.337,0,0,1,412.68,29.958Zm-25.159-18s7.416,0.88,11.585,4.753c4.264,3.961,4.9,11.794,4.9,11.794,0,0.832-.112,1.474-0.952,1.474l-2.852-.031a1.321,1.321,0,0,1-1.235-1.537,12.715,12.715,0,0,0-3.786-8.6c-2.877-2.641-7.694-2.8-7.694-2.8a1.437,1.437,0,0,1-1.521-1.412L386,13.371A1.436,1.436,0,0,1,387.521,11.96Zm2.488,10.03a4.012,4.012,0,1,1-4,4.012A4,4,0,0,1,390.009,21.989Z"/>
   <path id="sync" d="M381.914,17.518a13.937,13.937,0,0,1-.8,3.367,10.892,10.892,0,0,1-5.084,6.587,23.381,23.381,0,0,0,2.531,1.884,51.867,51.867,0,0,1-8.176.671c-0.073.012-.145-0.233-0.218-0.221l-0.009.219a19.383,19.383,0,0,1-5.989-1.271,10.818,10.818,0,0,0,3.225-4.19,16.7,16.7,0,0,0,1.216-6.063,36.351,36.351,0,0,0,2.73,4.119,8.152,8.152,0,0,0,4.263-6.1,7.53,7.53,0,0,0-1.165-4.689,7.645,7.645,0,0,0-3.463-2.839c0.461-.872,1-1.847,1.513-2.674a7.385,7.385,0,0,1,2.559-2.383A13.959,13.959,0,0,1,381.914,17.518ZM367.96,13.509s-2.271-2.971-3.244-4.054a8.006,8.006,0,0,0-4.306,7.011,7.6,7.6,0,0,0,4.837,6.526,11.93,11.93,0,0,1-1.982,2.818,21.3,21.3,0,0,1-2.45,2.158,13.955,13.955,0,0,1-5.641-17.528,10.883,10.883,0,0,1,4.232-5.453c0.189-.147.382-0.287,0.577-0.424-0.8-.739-3.667-1.049-3.667-1.049s5.431-2.093,13.959-1.16C367.87,6.295,367.96,13.509,367.96,13.509Z"/>
   <path id="save" d="M346.25,30h-20.5A1.755,1.755,0,0,1,324,28.25V3.75A1.755,1.755,0,0,1,325.75,2h13.5a5.164,5.164,0,0,1,3.033,1.19L346.717,7.3A4.6,4.6,0,0,1,348,10.241V28.25A1.755,1.755,0,0,1,346.25,30ZM345.774,9.293l-5-4.586C340.347,4.318,340,4.45,340,5v5h5.455C346.055,10,346.2,9.682,345.774,9.293Z"/>
   <path id="open" d="M319.749,13.924a67.491,67.491,0,0,0-1.34,7.977,37.552,37.552,0,0,0-.4,6.4,0.708,0.708,0,0,1-.714.7H290.679a0.709,0.709,0,0,1-.715-0.7,37.552,37.552,0,0,0-.4-6.4,67.491,67.491,0,0,0-1.34-7.977C287.973,12.779,288.606,12,289,12h29.974C319.368,12,320,12.779,319.749,13.924Zm-29.682-6.9h-0.076V5.019a1.987,1.987,0,0,1,1.968-2.006h8.105c1.087,0,2.276,1.755,2.276,1.755l1.635,2.222,13-.009a1.012,1.012,0,0,1,1.025,1V11H290.048Z"/>
   <path id="addOns" d="M277.051,30.97a1.987,1.987,0,0,0,1.977-2V21.86s0.3-1.829,1.515-1.829,1.088,1.934,3.356,1.934c1.133,0,3.085-.581,3.085-4.082s-1.952-3.924-3.085-3.924c-2.268,0-2.138,1.828-3.356,1.828s-1.515-1.881-1.515-1.881V10.994a1.988,1.988,0,0,0-1.977-2h-5.2s-1.725-.3-1.725-1.515,1.882-1.3,1.882-3.565c0-1.131-.632-2.926-4.135-2.926s-3.977,1.8-3.977,2.926c0,2.268,1.724,2.349,1.724,3.565S263.9,8.993,263.9,8.993h-4.951a1.989,1.989,0,0,0-1.976,2l0,3.906s-0.211,3.015,2.213,3.015c1.528,0,1.732-2.057,3.742-2.057,1,0,2.019.941,2.019,3.02S263.932,22,262.932,22c-2.01,0-2.214-2.055-3.742-2.055-2.424,0-2.213,2.909-2.213,2.909l0,6.115a1.988,1.988,0,0,0,1.976,2h6.638s3.154,0.212,3.154-2.214c0-1.528-1.991-1.824-1.991-3.835,0-1,1.109-2.238,3.19-2.238s3.314,1.238,3.314,2.238c0,2.012-1.928,2.307-1.928,3.835,0,2.425,3.154,2.214,3.154,2.214h2.572Z"/>
+#ifdef MOZ_PHOTON_THEME
+  <path id="downloads" d="M248.7 26.1h-17.5c-1 0-1.8.8-1.8 1.8s.8 1.8 1.8 1.8h17.5c1 0 1.8-.8 1.8-1.8s-.8-1.8-1.8-1.8zm-10-2.2c.7.7 1.8.7 2.5 0l8.8-8.8c.7-.7.6-1.8-.1-2.5-.7-.6-1.7-.6-2.4 0l-5.8 5.8v-15c0-1-.8-1.8-1.8-1.8s-1.8.8-1.8 1.8v15l-5.8-5.8c-.7-.7-1.8-.6-2.5.1-.6.7-.6 1.7 0 2.4l8.9 8.8z"/>
+#else
   <path id="downloads" d="M253.285,18.118L242.09,29.126a3.008,3.008,0,0,1-4.242,0L226.59,18.118c-1.166-1.166-.772-2.121.879-2.121h6.5l0.062-12a2.027,2.027,0,0,1,2.032-2H244a2,2,0,0,1,2,2V16h6.406C254.057,16,254.451,16.952,253.285,18.118Z"/>
+#endif
   <path id="history" d="M208.007,30.007a14,14,0,1,1,14-14A14,14,0,0,1,208.007,30.007Zm0-24.007a10.008,10.008,0,1,0,10,10.008A10,10,0,0,0,208.007,6ZM206.1,15.9V10.412a1.829,1.829,0,0,1,1.829-1.829,1.951,1.951,0,0,1,1.965,1.829v5.032a22.977,22.977,0,0,1,3.52,5.939s-4.106-1.8-6.059-3.773A1.811,1.811,0,0,1,206.1,15.9Z"/>
   <path id="bookmark-filled" d="M188.4,11.546l-2.241-.371-5.3-.872-1.354-2.728v0l-1.09-2.192-1.088-2.2c-0.743-1.5-1.96-1.5-2.7,0l-1.089,2.2-1.088,2.192v0L171.1,10.3l-5.295.872-2.242.371c-1.677.275-2.093,1.49-.928,2.7l5.452,5.634-0.834,5.464L166.879,27.8c-0.253,1.643.766,2.348,2.264,1.576L171.2,28.3l2.051-1.071a0.007,0.007,0,0,0,.005,0l2.726-1.427,2.725,1.427a0.016,0.016,0,0,0,.007,0l2.048,1.071,2.06,1.082c1.5,0.772,2.514.068,2.266-1.576l-0.376-2.461-0.828-5.464,5.444-5.628C190.5,13.037,190.08,11.821,188.4,11.546Z"/>
   <path id="Bookmark-hollow" d="M144,8.365l1.725,3.526,0.79,1.616,1.773,0.3,4.069,0.681-3.007,3.153-1.182,1.24,0.254,1.7,0.63,4.207-3.426-1.821-1.639-.871-1.639.871-3.423,1.819,0.632-4.2,0.255-1.7-1.184-1.241-3-3.15,4.111-.683,1.79-.3,0.787-1.636L144,8.365M143.984,2a1.671,1.671,0,0,0-1.351,1.139l-3.472,7.213-7.582,1.259c-1.675.279-2.091,1.509-.926,2.735l5.445,5.709-1.207,8.031c-0.183,1.207.3,1.914,1.151,1.914a2.448,2.448,0,0,0,1.111-.317l6.832-3.631,6.832,3.631a2.447,2.447,0,0,0,1.11.317c0.85,0,1.333-.707,1.152-1.914l-1.2-8.031,5.438-5.7c1.165-1.229.749-2.461-.926-2.74l-7.527-1.259-3.527-7.213A1.668,1.668,0,0,0,143.984,2h0Z"/>
   <path id="home" d="M124,16L112,6,100,16H96L112,2l16,14h-4Zm-2,0v13.96h-8V20h-4v9.96h-8V16l10-8Z"/>
   <path id="stop" d="M93.121,24.879l-4.243,4.243-8.9-8.9L71.206,29l-4.2-4.2,8.774-8.774-8.9-8.9,4.243-4.243,8.9,8.9L88.794,3l4.2,4.2L84.222,15.98Z"/>
   <path id="reload" d="M62,14a2,2,0,0,1-2,2H48l5.833-5.833a8.993,8.993,0,1,0,1,12.686l3.035,2.6A13,13,0,1,1,56.669,7.331L62,2V14Z"/>
   <path id="placeholder" fill-rule="evenodd" d="M16,0A16,16,0,1,1,0,16,16,16,0,0,1,16,0ZM8,24V8H24V24H8Zm14-2h0Zm-2.121,0L16,18.121,12.121,22h7.757Zm-6-6L10,12.121v7.757Zm-1.757-6L16,13.879,19.879,10H12.121Zm6,6L22,19.879V12.121Z"/>
 </svg>
--- a/browser/themes/shared/toolbarbutton-icons.inc.css
+++ b/browser/themes/shared/toolbarbutton-icons.inc.css
@@ -66,17 +66,21 @@ toolbar:not([brighttext]) #bookmarks-men
   list-style-image: url("chrome://browser/skin/bookmarksMenu.svg");
 }
 
 #history-panelmenu[cui-areatype="toolbar"] {
   list-style-image: url("chrome://browser/skin/history.svg");
 }
 
 #downloads-button[cui-areatype="toolbar"] {
+%ifdef MOZ_PHOTON_THEME
+  list-style-image: url("chrome://browser/skin/download-arrow-with-bar.svg");
+%else
   list-style-image: url("chrome://browser/skin/download.svg");
+%endif
 }
 
 #add-ons-button[cui-areatype="toolbar"] {
   list-style-image: url("chrome://browser/skin/addons.svg");
 }
 
 #open-file-button[cui-areatype="toolbar"] {
   list-style-image: url("chrome://browser/skin/open.svg");
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1588,29 +1588,32 @@ html|*.addon-webext-perm-list {
   margin-block-end: 0;
   padding-inline-start: 10px;
 }
 
 .addon-webext-perm-text {
   margin-inline-start: 0;
 }
 
-.popup-notification-description[popupid="addon-webext-permissions"] {
-  margin: 0;
-  padding: 0;
-}
-
+.popup-notification-description[popupid="addon-webext-permissions"],
 .popup-notification-description[popupid="addon-installed"] {
   display: none;
 }
 
+.addon-webext-perm-notification-content,
 .addon-installed-notification-content {
   margin-top: 0;
 }
 
+#addon-webext-perm-header {
+  /* Align the text more closely with the icon by clearing some top line height. */
+  margin-top: -1px;
+  margin-inline-start: 0;
+}
+
 #addon-installed-notification-header {
   /* Align the text more closely with the icon by clearing some top line height. */
   margin-top: -1px;
 }
 
 .addon-webext-name {
   display: inline;
   font-weight: bold;
--- a/browser/themes/windows/downloads/indicator.css
+++ b/browser/themes/windows/downloads/indicator.css
@@ -17,17 +17,21 @@
   position: relative;
   /* The selected tab may overlap #downloads-indicator-notification */
   z-index: 5;
 }
 
 /*** Main indicator icon ***/
 
 #downloads-button {
+%ifdef MOZ_PHOTON_THEME
+  --downloads-indicator-image: url("chrome://browser/skin/download-arrow-with-bar.svg");
+%else
   --downloads-indicator-image: url("chrome://browser/skin/download.svg");
+%endif
 }
 
 #downloads-indicator-icon {
   background: var(--downloads-indicator-image) center no-repeat;
   -moz-context-properties: fill;
   fill: var(--toolbarbutton-icon-fill);
   width: 16px;
   height: 16px;
--- a/devtools/client/framework/devtools.js
+++ b/devtools/client/framework/devtools.js
@@ -11,16 +11,21 @@ const {DevToolsShim} = Cu.import("chrome
 
 // Load gDevToolsBrowser toolbox lazily as they need gDevTools to be fully initialized
 loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
 loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
 loader.lazyRequireGetter(this, "ToolboxHostManager", "devtools/client/framework/toolbox-host-manager", true);
 loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
 loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
 
+// Dependencies required for addon sdk compatibility layer.
+loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
+loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
+loader.lazyImporter(this, "BrowserToolboxProcess", "resource://devtools/client/framework/ToolboxProcess.jsm");
+
 const {defaultTools: DefaultTools, defaultThemes: DefaultThemes} =
   require("devtools/client/definitions");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {JsonView} = require("devtools/client/jsonview/main");
 const AboutDevTools = require("devtools/client/framework/about-devtools-toolbox");
 const {Task} = require("devtools/shared/task");
 const {getTheme, setTheme, addThemeObserver, removeThemeObserver} =
   require("devtools/client/shared/theme");
@@ -529,16 +534,52 @@ DevTools.prototype = {
    *
    * @return {TabTarget} A target object
    */
   getTargetForTab: function (tab) {
     return TargetFactory.forTab(tab);
   },
 
   /**
+   * Compatibility layer for addon-sdk. Remove when Firefox 57 hits release.
+   * Initialize the debugger server if needed and and create a connection.
+   *
+   * @return {DebuggerTransport} a client-side DebuggerTransport for communicating with
+   *         the created connection.
+   */
+  connectDebuggerServer: function () {
+    if (!DebuggerServer.initialized) {
+      DebuggerServer.init();
+      DebuggerServer.addBrowserActors();
+    }
+
+    return DebuggerServer.connectPipe();
+  },
+
+  /**
+   * Compatibility layer for addon-sdk. Remove when Firefox 57 hits release.
+   *
+   * Create a connection to the debugger server and return a debugger client for this
+   * new connection.
+   */
+  createDebuggerClient: function () {
+    let transport = this.connectDebuggerServer();
+    return new DebuggerClient(transport);
+  },
+
+  /**
+   * Compatibility layer for addon-sdk. Remove when Firefox 57 hits release.
+   *
+   * Create a BrowserToolbox process linked to the provided addon id.
+   */
+  initBrowserToolboxProcessForAddon: function (addonID) {
+    BrowserToolboxProcess.init({ addonID });
+  },
+
+  /**
    * Either the SDK Loader has been destroyed by the add-on contribution
    * workflow, or firefox is shutting down.
 
    * @param {boolean} shuttingDown
    *        True if firefox is currently shutting down. We may prevent doing
    *        some cleanups to speed it up. Otherwise everything need to be
    *        cleaned up in order to be able to load devtools again.
    */
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -176,16 +176,20 @@ Inspector.prototype = {
   get canGetUniqueSelector() {
     return this._target.client.traits.getUniqueSelector;
   },
 
   get canGetCssPath() {
     return this._target.client.traits.getCssPath;
   },
 
+  get canGetXPath() {
+    return this._target.client.traits.getXPath;
+  },
+
   get canGetUsedFontFaces() {
     return this._target.client.traits.getUsedFontFaces;
   },
 
   get canPasteInnerOrAdjacentHTML() {
     return this._target.client.traits.pasteHTML;
   },
 
@@ -1241,16 +1245,25 @@ Inspector.prototype = {
       label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"),
       accesskey:
         INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"),
       disabled: !isSelectionElement,
       hidden: !this.canGetCssPath,
       click: () => this.copyCssPath(),
     }));
     copySubmenu.append(new MenuItem({
+      id: "node-menu-copyxpath",
+      label: INSPECTOR_L10N.getStr("inspectorCopyXPath.label"),
+      accesskey:
+        INSPECTOR_L10N.getStr("inspectorCopyXPath.accesskey"),
+      disabled: !isSelectionElement,
+      hidden: !this.canGetXPath,
+      click: () => this.copyXPath(),
+    }));
+    copySubmenu.append(new MenuItem({
       id: "node-menu-copyimagedatauri",
       label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"),
       disabled: !isSelectionElement || !markupContainer ||
                 !markupContainer.isPreviewable(),
       click: () => this.copyImageDataUri(),
     }));
 
     return copySubmenu;
@@ -1798,16 +1811,30 @@ Inspector.prototype = {
 
     this.telemetry.toolOpened("copyfullcssselector");
     this.selection.nodeFront.getCssPath().then(path => {
       clipboardHelper.copyString(path);
     }).catch(e => console.error);
   },
 
   /**
+   * Copy the XPath of the selected Node to the clipboard.
+   */
+  copyXPath: function () {
+    if (!this.selection.isNode()) {
+      return;
+    }
+
+    this.telemetry.toolOpened("copyxpath");
+    this.selection.nodeFront.getXPath().then(path => {
+      clipboardHelper.copyString(path);
+    }).catch(e => console.error);
+  },
+
+  /**
    * Initiate gcli screenshot command on selected node.
    */
   screenshotNode: Task.async(function* () {
     const command = Services.prefs.getBoolPref("devtools.screenshot.clipboard.enabled") ?
       "screenshot --file --clipboard --selector" :
       "screenshot --file --selector";
 
     // Bug 1332936 - it's possible to call `screenshotNode` while the BoxModel highlighter
--- a/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js
+++ b/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js
@@ -22,16 +22,17 @@ const ACTIVE_ON_DOCTYPE_ITEMS = [
 ];
 
 const ALL_MENU_ITEMS = [
   "node-menu-edithtml",
   "node-menu-copyinner",
   "node-menu-copyouter",
   "node-menu-copyuniqueselector",
   "node-menu-copycsspath",
+  "node-menu-copyxpath",
   "node-menu-copyimagedatauri",
   "node-menu-delete",
   "node-menu-pseudo-hover",
   "node-menu-pseudo-active",
   "node-menu-pseudo-focus",
   "node-menu-scrollnodeintoview",
   "node-menu-screenshotnode",
   "node-menu-add-attribute",
--- a/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js
+++ b/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js
@@ -3,16 +3,18 @@
 http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that the various copy items in the context menu works correctly.
 
 const TEST_URL = URL_ROOT + "doc_inspector_menu.html";
 const SELECTOR_UNIQUE = "devtools.copy.unique.css.selector.opened";
 const SELECTOR_FULL = "devtools.copy.full.css.selector.opened";
+const XPATH = "devtools.copy.xpath.opened";
+
 const COPY_ITEMS_TEST_DATA = [
   {
     desc: "copy inner html",
     id: "node-menu-copyinner",
     selector: "[data-id=\"copy\"]",
     text: "Paragraph for testing copy",
   },
   {
@@ -29,16 +31,22 @@ const COPY_ITEMS_TEST_DATA = [
   },
   {
     desc: "copy CSS path",
     id: "node-menu-copycsspath",
     selector: "[data-id=\"copy\"]",
     text: "html body div p",
   },
   {
+    desc: "copy XPath",
+    id: "node-menu-copyxpath",
+    selector: "[data-id=\"copy\"]",
+    text: "/html/body/div/p[1]",
+  },
+  {
     desc: "copy image data uri",
     id: "node-menu-copyimagedatauri",
     selector: "#copyimage",
     text: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" +
       "AAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==",
   },
 ];
 
@@ -68,20 +76,23 @@ function checkTelemetryResults(Telemetry
   for (let key in data) {
     if (key.toLowerCase() === key) {
       let pings = data[key].length;
 
       results.set(key, pings);
     }
   }
 
-  is(results.size, 2, "The correct number of scalars were logged");
+  is(results.size, 3, "The correct number of scalars were logged");
 
   let pings = checkPings(SELECTOR_UNIQUE, results);
   is(pings, 1, `${SELECTOR_UNIQUE} has just 1 ping`);
 
   pings = checkPings(SELECTOR_FULL, results);
   is(pings, 1, `${SELECTOR_FULL} has just 1 ping`);
+
+  pings = checkPings(XPATH, results);
+  is(pings, 1, `${XPATH} has just 1 ping`);
 }
 
 function checkPings(scalarId, results) {
   return results.get(scalarId);
 }
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -168,16 +168,22 @@ inspectorCopyCSSSelector.label=CSS Selec
 inspectorCopyCSSSelector.accesskey=S
 
 # LOCALIZATION NOTE (inspectorCopyCSSPath.label): This is the label
 # shown in the inspector contextual-menu for the item that lets users copy
 # the full CSS path of the current node
 inspectorCopyCSSPath.label=CSS Path
 inspectorCopyCSSPath.accesskey=P
 
+# LOCALIZATION NOTE (inspectorCopyXPath.label): This is the label
+# shown in the inspector contextual-menu for the item that lets users copy
+# the XPath of the current node
+inspectorCopyXPath.label=XPath
+inspectorCopyXPath.accesskey=X
+
 # LOCALIZATION NOTE (inspectorPasteOuterHTML.label): This is the label shown
 # in the inspector contextual-menu for the item that lets users paste outer
 # HTML in the current node
 inspectorPasteOuterHTML.label=Outer HTML
 inspectorPasteOuterHTML.accesskey=O
 
 # LOCALIZATION NOTE (inspectorPasteInnerHTML.label): This is the label shown
 # in the inspector contextual-menu for the item that lets users paste inner
--- a/devtools/client/shared/telemetry.js
+++ b/devtools/client/shared/telemetry.js
@@ -141,16 +141,19 @@ Telemetry.prototype = {
       scalar: "devtools.toolbar.eyedropper.opened",
     },
     copyuniquecssselector: {
       scalar: "devtools.copy.unique.css.selector.opened",
     },
     copyfullcssselector: {
       scalar: "devtools.copy.full.css.selector.opened",
     },
+    copyxpath: {
+      scalar: "devtools.copy.xpath.opened",
+    },
     developertoolbar: {
       histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_COUNT",
       timerHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS"
     },
     aboutdebugging: {
       histogram: "DEVTOOLS_ABOUTDEBUGGING_OPENED_COUNT",
       timerHistogram: "DEVTOOLS_ABOUTDEBUGGING_TIME_ACTIVE_SECONDS"
     },
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -147,17 +147,17 @@ NewConsoleOutputWrapper.prototype = {
     let action = actions.messageAdd(message);
     batchedMessageAdd(action);
 
     // Wait for the message to render to resolve with the DOM node.
     // This is just for backwards compatibility with old tests, and should
     // be removed once it's not needed anymore.
     // Can only wait for response if the action contains a valid message.
     if (waitForResponse && action.message) {
-      let messageId = action.message.get("id");
+      let messageId = action.message.id;
       return new Promise(resolve => {
         let jsterm = this.jsterm;
         jsterm.hud.on("new-messages", function onThisMessage(e, messages) {
           for (let m of messages) {
             if (m.messageId === messageId) {
               resolve(m.node);
               jsterm.hud.off("new-messages", onThisMessage);
               return;
--- a/devtools/client/webconsole/new-console-output/reducers/messages.js
+++ b/devtools/client/webconsole/new-console-output/reducers/messages.js
@@ -83,20 +83,20 @@ function messages(state = new MessageSta
             })
           );
         }
       }
 
       return state.withMutations(function (record) {
         // Add the new message with a reference to the parent group.
         let parentGroups = getParentGroups(currentGroup, groupsById);
-        const addedMessage = newMessage.withMutations(function (message) {
-          message.set("groupId", currentGroup);
-          message.set("indent", parentGroups.length);
-        });
+        newMessage.groupId = currentGroup;
+        newMessage.indent = parentGroups.length;
+
+        const addedMessage = Object.freeze(newMessage);
         record.set(
           "messagesById",
           messagesById.set(newMessage.id, addedMessage)
         );
 
         if (newMessage.type === "trace") {
           // We want the stacktrace to be open by default.
           record.set("messagesUiById", messagesUiById.push(newMessage.id));
--- a/devtools/client/webconsole/new-console-output/test/components/page-error.test.js
+++ b/devtools/client/webconsole/new-console-output/test/components/page-error.test.js
@@ -156,26 +156,26 @@ describe("PageError component:", () => {
 
     const notes = wrapper.find(".error-note");
 
     expect(notes.length).toBe(0);
   });
 
   it("can show an error note", () => {
     const origMessage = stubPreparedMessages.get("ReferenceError: asdf is not defined");
-    const message = origMessage.set("notes", [
-      {
+    const message = Object.assign({}, origMessage, {
+      "notes": [{
         "messageBody": "test note",
         "frame": {
           "source": "http://example.com/test.js",
           "line": 2,
           "column": 6
         }
-      }
-    ]);
+      }]
+    });
 
     let wrapper = render(PageError({ message, serviceContainer }));
 
     const notes = wrapper.find(".error-note");
     expect(notes.length).toBe(1);
 
     const note = notes.eq(0);
     expect(note.find(".message-body").text())
@@ -184,18 +184,18 @@ describe("PageError component:", () => {
     // There should be the location.
     const locationLink = note.find(`.message-location`);
     expect(locationLink.length).toBe(1);
     expect(locationLink.text()).toBe("test.js:2:6");
   });
 
   it("can show multiple error notes", () => {
     const origMessage = stubPreparedMessages.get("ReferenceError: asdf is not defined");
-    const message = origMessage.set("notes", [
-      {
+    const message = Object.assign({}, origMessage, {
+      "notes": [{
         "messageBody": "test note 1",
         "frame": {
           "source": "http://example.com/test1.js",
           "line": 2,
           "column": 6
         }
       },
       {
@@ -208,18 +208,18 @@ describe("PageError component:", () => {
       },
       {
         "messageBody": "test note 3",
         "frame": {
           "source": "http://example.com/test3.js",
           "line": 9,
           "column": 4
         }
-      }
-    ]);
+      }]
+    });
 
     let wrapper = render(PageError({ message, serviceContainer }));
 
     const notes = wrapper.find(".error-note");
     expect(notes.length).toBe(3);
 
     const note1 = notes.eq(0);
     expect(note1.find(".message-body").text())
--- a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js
@@ -20,17 +20,17 @@ stubPreparedMessages.set("console.log('f
   "timeStamp": 1479159894798,
   "type": "log",
   "level": "log",
   "messageText": null,
   "parameters": [
     "foobar",
     "test"
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foobar\",\"test\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foobar\",\"test\"],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -47,17 +47,17 @@ stubPreparedMessages.set("console.log(un
   "type": "log",
   "level": "log",
   "messageText": null,
   "parameters": [
     {
       "type": "undefined"
     }
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"undefined\"}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"undefined\"}],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -72,17 +72,17 @@ stubPreparedMessages.set("console.warn('
   "source": "console-api",
   "timeStamp": 1479159897333,
   "type": "warn",
   "level": "warn",
   "messageText": null,
   "parameters": [
     "danger, will robinson!"
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"warn\",\"level\":\"warn\",\"messageText\":null,\"parameters\":[\"danger, will robinson!\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"warn\",\"messageText\":null,\"parameters\":[\"danger, will robinson!\"],\"source\":\"console-api\",\"type\":\"warn\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -99,17 +99,17 @@ stubPreparedMessages.set("console.log(Na
   "type": "log",
   "level": "log",
   "messageText": null,
   "parameters": [
     {
       "type": "NaN"
     }
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"NaN\"}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"NaN\"}],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -126,17 +126,17 @@ stubPreparedMessages.set("console.log(nu
   "type": "log",
   "level": "log",
   "messageText": null,
   "parameters": [
     {
       "type": "null"
     }
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"null\"}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"null\"}],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -151,17 +151,17 @@ stubPreparedMessages.set("console.log('鼬')", new ConsoleMessage({
   "source": "console-api",
   "timeStamp": 1479159901470,
   "type": "log",
   "level": "log",
   "messageText": null,
   "parameters": [
     "鼬"
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"鼬\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"鼬\"],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -176,17 +176,17 @@ stubPreparedMessages.set("console.clear(
   "source": "console-api",
   "timeStamp": 1479159902721,
   "type": "clear",
   "level": "log",
   "messageText": null,
   "parameters": [
     "Console was cleared."
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"clear\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"Console was cleared.\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"Console was cleared.\"],\"source\":\"console-api\",\"type\":\"clear\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -199,17 +199,17 @@ stubPreparedMessages.set("console.count(
   "id": "1",
   "allowRepeating": true,
   "source": "console-api",
   "timeStamp": 1479159903982,
   "type": "log",
   "level": "debug",
   "messageText": "bar: 1",
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"debug\",\"messageText\":\"bar: 1\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"debug\",\"messageText\":\"bar: 1\",\"parameters\":null,\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -245,17 +245,17 @@ stubPreparedMessages.set("console.assert
             "value": "foobar"
           }
         },
         "ownPropertiesLength": 1,
         "safeGetterValues": {}
       }
     }
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"assert\",\"level\":\"error\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn8.child1/obj31\",\"class\":\"Object\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":1,\"preview\":{\"kind\":\"Object\",\"ownProperties\":{\"message\":{\"configurable\":true,\"enumerable\":true,\"writable\":true,\"value\":\"foobar\"}},\"ownPropertiesLength\":1,\"safeGetterValues\":{}}}],\"repeatId\":null,\"stacktrace\":[{\"columnNumber\":27,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"functionName\":\"triggerPacket\",\"language\":2,\"lineNumber\":1}],\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"error\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn8.child1/obj31\",\"class\":\"Object\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":1,\"preview\":{\"kind\":\"Object\",\"ownProperties\":{\"message\":{\"configurable\":true,\"enumerable\":true,\"writable\":true,\"value\":\"foobar\"}},\"ownPropertiesLength\":1,\"safeGetterValues\":{}}}],\"source\":\"console-api\",\"type\":\"assert\",\"userProvidedStyles\":[]}",
   "stacktrace": [
     {
       "columnNumber": 27,
       "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
       "functionName": "triggerPacket",
       "language": 2,
       "lineNumber": 1
     }
@@ -278,17 +278,17 @@ stubPreparedMessages.set("console.log('h
   "source": "console-api",
   "timeStamp": 1479159906444,
   "type": "log",
   "level": "log",
   "messageText": null,
   "parameters": [
     "hello \nfrom \rthe \"string world!"
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"hello \\nfrom \\rthe \\\"string world!\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"hello \\nfrom \\rthe \\\"string world!\"],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -303,17 +303,17 @@ stubPreparedMessages.set("console.log('úṇĩçödê țĕșť')", new ConsoleMessage({
   "source": "console-api",
   "timeStamp": 1479159907704,
   "type": "log",
   "level": "log",
   "messageText": null,
   "parameters": [
     "úṇĩçödê țĕșť"
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"úṇĩçödê țĕșť\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"úṇĩçödê țĕșť\"],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -340,17 +340,17 @@ stubPreparedMessages.set("console.dirxml
       "sealed": false,
       "ownPropertyLength": 815,
       "preview": {
         "kind": "ObjectWithURL",
         "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html"
       }
     }
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn11.child1/obj31\",\"class\":\"Window\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":815,\"preview\":{\"kind\":\"ObjectWithURL\",\"url\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\"}}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn11.child1/obj31\",\"class\":\"Window\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":815,\"preview\":{\"kind\":\"ObjectWithURL\",\"url\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\"}}],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -383,17 +383,17 @@ stubPreparedMessages.set("console.log('m
         "items": [
           "red",
           "green",
           "blue"
         ]
       }
     }
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"myarray\",{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj32\",\"class\":\"Array\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":4,\"preview\":{\"kind\":\"ArrayLike\",\"length\":3,\"items\":[\"red\",\"green\",\"blue\"]}}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"myarray\",{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj32\",\"class\":\"Array\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":4,\"preview\":{\"kind\":\"ArrayLike\",\"length\":3,\"items\":[\"red\",\"green\",\"blue\"]}}],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -418,17 +418,17 @@ stubPreparedMessages.set("console.log('m
       "class": "RegExp",
       "extensible": true,
       "frozen": false,
       "sealed": false,
       "ownPropertyLength": 1,
       "displayString": "/a.b.c/"
     }
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"myregex\",{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj33\",\"class\":\"RegExp\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":1,\"displayString\":\"/a.b.c/\"}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"myregex\",{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj33\",\"class\":\"RegExp\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":1,\"displayString\":\"/a.b.c/\"}],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -460,17 +460,17 @@ stubPreparedMessages.set("console.table(
         "items": [
           "red",
           "green",
           "blue"
         ]
       }
     }
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"table\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj34\",\"class\":\"Array\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":4,\"preview\":{\"kind\":\"ArrayLike\",\"length\":3,\"items\":[\"red\",\"green\",\"blue\"]}}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj34\",\"class\":\"Array\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":4,\"preview\":{\"kind\":\"ArrayLike\",\"length\":3,\"items\":[\"red\",\"green\",\"blue\"]}}],\"source\":\"console-api\",\"type\":\"table\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -519,17 +519,17 @@ stubPreparedMessages.set("console.log('m
             "value": "blueValue"
           }
         },
         "ownPropertiesLength": 3,
         "safeGetterValues": {}
       }
     }
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"myobject\",{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj35\",\"class\":\"Object\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":3,\"preview\":{\"kind\":\"Object\",\"ownProperties\":{\"red\":{\"configurable\":true,\"enumerable\":true,\"writable\":true,\"value\":\"redValue\"},\"green\":{\"configurable\":true,\"enumerable\":true,\"writable\":true,\"value\":\"greenValue\"},\"blue\":{\"configurable\":true,\"enumerable\":true,\"writable\":true,\"value\":\"blueValue\"}},\"ownPropertiesLength\":3,\"safeGetterValues\":{}}}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":27},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"myobject\",{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj35\",\"class\":\"Object\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":3,\"preview\":{\"kind\":\"Object\",\"ownProperties\":{\"red\":{\"configurable\":true,\"enumerable\":true,\"writable\":true,\"value\":\"redValue\"},\"green\":{\"configurable\":true,\"enumerable\":true,\"writable\":true,\"value\":\"greenValue\"},\"blue\":{\"configurable\":true,\"enumerable\":true,\"writable\":true,\"value\":\"blueValue\"}},\"ownPropertiesLength\":3,\"safeGetterValues\":{}}}],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 27
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -567,17 +567,17 @@ stubPreparedMessages.set("console.map('m
           [
             "key2",
             "value2"
           ]
         ]
       }
     }
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"mymap\",{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj36\",\"class\":\"Map\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":0,\"preview\":{\"kind\":\"MapLike\",\"size\":2,\"entries\":[[\"key1\",\"value1\"],[\"key2\",\"value2\"]]}}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":5,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":5,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"mymap\",{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj36\",\"class\":\"Map\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":0,\"preview\":{\"kind\":\"MapLike\",\"size\":2,\"entries\":[[\"key1\",\"value1\"],[\"key2\",\"value2\"]]}}],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 5,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -590,17 +590,17 @@ stubPreparedMessages.set("console.trace(
   "id": "1",
   "allowRepeating": true,
   "source": "console-api",
   "timeStamp": 1479159910198,
   "type": "trace",
   "level": "log",
   "messageText": null,
   "parameters": [],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"trace\",\"level\":\"log\",\"messageText\":null,\"parameters\":[],\"repeatId\":null,\"stacktrace\":[{\"columnNumber\":3,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"functionName\":\"testStacktraceFiltering\",\"language\":2,\"lineNumber\":3},{\"columnNumber\":3,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"functionName\":\"foo\",\"language\":2,\"lineNumber\":6},{\"columnNumber\":1,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"functionName\":\"triggerPacket\",\"language\":2,\"lineNumber\":9}],\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":3,\"column\":3},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":3,\"column\":3},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[],\"source\":\"console-api\",\"type\":\"trace\",\"userProvidedStyles\":[]}",
   "stacktrace": [
     {
       "columnNumber": 3,
       "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
       "functionName": "testStacktraceFiltering",
       "language": 2,
       "lineNumber": 3
     },
@@ -635,17 +635,17 @@ stubPreparedMessages.set("console.time('
   "id": "1",
   "allowRepeating": true,
   "source": "console-api",
   "timeStamp": 1479159911476,
   "type": "nullMessage",
   "level": "log",
   "messageText": null,
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"nullMessage\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"source\":\"console-api\",\"type\":\"nullMessage\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 2,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -658,17 +658,17 @@ stubPreparedMessages.set("timerAlreadyEx
   "id": "1",
   "allowRepeating": true,
   "source": "console-api",
   "timeStamp": 1494362489620,
   "type": "time",
   "level": "warn",
   "messageText": "Timer “bar” already exists.",
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"time\",\"level\":\"warn\",\"messageText\":\"Timer “bar” already exists.\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":3,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"warn\",\"messageText\":\"Timer “bar” already exists.\",\"parameters\":null,\"source\":\"console-api\",\"type\":\"time\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 3,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -681,17 +681,17 @@ stubPreparedMessages.set("console.timeEn
   "id": "1",
   "allowRepeating": true,
   "source": "console-api",
   "timeStamp": 1479159911478,
   "type": "timeEnd",
   "level": "log",
   "messageText": "bar: 1.36ms",
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"timeEnd\",\"level\":\"log\",\"messageText\":\"bar: 1.36ms\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":4,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":4,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":\"bar: 1.36ms\",\"parameters\":null,\"source\":\"console-api\",\"type\":\"timeEnd\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 4,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -704,17 +704,17 @@ stubPreparedMessages.set("timerDoesntExi
   "id": "1",
   "allowRepeating": true,
   "source": "console-api",
   "timeStamp": 1494362489622,
   "type": "timeEnd",
   "level": "warn",
   "messageText": "Timer “bar” doesn’t exist.",
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"timeEnd\",\"level\":\"warn\",\"messageText\":\"Timer “bar” doesn’t exist.\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":5,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":5,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"warn\",\"messageText\":\"Timer “bar” doesn’t exist.\",\"parameters\":null,\"source\":\"console-api\",\"type\":\"timeEnd\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 5,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -729,17 +729,17 @@ stubPreparedMessages.set("console.table(
   "source": "console-api",
   "timeStamp": 1479159912655,
   "type": "log",
   "level": "log",
   "messageText": null,
   "parameters": [
     "bar"
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"bar\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"bar\"],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 2,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -770,17 +770,17 @@ stubPreparedMessages.set("console.table(
         "items": [
           "a",
           "b",
           "c"
         ]
       }
     }
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"table\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"class\":\"Array\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":4,\"preview\":{\"kind\":\"ArrayLike\",\"length\":3,\"items\":[\"a\",\"b\",\"c\"]}}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"class\":\"Array\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":4,\"preview\":{\"kind\":\"ArrayLike\",\"length\":3,\"items\":[\"a\",\"b\",\"c\"]}}],\"source\":\"console-api\",\"type\":\"table\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 2,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -795,17 +795,17 @@ stubPreparedMessages.set("console.group(
   "source": "console-api",
   "timeStamp": 1479159914984,
   "type": "startGroup",
   "level": "log",
   "messageText": null,
   "parameters": [
     "bar"
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"startGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"bar\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"bar\"],\"source\":\"console-api\",\"type\":\"startGroup\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 2,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -818,17 +818,17 @@ stubPreparedMessages.set("console.groupE
   "id": "1",
   "allowRepeating": true,
   "source": "console-api",
   "timeStamp": 1492540770051,
   "type": "endGroup",
   "level": "log",
   "messageText": null,
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"endGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":3,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"source\":\"console-api\",\"type\":\"endGroup\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 3,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -843,17 +843,17 @@ stubPreparedMessages.set("console.groupC
   "source": "console-api",
   "timeStamp": 1479159916153,
   "type": "startGroupCollapsed",
   "level": "log",
   "messageText": null,
   "parameters": [
     "foo"
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"startGroupCollapsed\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foo\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foo\"],\"source\":\"console-api\",\"type\":\"startGroupCollapsed\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 2,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -866,17 +866,17 @@ stubPreparedMessages.set("console.groupE
   "id": "1",
   "allowRepeating": true,
   "source": "console-api",
   "timeStamp": 1492540770585,
   "type": "endGroup",
   "level": "log",
   "messageText": null,
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"endGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":3,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"source\":\"console-api\",\"type\":\"endGroup\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 3,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -891,17 +891,17 @@ stubPreparedMessages.set("console.group(
   "source": "console-api",
   "timeStamp": 1479159917524,
   "type": "startGroup",
   "level": "log",
   "messageText": null,
   "parameters": [
     "<no group label>"
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"startGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"<no group label>\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"<no group label>\"],\"source\":\"console-api\",\"type\":\"startGroup\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 2,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -914,17 +914,17 @@ stubPreparedMessages.set("console.groupE
   "id": "1",
   "allowRepeating": true,
   "source": "console-api",
   "timeStamp": 1491902018685,
   "type": "endGroup",
   "level": "log",
   "messageText": null,
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"endGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":3,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"source\":\"console-api\",\"type\":\"endGroup\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 3,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -940,17 +940,17 @@ stubPreparedMessages.set("console.log(%c
   "timeStamp": 1479159919144,
   "type": "log",
   "level": "log",
   "messageText": null,
   "parameters": [
     "foo",
     "bar"
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foo\",\"bar\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[\"color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px\",\"color:red;background:url('http://example.com/test')\"],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foo\",\"bar\"],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[\"color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px\",\"color:red;background:url('http://example.com/test')\"]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 2,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -969,17 +969,17 @@ stubPreparedMessages.set("console.group(
   "timeStamp": 1491902018670,
   "type": "startGroup",
   "level": "log",
   "messageText": null,
   "parameters": [
     "foo",
     "bar"
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"startGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foo\",\"bar\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[\"color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px\",\"color:red;background:url('http://example.com/test')\"],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foo\",\"bar\"],\"source\":\"console-api\",\"type\":\"startGroup\",\"userProvidedStyles\":[\"color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px\",\"color:red;background:url('http://example.com/test')\"]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 2,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -995,17 +995,17 @@ stubPreparedMessages.set("console.groupE
   "id": "1",
   "allowRepeating": true,
   "source": "console-api",
   "timeStamp": 1492540772083,
   "type": "endGroup",
   "level": "log",
   "messageText": null,
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"endGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":6,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":6,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"source\":\"console-api\",\"type\":\"endGroup\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 6,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -1021,17 +1021,17 @@ stubPreparedMessages.set("console.groupC
   "timeStamp": 1491902018683,
   "type": "startGroupCollapsed",
   "level": "log",
   "messageText": null,
   "parameters": [
     "foo",
     "baz"
   ],
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"startGroupCollapsed\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foo\",\"baz\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[\"color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px\",\"color:red;background:url('http://example.com/test')\"],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foo\",\"baz\"],\"source\":\"console-api\",\"type\":\"startGroupCollapsed\",\"userProvidedStyles\":[\"color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px\",\"color:red;background:url('http://example.com/test')\"]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 2,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
@@ -1047,17 +1047,17 @@ stubPreparedMessages.set("console.groupE
   "id": "1",
   "allowRepeating": true,
   "source": "console-api",
   "timeStamp": 1492540772669,
   "type": "endGroup",
   "level": "log",
   "messageText": null,
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"timeStamp\":null,\"type\":\"endGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":6,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[],\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":6,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"source\":\"console-api\",\"type\":\"endGroup\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 6,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
--- a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/cssMessage.js
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/cssMessage.js
@@ -17,17 +17,17 @@ stubPreparedMessages.set("Unknown property ‘such-unknown-property’.  Declaration dropped.", new ConsoleMessage({
   "id": "1",
   "allowRepeating": true,
   "source": "css",
   "timeStamp": 1479159920406,
   "type": "log",
   "level": "warn",
   "messageText": "Unknown property ‘such-unknown-property’.  Declaration dropped.",
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"css\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"warn\",\"messageText\":\"Unknown property ‘such-unknown-property’.  Declaration dropped.\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-css-message.html\",\"line\":3,\"column\":23},\"groupId\":null,\"userProvidedStyles\":null,\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-css-message.html\",\"line\":3,\"column\":23},\"groupId\":null,\"indent\":0,\"level\":\"warn\",\"messageText\":\"Unknown property ‘such-unknown-property’.  Declaration dropped.\",\"parameters\":null,\"source\":\"css\",\"type\":\"log\",\"userProvidedStyles\":null}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-css-message.html",
     "line": 3,
     "column": 23
   },
   "groupId": null,
   "userProvidedStyles": null,
@@ -39,17 +39,17 @@ stubPreparedMessages.set("Error in parsing value for ‘padding-top’.  Declaration dropped.", new ConsoleMessage({
   "id": "1",
   "allowRepeating": true,
   "source": "css",
   "timeStamp": 1479159920465,
   "type": "log",
   "level": "warn",
   "messageText": "Error in parsing value for ‘padding-top’.  Declaration dropped.",
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"css\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"warn\",\"messageText\":\"Error in parsing value for ‘padding-top’.  Declaration dropped.\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-css-message.html\",\"line\":3,\"column\":15},\"groupId\":null,\"userProvidedStyles\":null,\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-css-message.html\",\"line\":3,\"column\":15},\"groupId\":null,\"indent\":0,\"level\":\"warn\",\"messageText\":\"Error in parsing value for ‘padding-top’.  Declaration dropped.\",\"parameters\":null,\"source\":\"css\",\"type\":\"log\",\"userProvidedStyles\":null}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-css-message.html",
     "line": 3,
     "column": 15
   },
   "groupId": null,
   "userProvidedStyles": null,
--- a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/evaluationResult.js
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/evaluationResult.js
@@ -27,17 +27,17 @@ stubPreparedMessages.set("new Date(0)", 
     "extensible": true,
     "frozen": false,
     "sealed": false,
     "ownPropertyLength": 0,
     "preview": {
       "timestamp": 0
     }
   },
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"timeStamp\":null,\"type\":\"result\",\"level\":\"log\",\"parameters\":{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj30\",\"class\":\"Date\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":0,\"preview\":{\"timestamp\":0}},\"repeatId\":null,\"stacktrace\":null,\"frame\":null,\"groupId\":null,\"userProvidedStyles\":null,\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":null,\"groupId\":null,\"indent\":0,\"level\":\"log\",\"parameters\":{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj30\",\"class\":\"Date\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":0,\"preview\":{\"timestamp\":0}},\"source\":\"javascript\",\"type\":\"result\",\"userProvidedStyles\":null}",
   "stacktrace": null,
   "frame": null,
   "groupId": null,
   "userProvidedStyles": null,
   "notes": null,
   "indent": 0
 }));
 
@@ -47,17 +47,17 @@ stubPreparedMessages.set("asdf()", new C
   "source": "javascript",
   "timeStamp": 1479159921377,
   "type": "result",
   "level": "error",
   "messageText": "ReferenceError: asdf is not defined",
   "parameters": {
     "type": "undefined"
   },
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"timeStamp\":null,\"type\":\"result\",\"level\":\"error\",\"messageText\":\"ReferenceError: asdf is not defined\",\"parameters\":{\"type\":\"undefined\"},\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"debugger eval code\",\"line\":1,\"column\":1},\"groupId\":null,\"exceptionDocURL\":\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default\",\"userProvidedStyles\":null,\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"debugger eval code\",\"line\":1,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"error\",\"messageText\":\"ReferenceError: asdf is not defined\",\"parameters\":{\"type\":\"undefined\"},\"source\":\"javascript\",\"type\":\"result\",\"userProvidedStyles\":null}",
   "stacktrace": null,
   "frame": {
     "source": "debugger eval code",
     "line": 1,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default",
@@ -72,17 +72,17 @@ stubPreparedMessages.set("1 + @", new Co
   "source": "javascript",
   "timeStamp": 1479159921399,
   "type": "result",
   "level": "error",
   "messageText": "SyntaxError: illegal character",
   "parameters": {
     "type": "undefined"
   },
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"timeStamp\":null,\"type\":\"result\",\"level\":\"error\",\"messageText\":\"SyntaxError: illegal character\",\"parameters\":{\"type\":\"undefined\"},\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"debugger eval code\",\"line\":1,\"column\":4},\"groupId\":null,\"exceptionDocURL\":\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Illegal_character?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default\",\"userProvidedStyles\":null,\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"debugger eval code\",\"line\":1,\"column\":4},\"groupId\":null,\"indent\":0,\"level\":\"error\",\"messageText\":\"SyntaxError: illegal character\",\"parameters\":{\"type\":\"undefined\"},\"source\":\"javascript\",\"type\":\"result\",\"userProvidedStyles\":null}",
   "stacktrace": null,
   "frame": {
     "source": "debugger eval code",
     "line": 1,
     "column": 4
   },
   "groupId": null,
   "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Illegal_character?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default",
@@ -102,17 +102,17 @@ stubPreparedMessages.set("longString mes
     "type": "longString",
     "initial": "Error: Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Lon",
     "length": 110007,
     "actor": "server1.conn0.child1/longString37"
   },
   "parameters": {
     "type": "undefined"
   },
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"timeStamp\":null,\"type\":\"result\",\"level\":\"error\",\"messageText\":{\"type\":\"longString\",\"initial\":\"Error: Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Lon\",\"length\":110007,\"actor\":\"server1.conn0.child1/longString37\"},\"parameters\":{\"type\":\"undefined\"},\"repeatId\":null,\"stacktrace\":null,\"frame\":null,\"groupId\":null,\"userProvidedStyles\":null,\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":null,\"groupId\":null,\"indent\":0,\"level\":\"error\",\"messageText\":{\"type\":\"longString\",\"initial\":\"Error: Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Lon\",\"length\":110007,\"actor\":\"server1.conn0.child1/longString37\"},\"parameters\":{\"type\":\"undefined\"},\"source\":\"javascript\",\"type\":\"result\",\"userProvidedStyles\":null}",
   "stacktrace": null,
   "frame": null,
   "groupId": null,
   "userProvidedStyles": null,
   "notes": null,
   "indent": 0
 }));
 
--- a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/pageError.js
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/pageError.js
@@ -17,17 +17,17 @@ stubPreparedMessages.set("ReferenceError
   "id": "1",
   "allowRepeating": true,
   "source": "javascript",
   "timeStamp": 1476573167137,
   "type": "log",
   "level": "error",
   "messageText": "ReferenceError: asdf is not defined",
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"error\",\"messageText\":\"ReferenceError: asdf is not defined\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":[{\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"lineNumber\":3,\"columnNumber\":5,\"functionName\":\"bar\"},{\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"lineNumber\":6,\"columnNumber\":5,\"functionName\":\"foo\"},{\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"lineNumber\":9,\"columnNumber\":3,\"functionName\":null},{\"filename\":\"resource://testing-common/content-task.js line 52 > eval\",\"lineNumber\":6,\"columnNumber\":9,\"functionName\":null},{\"filename\":\"resource://testing-common/content-task.js\",\"lineNumber\":53,\"columnNumber\":20,\"functionName\":null}],\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":3,\"column\":5},\"groupId\":null,\"exceptionDocURL\":\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default\",\"userProvidedStyles\":null,\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":3,\"column\":5},\"groupId\":null,\"indent\":0,\"level\":\"error\",\"messageText\":\"ReferenceError: asdf is not defined\",\"parameters\":null,\"source\":\"javascript\",\"type\":\"log\",\"userProvidedStyles\":null}",
   "stacktrace": [
     {
       "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
       "lineNumber": 3,
       "columnNumber": 5,
       "functionName": "bar"
     },
     {
@@ -71,17 +71,17 @@ stubPreparedMessages.set("SyntaxError: r
   "id": "1",
   "allowRepeating": true,
   "source": "javascript",
   "timeStamp": 1487992945524,
   "type": "log",
   "level": "error",
   "messageText": "SyntaxError: redeclaration of let a",
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"error\",\"messageText\":\"SyntaxError: redeclaration of let a\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":[{\"filename\":\"resource://testing-common/content-task.js line 52 > eval\",\"lineNumber\":6,\"columnNumber\":9,\"functionName\":null},{\"filename\":\"resource://testing-common/content-task.js\",\"lineNumber\":53,\"columnNumber\":20,\"functionName\":null}],\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":9},\"groupId\":null,\"userProvidedStyles\":null,\"notes\":[{\"messageBody\":\"Previously declared at line 2, column 6\",\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":6}}],\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":9},\"groupId\":null,\"indent\":0,\"level\":\"error\",\"messageText\":\"SyntaxError: redeclaration of let a\",\"parameters\":null,\"source\":\"javascript\",\"type\":\"log\",\"userProvidedStyles\":null}",
   "stacktrace": [
     {
       "filename": "resource://testing-common/content-task.js line 52 > eval",
       "lineNumber": 6,
       "columnNumber": 9,
       "functionName": null
     },
     {
@@ -120,17 +120,17 @@ stubPreparedMessages.set("TypeError long
   "level": "error",
   "messageText": {
     "type": "longString",
     "initial": "Error: Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Lon",
     "length": 110007,
     "actor": "server1.conn0.child1/longString30"
   },
   "parameters": null,
-  "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"timeStamp\":null,\"type\":\"log\",\"level\":\"error\",\"messageText\":{\"type\":\"longString\",\"initial\":\"Error: Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Lon\",\"length\":110007,\"actor\":\"server1.conn0.child1/longString30\"},\"parameters\":null,\"repeatId\":null,\"stacktrace\":[{\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"lineNumber\":1,\"columnNumber\":7,\"functionName\":null},{\"filename\":\"resource://testing-common/content-task.js line 52 > eval\",\"lineNumber\":6,\"columnNumber\":9,\"functionName\":null},{\"filename\":\"resource://testing-common/content-task.js\",\"lineNumber\":53,\"columnNumber\":20,\"functionName\":null}],\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":7},\"groupId\":null,\"userProvidedStyles\":null,\"notes\":null,\"indent\":0}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":1,\"column\":7},\"groupId\":null,\"indent\":0,\"level\":\"error\",\"messageText\":{\"type\":\"longString\",\"initial\":\"Error: Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Lon\",\"length\":110007,\"actor\":\"server1.conn0.child1/longString30\"},\"parameters\":null,\"source\":\"javascript\",\"type\":\"log\",\"userProvidedStyles\":null}",
   "stacktrace": [
     {
       "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
       "lineNumber": 1,
       "columnNumber": 7,
       "functionName": null
     },
     {
--- a/devtools/client/webconsole/new-console-output/test/utils/getRepeatId.test.js
+++ b/devtools/client/webconsole/new-console-output/test/utils/getRepeatId.test.js
@@ -7,31 +7,33 @@ const { stubPreparedMessages } = require
 
 const expect = require("expect");
 
 describe("getRepeatId:", () => {
   it("returns same repeatId for duplicate values", () => {
     const baseMessage = stubPreparedMessages.get("console.log('foobar', 'test')");
 
     // Repeat ID must be the same even if the timestamp is different.
-    const message1 = baseMessage.set("timeStamp", 1);
-    const message2 = baseMessage.set("timeStamp", 2);
+    const message1 = Object.assign({}, baseMessage, {"timeStamp": 1});
+    const message2 = Object.assign({}, baseMessage, {"timeStamp": 2});
 
     expect(getRepeatId(message1)).toEqual(getRepeatId(message2));
   });
 
   it("returns different repeatIds for different values", () => {
     const message1 = stubPreparedMessages.get("console.log('foobar', 'test')");
-    const message2 = message1.set("parameters", ["funny", "monkey"]);
+    const message2 = Object.assign({}, message1, {
+      "parameters": ["funny", "monkey"]
+    });
     expect(getRepeatId(message1)).toNotEqual(getRepeatId(message2));
   });
 
   it("returns different repeatIds for different severities", () => {
     const message1 = stubPreparedMessages.get("console.log('foobar', 'test')");
-    const message2 = message1.set("level", "error");
+    const message2 = Object.assign({}, message1, {"level": "error"});
     expect(getRepeatId(message1)).toNotEqual(getRepeatId(message2));
   });
 
   it("handles falsy values distinctly", () => {
     const messageNaN = stubPreparedMessages.get("console.log(NaN)");
     const messageUnd = stubPreparedMessages.get("console.log(undefined)");
     const messageNul = stubPreparedMessages.get("console.log(null)");
 
--- a/devtools/client/webconsole/new-console-output/types.js
+++ b/devtools/client/webconsole/new-console-output/types.js
@@ -1,59 +1,63 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-const Immutable = require("devtools/client/shared/vendor/immutable");
-
 const {
   MESSAGE_SOURCE,
   MESSAGE_TYPE,
   MESSAGE_LEVEL
 } = require("devtools/client/webconsole/new-console-output/constants");
 
-exports.ConsoleCommand = Immutable.Record({
-  id: null,
-  allowRepeating: false,
-  messageText: null,
-  source: MESSAGE_SOURCE.JAVASCRIPT,
-  type: MESSAGE_TYPE.COMMAND,
-  level: MESSAGE_LEVEL.LOG,
-  groupId: null,
-  indent: 0,
-});
+exports.ConsoleCommand = function (props) {
+  return Object.assign({
+    id: null,
+    allowRepeating: false,
+    messageText: null,
+    source: MESSAGE_SOURCE.JAVASCRIPT,
+    type: MESSAGE_TYPE.COMMAND,
+    level: MESSAGE_LEVEL.LOG,
+    groupId: null,
+    indent: 0,
+  }, props);
+};
 
-exports.ConsoleMessage = Immutable.Record({
-  id: null,
-  allowRepeating: true,
-  source: null,
-  timeStamp: null,
-  type: null,
-  level: null,
-  messageText: null,
-  parameters: null,
-  repeatId: null,
-  stacktrace: null,
-  frame: null,
-  groupId: null,
-  exceptionDocURL: null,
-  userProvidedStyles: null,
-  notes: null,
-  indent: 0,
-});
+exports.ConsoleMessage = function (props) {
+  return Object.assign({
+    id: null,
+    allowRepeating: true,
+    source: null,
+    timeStamp: null,
+    type: null,
+    level: null,
+    messageText: null,
+    parameters: null,
+    repeatId: null,
+    stacktrace: null,
+    frame: null,
+    groupId: null,
+    exceptionDocURL: null,
+    userProvidedStyles: null,
+    notes: null,
+    indent: 0,
+  }, props);
+};
 
-exports.NetworkEventMessage = Immutable.Record({
-  id: null,
-  actor: null,
-  level: MESSAGE_LEVEL.LOG,
-  isXHR: false,
-  request: null,
-  response: null,
-  source: MESSAGE_SOURCE.NETWORK,
-  type: MESSAGE_TYPE.LOG,
-  groupId: null,
-  timeStamp: null,
-  totalTime: null,
-  indent: 0,
-});
+exports.NetworkEventMessage = function (props) {
+  return Object.assign({
+    id: null,
+    actor: null,
+    level: MESSAGE_LEVEL.LOG,
+    isXHR: false,
+    request: null,
+    response: null,
+    source: MESSAGE_SOURCE.NETWORK,
+    type: MESSAGE_TYPE.LOG,
+    groupId: null,
+    timeStamp: null,
+    totalTime: null,
+    indent: 0,
+  }, props);
+};
--- a/devtools/client/webconsole/new-console-output/utils/messages.js
+++ b/devtools/client/webconsole/new-console-output/utils/messages.js
@@ -20,19 +20,20 @@ const {
 
 function prepareMessage(packet, idGenerator) {
   // This packet is already in the expected packet structure. Simply return.
   if (!packet.source) {
     packet = transformPacket(packet);
   }
 
   if (packet.allowRepeating) {
-    packet = packet.set("repeatId", getRepeatId(packet));
+    packet.repeatId = getRepeatId(packet);
   }
-  return packet.set("id", idGenerator.getNextId(packet));
+  packet.id = idGenerator.getNextId(packet);
+  return packet;
 }
 
 /**
  * Transforms a packet from Firefox RDP structure to Chrome RDP structure.
  */
 function transformPacket(packet) {
   if (packet._type) {
     packet = convertCachedPacket(packet);
@@ -231,19 +232,27 @@ function transformPacket(packet) {
         notes,
       });
     }
   }
 }
 
 // Helpers
 function getRepeatId(message) {
-  message = message.toJS();
-  message.timeStamp = null;
-  return JSON.stringify(message);
+  return JSON.stringify({
+    frame: message.frame,
+    groupId: message.groupId,
+    indent: message.indent,
+    level: message.level,
+    messageText: message.messageText,
+    parameters: message.parameters,
+    source: message.source,
+    type: message.type,
+    userProvidedStyles: message.userProvidedStyles,
+  });
 }
 
 function convertCachedPacket(packet) {
   // The devtools server provides cached message packets in a different shape, so we
   // transform them here.
   let convertPacket = {};
   if (packet._type === "ConsoleAPI") {
     convertPacket.message = packet;
--- a/devtools/server/actors/inspector.js
+++ b/devtools/server/actors/inspector.js
@@ -154,16 +154,17 @@ loader.lazyGetter(this, "DOMParser", fun
 loader.lazyGetter(this, "eventListenerService", function () {
   return Cc["@mozilla.org/eventlistenerservice;1"]
            .getService(Ci.nsIEventListenerService);
 });
 
 loader.lazyRequireGetter(this, "CssLogic", "devtools/server/css-logic", true);
 loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
 loader.lazyRequireGetter(this, "getCssPath", "devtools/shared/inspector/css-logic", true);
+loader.lazyRequireGetter(this, "getXPath", "devtools/shared/inspector/css-logic", true);
 
 /**
  * We only send nodeValue up to a certain size by default.  This stuff
  * controls that size.
  */
 exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50;
 var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH;
 
@@ -661,16 +662,28 @@ var NodeActor = exports.NodeActor = prot
   getCssPath: function () {
     if (Cu.isDeadWrapper(this.rawNode)) {
       return "";
     }
     return getCssPath(this.rawNode);
   },
 
   /**
+   * Get the XPath for this node.
+   *
+   * @return {String} The XPath for finding this node on the page.
+   */
+  getXPath: function () {
+    if (Cu.isDeadWrapper(this.rawNode)) {
+      return "";
+    }
+    return getXPath(this.rawNode);
+  },
+
+  /**
    * Scroll the selected node into view.
    */
   scrollIntoView: function () {
     this.rawNode.scrollIntoView(true);
   },
 
   /**
    * Get the node's image data if any (for canvas and img nodes).
--- a/devtools/server/actors/root.js
+++ b/devtools/server/actors/root.js
@@ -147,16 +147,18 @@ RootActor.prototype = {
     selectorEditable: true,
     // Whether the page style actor implements the addNewRule method that
     // adds new rules to the page
     addNewRule: true,
     // Whether the dom node actor implements the getUniqueSelector method
     getUniqueSelector: true,
     // Whether the dom node actor implements the getCssPath method
     getCssPath: true,
+    // Whether the dom node actor implements the getXPath method
+    getXPath: true,
     // Whether the director scripts are supported
     directorScripts: true,
     // Whether the debugger server supports
     // blackboxing/pretty-printing (not supported in Fever Dream yet)
     noBlackBoxing: false,
     noPrettyPrinting: false,
     // Whether the page style actor implements the getUsedFontFaces method
     // that returns the font faces used on a node
--- a/devtools/shared/inspector/css-logic.js
+++ b/devtools/shared/inspector/css-logic.js
@@ -398,8 +398,68 @@ function getCssPath(ele) {
 
     paths.splice(0, 0, getElementSelector(ele));
     ele = ele.parentNode;
   }
 
   return paths.length ? paths.join(" ") : "";
 }
 exports.getCssPath = getCssPath;
+
+/**
+ * Get the xpath for a given element.
+ * @param {DomNode} ele
+ * @returns a string that can be used as an XPath to find the element uniquely.
+ */
+function getXPath(ele) {
+  ele = getRootBindingParent(ele);
+  const document = ele.ownerDocument;
+  if (!document || !document.contains(ele)) {
+    throw new Error("getXPath received element not inside document");
+  }
+
+  // Create a short XPath for elements with IDs.
+  if (ele.id) {
+    return `//*[@id="${ele.id}"]`;
+  }
+
+  // Otherwise walk the DOM up and create a part for each ancestor.
+  const parts = [];
+
+  // Use nodeName (instead of localName) so namespace prefix is included (if any).
+  while (ele && ele.nodeType === Node.ELEMENT_NODE) {
+    let nbOfPreviousSiblings = 0;
+    let hasNextSiblings = false;
+
+    // Count how many previous same-name siblings the element has.
+    let sibling = ele.previousSibling;
+    while (sibling) {
+      // Ignore document type declaration.
+      if (sibling.nodeType !== Node.DOCUMENT_TYPE_NODE &&
+          sibling.nodeName == ele.nodeName) {
+        nbOfPreviousSiblings++;
+      }
+
+      sibling = sibling.previousSibling;
+    }
+
+    // Check if the element has at least 1 next same-name sibling.
+    sibling = ele.nextSibling;
+    while (sibling) {
+      if (sibling.nodeName == ele.nodeName) {
+        hasNextSiblings = true;
+        break;
+      }
+      sibling = sibling.nextSibling;
+    }
+
+    const prefix = ele.prefix ? ele.prefix + ":" : "";
+    const nth = nbOfPreviousSiblings || hasNextSiblings
+                ? `[${nbOfPreviousSiblings + 1}]` : "";
+
+    parts.push(prefix + ele.localName + nth);
+
+    ele = ele.parentNode;
+  }
+
+  return parts.length ? "/" + parts.reverse().join("/") : "";
+}
+exports.getXPath = getXPath;
--- a/devtools/shared/specs/node.js
+++ b/devtools/shared/specs/node.js
@@ -38,16 +38,22 @@ const nodeSpec = generateActorSpec({
       }
     },
     getCssPath: {
       request: {},
       response: {
         value: RetVal("string")
       }
     },
+    getXPath: {
+      request: {},
+      response: {
+        value: RetVal("string")
+      }
+    },
     scrollIntoView: {
       request: {},
       response: {}
     },
     getImageData: {
       request: {maxDim: Arg(0, "nullable:number")},
       response: RetVal("imageData")
     },
--- a/devtools/shared/tests/mochitest/chrome.ini
+++ b/devtools/shared/tests/mochitest/chrome.ini
@@ -1,7 +1,8 @@
 [DEFAULT]
 tags = devtools
 skip-if = os == 'android'
 
 [test_css-logic-getCssPath.html]
+[test_css-logic-getXPath.html]
 [test_eventemitter_basic.html]
 skip-if = os == 'linux' && debug # Bug 1205739
new file mode 100644
--- /dev/null
+++ b/devtools/shared/tests/mochitest/test_css-logic-getXPath.html
@@ -0,0 +1,111 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=987877
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 987877</title>
+
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <script type="application/javascript">
+"use strict";
+
+const { utils: Cu } = Components;
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const CssLogic = require("devtools/shared/inspector/css-logic");
+
+const _tests = [];
+function addTest(test) {
+  _tests.push(test);
+}
+
+function runNextTest() {
+  if (_tests.length == 0) {
+    SimpleTest.finish();
+    return;
+  }
+  _tests.shift()();
+}
+
+window.onload = function () {
+  SimpleTest.waitForExplicitFinish();
+  runNextTest();
+};
+
+addTest(function getXPathForUnattachedElement() {
+  let unattached = document.createElement("div");
+  unattached.id = "unattached";
+  try {
+    CssLogic.getXPath(unattached);
+    ok(false, "Unattached node did not throw");
+  } catch (e) {
+    ok(e, "Unattached node throws an exception");
+  }
+
+  let unattachedChild = document.createElement("div");
+  unattached.appendChild(unattachedChild);
+  try {
+    CssLogic.getXPath(unattachedChild);
+    ok(false, "Unattached child node did not throw");
+  } catch (e) {
+    ok(e, "Unattached child node throws an exception");
+  }
+
+  let unattachedBody = document.createElement("body");
+  try {
+    CssLogic.getXPath(unattachedBody);
+    ok(false, "Unattached body node did not throw");
+  } catch (e) {
+    ok(e, "Unattached body node throws an exception");
+  }
+
+  runNextTest();
+});
+
+addTest(function getXPath() {
+  let data = [{
+    // Target elements that have an ID get a short XPath.
+    selector: "#i-have-an-id",
+    path: "//*[@id=\"i-have-an-id\"]"
+  }, {
+    selector: "html",
+    path: "/html"
+  }, {
+    selector: "body",
+    path: "/html/body"
+  }, {
+    selector: "body > div:nth-child(2) > div > div:nth-child(4)",
+    path: "/html/body/div[2]/div/div[4]"
+  }, {
+    // XPath should support namespace.
+    selector: "namespace\\:body",
+    path: "/html/body/namespace:test/namespace:body"
+  }];
+
+  for (let {selector, path} of data) {
+    let node = document.querySelector(selector);
+    is(CssLogic.getXPath(node), path, `Full css path is correct for ${selector}`);
+  }
+
+  runNextTest();
+});
+  </script>
+</head>
+<body>
+  <div id="i-have-an-id">find me</div>
+  <div>
+    <div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div>me too!</div>
+    </div>
+  </div>
+  <namespace:test>
+    <namespace:header></namespace:header>
+    <namespace:body>and me</namespace:body>
+  </namespace:test>
+</body>
+</html>
--- a/devtools/shim/DevToolsShim.jsm
+++ b/devtools/shim/DevToolsShim.jsm
@@ -191,8 +191,35 @@ this.DevToolsShim = {
       this.gDevTools.registerTheme(theme);
     }
 
     this.listeners = [];
     this.tools = [];
     this.themes = [];
   },
 };
+
+/**
+ * Compatibility layer for addon-sdk. Remove when Firefox 57 hits release.
+ *
+ * The methods below are used by classes and tests from addon-sdk/
+ * If DevTools are not installed when calling one of them, the call will throw.
+ */
+
+let addonSdkMethods = [
+  "closeToolbox",
+  "connectDebuggerServer",
+  "createDebuggerClient",
+  "getTargetForTab",
+  "getToolbox",
+  "initBrowserToolboxProcessForAddon",
+  "showToolbox",
+];
+
+for (let method of addonSdkMethods) {
+  this.DevToolsShim[method] = function () {
+    if (!this.isInstalled()) {
+      throw new Error(`Method ${method} unavailable if DevTools are not installed`);
+    }
+
+    return this.gDevTools[method].apply(this.gDevTools, arguments);
+  };
+}
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -4025,19 +4025,26 @@ Element::FontSizeInflation()
   return 1.0;
 }
 
 net::ReferrerPolicy
 Element::GetReferrerPolicyAsEnum()
 {
   if (IsHTMLElement()) {
     const nsAttrValue* referrerValue = GetParsedAttr(nsGkAtoms::referrerpolicy);
-    if (referrerValue && referrerValue->Type() == nsAttrValue::eEnum) {
-      return net::ReferrerPolicy(referrerValue->GetEnumValue());
-    }
+    return ReferrerPolicyFromAttr(referrerValue);
+  }
+  return net::RP_Unset;
+}
+
+net::ReferrerPolicy
+Element::ReferrerPolicyFromAttr(const nsAttrValue* aValue)
+{
+  if (aValue && aValue->Type() == nsAttrValue::eEnum) {
+    return net::ReferrerPolicy(aValue->GetEnumValue());
   }
   return net::RP_Unset;
 }
 
 already_AddRefed<nsDOMStringMap>
 Element::Dataset()
 {
   nsDOMSlots *slots = DOMSlots();
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -1332,16 +1332,17 @@ public:
    *
    * @note The font size inflation ratio that is returned is actually the
    *       font size inflation data for the element's _primary frame_, not the
    *       element itself, but for most purposes, this should be sufficient.
    */
   float FontSizeInflation();
 
   net::ReferrerPolicy GetReferrerPolicyAsEnum();
+  net::ReferrerPolicy ReferrerPolicyFromAttr(const nsAttrValue* aValue);
 
   /*
    * Helpers for .dataset.  This is implemented on Element, though only some
    * sorts of elements expose it to JS as a .dataset property
    */
   // Getter, to be called from bindings.
   already_AddRefed<nsDOMStringMap> Dataset();
   // Callback for destructor of dataset to ensure to null out our weak pointer
--- a/dom/html/HTMLImageElement.cpp
+++ b/dom/html/HTMLImageElement.cpp
@@ -113,17 +113,16 @@ private:
   // True if we want to set nsIClassOfService::UrgentStart to the channel to
   // get the response ASAP for better user responsiveness.
   bool mUseUrgentStartForChannel;
 };
 
 HTMLImageElement::HTMLImageElement(already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo)
   : nsGenericHTMLElement(aNodeInfo)
   , mForm(nullptr)
-  , mForceReload(false)
   , mInDocResponsiveContent(false)
   , mCurrentDensity(1.0)
 {
   // We start out broken
   AddStatesSilently(NS_EVENT_STATE_BROKEN);
 }
 
 HTMLImageElement::~HTMLImageElement()
@@ -373,20 +372,16 @@ HTMLImageElement::GetAttributeMappingFun
   return &MapAttributesIntoRule;
 }
 
 nsresult
 HTMLImageElement::BeforeSetAttr(int32_t aNameSpaceID, nsIAtom* aName,
                                 const nsAttrValueOrString* aValue,
                                 bool aNotify)
 {
-  if (aValue) {
-    BeforeMaybeChangeAttr(aNameSpaceID, aName, *aValue, aNotify);
-  }
-
   if (aNameSpaceID == kNameSpaceID_None && mForm &&
       (aName == nsGkAtoms::name || aName == nsGkAtoms::id)) {
     // remove the image from the hashtable as needed
     nsAutoString tmp;
     GetAttr(kNameSpaceID_None, aName, tmp);
 
     if (!tmp.IsEmpty()) {
       mForm->RemoveImageElementFromTable(this, tmp);
@@ -397,36 +392,37 @@ HTMLImageElement::BeforeSetAttr(int32_t 
                                              aValue, aNotify);
 }
 
 nsresult
 HTMLImageElement::AfterSetAttr(int32_t aNameSpaceID, nsIAtom* aName,
                                const nsAttrValue* aValue,
                                const nsAttrValue* aOldValue, bool aNotify)
 {
+  nsAttrValueOrString attrVal(aValue);
+
   if (aValue) {
-    AfterMaybeChangeAttr(aNameSpaceID, aName, aNotify);
+    AfterMaybeChangeAttr(aNameSpaceID, aName, attrVal, aOldValue, true,
+                         aNotify);
   }
 
   if (aNameSpaceID == kNameSpaceID_None && mForm &&
       (aName == nsGkAtoms::name || aName == nsGkAtoms::id) &&
       aValue && !aValue->IsEmptyString()) {
     // add the image to the hashtable as needed
     MOZ_ASSERT(aValue->Type() == nsAttrValue::eAtom,
                "Expected atom value for name/id");
     mForm->AddImageElementToTable(this,
       nsDependentAtomString(aValue->GetAtomValue()));
   }
 
   // Handle src/srcset updates. If aNotify is false, we are coming from the
   // parser or some such place; we'll get bound after all the attributes have
   // been set, so we'll do the image load from BindToTree.
 
-  nsAttrValueOrString attrVal(aValue);
-
   if (aName == nsGkAtoms::src &&
       aNameSpaceID == kNameSpaceID_None &&
       !aValue) {
     // Mark channel as urgent-start before load image if the image load is
     // initaiated by a user interaction.
     mUseUrgentStartForChannel = EventStateManager::IsHandlingUserInput();
 
     // SetAttr handles setting src since it needs to catch img.src =
@@ -461,41 +457,39 @@ HTMLImageElement::AfterSetAttr(int32_t a
                                             aValue, aOldValue, aNotify);
 }
 
 nsresult
 HTMLImageElement::OnAttrSetButNotChanged(int32_t aNamespaceID, nsIAtom* aName,
                                          const nsAttrValueOrString& aValue,
                                          bool aNotify)
 {
-  BeforeMaybeChangeAttr(aNamespaceID, aName, aValue, aNotify);
-  AfterMaybeChangeAttr(aNamespaceID, aName, aNotify);
+  AfterMaybeChangeAttr(aNamespaceID, aName, aValue, nullptr, false, aNotify);
 
   return nsGenericHTMLElement::OnAttrSetButNotChanged(aNamespaceID, aName,
                                                       aValue, aNotify);
 }
 
 void
-HTMLImageElement::BeforeMaybeChangeAttr(int32_t aNamespaceID, nsIAtom* aName,
-                                        const nsAttrValueOrString& aValue,
-                                        bool aNotify)
+HTMLImageElement::AfterMaybeChangeAttr(int32_t aNamespaceID, nsIAtom* aName,
+                                       const nsAttrValueOrString& aValue,
+                                       const nsAttrValue* aOldValue,
+                                       bool aValueMaybeChanged, bool aNotify)
 {
+  bool forceReload = false;
   // We need to force our image to reload.  This must be done here, not in
   // AfterSetAttr or BeforeSetAttr, because we want to do it even if the attr is
   // being set to its existing value, which is normally optimized away as a
   // no-op.
   //
   // If we are in responsive mode, we drop the forced reload behavior,
   // but still trigger a image load task for img.src = img.src per
   // spec.
   //
   // Both cases handle unsetting src in AfterSetAttr
-  //
-  // Much of this should probably happen in AfterMaybeChangeAttr.
-  // See Bug 1370705
   if (aNamespaceID == kNameSpaceID_None &&
       aName == nsGkAtoms::src) {
 
     // Mark channel as urgent-start before load image if the image load is
     // initaiated by a user interaction.
     mUseUrgentStartForChannel = EventStateManager::IsHandlingUserInput();
 
     if (InResponsiveMode()) {
@@ -510,78 +504,71 @@ HTMLImageElement::BeforeMaybeChangeAttr(
       // sync image load from BindToTree. Skip the LoadImage call in that case.
 
       // Note that this sync behavior is partially removed from the spec, bug 1076583
 
       // A hack to get animations to reset. See bug 594771.
       mNewRequestsWillNeedAnimationReset = true;
 
       // Force image loading here, so that we'll try to load the image from
-      // network if it's set to be not cacheable...  If we change things so that
-      // the state gets in Element's attr-setting happen around this
-      // LoadImage call, we could start passing false instead of aNotify
-      // here.
+      // network if it's set to be not cacheable.
+      // Potentially, false could be passed here rather than aNotify since
+      // UpdateState will be called by SetAttrAndNotify, but there are two
+      // obstacles to this: 1) LoadImage will end up calling
+      // UpdateState(aNotify), and we do not want it to call UpdateState(false)
+      // when aNotify is true, and 2) When this function is called by
+      // OnAttrSetButNotChanged, SetAttrAndNotify will not subsequently call
+      // UpdateState.
       LoadImage(aValue.String(), true, aNotify, eImageLoadType_Normal);
 
       mNewRequestsWillNeedAnimationReset = false;
     }
   } else if (aNamespaceID == kNameSpaceID_None &&
              aName == nsGkAtoms::crossorigin &&
              aNotify) {
-    nsAttrValue attrValue;
-    ParseCORSValue(aValue.String(), attrValue);
-    if (GetCORSMode() != AttrValueToCORSMode(&attrValue)) {
+    if (aValueMaybeChanged && GetCORSMode() != AttrValueToCORSMode(aOldValue)) {
       // Force a new load of the image with the new cross origin policy.
-      mForceReload = true;
+      forceReload = true;
     }
   } else if (aName == nsGkAtoms::referrerpolicy &&
       aNamespaceID == kNameSpaceID_None &&
       aNotify) {
-    ReferrerPolicy referrerPolicy = AttributeReferrerPolicyFromString(aValue.String());
+    ReferrerPolicy referrerPolicy = GetImageReferrerPolicy();
     if (!InResponsiveMode() &&
         referrerPolicy != RP_Unset &&
-        referrerPolicy != GetImageReferrerPolicy()) {
+        aValueMaybeChanged &&
+        referrerPolicy != ReferrerPolicyFromAttr(aOldValue)) {
       // XXX: Bug 1076583 - We still use the older synchronous algorithm
       // Because referrerPolicy is not treated as relevant mutations, setting
       // the attribute will neither trigger a reload nor update the referrer
       // policy of the loading channel (whether it has previously completed or
       // not). Force a new load of the image with the new referrerpolicy.
-      mForceReload = true;
+      forceReload = true;
     }
   }
 
-  return;
-}
-
-void
-HTMLImageElement::AfterMaybeChangeAttr(int32_t aNamespaceID, nsIAtom* aName,
-                                       bool aNotify)
-{
   // Because we load image synchronously in non-responsive-mode, we need to do
   // reload after the attribute has been set if the reload is triggerred by
   // cross origin changing.
-  if (mForceReload) {
-    mForceReload = false;
+  if (forceReload) {
     // Mark channel as urgent-start before load image if the image load is
     // initaiated by a user interaction.
     mUseUrgentStartForChannel = EventStateManager::IsHandlingUserInput();
 
     if (InResponsiveMode()) {
       // per spec, full selection runs when this changes, even though
       // it doesn't directly affect the source selection
       QueueImageLoadTask(true);
     } else if (OwnerDoc()->IsCurrentActiveDocument()) {
       // Bug 1076583 - We still use the older synchronous algorithm in
       // non-responsive mode. Force a new load of the image with the
       // new cross origin policy
       ForceReload(aNotify);
     }
   }
-
-  return;
 }
 
 nsresult
 HTMLImageElement::GetEventTargetParent(EventChainPreVisitor& aVisitor)
 {
   // We handle image element with attribute ismap in its corresponding frame
   // element. Set mMultipleActionsPrevented here to prevent the click event
   // trigger the behaviors in Element::PostHandleEventForLinks
--- a/dom/html/HTMLImageElement.h
+++ b/dom/html/HTMLImageElement.h
@@ -356,44 +356,35 @@ protected:
   RefPtr<ResponsiveImageSelector> mResponsiveSelector;
 
 private:
   bool SourceElementMatches(nsIContent* aSourceNode);
 
   static void MapAttributesIntoRule(const nsMappedAttributes* aAttributes,
                                     GenericSpecifiedValues* aGenericData);
   /**
-   * This function is called by BeforeSetAttr and OnAttrSetButNotChanged.
+   * This function is called by AfterSetAttr and OnAttrSetButNotChanged.
    * It will not be called if the value is being unset.
    *
    * @param aNamespaceID the namespace of the attr being set
    * @param aName the localname of the attribute being set
    * @param aValue the value it's being set to represented as either a string or
    *        a parsed nsAttrValue.
-   * @param aNotify Whether we plan to notify document observers.
-   */
-  void BeforeMaybeChangeAttr(int32_t aNamespaceID, nsIAtom* aName,
-                             const nsAttrValueOrString& aValue,
-                             bool aNotify);
-  /**
-   * This function is called by AfterSetAttr and OnAttrSetButNotChanged.
-   * It will not be called if the value is being unset.
-   *
-   * @param aNamespaceID the namespace of the attr being set
-   * @param aName the localname of the attribute being set
+   * @param aOldValue the value previously set. Will be null if no value was
+   *        previously set. This value should only be used when
+   *        aValueMaybeChanged is true; when aValueMaybeChanged is false,
+   *        aOldValue should be considered unreliable.
+   * @param aValueMaybeChanged will be false when this function is called from
+   *        OnAttrSetButNotChanged to indicate that the value was not changed.
    * @param aNotify Whether we plan to notify document observers.
    */
   void AfterMaybeChangeAttr(int32_t aNamespaceID, nsIAtom* aName,
-                            bool aNotify);
-  /**
-   * Used by BeforeMaybeChangeAttr and AfterMaybeChangeAttr to keep track of
-   * whether a reload needs to be forced after an attribute change that is
-   * currently in progress.
-   */
-  bool mForceReload;
+                            const nsAttrValueOrString& aValue,
+                            const nsAttrValue* aOldValue,
+                            bool aValueMaybeChanged, bool aNotify);
 
   bool mInDocResponsiveContent;
   RefPtr<ImageLoadTask> mPendingImageLoadTask;
 
   // Last URL that was attempted to load by this element.
   nsCOMPtr<nsIURI> mLastSelectedSource;
   // Last pixel density that was selected.
   double mCurrentDensity;
--- a/dom/media/DecoderTraits.cpp
+++ b/dom/media/DecoderTraits.cpp
@@ -17,16 +17,19 @@
 #include "WebMDecoder.h"
 #include "WebMDemuxer.h"
 
 #ifdef MOZ_ANDROID_OMX
 #include "AndroidMediaDecoder.h"
 #include "AndroidMediaReader.h"
 #include "AndroidMediaPluginHost.h"
 #endif
+#ifdef MOZ_ANDROID_HLS_SUPPORT
+#include "HLSDecoder.h"
+#endif
 #ifdef MOZ_FMP4
 #include "MP4Decoder.h"
 #include "MP4Demuxer.h"
 #endif
 #include "MediaFormatReader.h"
 
 #include "MP3Decoder.h"
 #include "MP3Demuxer.h"
@@ -40,43 +43,43 @@
 #include "FlacDecoder.h"
 #include "FlacDemuxer.h"
 
 #include "nsPluginHost.h"
 #include "MediaPrefs.h"
 
 namespace mozilla
 {
-
-static bool
-IsHttpLiveStreamingType(const MediaContainerType& aType)
-{
-  return // For m3u8.
-         // https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-10
-         aType.Type() == MEDIAMIMETYPE("application/vnd.apple.mpegurl")
-         // Some sites serve these as the informal m3u type.
-         || aType.Type() == MEDIAMIMETYPE("application/x-mpegurl")
-         || aType.Type() == MEDIAMIMETYPE("audio/x-mpegurl");
-}
-
 #ifdef MOZ_ANDROID_OMX
 static bool
 IsAndroidMediaType(const MediaContainerType& aType)
 {
   if (!MediaDecoder::IsAndroidMediaPluginEnabled()) {
     return false;
   }
 
   return aType.Type() == MEDIAMIMETYPE("audio/mpeg")
          || aType.Type() == MEDIAMIMETYPE("audio/mp4")
          || aType.Type() == MEDIAMIMETYPE("video/mp4")
          || aType.Type() == MEDIAMIMETYPE("video/x-m4v");
 }
 #endif
 
+
+/* static */ bool
+DecoderTraits::IsHttpLiveStreamingType(const MediaContainerType& aType)
+{
+  return // For m3u8.
+         // https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-10
+         aType.Type() == MEDIAMIMETYPE("application/vnd.apple.mpegurl")
+         // Some sites serve these as the informal m3u type.
+         || aType.Type() == MEDIAMIMETYPE("application/x-mpegurl")
+         || aType.Type() == MEDIAMIMETYPE("audio/x-mpegurl");
+}
+
 /* static */ bool
 DecoderTraits::IsMP4SupportedType(const MediaContainerType& aType,
                                   DecoderDoctorDiagnostics* aDiagnostics)
 {
 #ifdef MOZ_FMP4
   return MP4Decoder::IsSupportedType(aType, aDiagnostics);
 #else
   return false;
@@ -160,17 +163,23 @@ CanHandleCodecsType(const MediaContainer
 
 static
 CanPlayStatus
 CanHandleMediaType(const MediaContainerType& aType,
                    DecoderDoctorDiagnostics* aDiagnostics)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  if (IsHttpLiveStreamingType(aType)) {
+#ifdef MOZ_ANDROID_HLS_SUPPORT
+  if (HLSDecoder::IsSupportedType(aType)) {
+    return CANPLAY_MAYBE;
+  }
+#endif
+
+  if (DecoderTraits::IsHttpLiveStreamingType(aType)) {
     Telemetry::Accumulate(Telemetry::MEDIA_HLS_CANPLAY_REQUESTED, true);
   }
 
   if (aType.ExtendedType().HaveCodecs()) {
     CanPlayStatus result = CanHandleCodecsType(aType, aDiagnostics);
     if (result == CANPLAY_NO || result == CANPLAY_YES) {
       return result;
     }
@@ -258,16 +267,22 @@ static
 already_AddRefed<MediaDecoder>
 InstantiateDecoder(const MediaContainerType& aType,
                    MediaDecoderInit& aInit,
                    DecoderDoctorDiagnostics* aDiagnostics)
 {
   MOZ_ASSERT(NS_IsMainThread());
   RefPtr<MediaDecoder> decoder;
 
+#ifdef MOZ_ANDROID_HLS_SUPPORT
+  if (HLSDecoder::IsSupportedType(aType)) {
+    decoder = new HLSDecoder(aInit);
+    return decoder.forget();
+  }
+#endif
 #ifdef MOZ_FMP4
   if (MP4Decoder::IsSupportedType(aType, aDiagnostics)) {
     decoder = new MP4Decoder(aInit);
     return decoder.forget();
   }
 #endif
   if (MP3Decoder::IsSupportedType(aType)) {
     decoder = new MP3Decoder(aInit);
@@ -297,17 +312,17 @@ InstantiateDecoder(const MediaContainerT
   }
 #endif
 
   if (WebMDecoder::IsSupportedType(aType)) {
     decoder = new WebMDecoder(aInit);
     return decoder.forget();
   }
 
-  if (IsHttpLiveStreamingType(aType)) {
+  if (DecoderTraits::IsHttpLiveStreamingType(aType)) {
     // We don't have an HLS decoder.
     Telemetry::Accumulate(Telemetry::MEDIA_HLS_DECODER_SUCCESS, false);
   }
 
   return nullptr;
 }
 
 /* static */
--- a/dom/media/DecoderTraits.h
+++ b/dom/media/DecoderTraits.h
@@ -56,14 +56,17 @@ public:
   // or false otherwise. Not all platforms support all MIME types, and
   // vice versa.
   static bool IsSupportedInVideoDocument(const nsACString& aType);
 
   // Convenience function that returns false if MOZ_FMP4 is not defined,
   // otherwise defers to MP4Decoder::IsSupportedType().
   static bool IsMP4SupportedType(const MediaContainerType& aType,
                                  DecoderDoctorDiagnostics* aDiagnostics);
+
+  // Returns true if aType is MIME type of hls.
+  static bool IsHttpLiveStreamingType(const MediaContainerType& aType);
 };
 
 } // namespace mozilla
 
 #endif
 
--- a/dom/media/MP3Demuxer.cpp
+++ b/dom/media/MP3Demuxer.cpp
@@ -3,16 +3,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/. */
 
 #include "MP3Demuxer.h"
 
 #include <inttypes.h>
 #include <algorithm>
+#include <limits>
 
 #include "mozilla/Assertions.h"
 #include "mozilla/EndianUtils.h"
 #include "mozilla/SizePrintfMacros.h"
 #include "nsAutoPtr.h"
 #include "VideoUtils.h"
 #include "TimeUnits.h"
 #include "prenv.h"
@@ -101,16 +102,17 @@ MP3Demuxer::NotifyDataRemoved()
   MP3LOGV("NotifyDataRemoved()");
 }
 
 
 // MP3TrackDemuxer
 
 MP3TrackDemuxer::MP3TrackDemuxer(MediaResource* aSource)
   : mSource(aSource)
+  , mFrameLock(false)
   , mOffset(0)
   , mFirstFrameOffset(0)
   , mNumParsedFrames(0)
   , mFrameIndex(0)
   , mTotalFrameLen(0)
   , mSamplesPerFrame(0)
   , mSamplesPerSecond(0)
   , mChannels(0)
@@ -415,19 +417,22 @@ MP3TrackDemuxer::Duration(int64_t aNumFr
 
   const double usPerFrame = USECS_PER_S * mSamplesPerFrame / mSamplesPerSecond;
   return TimeUnit::FromMicroseconds(aNumFrames * usPerFrame);
 }
 
 MediaByteRange
 MP3TrackDemuxer::FindFirstFrame()
 {
-  // Get engough successive frames to avoid invalid frame from cut stream.
-  // However, some website use very short mp3 file so using the same value as Chrome.
+  // We attempt to find multiple successive frames to avoid locking onto a false
+  // positive if we're fed a stream that has been cut mid-frame.
+  // For compatibility reasons we have to use the same frame count as Chrome, since
+  // some web sites actually use a file that short to test our playback capabilities.
   static const int MIN_SUCCESSIVE_FRAMES = 3;
+  mFrameLock = false;
 
   MediaByteRange candidateFrame = FindNextFrame();
   int numSuccFrames = candidateFrame.Length() > 0;
   MediaByteRange currentFrame = candidateFrame;
   MP3LOGV("FindFirst() first candidate frame: mOffset=%" PRIu64
           " Length()=%" PRIu64,
           candidateFrame.mStart, candidateFrame.Length());
 
@@ -457,17 +462,18 @@ MP3TrackDemuxer::FindFirstFrame()
       MP3LOGV("FindFirst() new candidate frame: mOffset=%" PRIu64
               " Length()=%" PRIu64,
               candidateFrame.mStart, candidateFrame.Length());
     }
   }
 
   if (numSuccFrames >= MIN_SUCCESSIVE_FRAMES) {
     MP3LOG("FindFirst() accepting candidate frame: "
-            "successiveFrames=%d", numSuccFrames);
+           "successiveFrames=%d", numSuccFrames);
+    mFrameLock = true;
   } else {
     MP3LOG("FindFirst() no suitable first frame found");
   }
   return candidateFrame;
 }
 
 static bool
 VerifyFrameConsistency(const FrameParser::Frame& aFrame1,
@@ -486,36 +492,63 @@ VerifyFrameConsistency(const FrameParser
          && h1.RawVersion() == h2.RawVersion()
          && h1.RawProtection() == h2.RawProtection();
 }
 
 MediaByteRange
 MP3TrackDemuxer::FindNextFrame()
 {
   static const int BUFFER_SIZE = 64;
-  static const int MAX_SKIPPED_BYTES = 1024 * BUFFER_SIZE;
+  static const uint32_t MAX_SKIPPABLE_BYTES = 1024 * BUFFER_SIZE;
 
   MP3LOGV("FindNext() Begin mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
           " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
           " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d",
           mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
           mSamplesPerFrame, mSamplesPerSecond, mChannels);
 
   uint8_t buffer[BUFFER_SIZE];
   int32_t read = 0;
 
   bool foundFrame = false;
   int64_t frameHeaderOffset = 0;
+  int64_t startOffset = mOffset;
+  const bool searchingForID3 = !mParser.ID3Header().Size();
 
   // Check whether we've found a valid MPEG frame.
   while (!foundFrame) {
-    if ((!mParser.FirstFrame().Length()
-         && mOffset - mParser.ID3Header().Size() > MAX_SKIPPED_BYTES)
+    // How many bytes we can go without finding a valid MPEG frame
+    // (effectively rounded up to the next full buffer size multiple, as we
+    // only check this before reading the next set of data into the buffer).
+
+    // This default value of 0 will be used during testing whether we're being
+    // fed a valid stream, which shouldn't have any gaps between frames.
+    uint32_t maxSkippableBytes = 0;
+
+    if (!mParser.FirstFrame().Length()) {
+      // We're looking for the first valid frame. A well-formed file should
+      // have its first frame header right at the start (skipping an ID3 tag
+      // if necessary), but in order to support files that might have been
+      // improperly cut, we search the first few kB for a frame header.
+      maxSkippableBytes = MAX_SKIPPABLE_BYTES;
+      // Since we're counting the skipped bytes from the offset we started
+      // this parsing session with, we need to discount the ID3 tag size only
+      // if we were looking for one during the current frame parsing session.
+      if (searchingForID3) {
+        maxSkippableBytes += mParser.ID3Header().TotalTagSize();
+      }
+    } else if (mFrameLock) {
+      // We've found a valid MPEG stream, so don't impose any limits
+      // to allow skipping corrupted data until we hit EOS.
+      maxSkippableBytes = std::numeric_limits<uint32_t>::max();
+    }
+
+    if ((mOffset - startOffset > maxSkippableBytes)
         || (read = Read(buffer, mOffset, BUFFER_SIZE)) == 0) {
-      MP3LOG("FindNext() EOS or exceeded MAX_SKIPPED_BYTES without a frame");
+      MP3LOG("FindNext() EOS or exceeded maxSkippeableBytes without a frame");
       // This is not a valid MPEG audio stream or we've reached EOS, give up.
       break;
     }
 
     ByteReader reader(buffer, read);
     uint32_t bytesToSkip = 0;
     foundFrame = mParser.Parse(&reader, &bytesToSkip);
     frameHeaderOffset =
@@ -1315,21 +1348,17 @@ static const uint8_t MAX_MAJOR_VER = 4;
 
 uint32_t
 ID3Parser::Parse(ByteReader* aReader)
 {
   MOZ_ASSERT(aReader);
 
   while (aReader->CanRead8() && !mHeader.ParseNext(aReader->ReadU8())) { }
 
-  if (mHeader.IsValid()) {
-    // Header found, return total tag size.
-    return ID3Header::SIZE + Header().Size() + Header().FooterSize();
-  }
-  return 0;
+  return mHeader.TotalTagSize();
 }
 
 void
 ID3Parser::Reset()
 {
   mHeader.Reset();
 }
 
@@ -1384,16 +1413,26 @@ uint8_t
 ID3Parser::ID3Header::FooterSize() const
 {
   if (Flags() & (1 << 4)) {
     return SIZE;
   }
   return 0;
 }
 
+uint32_t
+ID3Parser::ID3Header::TotalTagSize() const
+{
+  if (IsValid()) {
+    // Header found, return total tag size.
+    return ID3Header::SIZE + Size() + FooterSize();
+  }
+  return 0;
+}
+
 bool
 ID3Parser::ID3Header::ParseNext(uint8_t c)
 {
   if (!Update(c)) {
     Reset();
     if (!Update(c)) {
       Reset();
     }
--- a/dom/media/MP3Demuxer.h
+++ b/dom/media/MP3Demuxer.h
@@ -67,16 +67,20 @@ public:
     uint8_t Flags() const;
 
     // The derived size based on the provided size fields.
     uint32_t Size() const;
 
     // Returns the size of an ID3v2.4 footer if present and zero otherwise.
     uint8_t FooterSize() const;
 
+    // The total size of the ID3 tag including header/footer, or zero if
+    // none has been found.
+    uint32_t TotalTagSize() const;
+
     // Returns whether the parsed data is a valid ID3 header up to the given
     // byte position.
     bool IsValid(int aPos) const;
 
     // Returns whether the parsed data is a complete and valid ID3 header.
     bool IsValid() const;
 
     // Parses the next provided byte.
@@ -445,16 +449,19 @@ private:
   double AverageFrameLength() const;
 
   // The (hopefully) MPEG resource.
   MediaResourceIndex mSource;
 
   // MPEG frame parser used to detect frames and extract side info.
   FrameParser mParser;
 
+  // Whether we've locked onto a valid sequence of frames or not.
+  bool mFrameLock;
+
   // Current byte offset in the source stream.
   int64_t mOffset;
 
   // Byte offset of the begin of the first frame, or 0 if none parsed yet.
   int64_t mFirstFrameOffset;
 
   // Total parsed frames.
   uint64_t mNumParsedFrames;
--- a/dom/media/MediaPrefs.h
+++ b/dom/media/MediaPrefs.h
@@ -178,16 +178,19 @@ private:
 #endif
 
   // Ogg
   DECL_MEDIA_PREF("media.ogg.enabled",                        OggEnabled, bool, true);
   // Flac
   DECL_MEDIA_PREF("media.ogg.flac.enabled",                   FlacInOgg, bool, false);
   DECL_MEDIA_PREF("media.flac.enabled",                       FlacEnabled, bool, true);
 
+  // Hls
+  DECL_MEDIA_PREF("media.hls.enabled",                        HLSEnabled, bool, false);
+
 #if !defined(RELEASE_OR_BETA)
   DECL_MEDIA_PREF("media.rust.test_mode",                     RustTestMode, bool, false);
 #endif
 
 #if defined(MOZ_WIDGET_GTK)
   DECL_MEDIA_PREF("media.rust.mp4parser",                     EnableRustMP4Parser, bool, true);
 #else
   DECL_MEDIA_PREF("media.rust.mp4parser",                     EnableRustMP4Parser, bool, false);
--- a/dom/media/MediaResource.cpp
+++ b/dom/media/MediaResource.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et cindent: */
 /* 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 "mozilla/DebugOnly.h"
 
+#include "DecoderTraits.h"
 #include "MediaResource.h"
 #include "MediaResourceCallback.h"
 
 #include "mozilla/Mutex.h"
 #include "nsDebug.h"
 #include "nsNetUtil.h"
 #include "nsThreadUtils.h"
 #include "nsIFile.h"
@@ -27,16 +28,20 @@
 #include "nsICachingChannel.h"
 #include "nsIAsyncVerifyRedirectCallback.h"
 #include "nsContentUtils.h"
 #include "nsHostObjectProtocolHandler.h"
 #include <algorithm>
 #include "nsProxyRelease.h"
 #include "nsIContentPolicy.h"
 
+#ifdef MOZ_ANDROID_HLS_SUPPORT
+#include "HLSResource.h"
+#endif
+
 using mozilla::media::TimeUnit;
 
 #undef LOG
 #undef ILOG
 
 mozilla::LazyLogModule gMediaResourceLog("MediaResource");
 // Debug logging macro with object pointer and class name.
 #define LOG(msg, ...) MOZ_LOG(gMediaResourceLog, mozilla::LogLevel::Debug, \
@@ -1542,16 +1547,23 @@ MediaResource::Create(MediaResourceCallb
   aChannel->GetContentType(contentTypeString);
   Maybe<MediaContainerType> containerType = MakeMediaContainerType(contentTypeString);
   if (!containerType) {
     return nullptr;
   }
 
   RefPtr<MediaResource> resource;
 
+#ifdef MOZ_ANDROID_HLS_SUPPORT
+  if (DecoderTraits::IsHttpLiveStreamingType(containerType.value())) {
+    resource = new HLSResource(aCallback, aChannel, uri, *containerType);
+    return resource.forget();
+  }
+#endif
+
   // Let's try to create a FileMediaResource in case the channel is a nsIFile
   nsCOMPtr<nsIFileChannel> fc = do_QueryInterface(aChannel);
   if (fc) {
     resource = new FileMediaResource(aCallback, aChannel, uri, *containerType);
   }
 
   // If the URL is blobURL with a seekable inputStream, we can still use a
   // FileMediaResource. This basically means that the blobURL and its Blob have
new file mode 100644
--- /dev/null
+++ b/dom/media/hls/HLSDecoder.cpp
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "HLSDecoder.h"
+#include "AndroidBridge.h"
+#include "DecoderTraits.h"
+#include "HLSDemuxer.h"
+#include "HLSUtils.h"
+#include "MediaContainerType.h"
+#include "MediaDecoderStateMachine.h"
+#include "MediaFormatReader.h"
+#include "MediaPrefs.h"
+
+namespace mozilla {
+
+MediaDecoderStateMachine*
+HLSDecoder::CreateStateMachine()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  mReader =
+    new MediaFormatReader(this,
+                          new HLSDemuxer(GetResource()),
+                          GetVideoFrameContainer());
+
+  return new MediaDecoderStateMachine(this, mReader);
+}
+
+MediaDecoder*
+HLSDecoder::Clone(MediaDecoderInit& aInit)
+{
+  if (!IsEnabled()) {
+    return nullptr;
+  }
+  return new HLSDecoder(aInit);
+}
+
+bool
+HLSDecoder::IsEnabled()
+{
+  return MediaPrefs::HLSEnabled() && (jni::GetAPIVersion() >= 16);
+}
+
+bool
+HLSDecoder::IsSupportedType(const MediaContainerType& aContainerType)
+{
+  return IsEnabled() &&
+         DecoderTraits::IsHttpLiveStreamingType(aContainerType);
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/media/hls/HLSDecoder.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=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/. */
+
+#ifndef HLSDecoder_h_
+#define HLSDecoder_h_
+
+#include "MediaDecoder.h"
+
+namespace mozilla {
+class MediaFormatReader;
+
+class HLSDecoder final : public MediaDecoder
+{
+public:
+  // MediaDecoder interface.
+    explicit HLSDecoder(MediaDecoderInit& aInit) : MediaDecoder(aInit) { }
+
+    MediaDecoder* Clone(MediaDecoderInit& aInit) override;
+
+    MediaDecoderStateMachine* CreateStateMachine() override;
+
+    // Returns true if the HLS backend is pref'ed on.
+    static bool IsEnabled();
+
+    // Returns true if aContainerType is an HLS type that we think we can render
+    // with the a platform decoder backend.
+    // If provided, codecs are checked for support.
+    static bool IsSupportedType(const MediaContainerType& aContainerType);
+
+private:
+  RefPtr<MediaFormatReader> mReader;
+};
+
+} // namespace mozilla
+
+#endif /* HLSDecoder_h_ */
new file mode 100644
--- /dev/null
+++ b/dom/media/hls/HLSDemuxer.cpp
@@ -0,0 +1,655 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "HLSDemuxer.h"
+
+#include <algorithm>
+#include <limits>
+#include <stdint.h>
+
+#include "HLSResource.h"
+#include "HLSUtils.h"
+#include "MediaCodec.h"
+#include "mozilla/Unused.h"
+#include "nsPrintfCString.h"
+
+using namespace mozilla::java;
+
+namespace mozilla {
+
+static Atomic<uint32_t> sStreamSourceID(0u);
+
+typedef TrackInfo::TrackType TrackType;
+using media::TimeUnit;
+using media::TimeIntervals;
+using media::TimeInterval;
+
+static
+VideoInfo::Rotation getVideoInfoRotation(int aRotation)
+{
+  switch (aRotation) {
+    case 0:
+      return VideoInfo::Rotation::kDegree_0;
+    case 90:
+      return VideoInfo::Rotation::kDegree_90;
+    case 180:
+      return VideoInfo::Rotation::kDegree_180;
+    case 270:
+      return VideoInfo::Rotation::kDegree_270;
+    default:
+      return VideoInfo::Rotation::kDegree_0;
+  }
+}
+
+static
+mozilla::StereoMode getStereoMode(int aMode)
+{
+  switch (aMode) {
+    case 0:
+      return mozilla::StereoMode::MONO;
+    case 1:
+      return mozilla::StereoMode::TOP_BOTTOM;
+    case 2:
+      return mozilla::StereoMode::LEFT_RIGHT;
+    default:
+      return mozilla::StereoMode::MONO;
+  }
+}
+
+// HlsDemuxerCallbacksSupport is a native implemented callback class for
+// HlsDemuxerCallbacks in GeckoHlsDemuxerWrapper.java.
+// The callback functions will be invoked by JAVA-side thread.
+// Should dispatch the task to the demuxer's task queue.
+// We ensure the callback will never be invoked after
+// HlsDemuxerCallbacksSupport::DisposeNative has been called in ~HLSDemuxer.
+class HLSDemuxer::HlsDemuxerCallbacksSupport
+ : public GeckoHlsDemuxerWrapper::HlsDemuxerCallbacks::Natives<HlsDemuxerCallbacksSupport>
+{
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(HlsDemuxerCallbacksSupport)
+public:
+  typedef GeckoHlsDemuxerWrapper::HlsDemuxerCallbacks::Natives<HlsDemuxerCallbacksSupport> NativeCallbacks;
+  using NativeCallbacks::DisposeNative;
+  using NativeCallbacks::AttachNative;
+
+  HlsDemuxerCallbacksSupport(HLSDemuxer* aDemuxer)
+    : mMutex("HlsDemuxerCallbacksSupport")
+    , mDemuxer(aDemuxer)
+  {
+    MOZ_ASSERT(mDemuxer);
+  }
+
+  void OnInitialized(bool aHasAudio, bool aHasVideo)
+  {
+    HLS_DEBUG("HlsDemuxerCallbacksSupport",
+              "OnInitialized");
+    MutexAutoLock lock(mMutex);
+    if (!mDemuxer) { return; }
+    RefPtr<HlsDemuxerCallbacksSupport> self = this;
+    mDemuxer->GetTaskQueue()->Dispatch(NS_NewRunnableFunction(
+     [=] () {
+       MutexAutoLock lock(self->mMutex);
+       if (self->mDemuxer) {
+         self->mDemuxer->OnInitialized(aHasAudio, aHasVideo);
+       }
+     }));
+  }
+
+  // TODO: Handle the unexpected error signal from the java implementation
+  // in bug 1368904.
+  void OnError(int aErrorCode)
+  {
+    HLS_DEBUG("HlsDemuxerCallbacksSupport",
+              "Got error(%d) from java side",
+              aErrorCode);
+  }
+  void Detach()
+  {
+    MutexAutoLock lock(mMutex);
+    mDemuxer = nullptr;
+  }
+
+  Mutex mMutex;
+private:
+  ~HlsDemuxerCallbacksSupport() { }
+  HLSDemuxer* mDemuxer;
+
+};
+
+HLSDemuxer::HLSDemuxer(MediaResource* aResource)
+  : mResource(aResource)
+  , mTaskQueue(new AutoTaskQueue(GetMediaThreadPool(MediaThreadType::PLAYBACK),
+                                 /* aSupportsTailDispatch = */ false))
+  , mMutex("HLSDemuxer")
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aResource);
+  HlsDemuxerCallbacksSupport::Init();
+  mJavaCallbacks = GeckoHlsDemuxerWrapper::HlsDemuxerCallbacks::New();
+  MOZ_ASSERT(mJavaCallbacks);
+
+  mCallbackSupport = new HlsDemuxerCallbacksSupport(this);
+  HlsDemuxerCallbacksSupport::AttachNative(mJavaCallbacks,
+                                           mCallbackSupport);
+
+  auto resourceWrapper = static_cast<HLSResource*>(aResource)->GetResourceWrapper();
+  mHlsDemuxerWrapper = GeckoHlsDemuxerWrapper::Create(resourceWrapper->GetPlayer(), mJavaCallbacks);
+  MOZ_ASSERT(mHlsDemuxerWrapper);
+}
+
+void
+HLSDemuxer::OnInitialized(bool aHasAudio, bool aHasVideo)
+{
+  MOZ_ASSERT(OnTaskQueue());
+
+  if (aHasAudio) {
+    UpdateAudioInfo(0);
+  }
+  if (aHasVideo) {
+    UpdateVideoInfo(0);
+  }
+
+  mInitPromise.ResolveIfExists(NS_OK, __func__);
+}
+
+RefPtr<HLSDemuxer::InitPromise>
+HLSDemuxer::Init()
+{
+  RefPtr<HLSDemuxer> self = this;
+  return InvokeAsync(GetTaskQueue(), __func__,
+    [self](){
+      RefPtr<InitPromise> p = self->mInitPromise.Ensure(__func__);
+      return p;
+    });
+}
+
+void HLSDemuxer::NotifyDataArrived()
+{
+  HLS_DEBUG("HLSDemuxer", "NotifyDataArrived");
+}
+
+bool
+HLSDemuxer::HasTrackType(TrackType aType) const
+{
+  MutexAutoLock lock(mMutex);
+  HLS_DEBUG("HLSDemuxer", "HasTrackType(%d)", aType);
+  switch (aType) {
+    case TrackType::kAudioTrack:
+      return mInfo.HasAudio();
+    case TrackType::kVideoTrack:
+      return mInfo.HasVideo();
+    default:
+      return false;
+  }
+}
+
+uint32_t
+HLSDemuxer::GetNumberTracks(TrackType aType) const
+{
+  switch (aType) {
+    case TrackType::kAudioTrack:
+      return mHlsDemuxerWrapper->GetNumberOfTracks(TrackType::kAudioTrack);
+    case TrackType::kVideoTrack:
+      return mHlsDemuxerWrapper->GetNumberOfTracks(TrackType::kVideoTrack);
+    default:
+      return 0;
+  }
+}
+
+already_AddRefed<MediaTrackDemuxer>
+HLSDemuxer::GetTrackDemuxer(TrackType aType, uint32_t aTrackNumber)
+{
+  RefPtr<HLSTrackDemuxer> e = new HLSTrackDemuxer(this, aType);
+  mDemuxers.AppendElement(e);
+  return e.forget();
+}
+
+bool
+HLSDemuxer::IsSeekable() const
+{
+  return !mHlsDemuxerWrapper->IsLiveStream();
+}
+
+UniquePtr<EncryptionInfo>
+HLSDemuxer::GetCrypto()
+{
+  // TODO: Currently, our HLS implementation doesn't support encrypted content.
+  // Return null at this stage.
+  return nullptr;
+}
+
+TrackInfo*
+HLSDemuxer::GetTrackInfo(TrackType aTrack)
+{
+  MutexAutoLock lock(mMutex);
+  switch (aTrack) {
+    case TrackType::kAudioTrack: {
+      return &mInfo.mAudio;
+    }
+    case TrackType::kVideoTrack: {
+      return &mInfo.mVideo;
+    }
+    default:
+      return nullptr;
+  }
+}
+
+TimeUnit
+HLSDemuxer::GetNextKeyFrameTime()
+{
+  MOZ_ASSERT(mHlsDemuxerWrapper);
+  return TimeUnit::FromMicroseconds(mHlsDemuxerWrapper->GetNextKeyFrameTime());
+}
+
+void
+HLSDemuxer::UpdateAudioInfo(int index)
+{
+  MOZ_ASSERT(OnTaskQueue());
+  MOZ_ASSERT(mHlsDemuxerWrapper);
+  HLS_DEBUG("HLSDemuxer", "UpdateAudioInfo (%d)", index);
+  MutexAutoLock lock(mMutex);
+  jni::Object::LocalRef infoObj = mHlsDemuxerWrapper->GetAudioInfo(index);
+  if (infoObj) {
+    java::GeckoAudioInfo::LocalRef audioInfo(Move(infoObj));
+    mInfo.mAudio.mRate = audioInfo->Rate();
+    mInfo.mAudio.mChannels = audioInfo->Channels();
+    mInfo.mAudio.mProfile = audioInfo->Profile();
+    mInfo.mAudio.mBitDepth = audioInfo->BitDepth();
+    mInfo.mAudio.mMimeType = NS_ConvertUTF16toUTF8(audioInfo->MimeType()->ToString());
+    mInfo.mAudio.mDuration = TimeUnit::FromMicroseconds(audioInfo->Duration());
+    auto&& csd = audioInfo->CodecSpecificData()->GetElements();
+    mInfo.mAudio.mCodecSpecificConfig->Clear();
+    mInfo.mAudio.mCodecSpecificConfig->AppendElements(reinterpret_cast<uint8_t*>(&csd[0]),
+                                                      csd.Length());
+  }
+}
+
+void
+HLSDemuxer::UpdateVideoInfo(int index)
+{
+  MOZ_ASSERT(OnTaskQueue());
+  MOZ_ASSERT(mHlsDemuxerWrapper);
+  MutexAutoLock lock(mMutex);
+  jni::Object::LocalRef infoObj = mHlsDemuxerWrapper->GetVideoInfo(index);
+  if (infoObj) {
+    java::GeckoVideoInfo::LocalRef videoInfo(Move(infoObj));
+    mInfo.mVideo.mStereoMode = getStereoMode(videoInfo->StereoMode());
+    mInfo.mVideo.mRotation = getVideoInfoRotation(videoInfo->Rotation());
+    mInfo.mVideo.mImage.width = videoInfo->DisplayWidth();
+    mInfo.mVideo.mImage.height = videoInfo->DisplayHeight();
+    mInfo.mVideo.mDisplay.width = videoInfo->PictureWidth();
+    mInfo.mVideo.mDisplay.height = videoInfo->PictureHeight();
+    mInfo.mVideo.mMimeType = NS_ConvertUTF16toUTF8(videoInfo->MimeType()->ToString());
+    mInfo.mVideo.mDuration = TimeUnit::FromMicroseconds(videoInfo->Duration());
+    HLS_DEBUG("HLSDemuxer", "UpdateVideoInfo (%d) / I(%dx%d) / D(%dx%d)",
+     index, mInfo.mVideo.mImage.width, mInfo.mVideo.mImage.height,
+     mInfo.mVideo.mDisplay.width, mInfo.mVideo.mDisplay.height);
+  }
+}
+
+bool
+HLSDemuxer::OnTaskQueue() const
+{
+  return mTaskQueue->IsCurrentThreadIn();
+}
+
+HLSDemuxer::~HLSDemuxer()
+{
+  HLS_DEBUG("HLSDemuxer", "~HLSDemuxer()");
+  mCallbackSupport->Detach();
+  if (mJavaCallbacks) {
+    HlsDemuxerCallbacksSupport::DisposeNative(mJavaCallbacks);
+    mJavaCallbacks = nullptr;
+  }
+  if (mHlsDemuxerWrapper) {
+    mHlsDemuxerWrapper->Destroy();
+    mHlsDemuxerWrapper = nullptr;
+  }
+  mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+}
+
+HLSTrackDemuxer::HLSTrackDemuxer(HLSDemuxer* aParent, TrackInfo::TrackType aType)
+  : mParent(aParent)
+  , mType(aType)
+{
+}
+
+UniquePtr<TrackInfo>
+HLSTrackDemuxer::GetInfo() const
+{
+  return mParent->GetTrackInfo(mType)->Clone();
+}
+
+RefPtr<HLSTrackDemuxer::SeekPromise>
+HLSTrackDemuxer::Seek(const TimeUnit& aTime)
+{
+  MOZ_ASSERT(mParent, "Called after BreackCycle()");
+  return InvokeAsync<TimeUnit&&>(mParent->GetTaskQueue(),
+                                 this,
+                                 __func__,
+                                 &HLSTrackDemuxer::DoSeek,
+                                 aTime);
+}
+
+RefPtr<HLSTrackDemuxer::SeekPromise>
+HLSTrackDemuxer::DoSeek(const TimeUnit& aTime)
+{
+  MOZ_ASSERT(mParent, "Called after BreackCycle()");
+  MOZ_ASSERT(mParent->OnTaskQueue());
+  mQueuedSample = nullptr;
+  int64_t seekTimeUs = aTime.ToMicroseconds();
+  bool result = mParent->mHlsDemuxerWrapper->Seek(seekTimeUs);
+  if (!result) {
+    return SeekPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA,
+                                        __func__);
+  }
+  TimeUnit seekTime = TimeUnit::FromMicroseconds(seekTimeUs);
+  return SeekPromise::CreateAndResolve(seekTime, __func__);
+}
+
+RefPtr<HLSTrackDemuxer::SamplesPromise>
+HLSTrackDemuxer::GetSamples(int32_t aNumSamples)
+{
+  MOZ_ASSERT(mParent, "Called after BreackCycle()");
+  return InvokeAsync(mParent->GetTaskQueue(), this, __func__,
+                     &HLSTrackDemuxer::DoGetSamples, aNumSamples);
+}
+
+RefPtr<HLSTrackDemuxer::SamplesPromise>
+HLSTrackDemuxer::DoGetSamples(int32_t aNumSamples)
+{
+  MOZ_ASSERT(mParent, "Called after BreackCycle()");
+  MOZ_ASSERT(mParent->OnTaskQueue());
+  if (!aNumSamples) {
+    return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+                                           __func__);
+  }
+  RefPtr<SamplesHolder> samples = new SamplesHolder;
+  if (mQueuedSample) {
+    if (mQueuedSample->mEOS) {
+      return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+                                             __func__);
+    }
+    MOZ_ASSERT(mQueuedSample->mKeyframe,
+               "mQueuedSample must be a keyframe");
+    samples->mSamples.AppendElement(mQueuedSample);
+    mQueuedSample = nullptr;
+    aNumSamples--;
+  }
+  if (aNumSamples == 0) {
+    // Return the queued sample.
+    return SamplesPromise::CreateAndResolve(samples, __func__);
+  }
+  mozilla::jni::ObjectArray::LocalRef demuxedSamples =
+    (mType == TrackInfo::kAudioTrack)
+    ? mParent->mHlsDemuxerWrapper->GetSamples(TrackInfo::kAudioTrack, aNumSamples)
+    : mParent->mHlsDemuxerWrapper->GetSamples(TrackInfo::kVideoTrack, aNumSamples);
+  nsTArray<jni::Object::LocalRef> sampleObjectArray(demuxedSamples->GetElements());
+
+  if (sampleObjectArray.IsEmpty()) {
+    return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA, __func__);
+  }
+
+  for (auto&& demuxedSample : sampleObjectArray) {
+    java::GeckoHlsSample::LocalRef sample(Move(demuxedSample));
+    if (sample->IsEOS()) {
+      HLS_DEBUG("HLSTrackDemuxer", "Met BUFFER_FLAG_END_OF_STREAM.");
+      if (samples->mSamples.IsEmpty()) {
+        return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+                                               __func__);
+      }
+      mQueuedSample = new MediaRawData();
+      mQueuedSample->mEOS = true;
+      break;
+    }
+    RefPtr<MediaRawData> mrd = ConvertToMediaRawData(sample);
+    if (!mrd) {
+      return SamplesPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
+    }
+    samples->mSamples.AppendElement(mrd);
+  }
+  if (mType == TrackInfo::kVideoTrack &&
+      (mNextKeyframeTime.isNothing() ||
+       samples->mSamples.LastElement()->mTime >= mNextKeyframeTime.value())) {
+    // Only need to find NextKeyFrame for Video
+    UpdateNextKeyFrameTime();
+  }
+
+  return SamplesPromise::CreateAndResolve(samples, __func__);
+}
+
+
+CryptoSample
+HLSTrackDemuxer::ExtractCryptoSample(size_t aSampleSize,
+                                     java::sdk::CryptoInfo::LocalRef aCryptoInfo)
+{
+  if (!aCryptoInfo) {
+    return CryptoSample{};
+  }
+  // Extract Crypto information
+  CryptoSample crypto;
+  char const* msg = "";
+  do {
+    HLS_DEBUG("HLSTrackDemuxer", "Sample has Crypto Info");
+    crypto.mValid = true;
+    int32_t mode = 0;
+    if (NS_FAILED(aCryptoInfo->Mode(&mode))) {
+      msg = "Error when extracting encryption mode.";
+      break;
+    }
+    crypto.mMode = mode;
+    mozilla::jni::ByteArray::LocalRef ivData;
+    if (NS_FAILED(aCryptoInfo->Iv(&ivData))) {
+      msg = "Error when extracting encryption IV.";
+      break;
+    }
+    // Data in mIV is uint8_t and jbyte is signed char
+    auto&& ivArr= ivData->GetElements();
+    crypto.mIV.AppendElements(reinterpret_cast<uint8_t*>(&ivArr[0]),
+                              ivArr.Length());
+    crypto.mIVSize = ivArr.Length();
+    mozilla::jni::ByteArray::LocalRef keyData;
+    if (NS_FAILED(aCryptoInfo->Key(&keyData))) {
+      msg = "Error when extracting encryption key.";
+      break;
+    }
+    auto&& keyArr = keyData->GetElements();
+    // Data in mKeyId is uint8_t and jbyte is signed char
+    crypto.mKeyId.AppendElements(reinterpret_cast<uint8_t*>(&keyArr[0]),
+                                 keyArr.Length());
+
+    mozilla::jni::IntArray::LocalRef clearData;
+    if (NS_FAILED(aCryptoInfo->NumBytesOfClearData(&clearData))) {
+      msg = "Error when extracting clear data.";
+      break;
+    }
+    // Data in mPlainSizes is uint16_t, NumBytesOfClearData is int32_t
+    // , so need a for loop to copy
+    for (const auto& b : clearData->GetElements()) {
+      crypto.mPlainSizes.AppendElement(b);
+    }
+
+    mozilla::jni::IntArray::LocalRef encryptedData;
+    if (NS_FAILED(aCryptoInfo->NumBytesOfEncryptedData(&encryptedData))) {
+      msg = "Error when extracting encrypted data.";
+      break;
+    }
+    auto&& encryptedArr = encryptedData->GetElements();
+    // Data in mEncryptedSizes is uint32_t, NumBytesOfEncryptedData is int32_t
+    crypto.mEncryptedSizes.AppendElements(reinterpret_cast<uint32_t*>(&encryptedArr[0]),
+                                          encryptedArr.Length());
+    int subSamplesNum = 0;
+    if (NS_FAILED(aCryptoInfo->NumSubSamples(&subSamplesNum))) {
+      msg = "Error when extracting subsamples.";
+      break;
+    }
+    crypto.mPlainSizes[0] -= (aSampleSize - subSamplesNum);
+
+    return crypto;
+  } while (false);
+
+  HLS_DEBUG("HLSTrackDemuxer",
+            "%s", msg);
+  return CryptoSample{};
+}
+
+RefPtr<MediaRawData>
+HLSTrackDemuxer::ConvertToMediaRawData(java::GeckoHlsSample::LocalRef aSample)
+{
+  java::sdk::BufferInfo::LocalRef info = aSample->Info();
+  // Currently extract PTS, Size and Data without Crypto information.
+  // Transform java Sample into MediaRawData
+  RefPtr<MediaRawData> mrd = new MediaRawData();
+  int64_t presentationTimeUs = 0;
+  bool ok = NS_SUCCEEDED(info->PresentationTimeUs(&presentationTimeUs));
+  mrd->mTime = TimeUnit::FromMicroseconds(presentationTimeUs);
+  mrd->mTimecode = TimeUnit::FromMicroseconds(presentationTimeUs);
+  mrd->mKeyframe = aSample->IsKeyFrame();
+  mrd->mDuration = (mType == TrackInfo::kVideoTrack)
+                   ? TimeUnit::FromMicroseconds(aSample->Duration())
+                   : TimeUnit::Zero();
+
+  int32_t size = 0;
+  ok &= NS_SUCCEEDED(info->Size(&size));
+  if (!ok) {
+    HLS_DEBUG("HLSTrackDemuxer", "Error occurred during extraction from Sample java object.");
+    return nullptr;
+  }
+
+  // Update streamSouceID & videoInfo for MFR.
+  if (mType == TrackInfo::kVideoTrack) {
+    auto sampleFormatIndex = aSample->FormatIndex();
+    if (mLastFormatIndex != sampleFormatIndex) {
+      mLastFormatIndex = sampleFormatIndex;
+      mParent->UpdateVideoInfo(mLastFormatIndex);
+      MutexAutoLock lock(mParent->mMutex);
+      mrd->mTrackInfo = new TrackInfoSharedPtr(mParent->mInfo.mVideo, ++sStreamSourceID);
+    }
+  }
+
+  // Write payload into MediaRawData
+  UniquePtr<MediaRawDataWriter> writer(mrd->CreateWriter());
+  if (!writer->SetSize(size)) {
+    HLS_DEBUG("HLSTrackDemuxer", "Exit failed to allocate media buffer");
+    return nullptr;
+  }
+  jni::ByteBuffer::LocalRef dest =
+    jni::ByteBuffer::New(writer->Data(), writer->Size());
+  aSample->WriteToByteBuffer(dest);
+
+  writer->mCrypto = ExtractCryptoSample(writer->Size(),
+                                        aSample->CryptoInfo());
+  return mrd;
+}
+
+void
+HLSTrackDemuxer::Reset()
+{
+  MOZ_ASSERT(mParent, "Called after BreackCycle()");
+  mQueuedSample = nullptr;
+}
+
+void
+HLSTrackDemuxer::UpdateNextKeyFrameTime()
+{
+  MOZ_ASSERT(mParent, "Called after BreackCycle()");
+  TimeUnit nextKeyFrameTime = mParent->GetNextKeyFrameTime();
+  if (nextKeyFrameTime != mNextKeyframeTime.refOr(TimeUnit::FromInfinity())) {
+    HLS_DEBUG("HLSTrackDemuxer", "Update mNextKeyframeTime to %" PRId64 , nextKeyFrameTime.ToMicroseconds());
+    mNextKeyframeTime = Some(nextKeyFrameTime);
+  }
+}
+
+nsresult
+HLSTrackDemuxer::GetNextRandomAccessPoint(TimeUnit* aTime)
+{
+  if (mNextKeyframeTime.isNothing()) {
+    // There's no next key frame.
+    *aTime = TimeUnit::FromInfinity();
+  } else {
+    *aTime = mNextKeyframeTime.value();
+  }
+  return NS_OK;
+}
+
+RefPtr<HLSTrackDemuxer::SkipAccessPointPromise>
+HLSTrackDemuxer::SkipToNextRandomAccessPoint(
+  const TimeUnit& aTimeThreshold)
+{
+  return InvokeAsync(
+           mParent->GetTaskQueue(), this, __func__,
+           &HLSTrackDemuxer::DoSkipToNextRandomAccessPoint,
+           aTimeThreshold);
+}
+
+RefPtr<HLSTrackDemuxer::SkipAccessPointPromise>
+HLSTrackDemuxer::DoSkipToNextRandomAccessPoint(
+  const TimeUnit& aTimeThreshold)
+{
+  MOZ_ASSERT(mParent, "Called after BreackCycle()");
+  MOZ_ASSERT(mParent->OnTaskQueue());
+  mQueuedSample = nullptr;
+  uint32_t parsed = 0;
+  bool found = false;
+  MediaResult result = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+  do {
+    mozilla::jni::ObjectArray::LocalRef demuxedSamples =
+      mParent->mHlsDemuxerWrapper->GetSamples(mType, 1);
+    nsTArray<jni::Object::LocalRef> sampleObjectArray(demuxedSamples->GetElements());
+    if (sampleObjectArray.IsEmpty()) {
+      result = NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA;
+      break;
+    }
+    parsed++;
+    java::GeckoHlsSample::LocalRef sample(Move(sampleObjectArray[0]));
+    if (sample->IsEOS()) {
+      result = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+      break;
+    }
+    if (sample->IsKeyFrame()) {
+      java::sdk::BufferInfo::LocalRef info = sample->Info();
+      int64_t presentationTimeUs = 0;
+      bool ok = NS_SUCCEEDED(info->PresentationTimeUs(&presentationTimeUs));
+      if (ok && TimeUnit::FromMicroseconds(presentationTimeUs) >= aTimeThreshold) {
+        found = true;
+        mQueuedSample = ConvertToMediaRawData(sample);
+        break;
+      }
+    }
+  } while(true);
+
+  if (!found) {
+    return SkipAccessPointPromise::CreateAndReject(
+      SkipFailureHolder(result, parsed),
+      __func__);
+  }
+  return SkipAccessPointPromise::CreateAndResolve(parsed, __func__);
+}
+
+TimeIntervals
+HLSTrackDemuxer::GetBuffered()
+{
+  MOZ_ASSERT(mParent, "Called after BreackCycle()");
+  int64_t bufferedTime = mParent->mHlsDemuxerWrapper->GetBuffered(); //us
+  return TimeIntervals(TimeInterval(TimeUnit(),
+                                    TimeUnit::FromMicroseconds(bufferedTime)));
+}
+
+void
+HLSTrackDemuxer::BreakCycles()
+{
+  RefPtr<HLSTrackDemuxer> self = this;
+  nsCOMPtr<nsIRunnable> task =
+    NS_NewRunnableFunction([self]() {
+      self->mParent = nullptr;
+    } );
+  mParent->GetTaskQueue()->Dispatch(task.forget());
+}
+
+HLSTrackDemuxer::~HLSTrackDemuxer()
+{
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/media/hls/HLSDemuxer.h
@@ -0,0 +1,130 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#if !defined(HLSDemuxer_h_)
+#define HLSDemuxer_h_
+
+#include "AutoTaskQueue.h"
+#include "GeneratedJNINatives.h"
+#include "GeneratedJNIWrappers.h"
+#include "MediaCodec.h"
+#include "MediaDataDemuxer.h"
+#include "MediaDecoder.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Mutex.h"
+
+#include "VideoUtils.h"
+
+namespace mozilla {
+
+class AbstractThread;
+class MediaResult;
+class HLSTrackDemuxer;
+
+class HLSDemuxer final : public MediaDataDemuxer
+{
+  class HlsDemuxerCallbacksSupport;
+public:
+  explicit HLSDemuxer(MediaResource* aResource);
+
+  RefPtr<InitPromise> Init() override;
+
+  bool HasTrackType(TrackInfo::TrackType aType) const override;
+
+  uint32_t GetNumberTracks(TrackInfo::TrackType aType) const override;
+
+  already_AddRefed<MediaTrackDemuxer>
+  GetTrackDemuxer(TrackInfo::TrackType aType, uint32_t aTrackNumber) override;
+
+  bool IsSeekable() const override;
+
+  UniquePtr<EncryptionInfo> GetCrypto() override;
+
+  bool ShouldComputeStartTime() const override { return true; }
+
+  void NotifyDataArrived() override;
+
+  AutoTaskQueue* GetTaskQueue() const { return mTaskQueue; }
+  void OnInitialized(bool aHasAudio, bool aHasVideo);
+
+private:
+  media::TimeUnit GetNextKeyFrameTime();
+  void UpdateVideoInfo(int index);
+  void UpdateAudioInfo(int index);
+  bool OnTaskQueue() const;
+  TrackInfo* GetTrackInfo(TrackInfo::TrackType);
+  ~HLSDemuxer();
+  RefPtr<MediaResource> mResource;
+  friend class HLSTrackDemuxer;
+
+  const RefPtr<AutoTaskQueue> mTaskQueue;
+  nsTArray<RefPtr<HLSTrackDemuxer>> mDemuxers;
+
+  MozPromiseHolder<InitPromise> mInitPromise;
+  RefPtr<HlsDemuxerCallbacksSupport> mCallbackSupport;
+
+  // Mutex to protect members below across multiple threads.
+  mutable Mutex mMutex;
+  MediaInfo mInfo;
+
+  java::GeckoHlsDemuxerWrapper::HlsDemuxerCallbacks::GlobalRef mJavaCallbacks;
+  java::GeckoHlsDemuxerWrapper::GlobalRef mHlsDemuxerWrapper;
+};
+
+class HLSTrackDemuxer : public MediaTrackDemuxer
+{
+public:
+  HLSTrackDemuxer(HLSDemuxer* aParent,
+                  TrackInfo::TrackType aType);
+  ~HLSTrackDemuxer();
+  UniquePtr<TrackInfo> GetInfo() const override;
+
+  RefPtr<SeekPromise> Seek(const media::TimeUnit& aTime) override;
+
+  RefPtr<SamplesPromise> GetSamples(int32_t aNumSamples = 1) override;
+
+  void Reset() override;
+
+  nsresult GetNextRandomAccessPoint(media::TimeUnit* aTime) override;
+
+  RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint(
+    const media::TimeUnit& aTimeThreshold) override;
+
+  media::TimeIntervals GetBuffered() override;
+
+  void BreakCycles() override;
+
+  bool GetSamplesMayBlock() const override
+  {
+    return false;
+  }
+
+private:
+  // Update the timestamp of the next keyframe if there's one.
+  void UpdateNextKeyFrameTime();
+
+  // Runs on HLSDemuxer's task queue.
+  RefPtr<SeekPromise> DoSeek(const media::TimeUnit& aTime);
+  RefPtr<SamplesPromise> DoGetSamples(int32_t aNumSamples);
+  RefPtr<SkipAccessPointPromise> DoSkipToNextRandomAccessPoint(
+    const media::TimeUnit& aTimeThreshold);
+
+  CryptoSample ExtractCryptoSample(size_t aSampleSize,
+                                   java::sdk::CryptoInfo::LocalRef aCryptoInfo);
+  RefPtr<MediaRawData> ConvertToMediaRawData(java::GeckoHlsSample::LocalRef aSample);
+
+  RefPtr<HLSDemuxer> mParent;
+  TrackInfo::TrackType mType;
+  Maybe<media::TimeUnit> mNextKeyframeTime;
+  int32_t mLastFormatIndex = -1;
+  // Queued samples extracted by the demuxer, but not yet returned.
+  RefPtr<MediaRawData> mQueuedSample;
+};
+
+} // namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/dom/media/hls/HLSResource.cpp
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "HLSResource.h"
+#include "HLSUtils.h"
+
+using namespace mozilla::java;
+
+namespace mozilla {
+
+HlsResourceCallbacksSupport::HlsResourceCallbacksSupport(HLSResource* aResource)
+{
+  MOZ_ASSERT(aResource);
+  mResource = aResource;
+}
+
+void
+HlsResourceCallbacksSupport::OnDataArrived()
+{
+  MOZ_ASSERT(mResource);
+  mResource->onDataAvailable();
+}
+
+void
+HlsResourceCallbacksSupport::OnError(int aErrorCode)
+{
+  MOZ_ASSERT(mResource);
+}
+
+HLSResource::HLSResource(MediaResourceCallback* aCallback,
+                         nsIChannel* aChannel,
+                         nsIURI* aURI,
+                         const MediaContainerType& aContainerType)
+  : BaseMediaResource(aCallback, aChannel, aURI, aContainerType)
+{
+  nsCString spec;
+  nsresult rv = aURI->GetSpec(spec);
+  (void)rv;
+  HlsResourceCallbacksSupport::Init();
+  mJavaCallbacks = GeckoHlsResourceWrapper::HlsResourceCallbacks::New();
+  HlsResourceCallbacksSupport::AttachNative(mJavaCallbacks,
+                                            mozilla::MakeUnique<HlsResourceCallbacksSupport>(this));
+  mHlsResourceWrapper = java::GeckoHlsResourceWrapper::Create(NS_ConvertUTF8toUTF16(spec),
+                                                              mJavaCallbacks);
+  MOZ_ASSERT(mHlsResourceWrapper);
+}
+
+void
+HLSResource::onDataAvailable()
+{
+  MOZ_ASSERT(mCallback);
+  HLS_DEBUG("HLSResource", "onDataAvailable");
+  mCallback->NotifyDataArrived();
+}
+
+HLSResource::~HLSResource()
+{
+  if (mJavaCallbacks) {
+    HlsResourceCallbacksSupport::DisposeNative(mJavaCallbacks);
+    mJavaCallbacks = nullptr;
+  }
+  if (mHlsResourceWrapper) {
+    mHlsResourceWrapper->Destroy();
+    mHlsResourceWrapper = nullptr;
+  }
+  HLS_DEBUG("HLSResource", "Destroy");
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/media/hls/HLSResource.h
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HLSResource_h_
+#define HLSResource_h_
+
+#include "GeneratedJNINatives.h"
+#include "GeneratedJNIWrappers.h"
+#include "HLSUtils.h"
+#include "nsContentUtils.h"
+
+#define UNIMPLEMENTED() HLS_DEBUG("HLSResource", "UNIMPLEMENTED FUNCTION")
+
+using namespace mozilla::java;
+
+namespace mozilla {
+
+class HLSResource;
+
+class HlsResourceCallbacksSupport
+  : public GeckoHlsResourceWrapper::HlsResourceCallbacks::Natives<HlsResourceCallbacksSupport>
+{
+public:
+  typedef GeckoHlsResourceWrapper::HlsResourceCallbacks::Natives<HlsResourceCallbacksSupport> NativeCallbacks;
+  using NativeCallbacks::DisposeNative;
+  using NativeCallbacks::AttachNative;
+
+  HlsResourceCallbacksSupport(HLSResource* aResource);
+  void OnDataArrived();
+  void OnError(int aErrorCode);
+
+private:
+  HLSResource* mResource;
+};
+
+class HLSResource final : public BaseMediaResource
+{
+public:
+  HLSResource(MediaResourceCallback* aCallback,
+              nsIChannel* aChannel,
+              nsIURI* aURI,
+              const MediaContainerType& aContainerType);
+  ~HLSResource();
+  nsresult Close() override { return NS_OK; }
+  void Suspend(bool aCloseImmediately) override { UNIMPLEMENTED(); }
+  void Resume() override { UNIMPLEMENTED(); }
+  bool CanClone() override { UNIMPLEMENTED(); return false; }
+  already_AddRefed<MediaResource> CloneData(MediaResourceCallback*) override { UNIMPLEMENTED(); return nullptr; }
+  void SetReadMode(MediaCacheStream::ReadMode aMode) override { UNIMPLEMENTED(); }
+  void SetPlaybackRate(uint32_t aBytesPerSecond) override  { UNIMPLEMENTED(); }
+  nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount, uint32_t* aBytes) override { UNIMPLEMENTED(); return NS_ERROR_FAILURE; }
+  bool ShouldCacheReads() override { UNIMPLEMENTED(); return false; }
+  int64_t Tell() override { UNIMPLEMENTED(); return -1; }
+  void Pin() override { UNIMPLEMENTED(); }
+  void Unpin() override { UNIMPLEMENTED(); }
+  double GetDownloadRate(bool* aIsReliable) override { UNIMPLEMENTED(); *aIsReliable = false; return 0; }
+  int64_t GetLength() override { UNIMPLEMENTED(); return -1; }
+  int64_t GetNextCachedData(int64_t aOffset) override { UNIMPLEMENTED(); return -1; }
+  int64_t GetCachedDataEnd(int64_t aOffset) override { UNIMPLEMENTED(); return -1; }
+  bool IsDataCachedToEndOfResource(int64_t aOffset) override { UNIMPLEMENTED(); return false; }
+  bool IsSuspendedByCache() override { UNIMPLEMENTED(); return false; }
+  bool IsSuspended() override { UNIMPLEMENTED(); return false; }
+  nsresult ReadFromCache(char* aBuffer, int64_t aOffset, uint32_t aCount) override { UNIMPLEMENTED(); return NS_ERROR_FAILURE; }
+  nsresult Open(nsIStreamListener** aStreamListener) override { UNIMPLEMENTED(); return NS_OK; }
+
+  already_AddRefed<nsIPrincipal> GetCurrentPrincipal() override
+  {
+    NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
+
+    nsCOMPtr<nsIPrincipal> principal;
+    nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager();
+    if (!secMan || !mChannel)
+      return nullptr;
+    secMan->GetChannelResultPrincipal(mChannel, getter_AddRefs(principal));
+    return principal.forget();
+  }
+
+  nsresult GetCachedRanges(MediaByteRangeSet& aRanges) override
+  {
+    UNIMPLEMENTED();
+    return NS_OK;
+  }
+
+  bool IsTransportSeekable() override { return true; }
+
+  const MediaContainerType& GetContentType() const override { return mContainerType; }
+
+  bool IsLiveStream() override
+  {
+    return false;
+  }
+
+  bool IsExpectingMoreData() override
+  {
+    return false;
+  }
+
+  java::GeckoHlsResourceWrapper::GlobalRef GetResourceWrapper() {
+    return mHlsResourceWrapper;
+  }
+
+private:
+  friend class HlsResourceCallbacksSupport;
+
+  void onDataAvailable();
+
+  size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override
+  {
+    size_t size = MediaResource::SizeOfExcludingThis(aMallocSizeOf);
+    size += mContainerType.SizeOfExcludingThis(aMallocSizeOf);
+
+    return size;
+  }
+
+  size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override
+  {
+    return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+  }
+
+  java::GeckoHlsResourceWrapper::GlobalRef mHlsResourceWrapper;
+  java::GeckoHlsResourceWrapper::HlsResourceCallbacks::GlobalRef mJavaCallbacks;
+};
+
+} // namespace mozilla
+#endif /* HLSResource_h_ */
new file mode 100644
--- /dev/null
+++ b/dom/media/hls/HLSUtils.cpp
@@ -0,0 +1,7 @@
+#include "HLSUtils.h"
+
+mozilla::LogModule* GetHLSLog()
+{
+  static mozilla::LazyLogModule sLogModule("HLS");
+  return sLogModule;
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/media/hls/HLSUtils.h
@@ -0,0 +1,17 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=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/. */
+
+#ifndef HLSUtils_h_
+#define HLSUtils_h_
+
+#include "mozilla/Logging.h"
+// Logger
+mozilla::LogModule* GetHLSLog();
+
+#define HLS_DEBUG(TAG, format, ...) MOZ_LOG(GetHLSLog(), mozilla::LogLevel::Debug, (TAG "(%p)::%s: " format, this, __func__, ##__VA_ARGS__))
+#define HLS_DEBUG_NON_MEMBER(TAG, format, ...) MOZ_LOG(GetHLSLog(), mozilla::LogLevel::Debug, (TAG " %s: " format, __func__, ##__VA_ARGS__))
+
+#endif // HLSUtils_h_
new file mode 100644
--- /dev/null
+++ b/dom/media/hls/moz.build
@@ -0,0 +1,26 @@
+# 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/.
+
+
+EXPORTS += [
+    'HLSDecoder.h',
+    'HLSDemuxer.h',
+    'HLSResource.h',
+    'HLSUtils.h',
+]
+
+UNIFIED_SOURCES += [
+    'HLSDecoder.cpp',
+    'HLSDemuxer.cpp',
+    'HLSResource.cpp',
+    'HLSUtils.cpp',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'
+
+if CONFIG['GNU_CXX']:
+    CXXFLAGS += ['-Wno-error=shadow']
--- a/dom/media/moz.build
+++ b/dom/media/moz.build
@@ -48,16 +48,19 @@ DIRS += [
     'webrtc',
     'webspeech',
     'webvtt',
 ]
 
 if CONFIG['MOZ_ANDROID_OMX']:
     DIRS += ['android']
 
+if CONFIG['MOZ_ANDROID_HLS_SUPPORT']:
+    DIRS += ['hls']
+
 if CONFIG['MOZ_FMP4']:
     DIRS += ['fmp4']
 
 if CONFIG['MOZ_WEBRTC']:
     DIRS += ['bridge']
 
 TEST_DIRS += [
     'gtest',
@@ -308,16 +311,19 @@ else:
     DEFINES['WEBRTC_POSIX'] = True
 
 if CONFIG['ANDROID_VERSION'] > '15':
     DEFINES['MOZ_OMX_WEBM_DECODER'] = True
 
 if CONFIG['MOZ_GONK_MEDIACODEC']:
     DEFINES['MOZ_GONK_MEDIACODEC'] = True
 
+if CONFIG['MOZ_ANDROID_HLS_SUPPORT']:
+    DEFINES['MOZ_ANDROID_HLS_SUPPORT'] = True
+
 include('/ipc/chromium/chromium-config.mozbuild')
 
 # Suppress some GCC warnings being treated as errors:
 #  - about attributes on forward declarations for types that are already
 #    defined, which complains about an important MOZ_EXPORT for android::AString
 if CONFIG['GNU_CC']:
     CXXFLAGS += [
         '-Wno-error=attributes',
--- a/dom/xbl/nsXBLService.cpp
+++ b/dom/xbl/nsXBLService.cpp
@@ -415,27 +415,17 @@ public:
   explicit AutoStyleNewChildren(Element* aElement) : mElement(aElement) { MOZ_ASSERT(mElement); }
   ~AutoStyleNewChildren()
   {
     nsIPresShell* presShell = mElement->OwnerDoc()->GetShell();
     if (!presShell || !presShell->DidInitialize()) {
       return;
     }
     if (ServoStyleSet* servoSet = presShell->StyleSet()->GetAsServo()) {
-      // In general the element is always styled by the time we're applying XBL
-      // bindings, because we need to style the element to know what the binding
-      // URI is. However, programmatic consumers of the XBL service (like the
-      // XML pretty printer) _can_ apply bindings without having styled the bound
-      // element. We could assert against this and require the callers manually
-      // resolve the style first, but it's easy enough to just handle here.
-      if (MOZ_UNLIKELY(!mElement->HasServoData())) {
-        servoSet->StyleNewSubtree(mElement);
-      } else {
-        servoSet->StyleNewChildren(mElement);
-      }
+      servoSet->StyleNewlyBoundElement(mElement);
     }
   }
 
 private:
   Element* mElement;
 };
 
 // This function loads a particular XBL file and installs all of the bindings
--- a/ipc/mscom/Ptr.h
+++ b/ipc/mscom/Ptr.h
@@ -238,17 +238,17 @@ ToProxyUniquePtr(RefPtr<T>&& aRefPtr)
 {
   return ProxyUniquePtr<T>(aRefPtr.forget().take());
 }
 
 template <typename T>
 inline ProxyUniquePtr<T>
 ToProxyUniquePtr(const RefPtr<T>& aRefPtr)
 {
-  MOZ_ASSERT(IsProxy(aRawPtr));
+  MOZ_ASSERT(IsProxy(aRefPtr));
   MOZ_ASSERT((XRE_IsParentProcess() && NS_IsMainThread()) ||
              (XRE_IsContentProcess() && IsCurrentThreadMTA()));
 
   return ProxyUniquePtr<T>(do_AddRef(aRefPtr).take());
 }
 
 template <typename T>
 inline ProxyUniquePtr<T>
--- a/js/xpconnect/loader/mozJSComponentLoader.cpp
+++ b/js/xpconnect/loader/mozJSComponentLoader.cpp
@@ -419,20 +419,16 @@ mozJSComponentLoader::LoadModule(FileLoc
         fprintf(stderr, "mJCL: couldn't get nsIModule from jsval\n");
 #endif
         return nullptr;
     }
 
     // Cache this module for later
     mModules.Put(spec, entry);
 
-    // Set the location information for the new global, so that tools like
-    // about:memory may use that information
-    xpc::SetLocationForGlobal(entryObj, spec);
-
     // The hash owns the ModuleEntry now, forget about it
     return entry.forget();
 }
 
 void
 mozJSComponentLoader::FindTargetObject(JSContext* aCx,
                                        MutableHandleObject aTargetObject)
 {
@@ -460,16 +456,17 @@ mozJSComponentLoader::SizeOfIncludingThi
     n += SizeOfTableExcludingThis(mModules, aMallocSizeOf);
     n += SizeOfTableExcludingThis(mImports, aMallocSizeOf);
     n += SizeOfTableExcludingThis(mInProgressImports, aMallocSizeOf);
     return n;
 }
 
 void
 mozJSComponentLoader::CreateLoaderGlobal(JSContext* aCx,
+                                         nsACString& aLocation,
                                          JSAddonId* aAddonID,
                                          MutableHandleObject aGlobal)
 {
     RefPtr<BackstagePass> backstagePass;
     nsresult rv = NS_NewBackstagePass(getter_AddRefs(backstagePass));
     NS_ENSURE_SUCCESS_VOID(rv);
 
     CompartmentOptions options;
@@ -502,28 +499,35 @@ mozJSComponentLoader::CreateLoaderGlobal
     backstagePass->SetGlobalObject(global);
 
     JSAutoCompartment ac(aCx, global);
     if (!JS_DefineFunctions(aCx, global, gGlobalFun) ||
         !JS_DefineProfilingFunctions(aCx, global)) {
         return;
     }
 
+    // Set the location information for the new global, so that tools like
+    // about:memory may use that information
+    xpc::SetLocationForGlobal(global, aLocation);
+
     aGlobal.set(global);
 }
 
 JSObject*
 mozJSComponentLoader::PrepareObjectForLocation(JSContext* aCx,
                                                nsIFile* aComponentFile,
                                                nsIURI* aURI,
                                                bool* aRealFile)
 {
+    nsAutoCString nativePath;
+    NS_ENSURE_SUCCESS(aURI->GetSpec(nativePath), nullptr);
+
     RootedObject globalObj(aCx);
 
-    CreateLoaderGlobal(aCx, MapURIToAddonID(aURI), &globalObj);
+    CreateLoaderGlobal(aCx, nativePath, MapURIToAddonID(aURI), &globalObj);
 
     // |thisObj| is the object we set properties on for a particular .jsm.
     // XXX Right now, thisObj is always globalObj, but if we start
     // sharing globals between jsms, they won't be the same.
     // See bug 1186409.
     RootedObject thisObj(aCx, globalObj);
     NS_ENSURE_TRUE(thisObj, nullptr);
 
@@ -553,20 +557,16 @@ mozJSComponentLoader::PrepareObjectForLo
             NS_ENSURE_SUCCESS(rv, nullptr);
             NS_ENSURE_TRUE(locationObj, nullptr);
 
             if (!JS_DefineProperty(aCx, thisObj, "__LOCATION__", locationObj, 0))
                 return nullptr;
         }
     }
 
-    nsAutoCString nativePath;
-    rv = aURI->GetSpec(nativePath);
-    NS_ENSURE_SUCCESS(rv, nullptr);
-
     // Expose the URI from which the script was imported through a special
     // variable that we insert into the JSM.
     RootedString exposedUri(aCx, JS_NewStringCopyN(aCx, nativePath.get(), nativePath.Length()));
     NS_ENSURE_TRUE(exposedUri, nullptr);
 
     if (!JS_DefineProperty(aCx, thisObj, "__URI__", exposedUri, 0))
         return nullptr;
 
@@ -986,20 +986,16 @@ mozJSComponentLoader::ImportInto(const n
                 JS_SetPendingException(callercx, exception);
                 return NS_OK;
             }
 
             // Something failed, but we don't know what it is, guess.
             return NS_ERROR_FILE_NOT_FOUND;
         }
 
-        // Set the location information for the new global, so that tools like
-        // about:memory may use that information
-        xpc::SetLocationForGlobal(newEntry->obj, aLocation);
-
         mod = newEntry;
     }
 
     MOZ_ASSERT(mod->obj, "Import table contains entry with no object");
     vp.set(mod->obj);
 
     if (targetObj) {
         // cxhelper must be created before jsapi, so that jsapi is destroyed and
--- a/js/xpconnect/loader/mozJSComponentLoader.h
+++ b/js/xpconnect/loader/mozJSComponentLoader.h
@@ -58,16 +58,17 @@ class mozJSComponentLoader : public mozi
     virtual ~mozJSComponentLoader();
 
     static mozJSComponentLoader* sSelf;
 
     nsresult ReallyInit();
     void UnloadModules();
 
     void CreateLoaderGlobal(JSContext* aCx,
+                            nsACString& aLocation,
                             JSAddonId* aAddonID,
                             JS::MutableHandleObject aGlobal);
 
     JSObject* PrepareObjectForLocation(JSContext* aCx,
                                        nsIFile* aComponentFile,
                                        nsIURI* aComponent,
                                        bool* aRealFile);
 
--- a/layout/reftests/bugs/reftest.list
+++ b/layout/reftests/bugs/reftest.list
@@ -1550,17 +1550,17 @@ needs-focus == 568441.html 568441-ref.ht
 == 569006-1.html 569006-1-ref.html
 == 571281-1a.html 571281-1-ref.html
 == 571281-1b.html 571281-1-ref.html
 == 571281-1c.html 571281-1-ref.html
 == 571347-1a.html 571347-1-ref.html
 == 571347-1b.html 571347-1-ref.html
 == 571347-2a.html 571347-2-ref.html
 == 571347-2b.html 571347-2-ref.html
-fails-if(styloVsGecko||stylo) == 571347-2c.html 571347-2-ref.html
+== 571347-2c.html 571347-2-ref.html
 == 571347-2d.html 571347-2-ref.html
 == 571347-3.html 571347-3-ref.html
 == 572598-1.html 572598-ref.html
 == 574898-1.html 574898-ref.html
 # 574907 is a windows-only issue, result on other platforms depends on details of font support
 random-if(!winWidget) random-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)) == 574907-1.html 574907-1-ref.html # Bug 1258240
 random-if(!winWidget) random-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)) == 574907-2.html 574907-2-ref.html # Bug 1258240
 # 574907-3 only worked under directwrite, and even there it now depends on the rendering mode; marking as random for now
@@ -1662,17 +1662,17 @@ random-if(d2d) fails-if(Android) fuzzy-i
 HTTP(..) == 635639-1.html 635639-1-ref.html
 HTTP(..) == 635639-2.html 635639-2-ref.html
 random == 637597-1.html 637597-1-ref.html # bug 637597 was never really fixed!
 fuzzy-if(Android,8,500) == 637852-1.html 637852-1-ref.html
 fuzzy-if(Android,8,500) fuzzy-if(skiaContent,2,1) fuzzy-if(webrender,3,19) == 637852-2.html 637852-2-ref.html
 fuzzy-if(Android,8,500) == 637852-3.html 637852-3-ref.html
 == 641770-1.html 641770-1-ref.html
 == 641856-1.html 641856-1-ref.html
-fails-if(styloVsGecko||stylo) == 645491-1.html 645491-1-ref.html
+== 645491-1.html 645491-1-ref.html
 == 645647-1.html 645647-1-ref.html
 == 645647-2.html 645647-2-ref.html
 == 645768-1.html 645768-1-ref.html
 fails-if(layersGPUAccelerated&&cocoaWidget) fuzzy-if(!layersGPUAccelerated,41,260) fuzzy-if(skiaContent,57,11000) == 650228-1.html 650228-1-ref.html # Quartz alpha blending doesn't match GL alpha blending
 needs-focus == 652301-1a.html 652301-1-ref.html
 needs-focus == 652301-1b.html 652301-1-ref.html
 fuzzy-if(skiaContent,2,5) == 652775-1.html 652775-1-ref.html
 HTTP == 652991-1a.html 652991-1-ref.html
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-import/1331291-1.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<style>
+@import url(subdir/sheet.css);
+</style>
+<script>
+document.styleSheets[0].cssRules[0].styleSheet
+  .insertRule("* { background: url(1x1-green-in-subdir.png) red; }", 0);
+</script>
--- a/layout/reftests/css-import/reftest.list
+++ b/layout/reftests/css-import/reftest.list
@@ -3,11 +3,12 @@
 == 436261-2.html 436261-ref.html
 fails-if(styloVsGecko||stylo) == 436261-3.html 436261-ref.html
 == 444723-1.html 444723-ref.html
 == 444723-2.html 444723-ref.html
 random-if(styloVsGecko||stylo) == 445415-1a.xhtml 445415-1-ref.xhtml
 == 445415-1b.xhtml 445415-1-ref.xhtml
 == 445415-2a.xhtml 445415-2-ref.xhtml
 fails-if(styloVsGecko||stylo) == 445415-2b.xhtml 445415-2-ref.xhtml
+== 1331291-1.html green.html
 == 1368782-1.html green.html
 == 1368782-2.html green.html
 == 1368782-3.html green.html
new file mode 100644
index 0000000000000000000000000000000000000000..b98ca0ba0a03c580ac339e4a3653539cfa8edc71
GIT binary patch
literal 135
zc%17D@N?(olHy`uVBq!ia0vp^j3CU&3?x-=hn)gaEa{HEjtmUzPnffIy#(@^1AIbU
z85$Vo{9HN_$mQ~MaSW-rm7D-%GchneRloNK$dW8^jVKAuPb(=;EJ|f?Ovz75Rq)JB
bOiv9;O-!jQJeg_(RK(!v>gTe~DWM4f4{;-!
new file mode 100644
--- a/layout/style/ServoStyleSet.cpp
+++ b/layout/style/ServoStyleSet.cpp
@@ -369,19 +369,24 @@ ServoStyleSet::PrepareAndTraverseSubtree
 
   const SnapshotTable& snapshots = Snapshots();
 
   bool isInitial = !aRoot->HasServoData();
   bool forReconstruct =
     aRestyleBehavior == TraversalRestyleBehavior::ForReconstruct;
   bool forAnimationOnly =
     aRestyleBehavior == TraversalRestyleBehavior::ForAnimationOnly;
+#ifdef DEBUG
+  bool forNewlyBoundElement =
+    aRestyleBehavior == TraversalRestyleBehavior::ForNewlyBoundElement;
+#endif
   bool postTraversalRequired = Servo_TraverseSubtree(
     aRoot, mRawSet.get(), &snapshots, aRootBehavior, aRestyleBehavior);
-  MOZ_ASSERT(!(isInitial || forReconstruct) || !postTraversalRequired);
+  MOZ_ASSERT(!(isInitial || forReconstruct || forNewlyBoundElement) ||
+             !postTraversalRequired);
 
   // Don't need to trigger a second traversal if this restyle only needs
   // animation-only restyle.
   if (forAnimationOnly) {
     return postTraversalRequired;
   }
 
   auto root = const_cast<Element*>(aRoot);
@@ -951,16 +956,44 @@ ServoStyleSet::StyleNewChildren(Element*
   PrepareAndTraverseSubtree(aParent,
                             TraversalRootBehavior::UnstyledChildrenOnly,
                             TraversalRestyleBehavior::Normal);
   // We can't assert that Servo_TraverseSubtree returns false, since aParent
   // or some of its other children might have pending restyles.
 }
 
 void
+ServoStyleSet::StyleNewlyBoundElement(Element* aElement)
+{
+  PreTraverse();
+
+  // In general the element is always styled by the time we're applying XBL
+  // bindings, because we need to style the element to know what the binding
+  // URI is. However, programmatic consumers of the XBL service (like the
+  // XML pretty printer) _can_ apply bindings without having styled the bound
+  // element. We could assert against this and require the callers manually
+  // resolve the style first, but it's easy enough to just handle here.
+  //
+  // Also, when applying XBL bindings to elements within a display:none or
+  // unstyled subtree (for example, when <object> elements are wrapped to be
+  // exposed to JS), we need to tell the traversal that it is OK to
+  // skip restyling, rather than panic when trying to unwrap the styles
+  // it expects to have just computed.
+
+  TraversalRootBehavior rootBehavior =
+    MOZ_UNLIKELY(!aElement->HasServoData())
+      ? TraversalRootBehavior::Normal
+      : TraversalRootBehavior::UnstyledChildrenOnly;
+
+  PrepareAndTraverseSubtree(aElement,
+                            rootBehavior,
+                            TraversalRestyleBehavior::ForNewlyBoundElement);
+}
+
+void
 ServoStyleSet::StyleSubtreeForReconstruct(Element* aRoot)
 {
   PreTraverse(aRoot);
 
   DebugOnly<bool> postTraversalRequired =
     PrepareAndTraverseSubtree(aRoot,
                               TraversalRootBehavior::Normal,
                               TraversalRestyleBehavior::ForReconstruct);
--- a/layout/style/ServoStyleSet.h
+++ b/layout/style/ServoStyleSet.h
@@ -301,16 +301,24 @@ public:
    * Like the above, but skips the root node, and only styles unstyled children.
    * When potentially appending multiple children, it's preferable to call
    * StyleNewChildren on the node rather than making multiple calls to
    * StyleNewSubtree on each child, since it allows for more parallelism.
    */
   void StyleNewChildren(dom::Element* aParent);
 
   /**
+   * Eagerly styles the children of an element that has just had an XBL
+   * binding applied to it.  Some XBL consumers attach bindings to elements
+   * that have not been styled yet, and in such cases, this will do the
+   * equivalent of StyleNewSubtree instead.
+   */
+  void StyleNewlyBoundElement(dom::Element* aElement);
+
+  /**
    * Like StyleNewSubtree, but in response to a request to reconstruct frames
    * for the given subtree, and so works on elements that already have
    * styles.  This will leave the subtree in a state just like after an initial
    * styling, i.e. with new styles, no change hints, and with the dirty
    * descendants bits cleared.  No comparison of old and new styles is done,
    * so no change hints will be processed.
    */
   void StyleSubtreeForReconstruct(dom::Element* aRoot);
--- a/layout/style/ServoStyleSheet.cpp
+++ b/layout/style/ServoStyleSheet.cpp
@@ -171,17 +171,22 @@ ServoStyleSheet::ParseSheet(css::Loader*
 
   Inner()->mURLData = extraData.forget();
   return NS_OK;
 }
 
 void
 ServoStyleSheet::LoadFailed()
 {
-  Inner()->mSheet = Servo_StyleSheet_Empty(mParsingMode).Consume();
+  if (!Inner()->mSheet) {
+    // Only create empty stylesheet if this is a top level stylesheet.
+    // The raw sheet for stylesheet of @import rule is already set in
+    // loader, and we should not touch it.
+    Inner()->mSheet = Servo_StyleSheet_Empty(mParsingMode).Consume();
+  }
   Inner()->mURLData = URLExtraData::Dummy();
 }
 
 nsresult
 ServoStyleSheet::ReparseSheet(const nsAString& aInput)
 {
   // TODO(kuoe0): Bug 1367996 - Need to call document notification
   // (StyleRuleAdded() and StyleRuleRemoved()) like what we do in
--- a/layout/style/ServoTypes.h
+++ b/layout/style/ServoTypes.h
@@ -58,16 +58,20 @@ enum class TraversalRootBehavior {
 // or whether it should traverse in a mode that doesn't generate any change
 // hints, which is what's required when handling frame reconstruction.
 // The change hints in this case are unneeded, since the old frames have
 // already been destroyed.
 // Indicates how the Servo style system should perform.
 enum class TraversalRestyleBehavior {
   // Normal processing.
   Normal,
+  // Normal processing, but tolerant to calls to restyle elements in unstyled
+  // or display:none subtrees (which can occur when styling elements with
+  // newly applied XBL bindings).
+  ForNewlyBoundElement,
   // Traverses in a mode that doesn't generate any change hints, which is what's
   // required when handling frame reconstruction.  The change hints in this case
   // are unneeded, since the old frames have already been destroyed.
   ForReconstruct,
   // Processes animation-only restyle.
   ForAnimationOnly,
   // Traverses as normal mode but tries to update all CSS animations.
   ForCSSRuleChanges,
new file mode 100644
--- /dev/null
+++ b/layout/style/crashtests/1370793-1.xhtml
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <bindings xmlns="http://www.mozilla.org/xbl">
+      <binding id="a">
+        <content>
+          <children/>
+        </content>
+      </binding>
+    </bindings>
+    <script>
+      function doTest() {
+        // Cause XBL bindings to be attached to the object element by
+        // wrapping it for JS.
+        document.querySelector("object");
+      }
+    </script>
+  </head>
+  <body onload="doTest()">
+    <div style="display: none;">
+      <object style="-moz-binding: url(#a);" data="about:blank"></object>
+    </div>
+  </body>
+</html>
--- a/layout/style/crashtests/crashtests.list
+++ b/layout/style/crashtests/crashtests.list
@@ -166,10 +166,11 @@ load 1319072-1.html
 HTTP load 1320423-1.html
 load 1321357-1.html
 load 1328535-1.html
 load 1331272.html
 HTTP load 1333001-1.html
 pref(dom.animations-api.core.enabled,true) load 1340344.html
 load 1342316-1.html
 load 1356601-1.html
+load 1370793-1.xhtml
 load content-only-on-link-before.html
 load content-only-on-visited-before.html
--- a/layout/style/test/mochitest.ini
+++ b/layout/style/test/mochitest.ini
@@ -149,16 +149,17 @@ support-files = file_bug829816.css
 [test_bug1055933.html]
 support-files = file_bug1055933_circle-xxl.png
 [test_bug1089417.html]
 support-files = file_bug1089417_iframe.html
 [test_bug1112014.html]
 [test_bug1203766.html]
 [test_bug1232829.html]
 [test_bug1292447.html]
+[test_bug1371488.html]
 [test_cascade.html]
 [test_ch_ex_no_infloops.html]
 [test_change_hint_optimizations.html]
 [test_clip-path_polygon.html]
 [test_color_rounding.html]
 [test_compute_data_with_start_struct.html]
 skip-if = toolkit == 'android'
 [test_computed_style.html]
new file mode 100644
--- /dev/null
+++ b/layout/style/test/test_bug1371488.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Test for bug 1371488</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+  <style id="test">
+    @import url(non-exist-file.css);
+  </style>
+</head>
+<body>
+<pre id="log">
+<script>
+  let sheet = document.getElementById("test").sheet;
+  let rule = sheet.cssRules[0];
+  ok(rule, "The import rule should not be null even if the file fails to load");
+  is(rule.type, CSSRule.IMPORT_RULE, "It is the import rule");
+  ok(rule.styleSheet, "The associated stylesheet should exists as well");
+  is(rule.styleSheet.cssRules.length, 0, "The stylesheet should be empty");
+</script>
+</pre>
+</body>
+</html>
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
@@ -2907,16 +2907,22 @@ PeerConnectionImpl::PluginCrash(uint32_t
 
 void
 PeerConnectionImpl::RecordEndOfCallTelemetry() const
 {
   if (!mJsepSession) {
     return;
   }
 
+  // Exit early if no connection information was ever exchanged,
+  // This prevents distortion of telemetry data.
+  if (mLocalRequestedSDP.empty() && mRemoteRequestedSDP.empty()) {
+    return;
+  }
+
   // Bitmask used for WEBRTC/LOOP_CALL_TYPE telemetry reporting
   static const uint32_t kAudioTypeMask = 1;
   static const uint32_t kVideoTypeMask = 2;
   static const uint32_t kDataChannelTypeMask = 4;
 
   // Report end-of-call Telemetry
   if (mJsepSession->GetNegotiations() > 0) {
     Telemetry::Accumulate(Telemetry::WEBRTC_RENEGOTIATIONS,
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
@@ -733,17 +733,17 @@ private:
   bool mDtlsConnected;
 
   nsCOMPtr<nsIThread> mThread;
   // TODO: Remove if we ever properly wire PeerConnection for cycle-collection.
   nsWeakPtr mPCObserver;
 
   nsCOMPtr<nsPIDOMWindowInner> mWindow;
 
-  // The SDP sent in from JS - here for debugging.
+  // The SDP sent in from JS
   std::string mLocalRequestedSDP;
   std::string mRemoteRequestedSDP;
 
   // DTLS fingerprint
   std::string mFingerprint;
   std::string mRemoteFingerprint;
 
   // identity-related fields
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -626,16 +626,20 @@ pref("media.mediasource.enabled", true);
 
 pref("media.mediadrm-widevinecdm.visible", true);
 
 #ifdef NIGHTLY_BUILD
 // Enable EME (Encrypted Media Extensions)
 pref("media.eme.enabled", true);
 #endif
 
+#ifdef NIGHTLY_BUILD
+pref("media.hls.enabled", true);
+#endif
+
 // optimize images memory usage
 pref("image.downscale-during-decode.enabled", true);
 
 pref("browser.safebrowsing.downloads.enabled", false);
 
 pref("browser.safebrowsing.id", @MOZ_APP_UA_NAME@);
 
 // True if this is the first time we are showing about:firstrun
--- a/mobile/android/base/java/org/mozilla/gecko/Tab.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tab.java
@@ -30,17 +30,16 @@ import org.mozilla.gecko.util.ThreadUtil
 import org.mozilla.gecko.webapps.WebAppIndexer;
 import org.mozilla.gecko.widget.SiteLogins;
 
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.drawable.BitmapDrawable;
-import android.os.Build;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
 
 public class Tab {
     private static final String LOGTAG = "GeckoTab";
@@ -244,23 +243,19 @@ public class Tab {
     }
 
     public void setMostRecentHomePanelData(Bundle data) {
         mMostRecentHomePanelData = data;
     }
 
     public Bitmap getThumbnailBitmap(int width, int height) {
         if (mThumbnailBitmap != null) {
-            // Bug 787318 - Honeycomb has a bug with bitmap caching, we can't
-            // reuse the bitmap there.
-            boolean honeycomb = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
-                              && Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB_MR2);
             boolean sizeChange = mThumbnailBitmap.getWidth() != width
                               || mThumbnailBitmap.getHeight() != height;
-            if (honeycomb || sizeChange) {
+            if (sizeChange) {
                 mThumbnailBitmap = null;
             }
         }
 
         if (mThumbnailBitmap == null) {
             Bitmap.Config config = (GeckoAppShell.getScreenDepth() == 24) ?
                 Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
             mThumbnailBitmap = Bitmap.createBitmap(width, height, config);
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
@@ -2182,33 +2182,22 @@ public final class BrowserDatabaseHelper
             cursor = db.rawQuery("PRAGMA synchronous=NORMAL", null);
         } finally {
             if (cursor != null)
                 cursor.close();
         }
 
         // From Honeycomb on, it's possible to run several db
         // commands in parallel using multiple connections.
-        if (Build.VERSION.SDK_INT >= 11) {
-            // Modern Android allows WAL to be enabled through a mode flag.
-            if (Build.VERSION.SDK_INT < 16) {
-                db.enableWriteAheadLogging();
+        // Modern Android allows WAL to be enabled through a mode flag.
+        if (Build.VERSION.SDK_INT < 16) {
+            db.enableWriteAheadLogging();
 
-                // This does nothing on 16+.
-                db.setLockingEnabled(false);
-            }
-        } else {
-            // Pre-Honeycomb, we can do some lesser optimizations.
-            cursor = null;
-            try {
-                cursor = db.rawQuery("PRAGMA journal_mode=PERSIST", null);
-            } finally {
-                if (cursor != null)
-                    cursor.close();
-            }
+            // This does nothing on 16+.
+            db.setLockingEnabled(false);
         }
     }
 
     // Calculate these once, at initialization. isLoggable is too expensive to
     // have in-line in each log call.
     private static final boolean logDebug   = Log.isLoggable(LOGTAG, Log.DEBUG);
     private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
     protected static void trace(String message) {
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -377,23 +377,16 @@ public class GeckoPreferences
         // likely other strange devices (other Asus devices, some Samsungs) could do the same.
         super.onCreate(savedInstanceState);
 
         if (onIsMultiPane()) {
             // So that Android doesn't put the fragment title (or nothing at
             // all) in the action bar.
             updateActionBarTitle(R.string.settings_title);
 
-            if (Build.VERSION.SDK_INT < 13) {
-                // Affected by Bug 1015209 -- no detach/attach.
-                // If we try rejigging fragments, we'll crash, so don't
-                // enable locale switching at all.
-                localeSwitchingIsEnabled = false;
-                throw new IllegalStateException("foobar");
-            }
         }
 
         // Use setResourceToOpen to specify these extras.
         Bundle intentExtras = getIntent().getExtras();
 
         EventDispatcher.getInstance().registerUiThreadListener(this, "Sanitize:Finished");
 
         // Add handling for long-press click.
--- a/mobile/android/components/extensions/schemas/tabs.json
+++ b/mobile/android/components/extensions/schemas/tabs.json
@@ -56,17 +56,17 @@
         "id": "Tab",
         "type": "object",
         "properties": {
           "id": {"type": "integer", "minimum": -1, "optional": true, "description": "The ID of the tab. Tab IDs are unique within a browser session. Under some circumstances a Tab may not be assigned an ID, for example when querying foreign tabs using the $(ref:sessions) API, in which case a session ID may be present. Tab ID can also be set to $(ref:tabs.TAB_ID_NONE) for apps and devtools windows."},
           "index": {"type": "integer", "minimum": -1, "description": "The zero-based index of the tab within its window."},
           "windowId": {"type": "integer", "minimum": 0, "description": "The ID of the window the tab is contained within."},
           "openerTabId": {"unsupported": true, "type": "integer", "minimum": 0, "optional": true, "description": "The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists."},
           "selected": {"type": "boolean", "description": "Whether the tab is selected.", "deprecated": "Please use $(ref:tabs.Tab.highlighted).", "unsupported": true},
-          "highlighted": {"type": "boolean", "description": "Whether the tab is highlighted."},
+          "highlighted": {"type": "boolean", "description": "Whether the tab is highlighted. Works as an alias of active."},
           "active": {"type": "boolean", "description": "Whether the tab is active in its window. (Does not necessarily mean the window is focused.)"},
           "pinned": {"type": "boolean", "description": "Whether the tab is pinned."},
           "audible": {"type": "boolean", "optional": true, "description": "Whether the tab has produced sound over the past couple of seconds (but it might not be heard if also muted). Equivalent to whether the speaker audio indicator is showing."},
           "mutedInfo": {"$ref": "MutedInfo", "optional": true, "description": "Current tab muted state and the reason for the last state change."},
           "url": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "title": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "favIconUrl": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
           "status": {"type": "string", "optional": true, "description": "Either <em>loading</em> or <em>complete</em>."},
@@ -470,17 +470,17 @@
               "muted": {
                 "type": "boolean",
                 "optional": true,
                 "description": "Whether the tabs are muted."
               },
               "highlighted": {
                 "type": "boolean",
                 "optional": true,
-                "description": "Whether the tabs are highlighted."
+                "description": "Whether the tabs are highlighted. Works as an alias of active."
               },
               "currentWindow": {
                 "type": "boolean",
                 "optional": true,
                 "description": "Whether the tabs are in the $(topic:current-window)[current window]."
               },
               "lastFocusedWindow": {
                 "type": "boolean",
@@ -544,16 +544,17 @@
           }
         ]
       },
       {
         "name": "highlight",
         "type": "function",
         "description": "Highlights the given tabs.",
         "async": "callback",
+        "unsupported": true,
         "parameters": [
           {
             "type": "object",
             "name": "highlightInfo",
             "properties": {
                "windowId": {
                  "type": "integer",
                  "optional": true,
--- a/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html
@@ -22,17 +22,16 @@ let expectedBackgroundApisTargetSpecific
   "tabs.WindowType",
   "tabs.ZoomSettingsMode",
   "tabs.ZoomSettingsScope",
   "tabs.connect",
   "tabs.create",
   "tabs.executeScript",
   "tabs.get",
   "tabs.getCurrent",
-  "tabs.highlight",
   "tabs.insertCSS",
   "tabs.onActivated",
   "tabs.onAttached",
   "tabs.onCreated",
   "tabs.onDetached",
   "tabs.onHighlighted",
   "tabs.onMoved",
   "tabs.onRemoved",
--- a/mobile/android/docs/localeswitching.rst
+++ b/mobile/android/docs/localeswitching.rst
@@ -32,26 +32,22 @@ Ideally, we also need to perform some am
 
 ``GeckoApp`` itself does some additional work, because it has particular performance constraints, and also is the typical root of the preferences activity.
 
 Here's an example of the work that a typical activity should do::
 
   // This is cribbed from o.m.g.sync.setup.activities.LocaleAware.
   public static void initializeLocale(Context context) {
     final LocaleManager localeManager = BrowserLocaleManager.getInstance();
-    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
-      localeManager.getAndApplyPersistedLocale(context);
-    } else {
-      final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
-      StrictMode.allowThreadDiskWrites();
-      try {
+    final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+    StrictMode.allowThreadDiskWrites();
+    try {
         localeManager.getAndApplyPersistedLocale(context);
-      } finally {
+    } finally {
         StrictMode.setThreadPolicy(savedPolicy);
-      }
     }
   }
 
   @Override
   public void onConfigurationChanged(Configuration newConfig) {
     final LocaleManager localeManager = BrowserLocaleManager.getInstance();
     final Locale changed = localeManager.onSystemConfigurationChanged(this, getResources(), newConfig, mLastLocale);
     if (changed != null) {
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsPlayer.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsPlayer.java
@@ -559,16 +559,19 @@ public class GeckoHlsPlayer implements E
 
     public ConcurrentLinkedQueue<GeckoHlsSample> getAudioSamples(int number) {
         return mARenderer != null ? mARenderer.getQueuedSamples(number) :
                                     new ConcurrentLinkedQueue<GeckoHlsSample>();
     }
 
     public long getDuration() {
         assertTrue(mPlayer != null);
+        if (isLiveStream()) {
+            return 0L;
+        }
         // Value returned by getDuration() is in milliseconds.
         long duration = mPlayer.getDuration() * 1000;
         if (DEBUG) { Log.d(LOGTAG, "getDuration : " + duration  + "(Us)"); }
         return duration;
     }
 
     public long getBufferedPosition() {
         assertTrue(mPlayer != null);
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java
@@ -368,27 +368,23 @@ public final class GeckoLoader {
             final boolean dashTwoExists = new File(dashTwo).exists();
             message.append(", -1x=" + dashOneExists);
             message.append(", -2x=" + dashTwoExists);
         } catch (Throwable e) {
             message.append(", dash fail, ");
         }
 
         try {
-            if (Build.VERSION.SDK_INT >= 9) {
-                final String nativeLibPath = context.getApplicationInfo().nativeLibraryDir;
-                final boolean nativeLibDirExists = new File(nativeLibPath).exists();
-                final boolean nativeLibLibExists = new File(nativeLibPath + "/lib" + lib + ".so").exists();
+            final String nativeLibPath = context.getApplicationInfo().nativeLibraryDir;
+            final boolean nativeLibDirExists = new File(nativeLibPath).exists();
+            final boolean nativeLibLibExists = new File(nativeLibPath + "/lib" + lib + ".so").exists();
 
-                message.append(", nativeLib: " + nativeLibPath);
-                message.append(", dirx=" + nativeLibDirExists);
-                message.append(", libx=" + nativeLibLibExists);
-            } else {
-                message.append(", <pre-9>");
-            }
+            message.append(", nativeLib: " + nativeLibPath);
+            message.append(", dirx=" + nativeLibDirExists);
+            message.append(", libx=" + nativeLibLibExists);
         } catch (Throwable e) {
             message.append(", nativeLib fail.");
         }
 
         return message.toString();
     }
 
     private static final boolean attemptLoad(final String path) {
@@ -411,21 +407,16 @@ public final class GeckoLoader {
     private static final Throwable doLoadLibraryExpected(final Context context, final String lib) {
         try {
             // Attempt 1: the way that should work.
             System.loadLibrary(lib);
             return null;
         } catch (Throwable e) {
             Log.wtf(LOGTAG, "Couldn't load " + lib + ". Trying native library dir.");
 
-            if (Build.VERSION.SDK_INT < 9) {
-                // We can't use nativeLibraryDir.
-                return e;
-            }
-
             // Attempt 2: use nativeLibraryDir, which should also work.
             final String libDir = context.getApplicationInfo().nativeLibraryDir;
             final String libPath = libDir + "/lib" + lib + ".so";
 
             // Does it even exist?
             if (new File(libPath).exists()) {
                 if (attemptLoad(libPath)) {
                     // Success!
@@ -445,21 +436,19 @@ public final class GeckoLoader {
         final Throwable e = doLoadLibraryExpected(context, lib);
         if (e == null) {
             // Success.
             return;
         }
 
         // If we're in a mismatched UID state (Bug 1042935 Comment 16) there's really
         // nothing we can do.
-        if (Build.VERSION.SDK_INT >= 9) {
-            final String nativeLibPath = context.getApplicationInfo().nativeLibraryDir;
-            if (nativeLibPath.contains("mismatched_uid")) {
-                throw new RuntimeException("Fatal: mismatched UID: cannot load.");
-            }
+        final String nativeLibPath = context.getApplicationInfo().nativeLibraryDir;
+        if (nativeLibPath.contains("mismatched_uid")) {
+            throw new RuntimeException("Fatal: mismatched UID: cannot load.");
         }
 
         // Attempt 3: try finding the path the pseudo-supported way using .dataDir.
         final String dataLibPath = context.getApplicationInfo().dataDir + "/lib/lib" + lib + ".so";
         if (attemptLoad(dataLibPath)) {
             return;
         }
 
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GamepadUtils.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GamepadUtils.java
@@ -19,19 +19,16 @@ public final class GamepadUtils {
     private static View.OnKeyListener sClickDispatcher;
     private static float sDeadZoneThresholdOverride = 1e-2f;
 
     private GamepadUtils() {
     }
 
     @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
     private static boolean isGamepadKey(KeyEvent event) {
-        if (Build.VERSION.SDK_INT < 12) {
-            return false;
-        }
         return (event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
     }
 
     public static boolean isActionKey(KeyEvent event) {
         return (isGamepadKey(event) && (event.getKeyCode() == KeyEvent.KEYCODE_BUTTON_A));
     }
 
     public static boolean isActionKeyDown(KeyEvent event) {
@@ -54,19 +51,16 @@ public final class GamepadUtils {
             InputDevice.MotionRange range = event.getDevice().getMotionRange(axis);
             threshold = range.getFlat() + range.getFuzz();
         }
         float value = event.getAxisValue(axis);
         return (Math.abs(value) < threshold);
     }
 
     public static boolean isPanningControl(MotionEvent event) {
-        if (Build.VERSION.SDK_INT < 12) {
-            return false;
-        }
         if ((event.getSource() & InputDevice.SOURCE_CLASS_MASK) != InputDevice.SOURCE_CLASS_JOYSTICK) {
             return false;
         }
         if (isValueInDeadZone(event, MotionEvent.AXIS_X)
                 && isValueInDeadZone(event, MotionEvent.AXIS_Y)
                 && isValueInDeadZone(event, MotionEvent.AXIS_Z)
                 && isValueInDeadZone(event, MotionEvent.AXIS_RZ)) {
             return false;
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java
@@ -40,26 +40,24 @@ public final class HardwareUtils {
         if (sInited) {
             // This is unavoidable, given that HardwareUtils is called from background services.
             Log.d(LOGTAG, "HardwareUtils already inited.");
             return;
         }
 
         // Pre-populate common flags from the context.
         final int screenLayoutSize = context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
-        if (Build.VERSION.SDK_INT >= 11) {
-            if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_XLARGE) {
-                sIsLargeTablet = true;
-            } else if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_LARGE) {
-                sIsSmallTablet = true;
-            }
-            if (Build.VERSION.SDK_INT >= 16) {
-                if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
-                    sIsTelevision = true;
-                }
+        if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_XLARGE) {
+            sIsLargeTablet = true;
+        } else if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_LARGE) {
+            sIsSmallTablet = true;
+        }
+        if (Build.VERSION.SDK_INT >= 16) {
+            if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
+                sIsTelevision = true;
             }
         }
 
         sInited = true;
     }
 
     public static boolean isTablet() {
         return sIsLargeTablet || sIsSmallTablet;
--- a/mobile/android/search/java/org/mozilla/search/SearchPreferenceActivity.java
+++ b/mobile/android/search/java/org/mozilla/search/SearchPreferenceActivity.java
@@ -9,17 +9,16 @@ import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract;
 
 import android.app.AlertDialog;
 import android.content.DialogInterface;
 import android.os.AsyncTask;
-import android.os.Build;
 import android.os.Bundle;
 import android.preference.Preference;
 import android.preference.PreferenceActivity;
 import android.util.Log;
 import android.view.MenuItem;
 import android.widget.Toast;
 
 /**
@@ -41,20 +40,18 @@ public class SearchPreferenceActivity ex
     @Override
     @SuppressWarnings("deprecation")
     protected void onCreate(Bundle savedInstanceState) {
         Locales.initializeLocale(getApplicationContext());
         super.onCreate(savedInstanceState);
 
         getPreferenceManager().setSharedPreferencesName(GeckoSharedPrefs.APP_PREFS_NAME);
 
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
-            if (getActionBar() != null) {
-                getActionBar().setDisplayHomeAsUpEnabled(true);
-            }
+        if (getActionBar() != null) {
+            getActionBar().setDisplayHomeAsUpEnabled(true);
         }
     }
 
     @Override
     protected void onPostCreate(Bundle savedInstanceState) {
         super.onPostCreate(savedInstanceState);
         setupPrefsScreen();
     }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusActivity.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusActivity.java
@@ -4,23 +4,21 @@
 
 package org.mozilla.gecko.fxa.activities;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.AccountManagerCallback;
 import android.accounts.AccountManagerFuture;
 import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.content.DialogInterface;
 import android.content.Intent;
-import android.os.Build;
 import android.os.Bundle;
 import android.support.v7.app.ActionBar;
 import android.support.v7.widget.Toolbar;
 import android.util.TypedValue;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
@@ -59,22 +57,17 @@ public class FxAccountStatusActivity ext
   }
 
   /**
    * Sufficiently recent Android versions need additional code to receive taps
    * on the status bar to go "up". See <a
    * href="http://stackoverflow.com/a/8953148">this stackoverflow answer</a> for
    * more information.
    */
-  @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
   protected void maybeSetHomeButtonEnabled() {
-    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
-      Logger.debug(LOG_TAG, "Not enabling home button; version too low.");
-      return;
-    }
     final ActionBar actionBar = getSupportActionBar();
     if (actionBar != null) {
       Logger.debug(LOG_TAG, "Enabling home button.");
       actionBar.setHomeButtonEnabled(true);
       actionBar.setDisplayHomeAsUpEnabled(true);
       return;
     }
     Logger.debug(LOG_TAG, "Not enabling home button.");
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/Prefs.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/Prefs.java
@@ -1,19 +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/. */
 
 package org.mozilla.mozstumbler.service;
 
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.location.Location;
-import android.os.Build.VERSION;
 import android.text.TextUtils;
 import android.util.Log;
 
 public  final class Prefs {
     private static final String LOG_TAG = AppGlobals.makeLogTag(Prefs.class.getSimpleName());
     private static final String NICKNAME_PREF = "nickname";
     private static final String USER_AGENT_PREF = "user-agent";
     private static final String VALUES_VERSION_PREF = "values_version";
@@ -77,17 +75,17 @@ public  final class Prefs {
     public synchronized void setGeofenceHere(boolean flag) {
         setBoolPref(GEOFENCE_HERE, flag);
     }
 
     public synchronized void setGeofenceLocation(Location location) {
         SharedPreferences.Editor editor = getPrefs().edit();
         editor.putFloat(LAT_PREF, (float) location.getLatitude());
         editor.putFloat(LON_PREF, (float) location.getLongitude());
-        apply(editor);
+        editor.apply();
     }
 
     public synchronized void setMozApiKey(String s) {
         setStringPref(MOZ_API_KEY, s);
     }
 
     ///
     /// Getters
@@ -137,17 +135,17 @@ public  final class Prefs {
 
     public synchronized void setFirefoxScanEnabled(boolean on) {
         setBoolPref(FIREFOX_SCAN_ENABLED, on);
     }
 
     public synchronized void setLastAttemptedUploadTime(long time) {
         SharedPreferences.Editor editor = getPrefs().edit();
         editor.putLong(LAST_ATTEMPTED_UPLOAD_TIME, time);
-        apply(editor);
+        editor.apply();
     }
 
     public synchronized void setNickname(String nick) {
         if (nick != null) {
             nick = nick.trim();
             if (nick.length() > 0) {
                 setStringPref(NICKNAME_PREF, nick);
             }
@@ -176,30 +174,21 @@ public  final class Prefs {
 
     private boolean getBoolPrefWithDefault(String key, boolean def) {
         return getPrefs().getBoolean(key, def);
     }
 
     private void setBoolPref(String key, Boolean state) {
         SharedPreferences.Editor editor = getPrefs().edit();
         editor.putBoolean(key,state);
-        apply(editor);
+        editor.apply();
     }
 
     private void setStringPref(String key, String value) {
         SharedPreferences.Editor editor = getPrefs().edit();
         editor.putString(key, value);
-        apply(editor);
-    }
-
-    @TargetApi(9)
-    private static void apply(SharedPreferences.Editor editor) {
-        if (VERSION.SDK_INT >= 9) {
-            editor.apply();
-        } else if (!editor.commit()) {
-            Log.e(LOG_TAG, "", new IllegalStateException("commit() failed?!"));
-        }
+        editor.apply();
     }
 
     private SharedPreferences getPrefs() {
         return mSharedPrefs;
     }
 }
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellInfo.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellInfo.java
@@ -187,20 +187,18 @@ public class CellInfo implements Parcela
             mCellRadio = getCellRadioTypeName(networkType);
             setNetworkOperator(networkOperator);
 
             lac = gcl.getLac();
             cid = gcl.getCid();
             if (lac >= 0) mLac = lac;
             if (cid >= 0) mCid = cid;
 
-            if (Build.VERSION.SDK_INT >= 9) {
-                final int psc = gcl.getPsc();
-                if (psc >= 0) mPsc = psc;
-            }
+            final int psc = gcl.getPsc();
+            if (psc >= 0) mPsc = psc;
 
             if (gsmSignalStrength != null) {
                 mAsu = gsmSignalStrength;
             }
         } else if (cl instanceof CdmaCellLocation) {
             final CdmaCellLocation cdl = (CdmaCellLocation) cl;
 
             reset();
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/AbstractCommunicator.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/AbstractCommunicator.java
@@ -81,17 +81,17 @@ public abstract class AbstractCommunicat
             Log.e(LOG_TAG, "Couldn't open a connection: " + e);
         }
         mHttpURLConnection.setDoOutput(true);
         mHttpURLConnection.setRequestProperty(USER_AGENT_HEADER, mUserAgent);
         mHttpURLConnection.setRequestProperty("Content-Type", "application/json");
 
         // Workaround for a bug in Android mHttpURLConnection. When the library
         // reuses a stale connection, the connection may fail with an EOFException
-        if (Build.VERSION.SDK_INT > 13 && Build.VERSION.SDK_INT < 19) {
+        if (Build.VERSION.SDK_INT < 19) {
             mHttpURLConnection.setRequestProperty("Connection", "Close");
         }
         String nickname = getNickname();
         if (nickname != null) {
             mHttpURLConnection.setRequestProperty(NICKNAME_HEADER, nickname);
         }
     }
 
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentProviderTest.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentProviderTest.java
@@ -233,19 +233,17 @@ abstract class ContentProviderTest exten
 
         mProviderAuthority = authority;
 
         setUpContentProvider(contentProviderFactory.call());
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (Build.VERSION.SDK_INT >= 11) {
-            mProvider.shutdown();
-        }
+        mProvider.shutdown();
 
         if (mDatabaseName != null) {
             mProviderContext.deleteDatabase(mDatabaseName);
         }
 
         super.tearDown();
     }
 
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventReplayer.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventReplayer.java
@@ -12,17 +12,16 @@ import java.lang.reflect.InvocationTarge
 import java.lang.reflect.Method;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.StringTokenizer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import android.app.Instrumentation;
-import android.os.Build;
 import android.os.SystemClock;
 import android.util.Log;
 import android.view.MotionEvent;
 
 class MotionEventReplayer {
     private static final String LOGTAG = "RobocopMotionEventReplayer";
 
     // the inner dimensions of the window on which the motion event capture was taken from
@@ -139,41 +138,24 @@ class MotionEventReplayer {
                 int deviceId = 0;
                 int edgeFlags = parseInt(eventProperties.get("edgeFlags"));
                 int source = parseInt(eventProperties.get("source"));
                 int flags = parseInt(eventProperties.get("flags"));
 
                 int pointerCount = parseInt(eventProperties.get("pointerCount"));
                 int[] pointerIds = new int[pointerCount];
                 Object pointerData;
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
-                    MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
-                    for (int i = 0; i < pointerCount; i++) {
-                        pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]"));
-                        pointerCoords[i] = new MotionEvent.PointerCoords();
-                        pointerCoords[i].x = mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]")));
-                        pointerCoords[i].y = mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]")));
-                    }
-                    pointerData = pointerCoords;
-                } else {
-                    // pre-gingerbread we have to use a hidden API to create the motion event, and we have
-                    // to create a flattened list of floats rather than an array of PointerCoords
-                    final int NUM_SAMPLE_DATA = 4; // MotionEvent.NUM_SAMPLE_DATA
-                    final int SAMPLE_X = 0; // MotionEvent.SAMPLE_X
-                    final int SAMPLE_Y = 1; // MotionEvent.SAMPLE_Y
-                    float[] sampleData = new float[pointerCount * NUM_SAMPLE_DATA];
-                    for (int i = 0; i < pointerCount; i++) {
-                        pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]"));
-                        sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_X] =
-                                mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]")));
-                        sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_Y] =
-                                mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]")));
-                    }
-                    pointerData = sampleData;
+                MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
+                for (int i = 0; i < pointerCount; i++) {
+                    pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]"));
+                    pointerCoords[i] = new MotionEvent.PointerCoords();
+                    pointerCoords[i].x = mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]")));
+                    pointerCoords[i].y = mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]")));
                 }
+                pointerData = pointerCoords;
 
                 // we want to adjust the timestamps on all the generated events so that they line up with
                 // the time that this function is executing on-device.
                 long now = SystemClock.uptimeMillis();
                 if (firstEvent) {
                     timeDelta = now - eventTime;
                     firstEvent = false;
                 }
@@ -186,32 +168,19 @@ class MotionEventReplayer {
                     try {
                         Thread.sleep(eventTime - now);
                     } catch (InterruptedException ie) {
                     }
                 }
 
                 // and finally we dispatch the event
                 MotionEvent event;
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
-                    event = MotionEvent.obtain(downTime, eventTime, action, pointerCount,
-                            pointerIds, (MotionEvent.PointerCoords[])pointerData, metaState,
-                            xPrecision, yPrecision, deviceId, edgeFlags, source, flags);
-                } else {
-                    // pre-gingerbread we have to use a hidden API to accomplish this
-                    if (mObtainNanoMethod == null) {
-                        mObtainNanoMethod = MotionEvent.class.getMethod("obtainNano", long.class,
-                            long.class, long.class, int.class, int.class, pointerIds.getClass(),
-                            pointerData.getClass(), int.class, float.class, float.class,
-                            int.class, int.class);
-                    }
-                    event = (MotionEvent)mObtainNanoMethod.invoke(null, downTime, eventTime,
-                            eventTime * 1000000, action, pointerCount, pointerIds, (float[])pointerData,
-                            metaState, xPrecision, yPrecision, deviceId, edgeFlags);
-                }
+                event = MotionEvent.obtain(downTime, eventTime, action, pointerCount,
+                        pointerIds, (MotionEvent.PointerCoords[])pointerData, metaState,
+                        xPrecision, yPrecision, deviceId, edgeFlags, source, flags);
                 try {
                     Log.v(LOGTAG, "Injecting " + event.toString());
                     mInstrumentation.sendPointerSync(event);
                 } finally {
                     event.recycle();
                     event = null;
                 }
 
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java
@@ -80,30 +80,21 @@ public class AboutHomeComponent extends 
         fAssertTrue("The HomePager is visible",
                     getHomePagerContainer().getVisibility() == View.VISIBLE &&
                     getHomePagerView().getVisibility() == View.VISIBLE);
         return this;
     }
 
     public AboutHomeComponent assertBannerNotVisible() {
         View banner = getHomeBannerView();
-        if (Build.VERSION.SDK_INT >= 11) {
-            fAssertTrue("The HomeBanner is not visible",
-                        getHomePagerContainer().getVisibility() != View.VISIBLE ||
-                        banner == null ||
-                        banner.getVisibility() != View.VISIBLE ||
-                        banner.getTranslationY() == banner.getHeight());
-        } else {
-            // getTranslationY is not available before api 11.
-            // This check is a little less specific.
-            fAssertTrue("The HomeBanner is not visible",
-                        getHomePagerContainer().getVisibility() != View.VISIBLE ||
-                        banner == null ||
-                        banner.isShown() == false);
-        }
+        fAssertTrue("The HomeBanner is not visible",
+                    getHomePagerContainer().getVisibility() != View.VISIBLE ||
+                    banner == null ||
+                    banner.getVisibility() != View.VISIBLE ||
+                    banner.getTranslationY() == banner.getHeight());
         return this;
     }
 
     public AboutHomeComponent assertBannerVisible() {
         fAssertTrue("The HomeBanner is visible",
                     getHomePagerContainer().getVisibility() == View.VISIBLE &&
                     getHomeBannerView().getVisibility() == View.VISIBLE);
         return this;
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAdobeFlash.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAdobeFlash.java
@@ -11,22 +11,16 @@ import android.os.Build;
 
 /**
  * Tests that Flash is working
  * - loads a page containing a Flash plugin
  * - verifies it rendered properly
  */
 public class testAdobeFlash extends PixelTest {
     public void testLoad() {
-        // This test only works on ICS and higher
-        if (Build.VERSION.SDK_INT < 15) {
-            blockForGeckoReady();
-            return;
-        }
-
         // Enable plugins
         setPreferenceAndWaitForChange("plugin.enable", "1");
 
         blockForGeckoReady();
 
         String url = getAbsoluteUrl(mStringHelper.ROBOCOP_ADOBE_FLASH_URL);
         PaintedSurface painted = loadAndGetPainted(url);
 
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java
@@ -575,18 +575,17 @@ public class testBrowserProvider extends
                 id = insertWithNullCol(BrowserContract.Bookmarks.POSITION);
                 mAsserter.is(id, -1L,
                              "Should not be able to insert bookmark with null position");
 
                 id = insertWithNullCol(BrowserContract.Bookmarks.TYPE);
                 mAsserter.is(id, -1L,
                              "Should not be able to insert bookmark with null type");
 
-                if (Build.VERSION.SDK_INT >= 8 &&
-                    Build.VERSION.SDK_INT < 16) {
+                if (Build.VERSION.SDK_INT < 16) {
                     b = createOneBookmark();
                     b.put(BrowserContract.Bookmarks.PARENT, -1);
                     id = -1;
 
                     try {
                         id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
                     } catch (Exception e) {}
 
@@ -703,18 +702,17 @@ public class testBrowserProvider extends
             mAsserter.is((deleted == 1), true,
                          "Inserted bookmark was deleted using URI with id");
 
             c = getBookmarkById(id);
             mAsserter.is(c.moveToFirst(), false,
                          "Inserted bookmark can't be found after deletion using URI with ID");
             c.close();
 
-            if (Build.VERSION.SDK_INT >= 8 &&
-                Build.VERSION.SDK_INT < 16) {
+            if (Build.VERSION.SDK_INT < 16) {
                 ContentValues b = createBookmark("Folder", null, mMobileFolderId,
                         BrowserContract.Bookmarks.TYPE_FOLDER, 0, "folderTags", "folderDescription", "folderKeyword");
 
                 long parentId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
                 c = getBookmarkById(parentId);
                 mAsserter.is(c.moveToFirst(), true, "Inserted bookmarks folder found");
                 c.close();
 
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGetUserMedia.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGetUserMedia.java
@@ -7,17 +7,16 @@ package org.mozilla.gecko.tests;
 import org.mozilla.gecko.AppConstants;
 
 import android.widget.Spinner;
 import android.view.View;
 
 import com.robotium.solo.Condition;
 
 import android.hardware.Camera;
-import android.os.Build;
 
 public class testGetUserMedia extends BaseTest {
     private static final String LOGTAG = testGetUserMedia.class.getSimpleName();
 
     private static final String GUM_MESSAGE = "Would you like to share your camera and microphone with";
     private static final String GUM_ALLOW = "^Share$";
     private static final String GUM_DENY = "^Don't Share$";
 
@@ -85,23 +84,16 @@ public class testGetUserMedia extends Ba
         mAsserter.is(mSolo.searchText(GUM_MESSAGE), false, "getUserMedia doorhanger hidden after dismissal");
         waitForText(GUM_SELECT_TAB);
         mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Tab selection dialog displayed");
         mSolo.clickOnText(GUM_PAGE_TITLE);
         waitForTextDismissed(GUM_SELECT_TAB);
         mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), false, "Tab selection dialog hidden");
         verifyUrlBarTitle(GUM_TAB_HTTPS_URL);
 
-        // Android 2.3 testers fail because of audio issues:
-        // E/AudioRecord(  650): Unsupported configuration: sampleRate 44100, format 1, channelCount 1
-        // E/libOpenSLES(  650): android_audioRecorder_realize(0x26d7d8) error creating AudioRecord object
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
-            return;
-        }
-
         loadUrlAndWait(GUM_TAB_HTTPS_URL);
         waitForText(GUM_MESSAGE);
         mAsserter.is(mSolo.searchText(GUM_MESSAGE), true, "getUserMedia doorhanger has been displayed");
 
         waitForSpinner();
         mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Video source selection available");
         mSolo.clickOnButton(GUM_ALLOW);
         waitForTextDismissed(GUM_MESSAGE);
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java
@@ -35,22 +35,18 @@ public class testShareLink extends About
 
         // FIXME: This is a temporary hack workaround for a permissions problem.
         openAboutHomeTab(AboutHomeTabs.HISTORY);
 
         inputAndLoadUrl(url);
         verifyUrlBarTitle(url); // Waiting for page title to ensure the page is loaded
 
         selectMenuItem(mStringHelper.SHARE_LABEL);
-        if (Build.VERSION.SDK_INT >= 14) {
-            // Check for our own sync in the submenu.
-            waitForText("Sync$");
-        } else {
-            waitForText("Share via");
-        }
+        // Check for our own sync in the submenu.
+        waitForText("Sync$");
 
         // Get list of current available share activities and verify them
         shareOptions = getShareOptions();
         ArrayList<String> displayedOptions = getShareOptionsList();
         for (String option:shareOptions) {
              // Verify if the option is present in the list of displayed share options
              mAsserter.ok(optionDisplayed(option, displayedOptions), "Share option found", option);
         }
@@ -218,21 +214,17 @@ public class testShareLink extends About
     public ArrayList<String> getShareSubMenuOption() {
         ArrayList<String> displayedOptions = new ArrayList<>();
         AbsListView shareMenu = getDisplayedShareList();
         getGroupTextViews(shareMenu, displayedOptions);
         return displayedOptions;
     }
 
     public ArrayList<String> getShareOptionsList() {
-        if (Build.VERSION.SDK_INT >= 14) {
-            return getShareSubMenuOption();
-        } else {
-            return getSharePopupOption();
-        }
+        return getShareSubMenuOption();
     }
 
     private boolean optionDisplayed(String shareOption, ArrayList<String> displayedOptions) {
         for (String displayedOption: displayedOptions) {
             if (shareOption.equals(displayedOption)) {
                 return true;
             }
         }
--- a/services/sync/modules/bookmark_validator.js
+++ b/services/sync/modules/bookmark_validator.js
@@ -818,26 +818,24 @@ class BookmarkValidator {
   }
 
   async _getServerState(engine) {
 // XXXXX - todo - we need to capture last-modified of the server here and
 // ensure the repairer only applys with if-unmodified-since that date.
     let collection = engine.itemSource();
     let collectionKey = engine.service.collectionKeys.keyForCollection(engine.name);
     collection.full = true;
-    let items = [];
-    collection.recordHandler = function(item) {
-      item.decrypt(collectionKey);
-      items.push(item.cleartext);
-    };
-    let resp = await collection.getBatched();
-    if (!resp.success) {
-      throw resp;
+    let result = await collection.getBatched();
+    if (!result.response.success) {
+      throw result.response;
     }
-    return items;
+    return result.records.map(record => {
+      record.decrypt(collectionKey);
+      return record.cleartext;
+    });
   }
 
   async validate(engine) {
     let start = Date.now();
     let clientTree = await PlacesUtils.promiseBookmarksTree("", {
       includeItemIds: true
     });
     let serverState = await this._getServerState(engine);
--- a/services/sync/modules/collection_validator.js
+++ b/services/sync/modules/collection_validator.js
@@ -64,26 +64,24 @@ class CollectionValidator {
   emptyProblemData() {
     return new CollectionProblemData();
   }
 
   async getServerItems(engine) {
     let collection = engine.itemSource();
     let collectionKey = engine.service.collectionKeys.keyForCollection(engine.name);
     collection.full = true;
-    let items = [];
-    collection.recordHandler = function(item) {
-      item.decrypt(collectionKey);
-      items.push(item.cleartext);
-    };
-    let resp = await collection.getBatched();
-    if (!resp.success) {
-      throw resp;
+    let result = await collection.getBatched();
+    if (!result.response.success) {
+      throw result.response;
     }
-    return items;
+    return result.records.map(record => {
+      record.decrypt(collectionKey);
+      return record.cleartext;
+    });
   }
 
   // Should return a promise that resolves to an array of client items.
   getClientItems() {
     return Promise.reject("Must implement");
   }
 
   /**
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -1118,17 +1118,17 @@ SyncEngine.prototype = {
     }
 
     let key = this.service.collectionKeys.keyForCollection(this.name);
 
     // Not binding this method to 'this' for performance reasons. It gets
     // called for every incoming record.
     let self = this;
 
-    newitems.recordHandler = function(item) {
+    let recordHandler = function(item) {
       if (aborting) {
         return;
       }
 
       // Grab a later last modified if possible
       if (self.lastModified == null || item.modified > self.lastModified)
         self.lastModified = item.modified;
 
@@ -1226,22 +1226,26 @@ SyncEngine.prototype = {
       if (applyBatch.length == self.applyIncomingBatchSize) {
         doApplyBatch.call(self);
       }
       self._store._sleep(0);
     };
 
     // Only bother getting data from the server if there's new things
     if (this.lastModified == null || this.lastModified > this.lastSync) {
-      let resp = Async.promiseSpinningly(newitems.getBatched());
+      let { response, records } = Async.promiseSpinningly(newitems.getBatched());
+      if (!response.success) {
+        response.failureCode = ENGINE_DOWNLOAD_FAIL;
+        throw response;
+      }
+
+      for (let record of records) {
+        recordHandler(record);
+      }
       doApplyBatchAndPersistFailed.call(this);
-      if (!resp.success) {
-        resp.failureCode = ENGINE_DOWNLOAD_FAIL;
-        throw resp;
-      }
 
       if (aborting) {
         throw aborting;
       }
     }
 
     // Mobile: check if we got the maximum that we requested; get the rest if so.
     if (handled.length == newitems.limit) {
@@ -1280,23 +1284,28 @@ SyncEngine.prototype = {
 
     while (fetchBatch.length && !aborting) {
       // Reuse the original query, but get rid of the restricting params
       // and batch remaining records.
       newitems.limit = 0;
       newitems.newer = 0;
       newitems.ids = fetchBatch.slice(0, batchSize);
 
-      // Reuse the existing record handler set earlier
       let resp = Async.promiseSpinningly(newitems.get());
       if (!resp.success) {
         resp.failureCode = ENGINE_DOWNLOAD_FAIL;
         throw resp;
       }
 
+      for (let json of resp.obj) {
+        let record = new this._recordObj();
+        record.deserialize(json);
+        recordHandler(record);
+      }
+
       // This batch was successfully applied. Not using
       // doApplyBatchAndPersistFailed() here to avoid writing toFetch twice.
       fetchBatch = fetchBatch.slice(batchSize);
       this.toFetch = Utils.arraySub(this.toFetch, newitems.ids);
       this.previousFailed = Utils.arrayUnion(this.previousFailed, failed);
       if (failed.length) {
         count.failed += failed.length;
         this._log.debug("Records that failed to apply: " + failed);
@@ -1810,25 +1819,25 @@ SyncEngine.prototype = {
 
     // Fetch the most recently uploaded record and try to decrypt it
     let test = new Collection(this.engineURL, this._recordObj, this.service);
     test.limit = 1;
     test.sort = "newest";
     test.full = true;
 
     let key = this.service.collectionKeys.keyForCollection(this.name);
-    test.recordHandler = function recordHandler(record) {
-      record.decrypt(key);
-      canDecrypt = true;
-    };
 
     // Any failure fetching/decrypting will just result in false
     try {
       this._log.trace("Trying to decrypt a record from the server..");
-      Async.promiseSpinningly(test.get());
+      let json = Async.promiseSpinningly(test.get()).obj[0];
+      let record = new this._recordObj();
+      record.deserialize(json);
+      record.decrypt(key);
+      canDecrypt = true;
     } catch (ex) {
       if (Async.isShutdownException(ex)) {
         throw ex;
       }
       this._log.debug("Failed test decrypt", ex);
     }
 
     return canDecrypt;
--- a/services/sync/modules/record.js
+++ b/services/sync/modules/record.js
@@ -702,47 +702,49 @@ Collection.prototype = {
   // deferring calling the record handler until we've gotten them all.
   //
   // Returns the last response processed, and doesn't run the record handler
   // on any items if a non-success status is received while downloading the
   // records (or if a network error occurs).
   async getBatched(batchSize = DEFAULT_DOWNLOAD_BATCH_SIZE) {
     let totalLimit = Number(this.limit) || Infinity;
     if (batchSize <= 0 || batchSize >= totalLimit) {
-      // Invalid batch sizes should arguably be an error, but they're easy to handle
-      return this.get();
+      throw new Error("Invalid batch size");
     }
 
     if (!this.full) {
       throw new Error("getBatched is unimplemented for guid-only GETs");
     }
 
     // _onComplete and _onProgress are reset after each `get` by AsyncResource.
-    // We overwrite _onRecord to something that stores the data in an array
-    // until the end.
-    let { _onComplete, _onProgress, _onRecord } = this;
+    let { _onComplete, _onProgress } = this;
     let recordBuffer = [];
     let resp;
     try {
-      this._onRecord = r => recordBuffer.push(r);
       let lastModifiedTime;
       this.limit = batchSize;
 
       do {
         this._onProgress = _onProgress;
         this._onComplete = _onComplete;
         if (batchSize + recordBuffer.length > totalLimit) {
           this.limit = totalLimit - recordBuffer.length;
         }
         this._log.trace("Performing batched GET", { limit: this.limit, offset: this.offset });
         // Actually perform the request
         resp = await this.get();
         if (!resp.success) {
+          recordBuffer = [];
           break;
         }
+        for (let json of resp.obj) {
+          let record = new this._recordObj();
+          record.deserialize(json);
+          recordBuffer.push(record);
+        }
 
         // Initialize last modified, or check that something broken isn't happening.
         let lastModified = resp.headers["x-last-modified"];
         if (!lastModifiedTime) {
           lastModifiedTime = lastModified;
           this.setHeader("X-If-Unmodified-Since", lastModified);
         } else if (lastModified != lastModifiedTime) {
           // Should be impossible -- We'd get a 412 in this case.
@@ -754,64 +756,22 @@ Collection.prototype = {
         this.offset = resp.headers["x-weave-next-offset"];
       } while (this.offset && totalLimit > recordBuffer.length);
     } finally {
       // Ensure we undo any temporary state so that subsequent calls to get()
       // or getBatched() work properly. We do this before calling the record
       // handler so that we can more convincingly pretend to be a normal get()
       // call. Note: we're resetting these to the values they had before this
       // function was called.
-      this._onRecord = _onRecord;
       this._limit = totalLimit;
       this._offset = null;
       delete this._headers["x-if-unmodified-since"];
       this._rebuildURL();
     }
-    if (resp.success && Async.checkAppReady()) {
-      // call the original _onRecord (e.g. the user supplied record handler)
-      // for each record we've stored
-      for (let record of recordBuffer) {
-        this._onRecord(record);
-      }
-    }
-    return resp;
-  },
-
-  set recordHandler(onRecord) {
-    // Save this because onProgress is called with this as the ChannelListener
-    let coll = this;
-
-    // Switch to newline separated records for incremental parsing
-    coll.setHeader("Accept", "application/newlines");
-
-    this._onRecord = onRecord;
-
-    this._onProgress = function(httpChannel) {
-      let newline, length = 0, contentLength = "unknown";
-
-      try {
-          // Content-Length of the value of this response header
-          contentLength = httpChannel.getResponseHeader("Content-Length");
-      } catch (ex) { }
-
-      while ((newline = this._data.indexOf("\n")) > 0) {
-        // Split the json record from the rest of the data
-        let json = this._data.slice(0, newline);
-        this._data = this._data.slice(newline + 1);
-
-        length += json.length;
-        coll._log.trace("Record: Content-Length = " + contentLength +
-                        ", ByteCount = " + length);
-
-        // Deserialize a record from json and give it to the callback
-        let record = new coll._recordObj();
-        record.deserialize(json);
-        coll._onRecord(record);
-      }
-    };
+    return { response: resp, records: recordBuffer };
   },
 
   // This object only supports posting via the postQueue object.
   post() {
     throw new Error("Don't directly post to a collection - use newPostQueue instead");
   },
 
   newPostQueue(log, timestamp, postCallback) {
--- a/services/sync/tests/unit/head_http_server.js
+++ b/services/sync/tests/unit/head_http_server.js
@@ -291,17 +291,17 @@ ServerCollection.prototype = {
     for (let wbo of Object.values(this._wbos)) {
       if (wbo.modified && this._inResultSet(wbo, options)) {
         c++;
       }
     }
     return c;
   },
 
-  get(options) {
+  get(options, request) {
     let result;
     if (options.full) {
       let data = [];
       for (let wbo of Object.values(this._wbos)) {
         // Drop deleted.
         if (wbo.modified && this._inResultSet(wbo, options)) {
           data.push(wbo.get());
         }
@@ -312,18 +312,23 @@ ServerCollection.prototype = {
         data = data.slice(start, start + options.limit);
         // use options as a backchannel to set x-weave-next-offset
         if (numItemsPastOffset > options.limit) {
           options.nextOffset = start + options.limit;
         }
       } else if (start) {
         data = data.slice(start);
       }
-      // Our implementation of application/newlines.
-      result = data.join("\n") + "\n";
+
+      if (request && request.getHeader("accept") == "application/newlines") {
+        this._log.error("Error: client requesting application/newlines content");
+        throw new Error("This server should not serve application/newlines content");
+      } else {
+        result = JSON.stringify(data);
+      }
 
       // Use options as a backchannel to report count.
       options.recordCount = data.length;
     } else {
       let data = [];
       for (let [id, wbo] of Object.entries(this._wbos)) {
         if (this._inResultSet(wbo, options)) {
           data.push(id);
--- a/services/sync/tests/unit/test_bookmark_duping.js
+++ b/services/sync/tests/unit/test_bookmark_duping.js
@@ -66,17 +66,17 @@ async function createBookmark(parentId, 
   let bookmark = await bms.insert({ parentGuid, url, index, title });
   let id = await PlacesUtils.promiseItemId(bookmark.guid);
   return { id, guid: bookmark.guid };
 }
 
 function getServerRecord(collection, id) {
   let wbo = collection.get({ full: true, ids: [id] });
   // Whew - lots of json strings inside strings.
-  return JSON.parse(JSON.parse(JSON.parse(wbo).payload).ciphertext);
+  return JSON.parse(JSON.parse(JSON.parse(JSON.parse(wbo)[0]).payload).ciphertext);
 }
 
 async function promiseNoLocalItem(guid) {
   // Check there's no item with the specified guid.
   let got = await bms.fetch({ guid });
   ok(!got, `No record remains with GUID ${guid}`);
   // and while we are here ensure the places cache doesn't still have it.
   await Assert.rejects(PlacesUtils.promiseItemId(guid));
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -530,33 +530,19 @@ add_task(async function test_misreconcil
     title: "Bookmarks Toolbar",
     description: "Now you're for it.",
     parentName: "",
     parentid: "mobile",   // Why not?
     children: [],
   };
 
   let rec = new FakeRecord(BookmarkFolder, to_apply);
-  let encrypted = encryptPayload(rec.cleartext);
-  encrypted.decrypt = function() {
-    for (let x in rec) {
-      encrypted[x] = rec[x];
-    }
-  };
 
   _("Applying record.");
-  engine._processIncoming({
-    getBatched() {
-      return this.get();
-    },
-    async get() {
-      this.recordHandler(encrypted);
-      return {success: true}
-    },
-  });
+  store.applyIncoming(rec);
 
   // Ensure that afterwards, toolbar is still there.
   // As of 2012-12-05, this only passes because Places doesn't use "toolbar" as
   // the real GUID, instead using a generated one. Sync does the translation.
   let toolbarAfter = store.createRecord("toolbar", "bookmarks");
   let parentGUIDAfter = toolbarAfter.parentid;
   let parentIDAfter = store.idForGUID(parentGUIDAfter);
   do_check_eq(store.GUIDForId(toolbarIDBefore), "toolbar");
--- a/services/sync/tests/unit/test_collection_getBatched.js
+++ b/services/sync/tests/unit/test_collection_getBatched.js
@@ -10,48 +10,45 @@ function run_test() {
   run_next_test();
 }
 
 function recordRange(lim, offset, total) {
   let res = [];
   for (let i = offset; i < Math.min(lim + offset, total); ++i) {
     res.push(JSON.stringify({ id: String(i), payload: "test:" + i }));
   }
-  return res.join("\n") + "\n";
+  return res;
 }
 
 function get_test_collection_info({ totalRecords, batchSize, lastModified,
                                     throwAfter = Infinity,
                                     interruptedAfter = Infinity }) {
   let coll = new Collection("http://example.com/test/", WBORecord, Service);
   coll.full = true;
   let requests = [];
   let responses = [];
-  let sawRecord = false;
   coll.get = async function() {
-    ok(!sawRecord); // make sure we call record handler after all requests.
     let limit = +this.limit;
     let offset = 0;
     if (this.offset) {
       equal(this.offset.slice(0, 6), "foobar");
       offset = +this.offset.slice(6);
     }
     requests.push({
       limit,
       offset,
       spec: this.spec,
       headers: Object.assign({}, this.headers)
     });
     if (--throwAfter === 0) {
       throw "Some Network Error";
     }
     let body = recordRange(limit, offset, totalRecords);
-    this._onProgress.call({ _data: body });
     let response = {
-      body,
+      obj: body,
       success: true,
       status: 200,
       headers: {}
     };
     if (--interruptedAfter === 0) {
       response.success = false;
       response.status = 412;
       response.body = "";
@@ -59,43 +56,34 @@ function get_test_collection_info({ tota
       // Ensure we're treating this as an opaque string, since the docs say
       // it might not be numeric.
       response.headers["x-weave-next-offset"] = "foobar" + (offset + batchSize);
     }
     response.headers["x-last-modified"] = lastModified;
     responses.push(response);
     return response;
   };
-
-  let records = [];
-  coll.recordHandler = function(record) {
-    sawRecord = true;
-    // ensure records are coming in in the right order
-    equal(record.id, String(records.length));
-    equal(record.payload, "test:" + records.length);
-    records.push(record);
-  };
-  return { records, responses, requests, coll };
+  return { responses, requests, coll };
 }
 
 add_task(async function test_success() {
   const totalRecords = 11;
   const batchSize = 2;
   const lastModified = "111111";
-  let { records, responses, requests, coll } = get_test_collection_info({
+  let { responses, requests, coll } = get_test_collection_info({
     totalRecords,
     batchSize,
     lastModified,
   });
-  let response = await coll.getBatched(batchSize);
+  let { response, records } = await coll.getBatched(batchSize);
 
   equal(requests.length, Math.ceil(totalRecords / batchSize));
 
-  // records are mostly checked in recordHandler, we just care about the length
   equal(records.length, totalRecords);
+  checkRecordsOrder(records);
 
   // ensure we're returning the last response
   equal(responses[responses.length - 1], response);
 
   // check first separately since its a bit of a special case
   ok(!requests[0].headers["x-if-unmodified-since"]);
   ok(!requests[0].offset);
   equal(requests[0].limit, batchSize);
@@ -119,23 +107,24 @@ add_task(async function test_success() {
 });
 
 add_task(async function test_total_limit() {
   _("getBatched respects the (initial) value of the limit property");
   const totalRecords = 100;
   const recordLimit = 11;
   const batchSize = 2;
   const lastModified = "111111";
-  let { records, requests, coll } = get_test_collection_info({
+  let { requests, coll } = get_test_collection_info({
     totalRecords,
     batchSize,
     lastModified,
   });
   coll.limit = recordLimit;
-  await coll.getBatched(batchSize);
+  let { records } = await coll.getBatched(batchSize);
+  checkRecordsOrder(records);
 
   equal(requests.length, Math.ceil(recordLimit / batchSize));
   equal(records.length, recordLimit);
 
   for (let i = 0; i < requests.length; ++i) {
     let req = requests[i];
     if (i !== requests.length - 1) {
       equal(req.limit, batchSize);
@@ -147,43 +136,50 @@ add_task(async function test_total_limit
   equal(coll._limit, recordLimit);
 });
 
 add_task(async function test_412() {
   _("We shouldn't record records if we get a 412 in the middle of a batch");
   const totalRecords = 11;
   const batchSize = 2;
   const lastModified = "111111";
-  let { records, responses, requests, coll } = get_test_collection_info({
+  let { responses, requests, coll } = get_test_collection_info({
     totalRecords,
     batchSize,
     lastModified,
     interruptedAfter: 3
   });
-  let response = await coll.getBatched(batchSize);
+  let { response, records } = await coll.getBatched(batchSize);
 
   equal(requests.length, 3);
-  equal(records.length, 0); // record handler shouldn't be called for anything
+  equal(records.length, 0); // we should not get any records
 
   // ensure we're returning the last response
   equal(responses[responses.length - 1], response);
 
   ok(!response.success);
   equal(response.status, 412);
 });
 
 add_task(async function test_get_throws() {
-  _("We shouldn't record records if get() throws for some reason");
+  _("getBatched() should throw if a get() throws");
   const totalRecords = 11;
   const batchSize = 2;
   const lastModified = "111111";
-  let { records, requests, coll } = get_test_collection_info({
+  let { requests, coll } = get_test_collection_info({
     totalRecords,
     batchSize,
     lastModified,
     throwAfter: 3
   });
 
-  await Assert.rejects(coll.getBatched(batchSize), "Some Network Error");
+  await Assert.rejects(coll.getBatched(batchSize), /Some Network Error/);
 
   equal(requests.length, 3);
-  equal(records.length, 0);
 });
+
+function checkRecordsOrder(records) {
+  ok(records.length > 0)
+  for (let i = 0; i < records.length; i++) {
+    equal(records[i].id, String(i));
+    equal(records[i].payload, "test:" + i);
+  }
+}
deleted file mode 100644
--- a/services/sync/tests/unit/test_collection_inc_get.js
+++ /dev/null
@@ -1,188 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-_("Make sure Collection can correctly incrementally parse GET requests");
-Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/service.js");
-
-function run_test() {
-  let base = "http://fake/";
-  let coll = new Collection("http://fake/uri/", WBORecord, Service);
-  let stream = { _data: "" };
-  let called, recCount, sum;
-
-  _("Not-JSON, string payloads are strings");
-  called = false;
-  stream._data = '{"id":"hello","payload":"world"}\n';
-  coll.recordHandler = function(rec) {
-    called = true;
-    _("Got record:", JSON.stringify(rec));
-    rec.collection = "uri";           // This would be done by an engine, so do it here.
-    do_check_eq(rec.collection, "uri");
-    do_check_eq(rec.id, "hello");
-    do_check_eq(rec.uri(base).spec, "http://fake/uri/hello");
-    do_check_eq(rec.payload, "world");
-  };
-  coll._onProgress.call(stream);
-  do_check_eq(stream._data, "");
-  do_check_true(called);
-  _("\n");
-
-
-  _("Parse record with payload");
-  called = false;
-  stream._data = '{"payload":"{\\"value\\":123}"}\n';
-  coll.recordHandler = function(rec) {
-    called = true;
-    _("Got record:", JSON.stringify(rec));
-    do_check_eq(rec.payload.value, 123);
-  };
-  coll._onProgress.call(stream);
-  do_check_eq(stream._data, "");
-  do_check_true(called);
-  _("\n");
-
-
-  _("Parse multiple records in one go");
-  called = false;
-  recCount = 0;
-  sum = 0;
-  stream._data = '{"id":"hundred","payload":"{\\"value\\":100}"}\n{"id":"ten","payload":"{\\"value\\":10}"}\n{"id":"one","payload":"{\\"value\\":1}"}\n';
-  coll.recordHandler = function(rec) {
-    called = true;
-    _("Got record:", JSON.stringify(rec));
-    recCount++;
-    sum += rec.payload.value;
-    _("Incremental status: count", recCount, "sum", sum);
-    rec.collection = "uri";
-    switch (recCount) {
-      case 1:
-        do_check_eq(rec.id, "hundred");
-        do_check_eq(rec.uri(base).spec, "http://fake/uri/hundred");
-        do_check_eq(rec.payload.value, 100);
-        do_check_eq(sum, 100);
-        break;
-      case 2:
-        do_check_eq(rec.id, "ten");
-        do_check_eq(rec.uri(base).spec, "http://fake/uri/ten");
-        do_check_eq(rec.payload.value, 10);
-        do_check_eq(sum, 110);
-        break;
-      case 3:
-        do_check_eq(rec.id, "one");
-        do_check_eq(rec.uri(base).spec, "http://fake/uri/one");
-        do_check_eq(rec.payload.value, 1);
-        do_check_eq(sum, 111);
-        break;
-      default:
-        do_throw("unexpected number of record counts", recCount);
-        break;
-    }
-  };
-  coll._onProgress.call(stream);
-  do_check_eq(recCount, 3);
-  do_check_eq(sum, 111);
-  do_check_eq(stream._data, "");
-  do_check_true(called);
-  _("\n");
-
-
-  _("Handle incremental data incoming");
-  called = false;
-  recCount = 0;
-  sum = 0;
-  stream._data = '{"payl';
-  coll.recordHandler = function(rec) {
-    called = true;
-    do_throw("shouldn't have gotten a record..");
-  };
-  coll._onProgress.call(stream);
-  _("shouldn't have gotten anything yet");
-  do_check_eq(recCount, 0);
-  do_check_eq(sum, 0);
-  _("leading array bracket should have been trimmed");
-  do_check_eq(stream._data, '{"payl');
-  do_check_false(called);
-  _();
-
-  _("adding more data enough for one record..");
-  called = false;
-  stream._data += 'oad":"{\\"value\\":100}"}\n';
-  coll.recordHandler = function(rec) {
-    called = true;
-    _("Got record:", JSON.stringify(rec));
-    recCount++;
-    sum += rec.payload.value;
-  };
-  coll._onProgress.call(stream);
-  _("should have 1 record with sum 100");
-  do_check_eq(recCount, 1);
-  do_check_eq(sum, 100);
-  _("all data should have been consumed including trailing comma");
-  do_check_eq(stream._data, "");
-  do_check_true(called);
-  _();
-
-  _("adding more data..");
-  called = false;
-  stream._data += '{"payload":"{\\"value\\":10}"';
-  coll.recordHandler = function(rec) {
-    called = true;
-    do_throw("shouldn't have gotten a record..");
-  };
-  coll._onProgress.call(stream);
-  _("should still have 1 record with sum 100");
-  do_check_eq(recCount, 1);
-  do_check_eq(sum, 100);
-  _("should almost have a record");
-  do_check_eq(stream._data, '{"payload":"{\\"value\\":10}"');
-  do_check_false(called);
-  _();
-
-  _("add data for two records..");
-  called = false;
-  stream._data += '}\n{"payload":"{\\"value\\":1}"}\n';
-  coll.recordHandler = function(rec) {
-    called = true;
-    _("Got record:", JSON.stringify(rec));
-    recCount++;
-    sum += rec.payload.value;
-    switch (recCount) {
-      case 2:
-        do_check_eq(rec.payload.value, 10);
-        do_check_eq(sum, 110);
-        break;
-      case 3:
-        do_check_eq(rec.payload.value, 1);
-        do_check_eq(sum, 111);
-        break;
-      default:
-        do_throw("unexpected number of record counts", recCount);
-        break;
-    }
-  };
-  coll._onProgress.call(stream);
-  _("should have gotten all 3 records with sum 111");
-  do_check_eq(recCount, 3);
-  do_check_eq(sum, 111);
-  _("should have consumed all data");
-  do_check_eq(stream._data, "");
-  do_check_true(called);
-  _();
-
-  _("add no extra data");
-  called = false;
-  stream._data += "";
-  coll.recordHandler = function(rec) {
-    called = true;
-    do_throw("shouldn't have gotten a record..");
-  };
-  coll._onProgress.call(stream);
-  _("should still have 3 records with sum 111");
-  do_check_eq(recCount, 3);
-  do_check_eq(sum, 111);
-  _("should have consumed nothing but still have nothing");
-  do_check_eq(stream._data, "");
-  do_check_false(called);
-  _("\n");
-}
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -44,17 +44,16 @@ tags = addons
 [test_resource.js]
 [test_resource_async.js]
 [test_resource_header.js]
 [test_resource_ua.js]
 [test_syncstoragerequest.js]
 
 # Generic Sync types.
 [test_browserid_identity.js]
-[test_collection_inc_get.js]
 [test_collection_getBatched.js]
 [test_collections_recovery.js]
 [test_keys.js]
 [test_records_crypto.js]
 [test_records_wbo.js]
 
 # Engine APIs.
 [test_engine.js]
--- a/services/sync/tps/extensions/tps/resource/tps.jsm
+++ b/services/sync/tps/extensions/tps/resource/tps.jsm
@@ -610,36 +610,38 @@ var TPS = {
     }
   },
 
   /**
    * Use Sync's bookmark validation code to see if we've corrupted the tree.
    */
   ValidateBookmarks() {
 
-    let getServerBookmarkState = () => {
+    let getServerBookmarkState = async () => {
       let bookmarkEngine = Weave.Service.engineManager.get("bookmarks");
       let collection = bookmarkEngine.itemSource();
       let collectionKey = bookmarkEngine.service.collectionKeys.keyForCollection(bookmarkEngine.name);
       collection.full = true;
       let items = [];
-      collection.recordHandler = function(item) {
-        item.decrypt(collectionKey);
-        items.push(item.cleartext);
-      };
-      Async.promiseSpinningly(collection.get());
+      let resp = await collection.get();
+      for (let json of resp.obj) {
+        let record = new collection._recordObj();
+        record.deserialize(json);
+        record.decrypt(collectionKey);
+        items.push(record.cleartext);
+      }
       return items;
     };
     let serverRecordDumpStr;
     try {
       Logger.logInfo("About to perform bookmark validation");
       let clientTree = Async.promiseSpinningly(PlacesUtils.promiseBookmarksTree("", {
         includeItemIds: true
       }));
-      let serverRecords = getServerBookmarkState();
+      let serverRecords = Async.promiseSpinningly(getServerBookmarkState());
       // We can't wait until catch to stringify this, since at that point it will have cycles.
       serverRecordDumpStr = JSON.stringify(serverRecords);
 
       let validator = new BookmarkValidator();
       let {problemData} = Async.promiseSpinningly(validator.compareServerWithClient(serverRecords, clientTree));
 
       for (let {name, count} of problemData.getSummary()) {
         // Exclude mobile showing up on the server hackily so that we don't
--- a/servo/Cargo.lock
+++ b/servo/Cargo.lock
@@ -1624,17 +1624,17 @@ name = "markup5ever"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "heapsize 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "heapsize_derive 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
  "phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
  "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
- "string_cache 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "string_cache 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "tendril 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "matches"
 version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2697,17 +2697,17 @@ dependencies = [
  "nodrop 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "servo_atoms"
 version = "0.0.1"
 dependencies = [
- "string_cache 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "string_cache 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "servo_config"
 version = "0.0.1"
 dependencies = [
  "android_injected_glue 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2846,17 +2846,17 @@ name = "smallvec"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "heapsize 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "string_cache"
-version = "0.5.1"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "debug_unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "heapsize 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
  "precomputed-hash 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3733,17 +3733,17 @@ dependencies = [
 "checksum shell32-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "72f20b8f3c060374edb8046591ba28f62448c369ccbdc7b02075103fb3a9e38d"
 "checksum sig 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c6649e43c1a1e68d29ed56d0dc3b5b6cf3b901da77cf107c4066b9e3da036df5"
 "checksum signpost 0.1.0 (git+https://github.com/pcwalton/signpost.git)" = "<none>"
 "checksum simd 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a94d14a2ae1f1f110937de5fb69e494372560181c7e1739a097fcc2cee37ba0"
 "checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537"
 "checksum skeptic 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dd7d8dc1315094150052d0ab767840376335a98ac66ef313ff911cdf439a5b69"
 "checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23"
 "checksum smallvec 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2e40af10aafe98b4d8294ae8388d8a5cd0707c65d364872efe72d063ec44bee0"
-"checksum string_cache 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2c77392ab481a7b315078ae0cbfd827c7fcd7b0840235f0f9c24d8c7443593b5"
+"checksum string_cache 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7c8ba7515dd502b75080d989b819d31fb72686a82320d8006f665003c42ef79"
 "checksum string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "479cde50c3539481f33906a387f2bd17c8e87cb848c35b6021d41fb81ff9b4d7"
 "checksum string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b1884d1bc09741d466d9b14e6d37ac89d6909cbcac41dd9ae982d4d063bbedfc"
 "checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694"
 "checksum swapper 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca610b32bb8bfc5e7f705480c3a1edfeb70b6582495d343872c8bee0dcf758c"
 "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
 "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
 "checksum synstructure 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cf318c34a2f8381a4f3d4db2c91b45bca2b1cd8cbe56caced900647be164800c"
 "checksum syntex 0.58.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a8f5e3aaa79319573d19938ea38d068056b826db9883a5d47f86c1cecc688f0e"
--- a/servo/components/atoms/static_atoms.txt
+++ b/servo/components/atoms/static_atoms.txt
@@ -63,8 +63,10 @@ toggle
 statechange
 controllerchange
 fetch
 characteristicvaluechanged
 fullscreenchange
 fullscreenerror
 gattserverdisconnected
 onchange
+
+reftest-wait
--- a/servo/components/script/dom/cssstyledeclaration.rs
+++ b/servo/components/script/dom/cssstyledeclaration.rs
@@ -142,17 +142,17 @@ impl CSSStyleOwner {
             CSSStyleOwner::CSSRule(ref rule, _) => Root::from_ref(rule.global().as_window()),
         }
     }
 
     fn base_url(&self) -> ServoUrl {
         match *self {
             CSSStyleOwner::Element(ref el) => window_from_node(&**el).Document().base_url(),
             CSSStyleOwner::CSSRule(ref rule, _) => {
-                rule.parent_stylesheet().style_stylesheet().url_data.clone()
+                (*rule.parent_stylesheet().style_stylesheet().url_data.read()).clone()
             }
         }
     }
 }
 
 #[derive(PartialEq, HeapSizeOf)]
 pub enum CSSModificationAccess {
     ReadWrite,
--- a/servo/components/script/dom/element.rs
+++ b/servo/components/script/dom/element.rs
@@ -80,41 +80,42 @@ use html5ever::serialize;
 use html5ever::serialize::SerializeOpts;
 use html5ever::serialize::TraversalScope;
 use html5ever::serialize::TraversalScope::{ChildrenOnly, IncludeNode};
 use js::jsapi::{HandleValue, JSAutoCompartment};
 use net_traits::request::CorsSettings;
 use ref_filter_map::ref_filter_map;
 use script_layout_interface::message::ReflowQueryType;
 use script_thread::Runnable;
-use selectors::attr::{AttrSelectorOperation, NamespaceConstraint};
+use selectors::attr::{AttrSelectorOperation, NamespaceConstraint, CaseSensitivity};
 use selectors::matching::{ElementSelectorFlags, LocalMatchingContext, MatchingContext, MatchingMode};
 use selectors::matching::{HAS_EDGE_CHILD_SELECTOR, HAS_SLOW_SELECTOR, HAS_SLOW_SELECTOR_LATER_SIBLINGS};
 use selectors::matching::{RelevantLinkStatus, matches_selector_list};
 use servo_atoms::Atom;
 use std::ascii::AsciiExt;
 use std::borrow::Cow;
 use std::cell::{Cell, Ref};
 use std::convert::TryFrom;
 use std::default::Default;
 use std::fmt;
 use std::rc::Rc;
+use style::CaseSensitivityExt;
+use style::applicable_declarations::ApplicableDeclarationBlock;
 use style::attr::{AttrValue, LengthOrPercentageOrAuto};
 use style::context::{QuirksMode, ReflowGoal};
 use style::element_state::*;
 use style::properties::{Importance, PropertyDeclaration, PropertyDeclarationBlock, parse_style_attribute};
 use style::properties::longhands::{self, background_image, border_spacing, font_family, font_size, overflow_x};
 use style::restyle_hints::RestyleHint;
 use style::rule_tree::CascadeLevel;
 use style::selector_parser::{NonTSPseudoClass, PseudoElement, RestyleDamage, SelectorImpl, SelectorParser};
 use style::selector_parser::extended_filtering;
 use style::shared_lock::{SharedRwLock, Locked};
 use style::sink::Push;
 use style::stylearc::Arc;
-use style::stylist::ApplicableDeclarationBlock;
 use style::thread_state;
 use style::values::{CSSFloat, Either};
 use style::values::specified;
 use stylesheet_loader::StylesheetOwner;
 
 // TODO: Update focus state when the top-level browsing context gains or loses system focus,
 // and when the element enters or leaves a browsing context container.
 // https://html.spec.whatwg.org/multipage/#selector-focus
@@ -340,17 +341,17 @@ impl RawLayoutElementHelpers for Element
               None
             }
         }).collect()
     }
 }
 
 pub trait LayoutElementHelpers {
     #[allow(unsafe_code)]
-    unsafe fn has_class_for_layout(&self, name: &Atom) -> bool;
+    unsafe fn has_class_for_layout(&self, name: &Atom, case_sensitivity: CaseSensitivity) -> bool;
     #[allow(unsafe_code)]
     unsafe fn get_classes_for_layout(&self) -> Option<&'static [Atom]>;
 
     #[allow(unsafe_code)]
     unsafe fn synthesize_presentational_hints_for_legacy_attributes<V>(&self, &mut V)
         where V: Push<ApplicableDeclarationBlock>;
     #[allow(unsafe_code)]
     unsafe fn get_colspan(self) -> u32;
@@ -368,19 +369,19 @@ pub trait LayoutElementHelpers {
     fn get_state_for_layout(&self) -> ElementState;
     fn insert_selector_flags(&self, flags: ElementSelectorFlags);
     fn has_selector_flags(&self, flags: ElementSelectorFlags) -> bool;
 }
 
 impl LayoutElementHelpers for LayoutJS<Element> {
     #[allow(unsafe_code)]
     #[inline]
-    unsafe fn has_class_for_layout(&self, name: &Atom) -> bool {
+    unsafe fn has_class_for_layout(&self, name: &Atom, case_sensitivity: CaseSensitivity) -> bool {
         get_attr_for_layout(&*self.unsafe_get(), &ns!(), &local_name!("class")).map_or(false, |attr| {
-            attr.value_tokens_forever().unwrap().iter().any(|atom| atom == name)
+            attr.value_tokens_forever().unwrap().iter().any(|atom| case_sensitivity.eq_atom(atom, name))
         })
     }
 
     #[allow(unsafe_code)]
     #[inline]
     unsafe fn get_classes_for_layout(&self) -> Option<&'static [Atom]> {
         get_attr_for_layout(&*self.unsafe_get(), &ns!(), &local_name!("class"))
             .map(|attr| attr.value_tokens_forever().unwrap())
@@ -1153,26 +1154,20 @@ impl Element {
             attr.set_owner(None);
             if attr.namespace() == &ns!() {
                 vtable_for(self.upcast()).attribute_mutated(&attr, AttributeMutation::Removed);
             }
             attr
         })
     }
 
-    pub fn has_class(&self, name: &Atom) -> bool {
-        let quirks_mode = document_from_node(self).quirks_mode();
-        let is_equal = |lhs: &Atom, rhs: &Atom| {
-            match quirks_mode {
-                QuirksMode::NoQuirks | QuirksMode::LimitedQuirks => lhs == rhs,
-                QuirksMode::Quirks => lhs.eq_ignore_ascii_case(&rhs),
-            }
-        };
-        self.get_attribute(&ns!(), &local_name!("class"))
-            .map_or(false, |attr| attr.value().as_tokens().iter().any(|atom| is_equal(name, atom)))
+    pub fn has_class(&self, name: &Atom, case_sensitivity: CaseSensitivity) -> bool {
+        self.get_attribute(&ns!(), &local_name!("class")).map_or(false, |attr| {
+            attr.value().as_tokens().iter().any(|atom| case_sensitivity.eq_atom(name, atom))
+        })
     }
 
     pub fn set_atomic_attribute(&self, local_name: &LocalName, value: DOMString) {
         assert!(*local_name == local_name.to_ascii_lowercase());
         let value = AttrValue::from_atomic(value.into());
         self.set_attribute(local_name, value);
     }
 
@@ -2498,22 +2493,22 @@ impl<'a> ::selectors::Element for Root<E
             NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLAreaElement)) |
             NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLLinkElement)) => {
                 self.has_attribute(&local_name!("href"))
             },
             _ => false,
         }
     }
 
-    fn get_id(&self) -> Option<Atom> {
-        self.id_attribute.borrow().clone()
+    fn has_id(&self, id: &Atom, case_sensitivity: CaseSensitivity) -> bool {
+        self.id_attribute.borrow().as_ref().map_or(false, |atom| case_sensitivity.eq_atom(id, atom))
     }
 
-    fn has_class(&self, name: &Atom) -> bool {
-        Element::has_class(&**self, name)
+    fn has_class(&self, name: &Atom, case_sensitivity: CaseSensitivity) -> bool {
+        Element::has_class(&**self, name, case_sensitivity)
     }
 
     fn is_html_element_in_html_document(&self) -> bool {
         self.html_element_in_html_document()
     }
 }
 
 
--- a/servo/components/script/dom/htmlcollection.rs
+++ b/servo/components/script/dom/htmlcollection.rs
@@ -6,17 +6,17 @@ use dom::bindings::codegen::Bindings::HT
 use dom::bindings::codegen::Bindings::HTMLCollectionBinding::HTMLCollectionMethods;
 use dom::bindings::inheritance::Castable;
 use dom::bindings::js::{JS, Root, MutNullableJS};
 use dom::bindings::reflector::{Reflector, reflect_dom_object};
 use dom::bindings::str::DOMString;
 use dom::bindings::trace::JSTraceable;
 use dom::bindings::xmlname::namespace_from_domstring;
 use dom::element::Element;
-use dom::node::Node;
+use dom::node::{Node, document_from_node};
 use dom::window::Window;
 use dom_struct::dom_struct;
 use html5ever::{LocalName, QualName};
 use servo_atoms::Atom;
 use std::cell::Cell;
 use style::str::split_html_space_chars;
 
 pub trait CollectionFilter : JSTraceable {
@@ -194,17 +194,20 @@ impl HTMLCollection {
     pub fn by_atomic_class_name(window: &Window, root: &Node, classes: Vec<Atom>)
                          -> Root<HTMLCollection> {
         #[derive(JSTraceable, HeapSizeOf)]
         struct ClassNameFilter {
             classes: Vec<Atom>
         }
         impl CollectionFilter for ClassNameFilter {
             fn filter(&self, elem: &Element, _root: &Node) -> bool {
-                self.classes.iter().all(|class| elem.has_class(class))
+                let case_sensitivity = document_from_node(elem)
+                    .quirks_mode()
+                    .classes_and_ids_case_sensitivity();
+                self.classes.iter().all(|class| elem.has_class(class, case_sensitivity))
             }
         }
         let filter = ClassNameFilter {
             classes: classes
         };
         HTMLCollection::create(window, root, box filter)
     }
 
--- a/servo/components/script/dom/htmlmetaelement.rs
+++ b/servo/components/script/dom/htmlmetaelement.rs
@@ -14,16 +14,17 @@ use dom::cssstylesheet::CSSStyleSheet;
 use dom::document::Document;
 use dom::element::{AttributeMutation, Element};
 use dom::htmlelement::HTMLElement;
 use dom::htmlheadelement::HTMLHeadElement;
 use dom::node::{Node, UnbindContext, document_from_node, window_from_node};
 use dom::virtualmethods::VirtualMethods;
 use dom_struct::dom_struct;
 use html5ever::{LocalName, Prefix};
+use parking_lot::RwLock;
 use servo_config::prefs::PREFS;
 use std::ascii::AsciiExt;
 use std::sync::atomic::AtomicBool;
 use style::attr::AttrValue;
 use style::media_queries::MediaList;
 use style::str::HTML_SPACE_CHARACTERS;
 use style::stylearc::Arc;
 use style::stylesheets::{Stylesheet, CssRule, CssRules, Origin, ViewportRule};
@@ -100,17 +101,17 @@ impl HTMLMetaElement {
                 if let Some(translated_rule) = ViewportRule::from_meta(&**content) {
                     let document = self.upcast::<Node>().owner_doc();
                     let shared_lock = document.style_shared_lock();
                     let rule = CssRule::Viewport(Arc::new(shared_lock.wrap(translated_rule)));
                     *self.stylesheet.borrow_mut() = Some(Arc::new(Stylesheet {
                         rules: CssRules::new(vec![rule], shared_lock),
                         origin: Origin::Author,
                         shared_lock: shared_lock.clone(),
-                        url_data: window_from_node(self).get_url(),
+                        url_data: RwLock::new(window_from_node(self).get_url()),
                         namespaces: Default::default(),
                         media: Arc::new(shared_lock.wrap(MediaList::empty())),
                         // Viewport constraints are always recomputed on resize; they don't need to
                         // force all styles to be recomputed.
                         dirty_on_viewport_size_change: AtomicBool::new(false),
                         disabled: AtomicBool::new(false),
                         quirks_mode: document.quirks_mode(),
                     }));
--- a/servo/components/script/dom/window.rs
+++ b/servo/components/script/dom/window.rs
@@ -79,17 +79,17 @@ use script_layout_interface::rpc::{Margi
 use script_layout_interface::rpc::{ResolvedStyleResponse, TextIndexResponse};
 use script_runtime::{CommonScriptMsg, ScriptChan, ScriptPort, ScriptThreadEventCategory};
 use script_thread::{ImageCacheMsg, MainThreadScriptChan, MainThreadScriptMsg, Runnable};
 use script_thread::{RunnableWrapper, ScriptThread, SendableMainThreadScriptChan};
 use script_traits::{ConstellationControlMsg, DocumentState, LoadData, MozBrowserEvent};
 use script_traits::{ScriptMsg as ConstellationMsg, ScrollState, TimerEvent, TimerEventId};
 use script_traits::{TimerSchedulerMsg, UntrustedNodeAddress, WindowSizeData, WindowSizeType};
 use script_traits::webdriver_msg::{WebDriverJSError, WebDriverJSResult};
-use servo_atoms::Atom;
+use selectors::attr::CaseSensitivity;
 use servo_config::opts;
 use servo_config::prefs::PREFS;
 use servo_geometry::{f32_rect_to_au_rect, max_rect};
 use servo_url::{Host, MutableOrigin, ImmutableOrigin, ServoUrl};
 use std::ascii::AsciiExt;
 use std::borrow::ToOwned;
 use std::cell::Cell;
 use std::collections::{HashMap, HashSet};
@@ -1360,17 +1360,17 @@ impl Window {
             opts::get().exit_after_load ||
             opts::get().webdriver_port.is_some()) && for_display {
             let document = self.Document();
 
             // Checks if the html element has reftest-wait attribute present.
             // See http://testthewebforward.org/docs/reftests.html
             let html_element = document.GetDocumentElement();
             let reftest_wait = html_element.map_or(false, |elem| {
-                elem.has_class(&Atom::from("reftest-wait"))
+                elem.has_class(&atom!("reftest-wait"), CaseSensitivity::CaseSensitive)
             });
 
             let ready_state = document.ReadyState();
 
             let pending_images = self.pending_layout_images.borrow().is_empty();
             if ready_state == DocumentReadyState::Complete && !reftest_wait && pending_images {
                 let global_scope = self.upcast::<GlobalScope>();
                 let event = ConstellationMsg::SetDocumentState(global_scope.pipeline_id(), DocumentState::Idle);
--- a/servo/components/script/layout_wrapper.rs
+++ b/servo/components/script/layout_wrapper.rs
@@ -44,44 +44,45 @@ use dom::text::Text;
 use gfx_traits::ByteIndex;
 use html5ever::{LocalName, Namespace};
 use msg::constellation_msg::{BrowsingContextId, PipelineId};
 use range::Range;
 use script_layout_interface::{HTMLCanvasData, LayoutNodeType, SVGSVGData, TrustedNodeAddress};
 use script_layout_interface::{OpaqueStyleAndLayoutData, StyleData};
 use script_layout_interface::wrapper_traits::{DangerousThreadSafeLayoutNode, GetLayoutData, LayoutNode};
 use script_layout_interface::wrapper_traits::{PseudoElementType, ThreadSafeLayoutElement, ThreadSafeLayoutNode};
-use selectors::attr::{AttrSelectorOperation, NamespaceConstraint};
+use selectors::attr::{AttrSelectorOperation, NamespaceConstraint, CaseSensitivity};
 use selectors::matching::{ElementSelectorFlags, LocalMatchingContext, MatchingContext, RelevantLinkStatus};
 use selectors::matching::VisitedHandlingMode;
 use servo_atoms::Atom;
 use servo_url::ServoUrl;
 use std::fmt;
 use std::fmt::Debug;
 use std::hash::{Hash, Hasher};
 use std::marker::PhantomData;
 use std::mem::transmute;
 use std::sync::atomic::Ordering;
 use style;
+use style::CaseSensitivityExt;
+use style::applicable_declarations::ApplicableDeclarationBlock;
 use style::attr::AttrValue;
 use style::computed_values::display;
 use style::context::{QuirksMode, SharedStyleContext};
 use style::data::ElementData;
 use style::dom::{DescendantsBit, DirtyDescendants, LayoutIterator, NodeInfo, OpaqueNode};
 use style::dom::{PresentationalHintsSynthesizer, TElement, TNode, UnsafeNode};
 use style::element_state::*;
 use style::font_metrics::ServoMetricsProvider;
 use style::properties::{ComputedValues, PropertyDeclarationBlock};
 use style::selector_parser::{AttrValue as SelectorAttrValue, NonTSPseudoClass, PseudoClassStringArg};
 use style::selector_parser::{PseudoElement, SelectorImpl, extended_filtering};
 use style::shared_lock::{SharedRwLock as StyleSharedRwLock, Locked as StyleLocked};
 use style::sink::Push;
 use style::str::is_whitespace;
 use style::stylearc::Arc;
-use style::stylist::ApplicableDeclarationBlock;
 
 #[derive(Copy, Clone)]
 pub struct ServoLayoutNode<'a> {
     /// The wrapped node.
     node: LayoutJS<Node>,
 
     /// Being chained to a PhantomData prevents `LayoutNode`s from escaping.
     chain: PhantomData<&'a ()>,
@@ -409,16 +410,23 @@ impl<'le> TElement for ServoLayoutElemen
         self.element.get_state_for_layout()
     }
 
     #[inline]
     fn has_attr(&self, namespace: &Namespace, attr: &LocalName) -> bool {
         self.get_attr(namespace, attr).is_some()
     }
 
+    #[inline]
+    fn get_id(&self) -> Option<Atom> {
+        unsafe {
+            (*self.element.id_attribute()).clone()
+        }
+    }
+
     #[inline(always)]
     fn each_class<F>(&self, mut callback: F) where F: FnMut(&Atom) {
         unsafe {
             if let Some(ref classes) = self.element.get_classes_for_layout() {
                 for class in *classes {
                     callback(class)
                 }
             }
@@ -774,26 +782,28 @@ impl<'le> ::selectors::Element for Servo
                 NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLLinkElement)) =>
                     (*self.element.unsafe_get()).get_attr_val_for_layout(&ns!(), &local_name!("href")).is_some(),
                 _ => false,
             }
         }
     }
 
     #[inline]
-    fn get_id(&self) -> Option<Atom> {
+    fn has_id(&self, id: &Atom, case_sensitivity: CaseSensitivity) -> bool {
         unsafe {
-            (*self.element.id_attribute()).clone()
+            (*self.element.id_attribute())
+                .as_ref()
+                .map_or(false, |atom| case_sensitivity.eq_atom(atom, id))
         }
     }
 
     #[inline]
-    fn has_class(&self, name: &Atom) -> bool {
+    fn has_class(&self, name: &Atom, case_sensitivity: CaseSensitivity) -> bool {
         unsafe {
-            self.element.has_class_for_layout(name)
+            self.element.has_class_for_layout(name, case_sensitivity)
         }
     }
 
     fn is_html_element_in_html_document(&self) -> bool {
         unsafe {
             self.element.html_element_in_html_document_for_layout()
         }
     }
@@ -1244,22 +1254,22 @@ impl<'le> ::selectors::Element for Servo
         false
     }
 
     fn is_link(&self) -> bool {
         warn!("ServoThreadSafeLayoutElement::is_link called");
         false
     }
 
-    fn get_id(&self) -> Option<Atom> {
-        debug!("ServoThreadSafeLayoutElement::get_id called");
-        None
+    fn has_id(&self, _id: &Atom, _case_sensitivity: CaseSensitivity) -> bool {
+        debug!("ServoThreadSafeLayoutElement::has_id called");
+        false
     }
 
-    fn has_class(&self, _name: &Atom) -> bool {
+    fn has_class(&self, _name: &Atom, _case_sensitivity: CaseSensitivity) -> bool {
         debug!("ServoThreadSafeLayoutElement::has_class called");
         false
     }
 
     fn is_empty(&self) -> bool {
         warn!("ServoThreadSafeLayoutElement::is_empty called");
         false
     }
--- a/servo/components/script/stylesheet_loader.rs
+++ b/servo/components/script/stylesheet_loader.rs
@@ -155,17 +155,17 @@ impl FetchResponseListener for Styleshee
                         link.set_stylesheet(sheet);
                     }
                 }
                 StylesheetContextSource::Import(ref stylesheet) => {
                     Stylesheet::update_from_bytes(&stylesheet,
                                                   &data,
                                                   protocol_encoding_label,
                                                   Some(environment_encoding),
-                                                  &final_url,
+                                                  final_url,
                                                   Some(&loader),
                                                   win.css_error_reporter());
                 }
             }
 
             document.invalidate_stylesheets();
 
             // FIXME: Revisit once consensus is reached at:
--- a/servo/components/selectors/attr.rs
+++ b/servo/components/selectors/attr.rs
@@ -122,17 +122,17 @@ impl AttrSelectorOperator {
     }
 }
 
 /// The definition of whitespace per CSS Selectors Level 3 § 4.
 pub static SELECTOR_WHITESPACE: &'static [char] = &[' ', '\t', '\n', '\r', '\x0C'];
 
 #[derive(Eq, PartialEq, Clone, Copy, Debug)]
 pub enum ParsedCaseSensitivity {
-    CaseSensitive,  // Selectors spec says language-defined, but HTML says sensitive.
+    CaseSensitive,
     AsciiCaseInsensitive,
     AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument,
 }
 
 impl ParsedCaseSensitivity {
     pub fn to_unconditional(self, is_html_element_in_html_document: bool) -> CaseSensitivity {
         match self {
             ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument
@@ -145,17 +145,17 @@ impl ParsedCaseSensitivity {
             ParsedCaseSensitivity::CaseSensitive => CaseSensitivity::CaseSensitive,
             ParsedCaseSensitivity::AsciiCaseInsensitive => CaseSensitivity::AsciiCaseInsensitive,
         }
     }
 }
 
 #[derive(Eq, PartialEq, Clone, Copy, Debug)]
 pub enum CaseSensitivity {
-    CaseSensitive,  // Selectors spec says language-defined, but HTML says sensitive.
+    CaseSensitive,
     AsciiCaseInsensitive,
 }
 
 impl CaseSensitivity {
     pub fn eq(self, a: &[u8], b: &[u8]) -> bool {
         match self {
             CaseSensitivity::CaseSensitive => a == b,
             CaseSensitivity::AsciiCaseInsensitive => a.eq_ignore_ascii_case(b),
--- a/servo/components/selectors/bloom.rs
+++ b/servo/components/selectors/bloom.rs
@@ -2,20 +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/. */
 
 //! Simple counting bloom filters.
 
 use fnv::FnvHasher;
 use std::hash::{Hash, Hasher};
 
+// The top 12 bits of the 32-bit hash value are not used by the bloom filter.
+// Consumers may rely on this to pack hashes more efficiently.
+pub const BLOOM_HASH_MASK: u32 = 0x00ffffff;
 const KEY_SIZE: usize = 12;
+
 const ARRAY_SIZE: usize = 1 << KEY_SIZE;
 const KEY_MASK: u32 = (1 << KEY_SIZE) - 1;
-const KEY_SHIFT: usize = 16;
 
 /// A counting Bloom filter with 8-bit counters.  For now we assume
 /// that having two hash functions is enough, but we may revisit that
 /// decision later.
 ///
 /// The filter uses an array with 2**KeySize entries.
 ///
 /// Assuming a well-distributed hash function, a Bloom filter with
@@ -178,17 +181,17 @@ fn hash<T: Hash>(elem: &T) -> u32 {
 
 #[inline]
 fn hash1(hash: u32) -> u32 {
     hash & KEY_MASK
 }
 
 #[inline]
 fn hash2(hash: u32) -> u32 {
-    (hash >> KEY_SHIFT) & KEY_MASK
+    (hash >> KEY_SIZE) & KEY_MASK
 }
 
 #[test]
 fn create_and_insert_some_stuff() {
     let mut bf = BloomFilter::new();
 
     for i in 0_usize .. 1000 {
         bf.insert(&i);
new file mode 100644
--- /dev/null
+++ b/servo/components/selectors/context.rs
@@ -0,0 +1,152 @@
+/* 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 attr::CaseSensitivity;
+use bloom::BloomFilter;
+
+bitflags! {
+    /// Set of flags that determine the different kind of elements affected by
+    /// the selector matching process.
+    ///
+    /// This is used to implement efficient sharing.
+    #[derive(Default)]
+    pub flags StyleRelations: usize {
+        /// Whether this element is affected by presentational hints. This is
+        /// computed externally (that is, in Servo).
+        const AFFECTED_BY_PRESENTATIONAL_HINTS = 1 << 0,
+        /// Whether this element has pseudo-element styles. Computed externally.
+        const AFFECTED_BY_PSEUDO_ELEMENTS = 1 << 1,
+    }
+}
+
+/// What kind of selector matching mode we should use.
+///
+/// There are two modes of selector matching. The difference is only noticeable
+/// in presence of pseudo-elements.
+#[derive(Debug, PartialEq, Copy, Clone)]
+pub enum MatchingMode {
+    /// Don't ignore any pseudo-element selectors.
+    Normal,
+
+    /// Ignores any stateless pseudo-element selectors in the rightmost sequence
+    /// of simple selectors.
+    ///
+    /// This is useful, for example, to match against ::before when you aren't a
+    /// pseudo-element yourself.
+    ///
+    /// For example, in presence of `::before:hover`, it would never match, but
+    /// `::before` would be ignored as in "matching".
+    ///
+    /// It's required for all the selectors you match using this mode to have a
+    /// pseudo-element.
+    ForStatelessPseudoElement,
+}
+
+/// The mode to use when matching unvisited and visited links.
+#[derive(PartialEq, Eq, Copy, Clone, Debug)]
+pub enum VisitedHandlingMode {
+    /// All links are matched as if they are unvisted.
+    AllLinksUnvisited,
+    /// A element's "relevant link" is the element being matched if it is a link
+    /// or the nearest ancestor link. The relevant link is matched as though it
+    /// is visited, and all other links are matched as if they are unvisited.
+    RelevantLinkVisited,
+}
+
+/// Which quirks mode is this document in.
+///
+/// See: https://quirks.spec.whatwg.org/
+#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
+pub enum QuirksMode {
+    /// Quirks mode.
+    Quirks,
+    /// Limited quirks mode.
+    LimitedQuirks,
+    /// No quirks mode.
+    NoQuirks,
+}
+
+impl QuirksMode {
+    #[inline]
+    pub fn classes_and_ids_case_sensitivity(self) -> CaseSensitivity {
+        match self {
+            QuirksMode::NoQuirks |
+            QuirksMode::LimitedQuirks => CaseSensitivity::CaseSensitive,
+            QuirksMode::Quirks => CaseSensitivity::AsciiCaseInsensitive,
+        }
+    }
+}
+
+/// Data associated with the matching process for a element.  This context is
+/// used across many selectors for an element, so it's not appropriate for
+/// transient data that applies to only a single selector.
+#[derive(Clone)]
+pub struct MatchingContext<'a> {
+    /// Output that records certains relations between elements noticed during
+    /// matching (and also extended after matching).
+    pub relations: StyleRelations,
+    /// Input with the matching mode we should use when matching selectors.
+    pub matching_mode: MatchingMode,
+    /// Input with the bloom filter used to fast-reject selectors.
+    pub bloom_filter: Option<&'a BloomFilter>,
+    /// Input that controls how matching for links is handled.
+    pub visited_handling: VisitedHandlingMode,
+    /// Output that records whether we encountered a "relevant link" while
+    /// matching _any_ selector for this element. (This differs from
+    /// `RelevantLinkStatus` which tracks the status for the _current_ selector
+    /// only.)
+    pub relevant_link_found: bool,
+
+    quirks_mode: QuirksMode,
+    classes_and_ids_case_sensitivity: CaseSensitivity,
+}
+
+impl<'a> MatchingContext<'a> {
+    /// Constructs a new `MatchingContext`.
+    pub fn new(matching_mode: MatchingMode,
+               bloom_filter: Option<&'a BloomFilter>,
+               quirks_mode: QuirksMode)
+               -> Self
+    {
+        Self {
+            relations: StyleRelations::empty(),
+            matching_mode: matching_mode,
+            bloom_filter: bloom_filter,
+            visited_handling: VisitedHandlingMode::AllLinksUnvisited,
+            relevant_link_found: false,
+            quirks_mode: quirks_mode,
+            classes_and_ids_case_sensitivity: quirks_mode.classes_and_ids_case_sensitivity(),
+        }
+    }
+
+    /// Constructs a new `MatchingContext` for use in visited matching.
+    pub fn new_for_visited(matching_mode: MatchingMode,
+                           bloom_filter: Option<&'a BloomFilter>,
+                           visited_handling: VisitedHandlingMode,
+                           quirks_mode: QuirksMode)
+                           -> Self
+    {
+        Self {
+            relations: StyleRelations::empty(),
+            matching_mode: matching_mode,
+            bloom_filter: bloom_filter,
+            visited_handling: visited_handling,
+            relevant_link_found: false,
+            quirks_mode: quirks_mode,
+            classes_and_ids_case_sensitivity: quirks_mode.classes_and_ids_case_sensitivity(),
+        }
+    }
+
+    /// The quirks mode of the document.
+    #[inline]
+    pub fn quirks_mode(&self) -> QuirksMode {
+        self.quirks_mode
+    }
+
+    /// The case-sensitivity for class and ID selectors
+    #[inline]
+    pub fn classes_and_ids_case_sensitivity(&self) -> CaseSensitivity {
+        self.classes_and_ids_case_sensitivity
+    }
+}
--- a/servo/components/selectors/lib.rs
+++ b/servo/components/selectors/lib.rs
@@ -10,16 +10,17 @@ extern crate fnv;
 extern crate phf;
 extern crate precomputed_hash;
 #[cfg(test)] #[macro_use] extern crate size_of_test;
 extern crate servo_arc;
 extern crate smallvec;
 
 pub mod attr;
 pub mod bloom;
+pub mod context;
 pub mod matching;
 pub mod parser;
 #[cfg(test)] mod size_of_tests;
 #[cfg(any(test, feature = "gecko_like_types"))] pub mod gecko_like_types;
 mod tree;
 pub mod visitor;
 
 pub use parser::{SelectorImpl, Parser, SelectorList};
--- a/servo/components/selectors/matching.rs
+++ b/servo/components/selectors/matching.rs
@@ -1,40 +1,27 @@
 /* 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 attr::{ParsedAttrSelectorOperation, AttrSelectorOperation, NamespaceConstraint};
-use bloom::BloomFilter;
+use bloom::{BLOOM_HASH_MASK, BloomFilter};
 use parser::{AncestorHashes, Combinator, Component, LocalName};
 use parser::{Selector, SelectorImpl, SelectorIter, SelectorList};
 use std::borrow::Borrow;
 use tree::Element;
 
+pub use context::*;
+
 // The bloom filter for descendant CSS selectors will have a <1% false
 // positive rate until it has this many selectors in it, then it will
 // rapidly increase.
 pub static RECOMMENDED_SELECTOR_BLOOM_FILTER_SIZE: usize = 4096;
 
 bitflags! {
-    /// Set of flags that determine the different kind of elements affected by
-    /// the selector matching process.
-    ///
-    /// This is used to implement efficient sharing.
-    #[derive(Default)]
-    pub flags StyleRelations: usize {
-        /// Whether this element is affected by presentational hints. This is
-        /// computed externally (that is, in Servo).
-        const AFFECTED_BY_PRESENTATIONAL_HINTS = 1 << 0,
-        /// Whether this element has pseudo-element styles. Computed externally.
-        const AFFECTED_BY_PSEUDO_ELEMENTS = 1 << 1,
-    }
-}
-
-bitflags! {
     /// Set of flags that are set on either the element or its parent (depending
     /// on the flag) if the element could potentially match a selector.
     pub flags ElementSelectorFlags: usize {
         /// When a child is added or removed from the parent, all the children
         /// must be restyled, because they may match :nth-last-child,
         /// :last-of-type, :nth-last-of-type, or :only-of-type.
         const HAS_SLOW_SELECTOR = 1 << 0,
 
@@ -61,121 +48,16 @@ impl ElementSelectorFlags {
     }
 
     /// Returns the subset of flags that apply to the parent.
     pub fn for_parent(self) -> ElementSelectorFlags {
         self & (HAS_SLOW_SELECTOR | HAS_SLOW_SELECTOR_LATER_SIBLINGS | HAS_EDGE_CHILD_SELECTOR)
     }
 }
 
-/// What kind of selector matching mode we should use.
-///
-/// There are two modes of selector matching. The difference is only noticeable
-/// in presence of pseudo-elements.
-#[derive(Debug, PartialEq, Copy, Clone)]
-pub enum MatchingMode {
-    /// Don't ignore any pseudo-element selectors.
-    Normal,
-
-    /// Ignores any stateless pseudo-element selectors in the rightmost sequence
-    /// of simple selectors.
-    ///
-    /// This is useful, for example, to match against ::before when you aren't a
-    /// pseudo-element yourself.
-    ///
-    /// For example, in presence of `::before:hover`, it would never match, but
-    /// `::before` would be ignored as in "matching".
-    ///
-    /// It's required for all the selectors you match using this mode to have a
-    /// pseudo-element.
-    ForStatelessPseudoElement,
-}
-
-/// The mode to use when matching unvisited and visited links.
-#[derive(PartialEq, Eq, Copy, Clone, Debug)]
-pub enum VisitedHandlingMode {
-    /// All links are matched as if they are unvisted.
-    AllLinksUnvisited,
-    /// A element's "relevant link" is the element being matched if it is a link
-    /// or the nearest ancestor link. The relevant link is matched as though it
-    /// is visited, and all other links are matched as if they are unvisited.
-    RelevantLinkVisited,
-}
-
-/// Which quirks mode is this document in.
-///
-/// See: https://quirks.spec.whatwg.org/
-#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
-pub enum QuirksMode {
-    /// Quirks mode.
-    Quirks,
-    /// Limited quirks mode.
-    LimitedQuirks,
-    /// No quirks mode.
-    NoQuirks,
-}
-
-/// Data associated with the matching process for a element.  This context is
-/// used across many selectors for an element, so it's not appropriate for
-/// transient data that applies to only a single selector.
-#[derive(Clone)]
-pub struct MatchingContext<'a> {
-    /// Output that records certains relations between elements noticed during
-    /// matching (and also extended after matching).
-    pub relations: StyleRelations,
-    /// Input with the matching mode we should use when matching selectors.
-    pub matching_mode: MatchingMode,
-    /// Input with the bloom filter used to fast-reject selectors.
-    pub bloom_filter: Option<&'a BloomFilter>,
-    /// Input that controls how matching for links is handled.
-    pub visited_handling: VisitedHandlingMode,
-    /// Output that records whether we encountered a "relevant link" while
-    /// matching _any_ selector for this element. (This differs from
-    /// `RelevantLinkStatus` which tracks the status for the _current_ selector
-    /// only.)
-    pub relevant_link_found: bool,
-    /// The quirks mode of the document.
-    pub quirks_mode: QuirksMode,
-}
-
-impl<'a> MatchingContext<'a> {
-    /// Constructs a new `MatchingContext`.
-    pub fn new(matching_mode: MatchingMode,
-               bloom_filter: Option<&'a BloomFilter>,
-               quirks_mode: QuirksMode)
-               -> Self
-    {
-        Self {
-            relations: StyleRelations::empty(),
-            matching_mode: matching_mode,
-            bloom_filter: bloom_filter,
-            visited_handling: VisitedHandlingMode::AllLinksUnvisited,
-            relevant_link_found: false,
-            quirks_mode: quirks_mode,
-        }
-    }
-
-    /// Constructs a new `MatchingContext` for use in visited matching.
-    pub fn new_for_visited(matching_mode: MatchingMode,
-                           bloom_filter: Option<&'a BloomFilter>,
-                           visited_handling: VisitedHandlingMode,
-                           quirks_mode: QuirksMode)
-                           -> Self
-    {
-        Self {
-            relations: StyleRelations::empty(),
-            matching_mode: matching_mode,
-            bloom_filter: bloom_filter,
-            visited_handling: visited_handling,
-            relevant_link_found: false,
-            quirks_mode: quirks_mode,
-        }
-    }
-}
-
 /// Holds per-element data alongside a pointer to MatchingContext.
 pub struct LocalMatchingContext<'a, 'b: 'a, Impl: SelectorImpl> {
     /// Shared `MatchingContext`.
     pub shared: &'a mut MatchingContext<'b>,
     /// A reference to the base selector we're matching against.
     pub selector: &'a Selector<Impl>,
     /// The offset of the current compound selector being matched, kept up to date by
     /// the callees when the iterator is advanced. This, in conjunction with the selector
@@ -203,25 +85,25 @@ impl<'a, 'b, Impl> LocalMatchingContext<
             offset: 0,
             within_functional_pseudo_class_argument: false,
         }
     }
 
     /// Updates offset of Selector to show new compound selector.
     /// To be able to correctly re-synthesize main SelectorIter.
     pub fn note_next_sequence(&mut self, selector_iter: &SelectorIter<Impl>) {
-        if let QuirksMode::Quirks = self.shared.quirks_mode {
+        if let QuirksMode::Quirks = self.shared.quirks_mode() {
             self.offset = self.selector.len() - selector_iter.selector_length();
         }
     }
 
     /// Returns true if current compound selector matches :active and :hover quirk.
     /// https://quirks.spec.whatwg.org/#the-active-and-hover-quirk
     pub fn active_hover_quirk_matches(&mut self) -> bool {
-        if self.shared.quirks_mode != QuirksMode::Quirks ||
+        if self.shared.quirks_mode() != QuirksMode::Quirks ||
            self.within_functional_pseudo_class_argument {
             return false;
         }
 
         let mut iter = if self.offset == 0 {
             self.selector.iter()
         } else {
             self.selector.iter_from(self.offset)
@@ -274,29 +156,40 @@ pub fn matches_selector_list<E>(selector
 }
 
 #[inline(always)]
 fn may_match<E>(hashes: &AncestorHashes,
                 bf: &BloomFilter)
                 -> bool
     where E: Element,
 {
-    // Check against the list of precomputed hashes.
-    for hash in hashes.0.iter() {
-        // If we hit the 0 sentinel hash, that means the rest are zero as well.
-        if *hash == 0 {
-            break;
+    // Check the first three hashes. Note that we can check for zero before
+    // masking off the high bits, since if any of the first three hashes is
+    // zero the fourth will be as well. We also take care to avoid the
+    // special-case complexity of the fourth hash until we actually reach it,
+    // because we usually don't.
+    //
+    // To be clear: this is all extremely hot.
+    for i in 0..3 {
+        let packed = hashes.packed_hashes[i];
+        if packed == 0 {
+            // No more hashes left - unable to fast-reject.
+            return true;
         }
 
-        if !bf.might_contain_hash(*hash) {
+        if !bf.might_contain_hash(packed & BLOOM_HASH_MASK) {
+            // Hooray! We fast-rejected on this hash.
             return false;
         }
     }
 
-    true
+    // Now do the slighty-more-complex work of synthesizing the fourth hash,
+    // and check it against the filter if it exists.
+    let fourth = hashes.fourth_hash();
+    fourth == 0 || bf.might_contain_hash(fourth)
 }
 
 /// Tracks whether we are currently looking for relevant links for a given
 /// complex selector. A "relevant link" is the element being matched if it is a
 /// link or the nearest ancestor link.
 ///
 /// `matches_complex_selector` creates a new instance of this for each complex
 /// selector we try to match for an element. This is done because `is_visited`
@@ -650,22 +543,21 @@ fn matches_simple_selector<E, F>(
         Component::Namespace(_, ref url) |
         Component::DefaultNamespace(ref url) => {
             element.get_namespace() == url.borrow()
         }
         Component::ExplicitNoNamespace => {
             let ns = ::parser::namespace_empty_string::<E::Impl>();
             element.get_namespace() == ns.borrow()
         }
-        // TODO: case-sensitivity depends on the document type and quirks mode
         Component::ID(ref id) => {
-            element.get_id().map_or(false, |attr| attr == *id)
+            element.has_id(id, context.shared.classes_and_ids_case_sensitivity())
         }
         Component::Class(ref class) => {
-            element.has_class(class)
+            element.has_class(class, context.shared.classes_and_ids_case_sensitivity())
         }
         Component::AttributeInNoNamespaceExists { ref local_name, ref local_name_lower } => {
             let is_html = element.is_html_element_in_html_document();
             element.attr_matches(
                 &NamespaceConstraint::Specific(&::parser::namespace_empty_string::<E::Impl>()),
                 select_name(is_html, local_name, local_name_lower),
                 &AttrSelectorOperation::Exists
             )
--- a/servo/components/selectors/parser.rs
+++ b/servo/components/selectors/parser.rs
@@ -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/. */
 
 use attr::{AttrSelectorWithNamespace, ParsedAttrSelectorOperation, AttrSelectorOperator};
 use attr::{ParsedCaseSensitivity, SELECTOR_WHITESPACE, NamespaceConstraint};
+use bloom::BLOOM_HASH_MASK;
 use cssparser::{ParseError, BasicParseError};
 use cssparser::{Token, Parser as CssParser, parse_nth, ToCss, serialize_identifier, CssStringWriter};
 use precomputed_hash::PrecomputedHash;
 use servo_arc::{Arc, HeaderWithLength, ThinArc};
 use smallvec::SmallVec;
 use std::ascii::AsciiExt;
 use std::borrow::{Borrow, Cow};
 use std::cmp;
@@ -198,47 +199,74 @@ impl<Impl: SelectorImpl> SelectorList<Im
     }
 
     /// Creates a SelectorList from a Vec of selectors. Used in tests.
     pub fn from_vec(v: Vec<Selector<Impl>>) -> Self {
         SelectorList(v.into_iter().map(SelectorAndHashes::new).collect())
     }
 }
 
-/// Copied from Gecko, who copied it from WebKit. Note that increasing the
-/// number of hashes here will adversely affect the cache hit when fast-
-/// rejecting long lists of Rules with inline hashes.
-const NUM_ANCESTOR_HASHES: usize = 4;
-
 /// Ancestor hashes for the bloom filter. We precompute these and store them
 /// inline with selectors to optimize cache performance during matching.
 /// This matters a lot.
+///
+/// We use 4 hashes, which is copied from Gecko, who copied it from WebKit.
+/// Note that increasing the number of hashes here will adversely affect the
+/// cache hit when fast-rejecting long lists of Rules with inline hashes.
+///
+/// Because the bloom filter only uses the bottom 24 bits of the hash, we pack
+/// the fourth hash into the upper bits of the first three hashes in order to
+/// shrink Rule (whose size matters a lot). This scheme minimizes the runtime
+/// overhead of the packing for the first three hashes (we just need to mask
+/// off the upper bits) at the expense of making the fourth somewhat more
+/// complicated to assemble, because we often bail out before checking all the
+/// hashes.
 #[derive(Eq, PartialEq, Clone, Debug)]
-pub struct AncestorHashes(pub [u32; NUM_ANCESTOR_HASHES]);
+pub struct AncestorHashes {
+    pub packed_hashes: [u32; 3],
+}
 
 impl AncestorHashes {
     pub fn new<Impl: SelectorImpl>(s: &Selector<Impl>) -> Self {
         Self::from_iter(s.iter())
     }
 
     pub fn from_iter<Impl: SelectorImpl>(iter: SelectorIter<Impl>) -> Self {
-        let mut hashes = [0; NUM_ANCESTOR_HASHES];
         // Compute ancestor hashes for the bloom filter.
+        let mut hashes = [0u32; 4];
         let mut hash_iter = AncestorIter::new(iter)
                              .map(|x| x.ancestor_hash())
                              .filter(|x| x.is_some())
                              .map(|x| x.unwrap());
-        for i in 0..NUM_ANCESTOR_HASHES {
+        for i in 0..4 {
             hashes[i] = match hash_iter.next() {
-                Some(x) => x,
+                Some(x) => x & BLOOM_HASH_MASK,
                 None => break,
             }
         }
 
-        AncestorHashes(hashes)
+        // Now, pack the fourth hash (if it exists) into the upper byte of each of
+        // the other three hashes.
+        let fourth = hashes[3];
+        if fourth != 0 {
+            hashes[0] |= (fourth & 0x000000ff) << 24;
+            hashes[1] |= (fourth & 0x0000ff00) << 16;
+            hashes[2] |= (fourth & 0x00ff0000) << 8;
+        }
+
+        AncestorHashes {
+            packed_hashes: [hashes[0], hashes[1], hashes[2]],
+        }
+    }
+
+    /// Returns the fourth hash, reassembled from parts.
+    pub fn fourth_hash(&self) -> u32 {
+        ((self.packed_hashes[0] & 0xff000000) >> 24) |
+        ((self.packed_hashes[1] & 0xff000000) >> 16) |
+        ((self.packed_hashes[2] & 0xff000000) >> 8)
     }
 }
 
 const HAS_PSEUDO_BIT: u32 = 1 << 30;
 
 pub trait SelectorMethods {
     type Impl: SelectorImpl;
 
@@ -1352,17 +1380,20 @@ fn parse_attribute_selector<'i, 't, P, E
     }
 }
 
 
 fn parse_attribute_flags<'i, 't, E>(input: &mut CssParser<'i, 't>)
                                     -> Result<ParsedCaseSensitivity,
                                               ParseError<'i, SelectorParseError<'i, E>>> {
     match input.next() {
-        Err(_) => Ok(ParsedCaseSensitivity::CaseSensitive),
+        Err(_) => {
+            // Selectors spec says language-defined, but HTML says sensitive.
+            Ok(ParsedCaseSensitivity::CaseSensitive)
+        }
         Ok(Token::Ident(ref value)) if value.eq_ignore_ascii_case("i") => {
             Ok(ParsedCaseSensitivity::AsciiCaseInsensitive)
         }
         Ok(t) => Err(ParseError::Basic(BasicParseError::UnexpectedToken(t)))
     }
 }
 
 
--- a/servo/components/selectors/tree.rs
+++ b/servo/components/selectors/tree.rs
@@ -1,16 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! Traits that nodes must implement. Breaks the otherwise-cyclic dependency
 //! between layout and style.
 
-use attr::{AttrSelectorOperation, NamespaceConstraint};
+use attr::{AttrSelectorOperation, NamespaceConstraint, CaseSensitivity};
 use matching::{ElementSelectorFlags, LocalMatchingContext, MatchingContext, RelevantLinkStatus};
 use parser::SelectorImpl;
 use std::fmt::Debug;
 
 pub trait Element: Sized + Debug {
     type Impl: SelectorImpl;
 
     fn parent_element(&self) -> Option<Self>;
@@ -58,19 +58,25 @@ pub trait Element: Sized + Debug {
     fn match_pseudo_element(&self,
                             pe: &<Self::Impl as SelectorImpl>::PseudoElement,
                             context: &mut MatchingContext)
                             -> bool;
 
     /// Whether this element is a `link`.
     fn is_link(&self) -> bool;
 
-    fn get_id(&self) -> Option<<Self::Impl as SelectorImpl>::Identifier>;
+    fn has_id(&self,
+              id: &<Self::Impl as SelectorImpl>::Identifier,
+              case_sensitivity: CaseSensitivity)
+              -> bool;
 
-    fn has_class(&self, name: &<Self::Impl as SelectorImpl>::ClassName) -> bool;
+    fn has_class(&self,
+                 name: &<Self::Impl as SelectorImpl>::ClassName,
+                 case_sensitivity: CaseSensitivity)
+                 -> bool;
 
     /// Returns whether this element matches `:empty`.
     ///
     /// That is, whether it does not contain any child e