Merge mozilla-central to inbound. a=merge CLOSED TREE
authorBrindusan Cristian <cbrindusan@mozilla.com>
Wed, 25 Apr 2018 00:59:04 +0300
changeset 471570 d3c93907c3edfd9aedcf0187cae127fcc1498803
parent 471569 e47d7dbc1cd289086eb63c24cce1600c2b5329c7 (current diff)
parent 471490 6eeb97ca94f40189d5aa552da9e0b0b11bfa0441 (diff)
child 471571 a31159fb68887f49dccc854cd0637dfe77ed6df6
push id1728
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:12:27 +0000
treeherdermozilla-release@c296fde26f5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone61.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 inbound. a=merge CLOSED TREE
--- a/browser/base/content/test/plugins/blocklist_proxy.js
+++ b/browser/base/content/test/plugins/blocklist_proxy.js
@@ -2,16 +2,18 @@ var Cm = Components.manager;
 
 const kBlocklistServiceUUID = "{66354bc9-7ed1-4692-ae1d-8da97d6b205e}";
 const kBlocklistServiceContractID = "@mozilla.org/extensions/blocklist;1";
 const kBlocklistServiceFactory = Cm.getClassObject(Cc[kBlocklistServiceContractID], Ci.nsIFactory);
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Timer.jsm");
 
+SimpleTest.requestFlakyTimeout("Need to simulate blocklist calls actually taking non-0 time to return");
+
 /*
  * A lightweight blocklist proxy for the testing purposes.
  */
 var BlocklistProxy = {
   _uuid: null,
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsIBlocklistService,
@@ -41,21 +43,22 @@ var BlocklistProxy = {
 
   notify(aTimer) {
   },
 
   observe(aSubject, aTopic, aData) {
   },
 
   async getAddonBlocklistState(aAddon, aAppVersion, aToolkitVersion) {
-    await new Promise(r => setTimeout(r, 0));
+    await new Promise(r => setTimeout(r, 150));
     return 0; // STATE_NOT_BLOCKED
   },
 
-  getPluginBlocklistState(aPluginTag, aAppVersion, aToolkitVersion) {
+  async getPluginBlocklistState(aPluginTag, aAppVersion, aToolkitVersion) {
+    await new Promise(r => setTimeout(r, 150));
     return 0; // STATE_NOT_BLOCKED
   },
 
   getPluginBlocklistURL(aPluginTag) {
     return "";
   },
 
   getPluginInfoURL(aPluginTag) {
--- a/browser/base/content/test/tabs/browser.ini
+++ b/browser/base/content/test/tabs/browser.ini
@@ -28,14 +28,15 @@ skip-if = !e10s # Pref and test only rel
 support-files = file_new_tab_page.html
 [browser_overflowScroll.js]
 [browser_pinnedTabs.js]
 [browser_pinnedTabs_closeByKeyboard.js]
 [browser_positional_attributes.js]
 [browser_preloadedBrowser_zoom.js]
 [browser_reload_deleted_file.js]
 skip-if = (debug && os == 'mac') || (debug && os == 'linux' && bits == 64) #Bug 1421183, disabled on Linux/OSX for leaked windows
+[browser_tabReorder_overflow.js]
 [browser_tabswitch_updatecommands.js]
 [browser_viewsource_of_data_URI_in_file_process.js]
 [browser_visibleTabs_bookmarkAllTabs.js]
 [browser_visibleTabs_contextMenu.js]
 [browser_open_newtab_start_observer_notification.js]
 [browser_bug_1387976_restore_lazy_tab_browser_muted_state.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabReorder_overflow.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+add_task(async function() {
+  let initialTabsLength = gBrowser.tabs.length;
+
+  let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox;
+  let tabs = gBrowser.tabs;
+  let tabMinWidth = parseInt(getComputedStyle(gBrowser.selectedTab, null).minWidth);
+
+  let rect = ele => ele.getBoundingClientRect();
+  let width = ele => rect(ele).width;
+  let height = ele => rect(ele).height;
+  let left = ele => rect(ele).left;
+  let top = ele => rect(ele).top;
+
+  let tabCountForOverflow = Math.ceil(width(arrowScrollbox) / tabMinWidth);
+
+  let newTab1 = gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:robots", {skipAnimation: true});
+  let newTab2 = gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:about", {skipAnimation: true});
+  let newTab3 = gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:config", {skipAnimation: true});
+
+  while (tabs.length < tabCountForOverflow) {
+    BrowserTestUtils.addTab(gBrowser, "about:blank", { skipAnimation: true });
+  }
+
+  registerCleanupFunction(function() {
+    while (tabs.length > initialTabsLength) {
+      gBrowser.removeTab(gBrowser.tabs[initialTabsLength]);
+    }
+  });
+
+  is(gBrowser.tabs.length, tabCountForOverflow, "new tabs are opened");
+  is(gBrowser.tabs[initialTabsLength], newTab1, "newTab1 position is correct");
+  is(gBrowser.tabs[initialTabsLength + 1], newTab2, "newTab2 position is correct");
+  is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 position is correct");
+
+  async function dragAndDrop(tab1, tab2) {
+    let event = {
+      clientX: left(tab2) + width(tab2) / 2 + 10,
+      clientY: top(tab2) + height(tab2) / 2,
+    };
+
+    let originalTPos = tab1._tPos;
+    EventUtils.synthesizeDrop(tab1, tab2, null, "move", window, window, event);
+    await BrowserTestUtils.waitForCondition(() => tab1._tPos != originalTPos,
+     "Waiting for tab position to be updated");
+  }
+
+  await dragAndDrop(newTab1, newTab2);
+  is(gBrowser.tabs.length, tabCountForOverflow, "tabs are still there");
+  is(gBrowser.tabs[initialTabsLength], newTab2, "newTab2 and newTab1 are swapped");
+  is(gBrowser.tabs[initialTabsLength + 1], newTab1, "newTab1 and newTab2 are swapped");
+  is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 stays same place");
+});
--- a/browser/components/extensions/parent/ext-sidebarAction.js
+++ b/browser/components/extensions/parent/ext-sidebarAction.js
@@ -1,15 +1,19 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 
 var {
+  ExtensionError,
+} = ExtensionUtils;
+
+var {
   IconDetails,
 } = ExtensionParent;
 
 var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 // WeakMap[Extension -> SidebarAction]
 let sidebarActionMap = new WeakMap();
 
@@ -45,18 +49,23 @@ this.sidebarAction = class extends Exten
     this.defaults = {
       enabled: true,
       title: options.default_title || extension.name,
       icon: IconDetails.normalize({path: options.default_icon}, extension),
       panel: options.default_panel || "",
     };
     this.globals = Object.create(this.defaults);
 
-    this.tabContext = new TabContext(tab => Object.create(this.globals),
-                                     extension);
+    this.tabContext = new TabContext(target => {
+      let window = target.ownerGlobal;
+      if (target === window) {
+        return Object.create(this.globals);
+      }
+      return Object.create(this.tabContext.get(window));
+    }, extension);
 
     // We need to ensure our elements are available before session restore.
     this.windowOpenListener = (window) => {
       this.createMenuItem(window, this.globals);
     };
     windowTracker.addOpenListener(this.windowOpenListener);
 
     this.updateHeader = (event) => {
@@ -243,75 +252,102 @@ this.sidebarAction = class extends Exten
    */
   updateWindow(window) {
     let nativeTab = window.gBrowser.selectedTab;
     this.updateButton(window, this.tabContext.get(nativeTab));
   }
 
   /**
    * Update the broadcaster and menuitem when the extension changes the icon,
-   * title, url, etc. If it only changes a parameter for a single
-   * tab, `tab` will be that tab. Otherwise it will be null.
+   * title, url, etc. If it only changes a parameter for a single tab, `target`
+   * will be that tab. If it only changes a parameter for a single window,
+   * `target` will be that window. Otherwise `target` will be null.
    *
-   * @param {XULElement|null} nativeTab
-   *        Browser tab, may be null.
+   * @param {XULElement|ChromeWindow|null} target
+   *        Browser tab or browser chrome window, may be null.
    */
-  updateOnChange(nativeTab) {
-    if (nativeTab) {
-      if (nativeTab.selected) {
-        this.updateWindow(nativeTab.ownerGlobal);
+  updateOnChange(target) {
+    if (target) {
+      let window = target.ownerGlobal;
+      if (target === window || target.selected) {
+        this.updateWindow(window);
       }
     } else {
       for (let window of windowTracker.browserWindows()) {
         this.updateWindow(window);
       }
     }
   }
 
   /**
-   * Set a default or tab specific property.
+   * Gets the target object and its associated values corresponding to
+   * the `details` parameter of the various get* and set* API methods.
    *
-   * @param {XULElement|null} nativeTab
-   *        Webextension tab object, may be null.
+   * @param {Object} details
+   *        An object with optional `tabId` or `windowId` properties.
+   * @throws if both `tabId` and `windowId` are specified, or if they are invalid.
+   * @returns {Object}
+   *        An object with two properties: `target` and `values`.
+   *        - If a `tabId` was specified, `target` will be the corresponding
+   *          XULElement tab. If a `windowId` was specified, `target` will be
+   *          the corresponding ChromeWindow. Otherwise it will be `null`.
+   *        - `values` will contain the icon, title and panel associated with
+   *          the target.
+   */
+  getContextData({tabId, windowId}) {
+    if (tabId != null && windowId != null) {
+      throw new ExtensionError("Only one of tabId and windowId can be specified.");
+    }
+    let target, values;
+    if (tabId != null) {
+      target = tabTracker.getTab(tabId);
+      values = this.tabContext.get(target);
+    } else if (windowId != null) {
+      target = windowTracker.getWindow(windowId);
+      values = this.tabContext.get(target);
+    } else {
+      target = null;
+      values = this.globals;
+    }
+    return {target, values};
+  }
+
+  /**
+   * Set a global, window specific or tab specific property.
+   *
+   * @param {Object} details
+   *        An object with optional `tabId` or `windowId` properties.
    * @param {string} prop
-   *        String property to retrieve ["icon", "title", or "panel"].
+   *        String property to set ["icon", "title", or "panel"].
    * @param {string} value
    *        Value for property.
    */
-  setProperty(nativeTab, prop, value) {
-    let values;
-    if (nativeTab === null) {
-      values = this.globals;
-    } else {
-      values = this.tabContext.get(nativeTab);
-    }
+  setProperty(details, prop, value) {
+    let {target, values} = this.getContextData(details);
     if (value === null) {
       delete values[prop];
     } else {
       values[prop] = value;
     }
 
-    this.updateOnChange(nativeTab);
+    this.updateOnChange(target);
   }
 
   /**
-   * Retrieve a property from the tab or globals if tab is null.
+   * Retrieve the value of a global, window specific or tab specific property.
    *
-   * @param {XULElement|null} nativeTab
-   *        Browser tab object, may be null.
+   * @param {Object} details
+   *        An object with optional `tabId` or `windowId` properties.
    * @param {string} prop
    *        String property to retrieve ["icon", "title", or "panel"]
    * @returns {string} value
-   *          Value for prop.
+   *          Value of prop.
    */
-  getProperty(nativeTab, prop) {
-    if (nativeTab === null) {
-      return this.globals[prop];
-    }
-    return this.tabContext.get(nativeTab)[prop];
+  getProperty(details, prop) {
+    return this.getContextData(details).values[prop];
   }
 
   /**
    * Triggers this sidebar action for the given window, with the same effects as
    * if it were toggled via menu or toolbarbutton by a user.
    *
    * @param {ChromeWindow} window
    */
@@ -355,69 +391,51 @@ this.sidebarAction = class extends Exten
     let {SidebarUI} = window;
     return SidebarUI.isOpen && this.id == SidebarUI.currentID;
   }
 
   getAPI(context) {
     let {extension} = context;
     const sidebarAction = this;
 
-    function getTab(tabId) {
-      if (tabId !== null) {
-        return tabTracker.getTab(tabId);
-      }
-      return null;
-    }
-
     return {
       sidebarAction: {
         async setTitle(details) {
-          let nativeTab = getTab(details.tabId);
-          sidebarAction.setProperty(nativeTab, "title", details.title);
+          sidebarAction.setProperty(details, "title", details.title);
         },
 
         getTitle(details) {
-          let nativeTab = getTab(details.tabId);
-
-          let title = sidebarAction.getProperty(nativeTab, "title");
-          return Promise.resolve(title);
+          return sidebarAction.getProperty(details, "title");
         },
 
         async setIcon(details) {
-          let nativeTab = getTab(details.tabId);
-
           let icon = IconDetails.normalize(details, extension, context);
           if (!Object.keys(icon).length) {
             icon = null;
           }
-          sidebarAction.setProperty(nativeTab, "icon", icon);
+          sidebarAction.setProperty(details, "icon", icon);
         },
 
         async setPanel(details) {
-          let nativeTab = getTab(details.tabId);
-
           let url;
           // Clear the url when given null or empty string.
           if (!details.panel) {
             url = null;
           } else {
             url = context.uri.resolve(details.panel);
             if (!context.checkLoadURL(url)) {
               return Promise.reject({message: `Access denied for URL ${url}`});
             }
           }
 
-          sidebarAction.setProperty(nativeTab, "panel", url);
+          sidebarAction.setProperty(details, "panel", url);
         },
 
         getPanel(details) {
-          let nativeTab = getTab(details.tabId);
-
-          let panel = sidebarAction.getProperty(nativeTab, "panel");
-          return Promise.resolve(panel);
+          return sidebarAction.getProperty(details, "panel");
         },
 
         open() {
           let window = windowTracker.topWindow;
           sidebarAction.open(window);
         },
 
         close() {
--- a/browser/components/extensions/schemas/sidebar_action.json
+++ b/browser/components/extensions/schemas/sidebar_action.json
@@ -69,16 +69,22 @@
                   {"type": "null"}
                 ],
                 "description": "The string the sidebar action should display when moused over."
               },
               "tabId": {
                 "type": "integer",
                 "optional": true,
                 "description": "Sets the sidebar title for the tab specified by tabId. Automatically resets when the tab is closed."
+              },
+              "windowId": {
+                "type": "integer",
+                "optional": true,
+                "minimum": -2,
+                "description": "Sets the sidebar title for the window specified by windowId."
               }
             }
           }
         ]
       },
       {
         "name": "getTitle",
         "type": "function",
@@ -87,17 +93,23 @@
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "tabId": {
                 "type": "integer",
                 "optional": true,
-                "description": "Specify the tab to get the title from. If no tab is specified, the non-tab-specific title is returned."
+                "description": "Specify the tab to get the title from. If no tab nor window is specified, the global title is returned."
+              },
+              "windowId": {
+                "type": "integer",
+                "optional": true,
+                "minimum": -2,
+                "description": "Specify the window to get the title from. If no tab nor window is specified, the global title is returned."
               }
             }
           }
         ]
       },
       {
         "name": "setIcon",
         "type": "function",
@@ -132,16 +144,22 @@
                 ],
                 "optional": true,
                 "description": "Either a relative image path or a dictionary {size -> relative image path} pointing to icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.imageData = {'19': foo}'"
               },
               "tabId": {
                 "type": "integer",
                 "optional": true,
                 "description": "Sets the sidebar icon for the tab specified by tabId. Automatically resets when the tab is closed."
+              },
+              "windowId": {
+                "type": "integer",
+                "optional": true,
+                "minimum": -2,
+                "description": "Sets the sidebar icon for the window specified by windowId."
               }
             }
           }
         ]
       },
       {
         "name": "setPanel",
         "type": "function",
@@ -153,16 +171,22 @@
             "type": "object",
             "properties": {
               "tabId": {
                 "type": "integer",
                 "optional": true,
                 "minimum": 0,
                 "description": "Sets the sidebar url for the tab specified by tabId. Automatically resets when the tab is closed."
               },
+              "windowId": {
+                "type": "integer",
+                "optional": true,
+                "minimum": -2,
+                "description": "Sets the sidebar url for the window specified by windowId."
+              },
               "panel": {
                 "choices": [
                   {"type": "string"},
                   {"type": "null"}
                 ],
                 "description": "The url to the html file to show in a sidebar.  If set to the empty string (''), no sidebar is shown."
               }
             }
@@ -177,17 +201,23 @@
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "tabId": {
                 "type": "integer",
                 "optional": true,
-                "description": "Specify the tab to get the sidebar from. If no tab is specified, the non-tab-specific sidebar is returned."
+                "description": "Specify the tab to get the panel from. If no tab nor window is specified, the global panel is returned."
+              },
+              "windowId": {
+                "type": "integer",
+                "optional": true,
+                "minimum": -2,
+                "description": "Specify the window to get the panel from. If no tab nor window is specified, the global panel is returned."
               }
             }
           }
         ]
       },
       {
         "name": "open",
         "type": "function",
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
@@ -6,32 +6,29 @@ ChromeUtils.import("resource://gre/modul
 
 SpecialPowers.pushPrefEnv({
   // Ignore toolbarbutton stuff, other test covers it.
   set: [["extensions.sidebar-button.shown", true]],
 });
 
 async function runTests(options) {
   async function background(getTests) {
-    async function checkDetails(expecting, tabId) {
-      let title = await browser.sidebarAction.getTitle({tabId});
+    async function checkDetails(expecting, details) {
+      let title = await browser.sidebarAction.getTitle(details);
       browser.test.assertEq(expecting.title, title,
-                            "expected value from getTitle");
+                            "expected value from getTitle in " + JSON.stringify(details));
 
-      let panel = await browser.sidebarAction.getPanel({tabId});
+      let panel = await browser.sidebarAction.getPanel(details);
       browser.test.assertEq(expecting.panel, panel,
-                            "expected value from getPanel");
+                            "expected value from getPanel in " + JSON.stringify(details));
     }
 
-    let expectDefaults = expecting => {
-      return checkDetails(expecting);
-    };
-
     let tabs = [];
-    let tests = getTests(tabs, expectDefaults);
+    let windows = [];
+    let tests = getTests(tabs, windows);
 
     {
       let tabId = 0xdeadbeef;
       let calls = [
         () => browser.sidebarAction.setTitle({tabId, title: "foo"}),
         () => browser.sidebarAction.setIcon({tabId, path: "foo.png"}),
         () => browser.sidebarAction.setPanel({tabId, panel: "foo.html"}),
       ];
@@ -44,52 +41,59 @@ async function runTests(options) {
       }
     }
 
     // Runs the next test in the `tests` array, checks the results,
     // and passes control back to the outer test scope.
     function nextTest() {
       let test = tests.shift();
 
-      test(async expecting => {
+      test(async (expectTab, expectWindow, expectGlobal, expectDefault) => {
+        expectGlobal = {...expectDefault, ...expectGlobal};
+        expectWindow = {...expectGlobal, ...expectWindow};
+        expectTab = {...expectWindow, ...expectTab};
+
         // Check that the API returns the expected values, and then
         // run the next test.
-        let tabs = await browser.tabs.query({active: true, currentWindow: true});
-        await checkDetails(expecting, tabs[0].id);
+        let [{windowId, id: tabId}] = await browser.tabs.query({active: true, currentWindow: true});
+        await checkDetails(expectTab, {tabId});
+        await checkDetails(expectWindow, {windowId});
+        await checkDetails(expectGlobal, {});
 
         // Check that the actual icon has the expected values, then
         // run the next test.
-        browser.test.sendMessage("nextTest", expecting, tests.length);
+        browser.test.sendMessage("nextTest", expectTab, windowId, tests.length);
       });
     }
 
     browser.test.onMessage.addListener((msg) => {
       if (msg != "runNextTest") {
         browser.test.fail("Expecting 'runNextTest' message");
       }
 
       nextTest();
     });
 
-    browser.tabs.query({active: true, currentWindow: true}, resultTabs => {
-      tabs[0] = resultTabs[0].id;
-    });
+    let [{id, windowId}] = await browser.tabs.query({active: true, currentWindow: true});
+    tabs.push(id);
+    windows.push(windowId);
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: options.manifest,
     useAddonManager: "temporary",
 
     files: options.files || {},
 
     background: `(${background})(${options.getTests})`,
   });
 
   let sidebarActionId;
-  function checkDetails(details) {
+  function checkDetails(details, windowId) {
+    let {document} = Services.wm.getOuterWindowWithId(windowId);
     if (!sidebarActionId) {
       sidebarActionId = `${makeWidgetId(extension.id)}-sidebar-action`;
     }
 
     let command = document.getElementById(sidebarActionId);
     ok(command, "command exists");
 
     let menuId = `menu_${sidebarActionId}`;
@@ -98,18 +102,18 @@ async function runTests(options) {
 
     let title = details.title || options.manifest.name;
 
     is(getListStyleImage(menu), details.icon, "icon URL is correct");
     is(menu.getAttribute("label"), title, "image label is correct");
   }
 
   let awaitFinish = new Promise(resolve => {
-    extension.onMessage("nextTest", (expecting, testsRemaining) => {
-      checkDetails(expecting);
+    extension.onMessage("nextTest", (expecting, windowId, testsRemaining) => {
+      checkDetails(expecting, windowId);
 
       if (testsRemaining) {
         extension.sendMessage("runNextTest");
       } else {
         resolve();
       }
     });
   });
@@ -144,166 +148,149 @@ add_task(async function testTabSwitchCon
 
       "default_locale": "en",
 
       "permissions": ["tabs"],
     },
 
     "files": {
       "default.html": sidebar,
-      "default-2.html": sidebar,
+      "global.html": sidebar,
       "2.html": sidebar,
 
       "_locales/en/messages.json": {
         "panel": {
           "message": "default.html",
           "description": "Panel",
         },
 
         "title": {
           "message": "Title",
           "description": "Title",
         },
       },
 
       "default.png": imageBuffer,
-      "default-2.png": imageBuffer,
+      "global.png": imageBuffer,
       "1.png": imageBuffer,
       "2.png": imageBuffer,
     },
 
-    getTests: function(tabs, expectDefaults) {
+    getTests: function(tabs) {
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "panel": browser.runtime.getURL("default.html"),
          "title": "Default Title",
         },
         {"icon": browser.runtime.getURL("1.png"),
-         "panel": browser.runtime.getURL("default.html"),
-         "title": "Default Title",
         },
         {"icon": browser.runtime.getURL("2.png"),
          "panel": browser.runtime.getURL("2.html"),
          "title": "Title 2",
         },
-        {"icon": browser.runtime.getURL("1.png"),
-         "panel": browser.runtime.getURL("default-2.html"),
-         "title": "Default Title 2",
-        },
-        {"icon": browser.runtime.getURL("1.png"),
-         "panel": browser.runtime.getURL("default-2.html"),
-         "title": "Default Title 2",
-        },
-        {"icon": browser.runtime.getURL("default-2.png"),
-         "panel": browser.runtime.getURL("default-2.html"),
-         "title": "Default Title 2",
+        {"icon": browser.runtime.getURL("global.png"),
+         "panel": browser.runtime.getURL("global.html"),
+         "title": "Global Title",
         },
         {"icon": browser.runtime.getURL("1.png"),
          "panel": browser.runtime.getURL("2.html"),
-         "title": "Default Title 2",
         },
       ];
 
       return [
         async expect => {
           browser.test.log("Initial state, expect default properties.");
 
-          await expectDefaults(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
         async expect => {
           browser.test.log("Change the icon in the current tab. Expect default properties excluding the icon.");
           await browser.sidebarAction.setIcon({tabId: tabs[0], path: "1.png"});
 
-          await expectDefaults(details[0]);
-          expect(details[1]);
+          expect(details[1], null, null, details[0]);
         },
         async expect => {
           browser.test.log("Create a new tab. Expect default properties.");
           let tab = await browser.tabs.create({active: true, url: "about:blank?0"});
           tabs.push(tab.id);
 
-          await expectDefaults(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
         async expect => {
           browser.test.log("Change properties. Expect new properties.");
           let tabId = tabs[1];
           await Promise.all([
             browser.sidebarAction.setIcon({tabId, path: "2.png"}),
             browser.sidebarAction.setPanel({tabId, panel: "2.html"}),
             browser.sidebarAction.setTitle({tabId, title: "Title 2"}),
           ]);
-          await expectDefaults(details[0]);
-          expect(details[2]);
+          expect(details[2], null, null, details[0]);
         },
         expect => {
           browser.test.log("Navigate to a new page. Expect no changes.");
 
           // TODO: This listener should not be necessary, but the |tabs.update|
           // callback currently fires too early in e10s windows.
           browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
             if (tabId == tabs[1] && changed.url) {
               browser.tabs.onUpdated.removeListener(listener);
-              expect(details[2]);
+              expect(details[2], null, null, details[0]);
             }
           });
 
           browser.tabs.update(tabs[1], {url: "about:blank?1"});
         },
         async expect => {
           browser.test.log("Switch back to the first tab. Expect previously set properties.");
           await browser.tabs.update(tabs[0], {active: true});
-          expect(details[1]);
+          expect(details[1], null, null, details[0]);
         },
         async expect => {
-          browser.test.log("Change default values, expect those changes reflected.");
+          browser.test.log("Change global values, expect those changes reflected.");
           await Promise.all([
-            browser.sidebarAction.setIcon({path: "default-2.png"}),
-            browser.sidebarAction.setPanel({panel: "default-2.html"}),
-            browser.sidebarAction.setTitle({title: "Default Title 2"}),
+            browser.sidebarAction.setIcon({path: "global.png"}),
+            browser.sidebarAction.setPanel({panel: "global.html"}),
+            browser.sidebarAction.setTitle({title: "Global Title"}),
           ]);
 
-          await expectDefaults(details[3]);
-          expect(details[3]);
+          expect(details[1], null, details[3], details[0]);
         },
         async expect => {
-          browser.test.log("Switch back to tab 2. Expect former value, unaffected by changes to defaults in previous step.");
+          browser.test.log("Switch back to tab 2. Expect former tab values, and new global values from previous step.");
           await browser.tabs.update(tabs[1], {active: true});
 
-          await expectDefaults(details[3]);
-          expect(details[2]);
+          expect(details[2], null, details[3], details[0]);
         },
         async expect => {
           browser.test.log("Delete tab, switch back to tab 1. Expect previous results again.");
           await browser.tabs.remove(tabs[1]);
-          expect(details[4]);
+          expect(details[1], null, details[3], details[0]);
         },
         async expect => {
-          browser.test.log("Create a new tab. Expect new default properties.");
+          browser.test.log("Create a new tab. Expect new global properties.");
           let tab = await browser.tabs.create({active: true, url: "about:blank?2"});
           tabs.push(tab.id);
-          expect(details[5]);
+          expect(null, null, details[3], details[0]);
         },
         async expect => {
           browser.test.log("Delete tab.");
           await browser.tabs.remove(tabs[2]);
-          expect(details[4]);
+          expect(details[1], null, details[3], details[0]);
         },
         async expect => {
           browser.test.log("Change tab panel.");
           let tabId = tabs[0];
           await browser.sidebarAction.setPanel({tabId, panel: "2.html"});
-          expect(details[6]);
+          expect(details[4], null, details[3], details[0]);
         },
         async expect => {
           browser.test.log("Revert tab panel.");
           let tabId = tabs[0];
           await browser.sidebarAction.setPanel({tabId, panel: null});
-          expect(details[4]);
+          expect(details[1], null, details[3], details[0]);
         },
       ];
     },
   });
 });
 
 add_task(async function testDefaultTitle() {
   await runTests({
@@ -318,72 +305,62 @@ add_task(async function testDefaultTitle
       "permissions": ["tabs"],
     },
 
     files: {
       "sidebar.html": sidebar,
       "icon.png": imageBuffer,
     },
 
-    getTests: function(tabs, expectGlobals) {
+    getTests: function(tabs) {
       let details = [
         {"title": "Foo Extension",
          "panel": browser.runtime.getURL("sidebar.html"),
          "icon": browser.runtime.getURL("icon.png")},
-        {"title": "Foo Title",
-         "panel": browser.runtime.getURL("sidebar.html"),
-         "icon": browser.runtime.getURL("icon.png")},
-        {"title": "Bar Title",
-         "panel": browser.runtime.getURL("sidebar.html"),
-         "icon": browser.runtime.getURL("icon.png")},
+        {"title": "Foo Title"},
+        {"title": "Bar Title"},
       ];
 
       return [
         async expect => {
           browser.test.log("Initial state. Expect default extension title.");
 
-          await expectGlobals(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
         async expect => {
           browser.test.log("Change the tab title. Expect new title.");
           browser.sidebarAction.setTitle({tabId: tabs[0], title: "Foo Title"});
 
-          await expectGlobals(details[0]);
-          expect(details[1]);
+          expect(details[1], null, null, details[0]);
         },
         async expect => {
           browser.test.log("Change the global title. Expect same properties.");
           browser.sidebarAction.setTitle({title: "Bar Title"});
 
-          await expectGlobals(details[2]);
-          expect(details[1]);
+          expect(details[1], null, details[2], details[0]);
         },
         async expect => {
           browser.test.log("Clear the tab title. Expect new global title.");
           browser.sidebarAction.setTitle({tabId: tabs[0], title: null});
 
-          await expectGlobals(details[2]);
-          expect(details[2]);
+          expect(null, null, details[2], details[0]);
         },
         async expect => {
           browser.test.log("Clear the global title. Expect default title.");
           browser.sidebarAction.setTitle({title: null});
 
-          await expectGlobals(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
         async expect => {
           browser.test.assertRejects(
             browser.sidebarAction.setPanel({panel: "about:addons"}),
             /Access denied for URL about:addons/,
             "unable to set panel to about:addons");
 
-          await expectGlobals(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
       ];
     },
   });
 });
 
 add_task(async function testPropertyRemoval() {
   await runTests({
@@ -396,98 +373,233 @@ add_task(async function testPropertyRemo
         "default_title": "Default Title",
       },
 
       "permissions": ["tabs"],
     },
 
     files: {
       "default.html": sidebar,
-      "p1.html": sidebar,
-      "p2.html": sidebar,
-      "p3.html": sidebar,
+      "global.html": sidebar,
+      "global2.html": sidebar,
+      "window.html": sidebar,
+      "tab.html": sidebar,
       "default.png": imageBuffer,
-      "i1.png": imageBuffer,
-      "i2.png": imageBuffer,
-      "i3.png": imageBuffer,
+      "global.png": imageBuffer,
+      "global2.png": imageBuffer,
+      "window.png": imageBuffer,
+      "tab.png": imageBuffer,
     },
 
-    getTests: function(tabs, expectGlobals) {
+    getTests: function(tabs, windows) {
       let defaultIcon = "chrome://browser/content/extension.svg";
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "panel": browser.runtime.getURL("default.html"),
          "title": "Default Title"},
-        {"icon": browser.runtime.getURL("i1.png"),
-         "panel": browser.runtime.getURL("p1.html"),
-         "title": "t1"},
-        {"icon": browser.runtime.getURL("i2.png"),
-         "panel": browser.runtime.getURL("p2.html"),
-         "title": "t2"},
+        {"icon": browser.runtime.getURL("global.png"),
+         "panel": browser.runtime.getURL("global.html"),
+         "title": "global"},
+        {"icon": browser.runtime.getURL("window.png"),
+         "panel": browser.runtime.getURL("window.html"),
+         "title": "window"},
+        {"icon": browser.runtime.getURL("tab.png"),
+         "panel": browser.runtime.getURL("tab.html"),
+         "title": "tab"},
         {"icon": defaultIcon,
-         "panel": browser.runtime.getURL("p1.html"),
          "title": ""},
-        {"icon": browser.runtime.getURL("i3.png"),
-         "panel": browser.runtime.getURL("p3.html"),
-         "title": "t3"},
+        {"icon": browser.runtime.getURL("global2.png"),
+         "panel": browser.runtime.getURL("global2.html"),
+         "title": "global2"},
       ];
 
       return [
         async expect => {
           browser.test.log("Initial state, expect default properties.");
-          await expectGlobals(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
         async expect => {
           browser.test.log("Set global values, expect the new values.");
-          browser.sidebarAction.setIcon({path: "i1.png"});
-          browser.sidebarAction.setPanel({panel: "p1.html"});
-          browser.sidebarAction.setTitle({title: "t1"});
-          await expectGlobals(details[1]);
-          expect(details[1]);
+          browser.sidebarAction.setIcon({path: "global.png"});
+          browser.sidebarAction.setPanel({panel: "global.html"});
+          browser.sidebarAction.setTitle({title: "global"});
+          expect(null, null, details[1], details[0]);
+        },
+        async expect => {
+          browser.test.log("Set window values, expect the new values.");
+          let windowId = windows[0];
+          browser.sidebarAction.setIcon({windowId, path: "window.png"});
+          browser.sidebarAction.setPanel({windowId, panel: "window.html"});
+          browser.sidebarAction.setTitle({windowId, title: "window"});
+          expect(null, details[2], details[1], details[0]);
         },
         async expect => {
           browser.test.log("Set tab values, expect the new values.");
           let tabId = tabs[0];
-          browser.sidebarAction.setIcon({tabId, path: "i2.png"});
-          browser.sidebarAction.setPanel({tabId, panel: "p2.html"});
-          browser.sidebarAction.setTitle({tabId, title: "t2"});
-          await expectGlobals(details[1]);
-          expect(details[2]);
+          browser.sidebarAction.setIcon({tabId, path: "tab.png"});
+          browser.sidebarAction.setPanel({tabId, panel: "tab.html"});
+          browser.sidebarAction.setTitle({tabId, title: "tab"});
+          expect(details[3], details[2], details[1], details[0]);
         },
         async expect => {
           browser.test.log("Set empty tab values.");
           let tabId = tabs[0];
           browser.sidebarAction.setIcon({tabId, path: ""});
           browser.sidebarAction.setPanel({tabId, panel: ""});
           browser.sidebarAction.setTitle({tabId, title: ""});
-          await expectGlobals(details[1]);
-          expect(details[3]);
+          expect(details[4], details[2], details[1], details[0]);
         },
         async expect => {
-          browser.test.log("Remove tab values, expect global values.");
+          browser.test.log("Remove tab values, expect window values.");
           let tabId = tabs[0];
           browser.sidebarAction.setIcon({tabId, path: null});
           browser.sidebarAction.setPanel({tabId, panel: null});
           browser.sidebarAction.setTitle({tabId, title: null});
-          await expectGlobals(details[1]);
-          expect(details[1]);
+          expect(null, details[2], details[1], details[0]);
+        },
+        async expect => {
+          browser.test.log("Remove window values, expect global values.");
+          let windowId = windows[0];
+          browser.sidebarAction.setIcon({windowId, path: null});
+          browser.sidebarAction.setPanel({windowId, panel: null});
+          browser.sidebarAction.setTitle({windowId, title: null});
+          expect(null, null, details[1], details[0]);
         },
         async expect => {
           browser.test.log("Change global values, expect the new values.");
-          browser.sidebarAction.setIcon({path: "i3.png"});
-          browser.sidebarAction.setPanel({panel: "p3.html"});
-          browser.sidebarAction.setTitle({title: "t3"});
-          await expectGlobals(details[4]);
-          expect(details[4]);
+          browser.sidebarAction.setIcon({path: "global2.png"});
+          browser.sidebarAction.setPanel({panel: "global2.html"});
+          browser.sidebarAction.setTitle({title: "global2"});
+          expect(null, null, details[5], details[0]);
         },
         async expect => {
           browser.test.log("Remove global values, expect defaults.");
           browser.sidebarAction.setIcon({path: null});
           browser.sidebarAction.setPanel({panel: null});
           browser.sidebarAction.setTitle({title: null});
-          await expectGlobals(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
       ];
     },
   });
 });
+
+add_task(async function testMultipleWindows() {
+  await runTests({
+    manifest: {
+      "name": "Foo Extension",
+
+      "sidebar_action": {
+        "default_icon": "default.png",
+        "default_panel": "default.html",
+        "default_title": "Default Title",
+      },
+
+      "permissions": ["tabs"],
+    },
+
+    files: {
+      "default.html": sidebar,
+      "window1.html": sidebar,
+      "window2.html": sidebar,
+      "default.png": imageBuffer,
+      "window1.png": imageBuffer,
+      "window2.png": imageBuffer,
+    },
+
+    getTests: function(tabs, windows) {
+      let details = [
+        {"icon": browser.runtime.getURL("default.png"),
+         "panel": browser.runtime.getURL("default.html"),
+         "title": "Default Title"},
+        {"icon": browser.runtime.getURL("window1.png"),
+         "panel": browser.runtime.getURL("window1.html"),
+         "title": "window1"},
+        {"icon": browser.runtime.getURL("window2.png"),
+         "panel": browser.runtime.getURL("window2.html"),
+         "title": "window2"},
+        {"title": "tab"},
+      ];
+
+      return [
+        async expect => {
+          browser.test.log("Initial state, expect default properties.");
+          expect(null, null, null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Set window values, expect the new values.");
+          let windowId = windows[0];
+          browser.sidebarAction.setIcon({windowId, path: "window1.png"});
+          browser.sidebarAction.setPanel({windowId, panel: "window1.html"});
+          browser.sidebarAction.setTitle({windowId, title: "window1"});
+          expect(null, details[1], null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Create a new tab, expect window values.");
+          let tab = await browser.tabs.create({active: true});
+          tabs.push(tab.id);
+          expect(null, details[1], null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Set a tab title, expect it.");
+          await browser.sidebarAction.setTitle({tabId: tabs[1], title: "tab"});
+          expect(details[3], details[1], null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Open a new window, expect default values.");
+          let {id} = await browser.windows.create();
+          windows.push(id);
+          expect(null, null, null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Set window values, expect the new values.");
+          let windowId = windows[1];
+          browser.sidebarAction.setIcon({windowId, path: "window2.png"});
+          browser.sidebarAction.setPanel({windowId, panel: "window2.html"});
+          browser.sidebarAction.setTitle({windowId, title: "window2"});
+          expect(null, details[2], null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Move tab from old window to the new one. Tab-specific data"
+            + " is cleared (bug 1451176) and inheritance is from the new window");
+          await browser.tabs.move(tabs[1], {windowId: windows[1], index: -1});
+          await browser.tabs.update(tabs[1], {active: true});
+          expect(null, details[2], null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Close the tab, expect window values.");
+          await browser.tabs.remove(tabs[1]);
+          expect(null, details[2], null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Close the new window and go back to the previous one.");
+          await browser.windows.remove(windows[1]);
+          expect(null, details[1], null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Assert failures for bad parameters. Expect no change");
+
+          let calls = {
+            setIcon: {path: "default.png"},
+            setPanel: {panel: "default.html"},
+            setTitle: {title: "Default Title"},
+            getPanel: {},
+            getTitle: {},
+          };
+          for (let [method, arg] of Object.entries(calls)) {
+            browser.test.assertThrows(
+              () => browser.sidebarAction[method]({...arg, windowId: -3}),
+              /-3 is too small \(must be at least -2\)/,
+              method + " with invalid windowId",
+            );
+            await browser.test.assertRejects(
+              browser.sidebarAction[method]({...arg, tabId: tabs[0], windowId: windows[0]}),
+              /Only one of tabId and windowId can be specified/,
+              method + " with both tabId and windowId",
+            );
+          }
+
+          expect(null, details[1], null, details[0]);
+        },
+      ];
+    },
+  });
+});
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -275,17 +275,17 @@
 
 <!-- Tracking -->
 <groupbox id="trackingGroup" data-category="panePrivacy" hidden="true">
   <caption><label data-l10n-id="tracking-header"/></caption>
   <vbox>
     <hbox align="start">
       <vbox flex="1">
         <description data-l10n-id="tracking-desc">
-          <a id="trackingProtectionLearnMore" data-l10n-name="learn-more" target="_blank" class="learnMore text-link"/>
+          <html:a id="trackingProtectionLearnMore" data-l10n-name="learn-more" target="_blank" class="learnMore text-link"/>
         </description>
       </vbox>
       <spacer flex="1"/>
     </hbox>
     <hbox>
       <vbox id="trackingProtectionBox" flex="1" hidden="true">
         <vbox>
           <hbox id="trackingProtectionExtensionContentLabel" align="center" hidden="true">
--- a/browser/locales/en-US/chrome/browser/accounts.properties
+++ b/browser/locales/en-US/chrome/browser/accounts.properties
@@ -68,17 +68,17 @@ sendTabToDevice.verify = Verify Your Account…
 
 # LOCALIZATION NOTE (tabArrivingNotification.title, tabArrivingNotificationWithDevice.title,
 # multipleTabsArrivingNotification.title, unnamedTabsArrivingNotification2.body,
 # unnamedTabsArrivingNotificationMultiple2.body, unnamedTabsArrivingNotificationNoDevice.body,
 # singleTabArrivingWithTruncatedURL.body)
 # These strings are used in a notification shown when we're opening tab(s) another device sent us to display.
 
 # LOCALIZATION NOTE (tabArrivingNotification.title, tabArrivingNotificationWithDevice.title)
-# The body for these is the URL of the tab recieved
+# The body for these is the URL of the tab received
 tabArrivingNotification.title = Tab Received
 # LOCALIZATION NOTE (tabArrivingNotificationWithDevice.title) %S is the device name
 tabArrivingNotificationWithDevice.title = Tab from %S
 
 multipleTabsArrivingNotification.title = Tabs Received
 # LOCALIZATION NOTE (unnamedTabsArrivingNotification2.body):
 # Semi-colon list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -249,17 +249,17 @@ popupWarningButton=Options
 popupWarningButton.accesskey=O
 popupWarningButtonUnix=Preferences
 popupWarningButtonUnix.accesskey=P
 popupAllow=Allow pop-ups for %S
 popupBlock=Block pop-ups for %S
 popupWarningDontShowFromMessage=Don’t show this message when pop-ups are blocked
 popupShowPopupPrefix=Show ‘%S’
 
-# LOCALIZATION NOTE (popupShowBlockedPopupsIndicatorText): Semicolon seperated list of plural forms.
+# LOCALIZATION NOTE (popupShowBlockedPopupsIndicatorText): Semicolon separated list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # #1 is the number of pop-ups blocked.
 popupShowBlockedPopupsIndicatorText=Show #1 blocked pop-up…;Show #1 blocked pop-ups…
 
 # Bad Content Blocker Doorhanger Notification
 # %S is brandShortName
 badContentBlocked.blocked.message=%S is blocking content on this page.
 badContentBlocked.notblocked.message=%S is not blocking any content on this page.
--- a/devtools/client/framework/attach-thread.js
+++ b/devtools/client/framework/attach-thread.js
@@ -22,17 +22,17 @@ function handleThreadState(toolbox, even
 
   if (event === "paused") {
     toolbox.highlightTool("jsdebugger");
 
     if (packet.why.type === "debuggerStatement" ||
        packet.why.type === "breakpoint" ||
        packet.why.type === "exception") {
       toolbox.raise();
-      toolbox.selectTool("jsdebugger");
+      toolbox.selectTool("jsdebugger", packet.why.type);
     }
   } else if (event === "resumed") {
     toolbox.unhighlightTool("jsdebugger");
   }
 }
 
 function attachThread(toolbox) {
   let deferred = defer();
--- a/devtools/client/framework/components/toolbox-tab.js
+++ b/devtools/client/framework/components/toolbox-tab.js
@@ -56,20 +56,20 @@ class ToolboxTab extends Component {
         className,
         id: `toolbox-tab-${id}`,
         "data-id": id,
         title: tooltip,
         type: "button",
         "aria-pressed": currentToolId === id ? "true" : "false",
         tabIndex: focusedButton === id ? "0" : "-1",
         onFocus: () => focusButton(id),
-        onMouseDown: () => selectTool(id),
+        onMouseDown: () => selectTool(id, "tab_switch"),
         onKeyDown: (evt) => {
           if (evt.key === "Enter" || evt.key === " ") {
-            selectTool(id);
+            selectTool(id, "tab_switch");
           }
         },
       },
       span(
         {
           className: "devtools-tab-line"
         }
       ),
--- a/devtools/client/framework/components/toolbox-tabs.js
+++ b/devtools/client/framework/components/toolbox-tabs.js
@@ -199,17 +199,17 @@ class ToolboxTabs extends Component {
         let menu = new Menu({
           id: "tools-chevron-menupopup"
         });
 
         panelDefinitions.forEach(({id, label}) => {
           if (this.state.overflowedTabIds.includes(id)) {
             menu.append(new MenuItem({
               click: () => {
-                selectTool(id);
+                selectTool(id, "tab_switch");
               },
               id: "tools-chevron-menupopup-" + id,
               label,
               type: "checkbox",
             }));
           }
         });
 
--- a/devtools/client/framework/devtools.js
+++ b/devtools/client/framework/devtools.js
@@ -454,17 +454,17 @@ DevTools.prototype = {
   async showToolbox(target, toolId, hostType, hostOptions, startTime) {
     let toolbox = this._toolboxes.get(target);
     if (toolbox) {
       if (hostType != null && toolbox.hostType != hostType) {
         await toolbox.switchHost(hostType);
       }
 
       if (toolId != null && toolbox.currentToolId != toolId) {
-        await toolbox.selectTool(toolId);
+        await toolbox.selectTool(toolId, "toolbox_show");
       }
 
       toolbox.raise();
     } else {
       // As toolbox object creation is async, we have to be careful about races
       // Check for possible already in process of loading toolboxes before
       // actually trying to create a new one.
       let promise = this._creatingToolboxes.get(target);
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -9,16 +9,17 @@ const SOURCE_MAP_WORKER = "resource://de
 const MAX_ORDINAL = 99;
 const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled";
 const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight";
 const DISABLE_AUTOHIDE_PREF = "ui.popup.disable_autohide";
 const HOST_HISTOGRAM = "DEVTOOLS_TOOLBOX_HOST";
 const SCREENSIZE_HISTOGRAM = "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER";
 const CURRENT_THEME_SCALAR = "devtools.current_theme";
 const HTML_NS = "http://www.w3.org/1999/xhtml";
+const REGEX_PANEL = /webconsole|inspector|jsdebugger|styleeditor|netmonitor|storage/;
 
 var {Ci, Cc} = require("chrome");
 var promise = require("promise");
 var defer = require("devtools/shared/defer");
 var Services = require("Services");
 var ChromeUtils = require("ChromeUtils");
 var {gDevTools} = require("devtools/client/framework/devtools");
 var EventEmitter = require("devtools/shared/event-emitter");
@@ -155,16 +156,17 @@ function Toolbox(target, selectedTool, h
   this._onPickerClick = this._onPickerClick.bind(this);
   this._onPickerKeypress = this._onPickerKeypress.bind(this);
   this._onPickerStarted = this._onPickerStarted.bind(this);
   this._onPickerStopped = this._onPickerStopped.bind(this);
   this._onInspectObject = this._onInspectObject.bind(this);
   this._onNewSelectedNodeFront = this._onNewSelectedNodeFront.bind(this);
   this.updatePickerButton = this.updatePickerButton.bind(this);
   this.selectTool = this.selectTool.bind(this);
+  this._pingTelemetrySelectTool = this._pingTelemetrySelectTool.bind(this);
   this.toggleSplitConsole = this.toggleSplitConsole.bind(this);
 
   this._target.on("close", this.destroy);
 
   if (!selectedTool) {
     selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
   }
   this._defaultToolId = selectedTool;
@@ -531,17 +533,17 @@ Toolbox.prototype = {
       // react modules and freeze the event loop for a significant time.
       // requestIdleCallback allows releasing it to allow user events to be processed.
       // Use 16ms maximum delay to allow one frame to be rendered at 60FPS
       // (1000ms/60FPS=16ms)
       this.win.requestIdleCallback(() => {
         this.component.setCanRender();
       }, {timeout: 16});
 
-      await this.selectTool(this._defaultToolId);
+      await this.selectTool(this._defaultToolId, "initial_panel");
 
       // Wait until the original tool is selected so that the split
       // console input will receive focus.
       let splitConsolePromise = promise.resolve();
       if (Services.prefs.getBoolPref(SPLITCONSOLE_ENABLED_PREF)) {
         splitConsolePromise = this.openSplitConsole();
         this._telemetry.addEventProperty(
           "devtools.main", "open", "tools", null, "splitconsole", true);
@@ -839,19 +841,19 @@ Toolbox.prototype = {
   },
 
   _buildOptions: function() {
     let selectOptions = event => {
       // Flip back to the last used panel if we are already
       // on the options panel.
       if (this.currentToolId === "options" &&
           gDevTools.getToolDefinition(this.lastUsedToolId)) {
-        this.selectTool(this.lastUsedToolId);
+        this.selectTool(this.lastUsedToolId, "toggle_settings_off");
       } else {
-        this.selectTool("options");
+        this.selectTool("options", "toggle_settings_on");
       }
     };
     this.shortcuts.on(L10N.getStr("toolbox.help.key"), selectOptions);
   },
 
   _splitConsoleOnKeypress: function(e) {
     if (e.keyCode === KeyCodes.DOM_VK_ESCAPE) {
       this.toggleSplitConsole();
@@ -1011,17 +1013,17 @@ Toolbox.prototype = {
       } else {
         key.setAttribute("key", shortcut);
       }
 
       key.setAttribute("modifiers", modifiers);
       // needed. See bug 371900
       key.setAttribute("oncommand", "void(0);");
       key.addEventListener("command", () => {
-        this.selectTool(toolId).then(() => this.fireCustomKey(toolId));
+        this.selectTool(toolId, "key_shortcut").then(() => this.fireCustomKey(toolId));
       }, true);
       doc.getElementById("toolbox-keyset").appendChild(key);
     }
 
     // Add key for toggling the browser console from the detached window
     if (!doc.getElementById("key_browserconsole")) {
       let key = doc.createElement("key");
       key.id = "key_browserconsole";
@@ -1813,18 +1815,20 @@ Toolbox.prototype = {
     }
   },
 
   /**
    * Switch to the tool with the given id
    *
    * @param {string} id
    *        The id of the tool to switch to
+   * @param {string} reason
+   *        Reason the tool was opened
    */
-  selectTool: function(id) {
+  selectTool: function(id, reason = "unknown") {
     if (this.currentToolId == id) {
       let panel = this._toolPanels.get(id);
       if (panel) {
         // We have a panel instance, so the tool is already fully loaded.
 
         // re-focus tool to get key events again
         this.focusTool(id);
 
@@ -1842,17 +1846,18 @@ Toolbox.prototype = {
 
     // Check if the tool exists.
     if (this.panelDefinitions.find((definition) => definition.id === id) ||
         id === "options" ||
         this.additionalToolDefinitions.get(id)) {
       if (this.currentToolId) {
         this._telemetry.toolClosed(this.currentToolId);
       }
-      this._telemetry.toolOpened(id);
+
+      this._pingTelemetrySelectTool(id, reason);
     } else {
       throw new Error("No tool found");
     }
 
     // and select the right iframe
     let toolboxPanels = this.doc.querySelectorAll(".toolbox-panel");
     this.selectSingleNode(toolboxPanels, "toolbox-panel-" + id);
 
@@ -1868,16 +1873,50 @@ Toolbox.prototype = {
       this.focusTool(id);
 
       this.emit("select", id);
       this.emit(id + "-selected", panel);
       return panel;
     });
   },
 
+  _pingTelemetrySelectTool(id, reason) {
+    const width = Math.ceil(this.win.outerWidth / 50) * 50;
+    const panelName = this.getTelemetryPanelName(id);
+    const prevPanelName = this.getTelemetryPanelName(this.currentToolId);
+
+    this._telemetry.addEventProperties("devtools.main", "enter", panelName, null, {
+      "host": this._hostType,
+      "width": width,
+      "start_state": reason,
+      "panel_name": id,
+      "cold": !this.getPanel(id)
+    });
+
+    // On first load this.currentToolId === undefined so we need to skip sending
+    // a devtools.main.exit telemetry event.
+    if (this.currentToolId) {
+      this._telemetry.recordEvent("devtools.main", "exit", prevPanelName, null, {
+        "host": this._hostType,
+        "width": width,
+        "panel_name": prevPanelName,
+        "next_panel": id,
+        "reason": reason
+      });
+    }
+
+    const pending = ["host", "width", "start_state", "panel_name", "cold"];
+    if (id === "webconsole") {
+      pending.push("message_count");
+    }
+    this._telemetry.preparePendingEvent(
+      "devtools.main", "enter", panelName, null, pending);
+    this._telemetry.toolOpened(id);
+  },
+
   /**
    * Focus a tool's panel by id
    * @param  {string} id
    *         The id of tool to focus
    */
   focusTool: function(id, state = true) {
     let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
 
@@ -1933,16 +1972,20 @@ Toolbox.prototype = {
     // Ensure split console is visible if console was already loaded in background
     let iframe = this.webconsolePanel.querySelector(".toolbox-panel-iframe");
     if (iframe) {
       this.setIframeVisible(iframe, true);
     }
 
     return this.loadTool("webconsole").then(() => {
       this.component.setIsSplitConsoleActive(true);
+      this._telemetry.recordEvent("devtools.main", "activate", "split_console", null, {
+        "host": this._getTelemetryHostString(),
+        "width": Math.ceil(this.win.outerWidth / 50) * 50
+      });
       this.emit("split-console");
       this.focusConsoleInput();
     });
   },
 
   /**
    * Closes the split console.
    *
@@ -1990,29 +2033,29 @@ Toolbox.prototype = {
    * Loads the tool next to the currently selected tool.
    */
   selectNextTool: function() {
     let definitions = this.component.panelDefinitions;
     const index = definitions.findIndex(({id}) => id === this.currentToolId);
     let definition = index === -1 || index >= definitions.length - 1
                      ? definitions[0]
                      : definitions[index + 1];
-    return this.selectTool(definition.id);
+    return this.selectTool(definition.id, "select_next_key");
   },
 
   /**
    * Loads the tool just left to the currently selected tool.
    */
   selectPreviousTool: function() {
     let definitions = this.component.panelDefinitions;
     const index = definitions.findIndex(({id}) => id === this.currentToolId);
     let definition = index === -1 || index < 1
                      ? definitions[definitions.length - 1]
                      : definitions[index - 1];
-    return this.selectTool(definition.id);
+    return this.selectTool(definition.id, "select_prev_key");
   },
 
   /**
    * Highlights the tool's tab if it is not the currently selected tool.
    *
    * @param {string} id
    *        The id of the tool to highlight
    */
@@ -2465,17 +2508,17 @@ Toolbox.prototype = {
 
       if (nextTool) {
         toolNameToSelect = nextTool.id;
       }
       if (previousTool) {
         toolNameToSelect = previousTool.id;
       }
       if (toolNameToSelect) {
-        this.selectTool(toolNameToSelect);
+        this.selectTool(toolNameToSelect, "tool_unloaded");
       }
     }
 
     // Remove this tool from the current panel definitions.
     this.panelDefinitions = this.panelDefinitions.filter(({id}) => id !== toolId);
     this.visibleAdditionalTools = this.visibleAdditionalTools
                                       .filter(id => id !== toolId);
     this._combineAndSortPanelDefinitions();
@@ -2576,17 +2619,17 @@ Toolbox.prototype = {
     if (objectActor.preview &&
         objectActor.preview.nodeType === domNodeConstants.ELEMENT_NODE) {
       // Open the inspector and select the DOM Element.
       await this.loadTool("inspector");
       const inspector = this.getPanel("inspector");
       const nodeFound = await inspector.inspectNodeActor(objectActor.actor,
                                                          inspectFromAnnotation);
       if (nodeFound) {
-        await this.selectTool("inspector");
+        await this.selectTool("inspector", "inspect_dom");
       }
     } else if (objectActor.type !== "null" &&
                objectActor.type !== "undefined") {
       // Open then split console and inspect the object in the variables view,
       // when the objectActor doesn't represent an undefined or null value.
       await this.openSplitConsole();
       const panel = this.getPanel("webconsole");
       const jsterm = panel.hud.jsterm;
@@ -2773,22 +2816,32 @@ Toolbox.prototype = {
     this.toolbarButtons.forEach(button => {
       if (typeof button.teardown == "function") {
         // teardown arguments have already been bound in _createButtonState
         button.teardown();
       }
     });
 
     // We need to grab a reference to win before this._host is destroyed.
-    let win = this.win;
+    const win = this.win;
+    const host = this._getTelemetryHostString();
+    const width = Math.ceil(win.outerWidth / 50) * 50;
+    const prevPanelName = this.getTelemetryPanelName(this.currentToolId);
 
     this._telemetry.toolClosed("toolbox");
     this._telemetry.recordEvent("devtools.main", "close", "tools", null, {
-      host: this._getTelemetryHostString(),
-      width: Math.ceil(win.outerWidth / 50) * 50
+      host: host,
+      width: width
+    });
+    this._telemetry.recordEvent("devtools.main", "exit", prevPanelName, null, {
+      "host": host,
+      "width": width,
+      "panel_name": this.currentToolId,
+      "next_panel": "none",
+      "reason": "toolbox_close"
     });
     this._telemetry.destroy();
 
     // Finish all outstanding tasks (which means finish destroying panels and
     // then destroying the host, successfully or not) before destroying the
     // target.
     deferred.resolve(settleAll(outstanding)
         .catch(console.error)
@@ -3189,9 +3242,16 @@ Toolbox.prototype = {
    * as active for the toolbox and has its related devtools about:config preference set
    * to true.
    * @see browser/components/extensions/ext-devtools.js
    */
   isWebExtensionEnabled: function(extensionUUID) {
     let extInfo = this._webExtensions.get(extensionUUID);
     return extInfo && Services.prefs.getBoolPref(extInfo.pref, false);
   },
+
+  getTelemetryPanelName: function(id) {
+    if (!REGEX_PANEL.test(id)) {
+      return "other";
+    }
+    return id;
+  }
 };
--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -465,23 +465,23 @@ certmgr.certdetail.sha1fingerprint=SHA1 
 # LOCALIZATION NOTE (certmgr.certificateTransparency.label):
 # This string is used as a label in the security tab.
 certmgr.certificateTransparency.label=Transparency:
 
 # LOCALIZATION NOTE (certmgr.certificateTransparency.status.none):
 # This string is used to indicate that there are no signed certificate
 # timestamps available. This is a property for the 'Transparency'
 # field in the security tab.
-certmgr.certificateTransparency.status.none=No SCTs records
+certmgr.certificateTransparency.status.none=No SCT records
 
 # LOCALIZATION NOTE (certmgr.certificateTransparency.status.ok):
 # This string is used to indicate that there are valid signed certificate
 # timestamps. This is a property for the 'Transparency'
 # field in the security tab.
-certmgr.certificateTransparency.status.ok=Valid SCTs records
+certmgr.certificateTransparency.status.ok=Valid SCT records
 
 # LOCALIZATION NOTE (certmgr.certificateTransparency.status.notEnoughSCTS):
 # This string is used to indicate that there are not enough valid signed
 # certificate timestamps. This is a property for the 'Transparency'
 # field in the security tab.
 certmgr.certificateTransparency.status.notEnoughSCTS=Not enough SCTs
 
 # LOCALIZATION NOTE (certmgr.certificateTransparency.status.notDiverseSCTS):
@@ -798,17 +798,17 @@ netmonitor.timings.wait=Waiting:
 netmonitor.timings.receive=Receiving:
 
 # LOCALIZATION NOTE (netmonitor.security.warning.cipher): A tooltip
 # for warning icon that indicates a connection uses insecure cipher suite.
 netmonitor.security.warning.cipher=The cipher used for encryption is deprecated and insecure.
 
 # LOCALIZATION NOTE (netmonitor.security.error): This is the label displayed
 # in the security tab if a security error prevented the connection.
-netmonitor.security.error=An error occured:
+netmonitor.security.error=An error occurred:
 
 # LOCALIZATION NOTE (netmonitor.security.protocolVersion): This is the label displayed
 # in the security tab describing TLS/SSL protocol version.
 netmonitor.security.protocolVersion=Protocol version:
 
 # LOCALIZATION NOTE (netmonitor.security.cipherSuite): This is the label displayed
 # in the security tab describing the cipher suite used to secure this connection.
 netmonitor.security.cipherSuite=Cipher suite:
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -18,29 +18,32 @@ loader.lazyRequireGetter(this, "swapToIn
 loader.lazyRequireGetter(this, "startup", "devtools/client/responsive.html/utils/window", true);
 loader.lazyRequireGetter(this, "message", "devtools/client/responsive.html/utils/message");
 loader.lazyRequireGetter(this, "showNotification", "devtools/client/responsive.html/utils/notification", true);
 loader.lazyRequireGetter(this, "l10n", "devtools/client/responsive.html/utils/l10n");
 loader.lazyRequireGetter(this, "EmulationFront", "devtools/shared/fronts/emulation", true);
 loader.lazyRequireGetter(this, "PriorityLevels", "devtools/client/shared/components/NotificationBox", true);
 loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
 loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
 
 const RELOAD_CONDITION_PREF_PREFIX = "devtools.responsive.reloadConditions.";
 const RELOAD_NOTIFICATION_PREF = "devtools.responsive.reloadNotification.enabled";
 
 function debug(msg) {
   // console.log(`RDM manager: ${msg}`);
 }
 
 /**
  * ResponsiveUIManager is the external API for the browser UI, etc. to use when
  * opening and closing the responsive UI.
  */
 const ResponsiveUIManager = exports.ResponsiveUIManager = {
+  _telemetry: new Telemetry(),
+
   activeTabs: new Map(),
 
   /**
    * Toggle the responsive UI for a tab.
    *
    * @param window
    *        The main browser chrome window.
    * @param tab
@@ -89,21 +92,29 @@ const ResponsiveUIManager = exports.Resp
     if (tab.linkedBrowser.hasAttribute("usercontextid")) {
       this.showNoContainerTabsNotification(window, tab, options);
       return promise.reject(new Error("RDM not available for container tabs."));
     }
     if (!this.isActiveForTab(tab)) {
       this.initMenuCheckListenerFor(window);
 
       // Track whether a toolbox was opened before RDM was opened.
-      let hasToolbox = !!gDevTools.getToolbox(TargetFactory.forTab(tab));
+      const toolbox = gDevTools.getToolbox(TargetFactory.forTab(tab));
+      const hostType = toolbox ? toolbox.hostType : "none";
+      const hasToolbox = !!toolbox;
       if (hasToolbox) {
         Services.telemetry.scalarAdd("devtools.responsive.toolbox_opened_first", 1);
       }
 
+      const t = this._telemetry;
+      t.recordEvent("devtools.main", "activate", "responsive_design", null, {
+        "host": hostType,
+        "width": Math.ceil(window.outerWidth / 50) * 50
+      });
+
       // Track opens keyed by the UI entry point used.
       let { trigger } = options;
       if (!trigger) {
         trigger = "unknown";
       }
       Services.telemetry.keyedScalarAdd("devtools.responsive.open_trigger", trigger, 1);
 
       let ui = new ResponsiveUI(window, tab);
--- a/devtools/client/shared/telemetry.js
+++ b/devtools/client/shared/telemetry.js
@@ -552,16 +552,43 @@ class Telemetry {
       // The property was not expected, warn and bail.
       throw new Error(`An attempt was made to add the unexpected property ` +
                       `"${pendingPropName}" to a telemetry event with the ` +
                       `signature "${sig}"\n`);
     }
   }
 
   /**
+   * Adds expected properties for either a current or future pending event.
+   * This means that if preparePendingEvent() is called before or after sending
+   * the event properties they will automatically added to the event.
+   *
+   * @param {String} category
+   *        The telemetry event category (a group name for events and helps to
+   *        avoid name conflicts) e.g. "devtools.main"
+   * @param {String} method
+   *        The telemetry event method (describes the type of event that
+   *        occurred e.g. "open")
+   * @param {String} object
+   *        The telemetry event object name (the name of the object the event
+   *        occurred on) e.g. "tools" or "setting"
+   * @param {String|null} value
+   *        The telemetry event value (a user defined value, providing context
+   *        for the event) e.g. "console"
+   * @param {String} pendingObject
+   *        An object containing key, value pairs that should be added to the
+   *        event as properties.
+   */
+  addEventProperties(category, method, object, value, pendingObject) {
+    for (let [key, val] of Object.entries(pendingObject)) {
+      this.addEventProperty(category, method, object, value, key, val);
+    }
+  }
+
+  /**
    * Send a telemetry event.
    *
    * @param {String} category
    *        The telemetry event category (a group name for events and helps to
    *        avoid name conflicts) e.g. "devtools.main"
    * @param {String} method
    *        The telemetry event method (describes the type of event that
    *        occurred e.g. "open")
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -1340,17 +1340,18 @@ class JSTerm extends Component {
       return null;
     }
 
     return [
       dom.div({id: "webconsole-notificationbox", key: "notification"}),
       dom.div({
         className: "jsterm-input-container",
         key: "jsterm-container",
-        style: {direction: "ltr"}
+        style: {direction: "ltr"},
+        "aria-live": "off",
       },
         dom.textarea({
           className: "jsterm-complete-node devtools-monospace",
           key: "complete",
           tabIndex: "-1",
           ref: node => {
             this.completeNode = node;
           },
--- a/devtools/client/webconsole/test/mochitest/browser_jsterm_accessibility.js
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_accessibility.js
@@ -11,16 +11,19 @@
 const TEST_URI = "data:text/html;charset=utf8,<p>test code completion";
 
 add_task(async function() {
   let hud = await openNewTabAndConsole(TEST_URI);
 
   let jsterm = hud.jsterm;
   let input = jsterm.inputNode;
 
+  info("Test that the console input is not treated as a live region");
+  ok(!isElementInLiveRegion(input), "Console input is not treated as a live region");
+
   info("Type 'd' to open the autocomplete popup");
   await autocomplete(jsterm, "d");
 
   // Add listeners for focus and blur events.
   let wasBlurred = false;
   input.addEventListener("blur", () => {
     wasBlurred = true;
   }, {
@@ -51,8 +54,20 @@ async function autocomplete(jsterm, valu
   await new Promise(resolve => {
     jsterm.setInputValue(value);
     jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
   });
 
   ok(popup.isOpen && popup.itemCount > 0,
     "Autocomplete popup is open and contains suggestions");
 }
+
+function isElementInLiveRegion(element) {
+  if (!element) {
+    return false;
+  }
+
+  if (element.hasAttribute("aria-live")) {
+    return element.getAttribute("aria-live") !== "off";
+  }
+
+  return isElementInLiveRegion(element.parentNode);
+}
--- a/devtools/docs/frontend/telemetry.md
+++ b/devtools/docs/frontend/telemetry.md
@@ -274,16 +274,25 @@ this._telemetry.preparePendingEvent("dev
 this._telemetry.addEventProperty(
   "devtools.main", "open", "tools", null, "first_panel", "inspector");
 this._telemetry.addEventProperty(
   "devtools.main", "open", "tools", null, "host", "bottom");
 this._telemetry.addEventProperty(
   "devtools.main", "open", "tools", null, "splitconsole", false);
 this._telemetry.addEventProperty(
   "devtools.main", "open", "tools", null, "width", 1024);
+
+// You can also add properties in batches using e.g.:
+this._telemetry.addEventProperties("devtools.main", "open", "tools", null, {
+  "first_panel": "inspector",
+  "host": "bottom",
+  "splitconsole": false,
+  "width": 1024
+});
+
 ```
 
 Notes:
 
 - `mytoolname` is the id we declared in the `Scalars.yaml` module.
 - Because we are not logging tool's time opened in `Scalars.yaml` we don't care
   about toolClosed. Of course, if there was an accompanying `timerHistogram`
   field defined in `telemetry.js` and `histograms.json` then `toolClosed` should
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -25,16 +25,17 @@ loader.lazyRequireGetter(this, "StackTra
 loader.lazyRequireGetter(this, "JSPropertyProvider", "devtools/shared/webconsole/js-property-provider", true);
 loader.lazyRequireGetter(this, "Parser", "resource://devtools/shared/Parser.jsm", true);
 loader.lazyRequireGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm", true);
 loader.lazyRequireGetter(this, "addWebConsoleCommands", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "CONSOLE_WORKER_IDS", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "WebConsoleUtils", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "EnvironmentActor", "devtools/server/actors/environment", true);
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
 
 // Overwrite implemented listeners for workers so that we don't attempt
 // to load an unsupported module.
 if (isWorker) {
   loader.lazyRequireGetter(this, "ConsoleAPIListener", "devtools/server/actors/webconsole/worker-listeners", true);
   loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/server/actors/webconsole/worker-listeners", true);
 } else {
   loader.lazyRequireGetter(this, "ConsoleAPIListener", "devtools/server/actors/webconsole/listeners", true);
@@ -70,16 +71,18 @@ function WebConsoleActor(connection, par
   this.dbg = this.parentActor.makeDebugger();
 
   this._netEvents = new Map();
   this._networkEventActorsByURL = new Map();
   this._gripDepth = 0;
   this._listeners = new Set();
   this._lastConsoleInputEvaluation = undefined;
 
+  this._telemetry = new Telemetry();
+
   this.objectGrip = this.objectGrip.bind(this);
   this._onWillNavigate = this._onWillNavigate.bind(this);
   this._onChangedToplevelDocument = this._onChangedToplevelDocument.bind(this);
   EventEmitter.on(this.parentActor, "changed-toplevel-document",
             this._onChangedToplevelDocument);
   this._onObserverNotification = this._onObserverNotification.bind(this);
   if (this.parentActor.isRootActor) {
     Services.obs.addObserver(this._onObserverNotification,
@@ -857,16 +860,19 @@ WebConsoleActor.prototype =
             }
             messages.push(message);
           });
           break;
         }
       }
     }
 
+    this._telemetry.addEventProperty(
+      "devtools.main", "enter", "webconsole", null, "message_count", messages.length);
+
     return {
       from: this.actorID,
       messages: messages,
     };
   },
 
   /**
    * Handler for the "evaluateJSAsync" request. This method evaluates the given
--- a/devtools/shared/locales/en-US/gcli.properties
+++ b/devtools/shared/locales/en-US/gcli.properties
@@ -12,17 +12,17 @@
 # documentation on web development on the web.
 
 # For each command there are in general two strings. As an example consider
 # the 'pref' command.
 # commandDesc (e.g. prefDesc for the command 'pref'): this string contains a
 # very short description of the command. It's designed to be shown in a menu
 # alongside the command name, which is why it should be as short as possible.
 # commandManual (e.g. prefManual for the command 'pref'): this string will
-# contain a fuller description of the command. It's diplayed when the user
+# contain a fuller description of the command. It's displayed when the user
 # asks for help about a specific command (e.g. 'help pref').
 
 # LOCALIZATION NOTE: This message is used to describe any command or command
 # parameter when no description has been provided.
 canonDescNone=(No description)
 
 # LOCALIZATION NOTE: The default name for a group of parameters.
 canonDefaultGroupName=Options
--- a/dom/animation/KeyframeEffectReadOnly.cpp
+++ b/dom/animation/KeyframeEffectReadOnly.cpp
@@ -1581,29 +1581,40 @@ KeyframeEffectReadOnly::MarkCascadeNeeds
   EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
                                                  mTarget->mPseudoType);
   if (!effectSet) {
     return;
   }
   effectSet->MarkCascadeNeedsUpdate();
 }
 
-bool
-KeyframeEffectReadOnly::HasComputedTimingChanged() const
+/* static */ bool
+KeyframeEffectReadOnly::HasComputedTimingChanged(
+  const ComputedTiming& aComputedTiming,
+  IterationCompositeOperation aIterationComposite,
+  const Nullable<double>& aProgressOnLastCompose,
+  uint64_t aCurrentIterationOnLastCompose)
 {
   // Typically we don't need to request a restyle if the progress hasn't
   // changed since the last call to ComposeStyle. The one exception is if the
   // iteration composite mode is 'accumulate' and the current iteration has
   // changed, since that will often produce a different result.
+  return aComputedTiming.mProgress != aProgressOnLastCompose ||
+         (aIterationComposite == IterationCompositeOperation::Accumulate &&
+          aComputedTiming.mCurrentIteration != aCurrentIterationOnLastCompose);
+}
+
+bool
+KeyframeEffectReadOnly::HasComputedTimingChanged() const
+{
   ComputedTiming computedTiming = GetComputedTiming();
-  return computedTiming.mProgress != mProgressOnLastCompose ||
-         (mEffectOptions.mIterationComposite ==
-            IterationCompositeOperation::Accumulate &&
-         computedTiming.mCurrentIteration !=
-          mCurrentIterationOnLastCompose);
+  return HasComputedTimingChanged(computedTiming,
+                                  mEffectOptions.mIterationComposite,
+                                  mProgressOnLastCompose,
+                                  mCurrentIterationOnLastCompose);
 }
 
 bool
 KeyframeEffectReadOnly::ContainsAnimatedScale(const nsIFrame* aFrame) const
 {
   if (!IsCurrent()) {
     return false;
   }
--- a/dom/animation/KeyframeEffectReadOnly.h
+++ b/dom/animation/KeyframeEffectReadOnly.h
@@ -261,16 +261,22 @@ public:
     // We cannot use getters_AddRefs on RawServoAnimationValue because it is
     // an incomplete type, so Get() doesn't work. Instead, use GetWeak, and
     // then assign the raw pointer to a RefPtr.
     result.mServo = mBaseStyleValuesForServo.GetWeak(aProperty, &hasProperty);
     MOZ_ASSERT(hasProperty || result.IsNull());
     return result;
   }
 
+  static bool HasComputedTimingChanged(
+    const ComputedTiming& aComputedTiming,
+    IterationCompositeOperation aIterationComposite,
+    const Nullable<double>& aProgressOnLastCompose,
+    uint64_t aCurrentIterationOnLastCompose);
+
 protected:
   KeyframeEffectReadOnly(nsIDocument* aDocument,
                          const Maybe<OwningAnimationTarget>& aTarget,
                          AnimationEffectTimingReadOnly* aTiming,
                          const KeyframeEffectParams& aOptions);
 
   ~KeyframeEffectReadOnly() override = default;
 
--- a/dom/chrome-webidl/ChromeUtils.webidl
+++ b/dom/chrome-webidl/ChromeUtils.webidl
@@ -118,20 +118,16 @@ namespace ChromeUtils {
   readonly attribute any recentJSDevError;
 
   /**
    * Reset `recentJSDevError` to `undefined` for the current JSRuntime.
    */
   void clearRecentJSDevError();
 #endif // NIGHTLY_BUILD
 
-#ifndef RELEASE_OR_BETA
-  void requestPerformanceMetrics();
-#endif
-
   /**
    * IF YOU ADD NEW METHODS HERE, MAKE SURE THEY ARE THREAD-SAFE.
    */
 };
 
 /**
  * Additional ChromeUtils methods that are _not_ thread-safe, and hence not
  * exposed in workers.
@@ -309,16 +305,23 @@ partial namespace ChromeUtils {
   /**
    * Creates a JS Error object with the given message and stack.
    *
    * If a stack object is provided, the error object is created in the global
    * that it belongs to.
    */
   [Throws]
   object createError(DOMString message, optional object? stack = null);
+
+#ifndef RELEASE_OR_BETA
+  /**
+   * Request performance metrics to the current process & all ontent processes.
+   */
+  void requestPerformanceMetrics();
+#endif
 };
 
 /**
  * Used by principals and the script security manager to represent origin
  * attributes. The first dictionary is designed to contain the full set of
  * OriginAttributes, the second is used for pattern-matching (i.e. does this
  * OriginAttributesDictionary match the non-empty attributes in this pattern).
  *
--- a/dom/events/EventStateManager.cpp
+++ b/dom/events/EventStateManager.cpp
@@ -638,33 +638,33 @@ EventStateManager::PreHandleEvent(nsPres
       mMClickCount = mouseEvent->mClickCount;
       SetClickCount(mouseEvent, aStatus);
       break;
     case WidgetMouseEvent::eRightButton:
       mRClickCount = mouseEvent->mClickCount;
       SetClickCount(mouseEvent, aStatus);
       break;
     }
+    NotifyTargetUserActivation(aEvent, aTargetContent);
     break;
   }
   case eMouseUp: {
     switch (mouseEvent->button) {
       case WidgetMouseEvent::eLeftButton:
         if (Prefs::ClickHoldContextMenu()) {
           KillClickHoldTimer();
         }
         StopTrackingDragGesture();
         sNormalLMouseEventInProcess = false;
         // then fall through...
         MOZ_FALLTHROUGH;
       case WidgetMouseEvent::eRightButton:
       case WidgetMouseEvent::eMiddleButton:
         RefPtr<EventStateManager> esm = ESMFromContentOrThis(aOverrideClickTarget);
         esm->SetClickCount(mouseEvent, aStatus, aOverrideClickTarget);
-        NotifyTargetUserActivation(aEvent, aTargetContent);
         break;
     }
     break;
   }
   case eMouseEnterIntoWidget:
     // In some cases on e10s eMouseEnterIntoWidget
     // event was sent twice into child process of content.
     // (From specific widget code (sending is not permanent) and
@@ -709,16 +709,23 @@ EventStateManager::PreHandleEvent(nsPres
       aEvent->mMessage = eVoidEvent;
       break;
     }
     MOZ_FALLTHROUGH;
   case eMouseMove:
   case ePointerDown:
     if (aEvent->mMessage == ePointerDown) {
       PointerEventHandler::ImplicitlyCapturePointer(aTargetFrame, aEvent);
+#ifndef MOZ_WIDGET_ANDROID
+      // Pointer events aren't enabled on Android yet, but when they
+      // are enabled, we should not activate on pointerdown, as that
+      // fires for touches that turn into moves on Android, and we don't
+      // want to gesture activate for scroll actions.
+      NotifyTargetUserActivation(aEvent, aTargetContent);
+#endif
     }
     MOZ_FALLTHROUGH;
   case ePointerMove: {
     // on the Mac, GenerateDragGesture() may not return until the drag
     // has completed and so |aTargetFrame| may have been deleted (moving
     // a bookmark, for example).  If this is the case, however, we know
     // that ClearFrameRefs() has been called and it cleared out
     // |mCurrentTarget|. As a result, we should pass |mCurrentTarget|
@@ -789,16 +796,20 @@ EventStateManager::PreHandleEvent(nsPres
             *aStatus = nsEventStatus_eConsumeNoDefault;
           }
         }
       }
     }
     // then fall through...
     MOZ_FALLTHROUGH;
   case eKeyDown:
+    if (aEvent->mMessage == eKeyDown) {
+      NotifyTargetUserActivation(aEvent, aTargetContent);
+    }
+    MOZ_FALLTHROUGH;
   case eKeyUp:
     {
       nsIContent* content = GetFocusedContent();
       if (content)
         mCurrentTargetContent = content;
 
       // NOTE: Don't refer TextComposition::IsComposing() since UI Events
       //       defines that KeyboardEvent.isComposing is true when it's
@@ -818,19 +829,16 @@ EventStateManager::PreHandleEvent(nsPres
       // to reset it if the target (focused content) isn't in a remote process
       // because PresShell needs to check if it's marked as so before
       // dispatching events into the DOM tree.
       if (aEvent->IsWaitingReplyFromRemoteProcess() &&
           !aEvent->PropagationStopped() &&
           !IsRemoteTarget(content)) {
         aEvent->ResetWaitingReplyFromRemoteProcessState();
       }
-      if (aEvent->mMessage == eKeyUp) {
-        NotifyTargetUserActivation(aEvent, aTargetContent);
-      }
     }
     break;
   case eWheel:
   case eWheelOperationStart:
   case eWheelOperationEnd:
     {
       NS_ASSERTION(aEvent->IsTrusted(),
                    "Untrusted wheel event shouldn't be here");
@@ -916,18 +924,19 @@ EventStateManager::NotifyTargetUserActiv
     return;
   }
 
   nsIDocument* doc = node->OwnerDoc();
   if (!doc || doc->HasBeenUserActivated()) {
     return;
   }
 
-  MOZ_ASSERT(aEvent->mMessage == eKeyUp   ||
-             aEvent->mMessage == eMouseUp ||
+  MOZ_ASSERT(aEvent->mMessage == eKeyDown   ||
+             aEvent->mMessage == eMouseDown ||
+             aEvent->mMessage == ePointerDown ||
              aEvent->mMessage == eTouchEnd);
   doc->NotifyUserActivation();
 }
 
 already_AddRefed<EventStateManager>
 EventStateManager::ESMFromContentOrThis(nsIContent* aContent)
 {
   if (aContent) {
new file mode 100644
--- /dev/null
+++ b/dom/media/test/file_autoplay_policy_eventdown_activation.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+  <title>Autoplay policy window</title>
+  <style>
+    video {
+      width: 50%;
+      height: 50%;
+    }
+  </style>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="manifest.js"></script>
+  <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+</head>
+
+<body>
+  <pre id="test">
+      <script>
+
+        window.ok = window.opener.ok;
+        window.is = window.opener.is;
+        window.info = window.opener.info;
+
+        async function testEventDownActivates(eventNames, activator) {
+          let element = document.createElement("video");
+          element.preload = "auto";
+          element.src = "short.mp4";
+          document.body.appendChild(element);
+
+          await once(element, "loadedmetadata");
+
+          let played = await element.play().then(() => true, () => false);
+          ok(!played, "Document should start out not activated, with playback blocked.");
+
+          let x = eventNames.map(
+            (eventName) => {
+              return new Promise(async function (resolve, reject) {
+                window.addEventListener(eventName, async function (event) {
+                  let played = await element.play().then(() => true, () => false);
+                  ok(played, "Expect to be activated already in " + eventName);
+                  resolve();
+                }, false);
+              });
+            });
+
+          activator();
+
+          await Promise.all(x);
+
+          removeNodeAndSource(element);
+        }
+
+        nextWindowMessage().then(
+          async (event) => {
+            try {
+              if (event.data == "run keydown test") {
+                await testEventDownActivates(["keydown", "keypress", "keyup"], () => {
+                  document.body.focus();
+                  synthesizeKey(" ");
+                });
+              } else if (event.data == "run mousedown test") {
+                let events = ["mousedown", "mouseup", "click"];
+                if (getAndroidVersion() < 0) {
+                  // Non-Android, also listen on pointer events.
+                  events.push("pointerdown", "pointerup");
+                }
+                await testEventDownActivates(events, () => {
+                  synthesizeMouseAtCenter(document.body, {});
+                });
+              } else {
+                ok(false, "unexpected message");
+              }
+            } catch (e) {
+              ok(false, "Caught exception " + e + " " + e.message + " " + e.stackTrace);
+            }
+            event.source.postMessage("done", "*");
+          });
+
+      </script>
+    </pre>
+</body>
+
+</html>
--- a/dom/media/test/mochitest.ini
+++ b/dom/media/test/mochitest.ini
@@ -432,16 +432,17 @@ support-files =
   detodos-recorder-test.opus^headers^
   detodos-short.opus
   detodos-short.opus^headers^
   dirac.ogg
   dirac.ogg^headers^
   dynamic_resource.sjs
   eme.js
   file_access_controls.html
+  file_autoplay_policy_eventdown_activation.html
   file_autoplay_policy_unmute_pauses.html
   file_autoplay_policy_activation_window.html
   file_autoplay_policy_activation_frame.html
   file_autoplay_policy_play_before_loadedmetadata.html
   flac-s24.flac
   flac-s24.flac^headers^
   flac-noheader-s16.flac
   flac-noheader-s16.flac^headers^
@@ -689,16 +690,18 @@ skip-if = android_version == '15' # andr
 skip-if = true # bug 475110 - disabled since we don't play Wave files standalone
 [test_autoplay.html]
 [test_autoplay_contentEditable.html]
 skip-if = android_version == '15' || android_version == '17' || android_version == '22' # android(bug 1232305, bug 1232318, bug 1372457)
 [test_autoplay_policy.html]
 skip-if = android_version == '23' # bug 1424903
 [test_autoplay_policy_activation.html]
 skip-if = android_version == '23' # bug 1424903
+[test_autoplay_policy_eventdown_activation.html]
+skip-if = android_version == '23' # bug 1424903
 [test_autoplay_policy_unmute_pauses.html]
 skip-if = android_version == '23' # bug 1424903
 [test_autoplay_policy_play_before_loadedmetadata.html]
 skip-if = android_version == '23' # bug 1424903
 [test_buffered.html]
 skip-if = android_version == '15' || android_version == '22' # bug 1308388, android(bug 1232305)
 [test_bug448534.html]
 [test_bug463162.xhtml]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/test_autoplay_policy_eventdown_activation.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+  <title>Autoplay policy test</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="manifest.js"></script>
+  <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+</head>
+
+<body>
+  <pre id="test">
+      <script>
+
+        // Tests that we gesture activate on mousedown and keydown.
+
+        gTestPrefs.push(["media.autoplay.enabled", false],
+          ["media.autoplay.enabled.user-gestures-needed", true]);
+
+        SpecialPowers.pushPrefEnv({ 'set': gTestPrefs }, () => {
+          runTest();
+        });
+
+        let child_url = "file_autoplay_policy_eventdown_activation.html";
+
+        async function runTest() {
+          // Run test in a new window, to ensure its user gesture
+          // activation state isn't tainted by preceeding tests.
+          {
+            let child = window.open(child_url, "", "width=500,height=500");
+            await once(child, "load");
+            child.postMessage("run keydown test", window.origin);
+            let result = await nextWindowMessage();
+            child.close();
+          }
+
+          {
+            let child = window.open(child_url, "", "width=500,height=500");
+            await once(child, "load");
+            child.postMessage("run mousedown test", window.origin);
+            let result = await nextWindowMessage();
+            child.close();
+          }
+
+          SimpleTest.finish();
+        }
+
+        SimpleTest.waitForExplicitFinish();
+
+      </script>
+    </pre>
+</body>
+
+</html>
\ No newline at end of file
--- a/dom/performance/PerformanceMainThread.cpp
+++ b/dom/performance/PerformanceMainThread.cpp
@@ -292,16 +292,30 @@ PerformanceMainThread::EnsureDocEntry()
     if (httpChannel) {
       timing->SetPropertiesFromHttpChannel(httpChannel);
     }
 
     mDocEntry = new PerformanceNavigationTiming(Move(timing), this);
   }
 }
 
+void
+PerformanceMainThread::CreateDocumentEntry(nsITimedChannel* aChannel)
+{
+  MOZ_ASSERT(aChannel);
+  MOZ_ASSERT(!mDocEntry, "mDocEntry should be null.");
+
+  if (!nsContentUtils::IsPerformanceNavigationTimingEnabled()) {
+    return;
+  }
+
+  UniquePtr<PerformanceTimingData> timing(
+      new PerformanceTimingData(aChannel, nullptr, 0));
+  mDocEntry = new PerformanceNavigationTiming(Move(timing), this);
+}
 
 void
 PerformanceMainThread::GetEntries(nsTArray<RefPtr<PerformanceEntry>>& aRetval)
 {
   // We return an empty list when 'privacy.resistFingerprinting' is on.
   if (nsContentUtils::ShouldResistFingerprinting()) {
     aRetval.Clear();
     return;
--- a/dom/performance/PerformanceMainThread.h
+++ b/dom/performance/PerformanceMainThread.h
@@ -32,16 +32,18 @@ public:
 
   virtual PerformanceTiming* Timing() override;
 
   virtual PerformanceNavigation* Navigation() override;
 
   virtual void AddEntry(nsIHttpChannel* channel,
                         nsITimedChannel* timedChannel) override;
 
+  void CreateDocumentEntry(nsITimedChannel* aChannel) override;
+
   TimeStamp CreationTimeStamp() const override;
 
   DOMHighResTimeStamp CreationTime() const override;
 
   virtual void GetMozMemory(JSContext *aCx,
                             JS::MutableHandle<JSObject*> aObj) override;
 
   virtual nsDOMNavigationTiming* GetDOMTiming() const override
--- a/dom/performance/PerformanceResourceTiming.cpp
+++ b/dom/performance/PerformanceResourceTiming.cpp
@@ -2,16 +2,17 @@
 /* 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/. */
 
 #include "PerformanceResourceTiming.h"
 #include "mozilla/dom/PerformanceResourceTimingBinding.h"
 #include "nsNetUtil.h"
+#include "nsArrayUtils.h"
 
 using namespace mozilla::dom;
 
 NS_IMPL_CYCLE_COLLECTION_INHERITED(PerformanceResourceTiming,
                                    PerformanceEntry,
                                    mPerformance)
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PerformanceResourceTiming,
@@ -82,16 +83,37 @@ PerformanceResourceTiming::SizeOfExcludi
 {
   return PerformanceEntry::SizeOfExcludingThis(aMallocSizeOf) +
          mInitiatorType.SizeOfExcludingThisIfUnshared(aMallocSizeOf) +
          (mTimingData
             ? mTimingData->NextHopProtocol().SizeOfExcludingThisIfUnshared(aMallocSizeOf)
             : 0);
 }
 
+void
+PerformanceResourceTiming::GetServerTiming(
+                            nsTArray<RefPtr<PerformanceServerTiming>>& aRetval,
+                            Maybe<nsIPrincipal*>& aSubjectPrincipal)
+{
+  aRetval.Clear();
+  if (!TimingAllowedForCaller(aSubjectPrincipal)) {
+    return;
+  }
+
+  nsTArray<nsCOMPtr<nsIServerTiming>> serverTimingArray = mTimingData->GetServerTiming();
+  uint32_t length = serverTimingArray.Length();
+  for (uint32_t index = 0; index < length; ++index) {
+    nsCOMPtr<nsIServerTiming> serverTiming = serverTimingArray.ElementAt(index);
+    MOZ_ASSERT(serverTiming);
+
+    aRetval.AppendElement(
+      new PerformanceServerTiming(GetParentObject(), serverTiming));
+  }
+}
+
 bool
 PerformanceResourceTiming::TimingAllowedForCaller(Maybe<nsIPrincipal*>& aCaller) const
 {
   if (!mTimingData) {
     return false;
   }
 
   if (mTimingData->TimingAllowed()) {
--- a/dom/performance/PerformanceResourceTiming.h
+++ b/dom/performance/PerformanceResourceTiming.h
@@ -6,16 +6,17 @@
 
 #ifndef mozilla_dom_PerformanceResourceTiming_h___
 #define mozilla_dom_PerformanceResourceTiming_h___
 
 #include "mozilla/UniquePtr.h"
 #include "nsCOMPtr.h"
 #include "Performance.h"
 #include "PerformanceEntry.h"
+#include "PerformanceServerTiming.h"
 #include "PerformanceTiming.h"
 
 namespace mozilla {
 namespace dom {
 
 // http://www.w3.org/TR/resource-timing/#performanceresourcetiming
 class PerformanceResourceTiming : public PerformanceEntry
 {
@@ -156,16 +157,19 @@ public:
 
   uint64_t DecodedBodySize(Maybe<nsIPrincipal*>& aSubjectPrincipal) const
   {
     return TimingAllowedForCaller(aSubjectPrincipal)
         ? mTimingData->DecodedBodySize()
         : 0;
   }
 
+  void GetServerTiming(nsTArray<RefPtr<PerformanceServerTiming>>& aRetval,
+                       Maybe<nsIPrincipal*>& aSubjectPrincipal);
+
   size_t
   SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const override;
 
 protected:
   virtual ~PerformanceResourceTiming();
 
   size_t
   SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const override;
new file mode 100644
--- /dev/null
+++ b/dom/performance/PerformanceServerTiming.cpp
@@ -0,0 +1,79 @@
+/* -*- 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/. */
+
+#include "PerformanceServerTiming.h"
+
+#include "mozilla/dom/PerformanceServerTimingBinding.h"
+#include "nsITimedChannel.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PerformanceServerTiming, mParent)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PerformanceServerTiming)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PerformanceServerTiming)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PerformanceServerTiming)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+JSObject*
+PerformanceServerTiming::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+  return mozilla::dom::PerformanceServerTimingBinding::Wrap(aCx, this, aGivenProto);
+}
+
+void
+PerformanceServerTiming::GetName(nsAString& aName) const
+{
+  aName.Truncate();
+
+  if (!mServerTiming) {
+    return;
+  }
+
+  nsAutoCString name;
+  if (NS_WARN_IF(NS_FAILED(mServerTiming->GetName(name)))) {
+    return;
+  }
+
+  aName.Assign(NS_ConvertUTF8toUTF16(name));
+}
+
+DOMHighResTimeStamp
+PerformanceServerTiming::Duration() const
+{
+  if (!mServerTiming) {
+    return 0;
+  }
+
+  double duration = 0;
+  if (NS_WARN_IF(NS_FAILED(mServerTiming->GetDuration(&duration)))) {
+    return 0;
+  }
+
+  return duration;
+}
+
+void
+PerformanceServerTiming::GetDescription(nsAString& aDescription) const
+{
+  if (!mServerTiming) {
+    return;
+  }
+
+  nsAutoCString description;
+  if (NS_WARN_IF(NS_FAILED(mServerTiming->GetDescription(description)))) {
+    return;
+  }
+
+  aDescription.Assign(NS_ConvertUTF8toUTF16(description));
+}
+
+} // dom namespace
+} // mozilla namespace
new file mode 100644
--- /dev/null
+++ b/dom/performance/PerformanceServerTiming.h
@@ -0,0 +1,58 @@
+/* -*- 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 mozilla_dom_PerformanceServerTiming_h
+#define mozilla_dom_PerformanceServerTiming_h
+
+#include "mozilla/Attributes.h"
+#include "nsWrapperCache.h"
+#include "nsString.h"
+
+class nsIServerTiming;
+class nsISupports;
+
+namespace mozilla {
+namespace dom {
+
+class PerformanceServerTiming final : public nsISupports,
+                                      public nsWrapperCache
+{
+public:
+  PerformanceServerTiming(nsISupports* aParent, nsIServerTiming* aServerTiming)
+    : mParent(aParent)
+    , mServerTiming(aServerTiming)
+  {
+    MOZ_ASSERT(mServerTiming);
+  }
+
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(PerformanceServerTiming)
+
+  JSObject*
+  WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+  nsISupports* GetParentObject() const
+  {
+    return mParent;
+  }
+
+  void GetName(nsAString& aName) const;
+
+  DOMHighResTimeStamp Duration() const;
+
+  void GetDescription(nsAString& aDescription) const;
+
+private:
+  ~PerformanceServerTiming() = default;
+
+  nsCOMPtr<nsISupports> mParent;
+  nsCOMPtr<nsIServerTiming> mServerTiming;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PerformanceServerTiming_h
--- a/dom/performance/PerformanceStorage.h
+++ b/dom/performance/PerformanceStorage.h
@@ -20,16 +20,18 @@ class PerformanceTimingData;
 class PerformanceStorage
 {
 public:
   NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
 
   virtual void AddEntry(nsIHttpChannel* aChannel,
                         nsITimedChannel* aTimedChannel) = 0;
 
+  virtual void CreateDocumentEntry(nsITimedChannel* aChannel) = 0;
+
 protected:
   virtual ~PerformanceStorage() {}
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_PerformanceStorage_h
--- a/dom/performance/PerformanceStorageWorker.h
+++ b/dom/performance/PerformanceStorageWorker.h
@@ -25,16 +25,21 @@ public:
   static already_AddRefed<PerformanceStorageWorker>
   Create(WorkerPrivate* aWorkerPrivate);
 
   void ShutdownOnWorker();
 
   void AddEntry(nsIHttpChannel* aChannel,
                 nsITimedChannel* aTimedChannel) override;
 
+  void CreateDocumentEntry(nsITimedChannel* aChannel) override
+  {
+    MOZ_CRASH("This should not be called on workers.");
+  }
+
   void AddEntryOnWorker(UniquePtr<PerformanceProxyData>&& aData);
 
 private:
   PerformanceStorageWorker();
   ~PerformanceStorageWorker();
 
   Mutex mMutex;
 
--- a/dom/performance/PerformanceTiming.cpp
+++ b/dom/performance/PerformanceTiming.cpp
@@ -157,16 +157,18 @@ PerformanceTimingData::PerformanceTiming
     aChannel->GetCacheReadEnd(&mCacheReadEnd);
 
     aChannel->GetDispatchFetchEventStart(&mWorkerStart);
     aChannel->GetHandleFetchEventStart(&mWorkerRequestStart);
     // TODO: Track when FetchEvent.respondWith() promise resolves as
     //       ServiceWorker interception responseStart?
     aChannel->GetHandleFetchEventEnd(&mWorkerResponseEnd);
 
+    aChannel->GetNativeServerTiming(mServerTiming);
+
     // The performance timing api essentially requires that the event timestamps
     // have a strict relation with each other. The truth, however, is the
     // browser engages in a number of speculative activities that sometimes mean
     // connections and lookups begin at different times. Workaround that here by
     // clamping these values to what we expect FetchStart to be.  This means the
     // later of AsyncOpen or WorkerStart times.
     if (!mAsyncOpen.IsNull()) {
       // We want to clamp to the expected FetchStart value.  This is later of
@@ -666,10 +668,22 @@ PerformanceTiming::IsTopLevelContentDocu
   nsCOMPtr<nsIDocShellTreeItem> rootItem;
   Unused << docShell->GetSameTypeRootTreeItem(getter_AddRefs(rootItem));
   if (rootItem.get() != static_cast<nsIDocShellTreeItem*>(docShell.get())) {
     return false;
   }
   return rootItem->ItemType() == nsIDocShellTreeItem::typeContent;
 }
 
+nsTArray<nsCOMPtr<nsIServerTiming>>
+PerformanceTimingData::GetServerTiming()
+{
+  if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() ||
+      !TimingAllowed() ||
+      nsContentUtils::ShouldResistFingerprinting()) {
+    return nsTArray<nsCOMPtr<nsIServerTiming>>();
+  }
+
+  return nsTArray<nsCOMPtr<nsIServerTiming>>(mServerTiming);
+}
+
 } // dom namespace
 } // mozilla namespace
--- a/dom/performance/PerformanceTiming.h
+++ b/dom/performance/PerformanceTiming.h
@@ -8,16 +8,17 @@
 #define mozilla_dom_PerformanceTiming_h
 
 #include "mozilla/Attributes.h"
 #include "nsContentUtils.h"
 #include "nsDOMNavigationTiming.h"
 #include "nsRFPService.h"
 #include "nsWrapperCache.h"
 #include "Performance.h"
+#include "nsITimedChannel.h"
 
 class nsIHttpChannel;
 class nsITimedChannel;
 
 namespace mozilla {
 namespace dom {
 
 class PerformanceTiming;
@@ -164,23 +165,26 @@ public:
 
   // Cached result of CheckAllowedOrigin. If false, security sensitive
   // attributes of the resourceTiming object will be set to 0
   bool TimingAllowed() const
   {
     return mTimingAllowed;
   }
 
+  nsTArray<nsCOMPtr<nsIServerTiming>> GetServerTiming();
+
 private:
   // Checks if the resource is either same origin as the page that started
   // the load, or if the response contains the Timing-Allow-Origin header
   // with a value of * or matching the domain of the loading Principal
   bool CheckAllowedOrigin(nsIHttpChannel* aResourceChannel,
                           nsITimedChannel* aChannel);
 
+  nsTArray<nsCOMPtr<nsIServerTiming>> mServerTiming;
   nsString mNextHopProtocol;
 
   TimeStamp mAsyncOpen;
   TimeStamp mRedirectStart;
   TimeStamp mRedirectEnd;
   TimeStamp mDomainLookupStart;
   TimeStamp mDomainLookupEnd;
   TimeStamp mConnectStart;
--- a/dom/performance/moz.build
+++ b/dom/performance/moz.build
@@ -12,16 +12,17 @@ EXPORTS.mozilla.dom += [
     'PerformanceEntry.h',
     'PerformanceMark.h',
     'PerformanceMeasure.h',
     'PerformanceNavigation.h',
     'PerformanceNavigationTiming.h',
     'PerformanceObserver.h',
     'PerformanceObserverEntryList.h',
     'PerformanceResourceTiming.h',
+    'PerformanceServerTiming.h',
     'PerformanceService.h',
     'PerformanceStorage.h',
     'PerformanceStorageWorker.h',
     'PerformanceTiming.h',
 ]
 
 UNIFIED_SOURCES += [
     'Performance.cpp',
@@ -29,16 +30,17 @@ UNIFIED_SOURCES += [
     'PerformanceMainThread.cpp',
     'PerformanceMark.cpp',
     'PerformanceMeasure.cpp',
     'PerformanceNavigation.cpp',
     'PerformanceNavigationTiming.cpp',
     'PerformanceObserver.cpp',
     'PerformanceObserverEntryList.cpp',
     'PerformanceResourceTiming.cpp',
+    'PerformanceServerTiming.cpp',
     'PerformanceService.cpp',
     'PerformanceStorageWorker.cpp',
     'PerformanceTiming.cpp',
     'PerformanceWorker.cpp',
 ]
 
 MOCHITEST_MANIFESTS += [ 'tests/mochitest.ini' ]
 
--- a/dom/performance/tests/mochitest.ini
+++ b/dom/performance/tests/mochitest.ini
@@ -4,17 +4,21 @@ support-files =
   test_performance_user_timing.js
   test_worker_performance_now.js
   worker_performance_user_timing.js
   worker_performance_observer.js
   sharedworker_performance_user_timing.js
   test_worker_performance_entries.js
   test_worker_performance_entries.sjs
   empty.js
+  serverTiming.sjs
 
 [test_performance_observer.html]
 [test_performance_user_timing.html]
 [test_worker_user_timing.html]
 [test_worker_observer.html]
 [test_sharedWorker_performance_user_timing.html]
 [test_worker_performance_now.html]
 [test_timeOrigin.html]
 [test_worker_performance_entries.html]
+[test_performance_server_timing.html]
+scheme = https
+[test_performance_server_timing_plain_http.html]
new file mode 100644
--- /dev/null
+++ b/dom/performance/tests/serverTiming.sjs
@@ -0,0 +1,32 @@
+
+var responseServerTiming = [{metric:"metric1", duration:"123.4", description:"description1"},
+                           {metric:"metric2", duration:"456.78", description:"description2"}];
+var trailerServerTiming = [{metric:"metric3", duration:"789.11", description:"description3"},
+                           {metric:"metric4", duration:"1112.13", description:"description4"}];
+
+function createServerTimingHeader(headerData) {
+  var header = "";
+  for (var i = 0; i < headerData.length; i++) {
+    header += "Server-Timing:" + headerData[i].metric + ";" +
+              "dur=" + headerData[i].duration + ";" +
+              "desc=" + headerData[i].description + "\r\n";
+  }
+  return header;
+}
+
+function handleRequest(request, response)
+{
+  var body = "c\r\ndata reached\r\n3\r\nhej\r\n0\r\n";
+
+  response.seizePower();
+  response.write("HTTP/1.1 200 OK\r\n");
+  response.write("Content-Type: text/plain\r\n");
+  response.write(createServerTimingHeader(responseServerTiming));
+
+  response.write("Transfer-Encoding: chunked\r\n");
+  response.write("\r\n");
+  response.write(body);
+  response.write(createServerTimingHeader(trailerServerTiming));
+  response.write("\r\n");
+  response.finish();
+}
new file mode 100644
--- /dev/null
+++ b/dom/performance/tests/test_performance_server_timing.html
@@ -0,0 +1,58 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset=utf-8>
+<title>Test for PerformanceServerTiming</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+function makeXHR(aUrl) {
+  var xmlhttp = new XMLHttpRequest();
+  xmlhttp.open("get", aUrl, true);
+  xmlhttp.send();
+}
+
+// Note that |responseServerTiming| and |trailerServerTiming| SHOULD be synced with
+// the ones in serverTiming.sjs.
+var responseServerTiming = [{metric:"metric1", duration:"123.4", description:"description1"},
+                            {metric:"metric2", duration:"456.78", description:"description2"}];
+var trailerServerTiming = [{metric:"metric3", duration:"789.11", description:"description3"},
+                           {metric:"metric4", duration:"1112.13", description:"description4"}];
+
+function checkServerTimingContent(serverTiming) {
+  var expectedResult = responseServerTiming.concat(trailerServerTiming);
+  assert_equals(serverTiming.length, expectedResult.length);
+
+  for (var i = 0; i < expectedResult.length; i++) {
+    assert_equals(serverTiming[i].name, expectedResult[i].metric);
+    assert_equals(serverTiming[i].description, expectedResult[i].description);
+    assert_equals(serverTiming[i].duration, parseFloat(expectedResult[i].duration));
+  }
+}
+
+promise_test(t => {
+  var promise = new Promise(resolve => {
+    performance.clearResourceTimings();
+
+    var observer = new PerformanceObserver(list => resolve(list));
+    observer.observe({entryTypes: ['resource']});
+    t.add_cleanup(() => observer.disconnect());
+  });
+
+  makeXHR("serverTiming.sjs");
+
+  return promise.then(list => {
+    assert_equals(list.getEntries().length, 1);
+    checkServerTimingContent(list.getEntries()[0].serverTiming);
+  });
+}, "server-timing test");
+
+</script>
+</body>
new file mode 100644
--- /dev/null
+++ b/dom/performance/tests/test_performance_server_timing_plain_http.html
@@ -0,0 +1,40 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset=utf-8>
+<title>Plain HTTP Test for PerformanceServerTiming</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+function makeXHR(aUrl) {
+  var xmlhttp = new XMLHttpRequest();
+  xmlhttp.open("get", aUrl, true);
+  xmlhttp.send();
+}
+
+promise_test(t => {
+  var promise = new Promise(resolve => {
+    performance.clearResourceTimings();
+
+    var observer = new PerformanceObserver(list => resolve(list));
+    observer.observe({entryTypes: ['resource']});
+    t.add_cleanup(() => observer.disconnect());
+  });
+
+  makeXHR("serverTiming.sjs");
+
+  return promise.then(list => {
+    assert_equals(list.getEntries().length, 1);
+    assert_equals(list.getEntries()[0].serverTiming, undefined);
+  });
+}, "server-timing test");
+
+</script>
+</body>
--- a/dom/plugins/base/nsPluginHost.cpp
+++ b/dom/plugins/base/nsPluginHost.cpp
@@ -93,31 +93,34 @@
 #include "nsContentPolicyUtils.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/Telemetry.h"
 #include "nsIImageLoadingContent.h"
 #include "mozilla/Preferences.h"
 #include "nsVersionComparator.h"
 #include "NullPrincipal.h"
 
+#include "mozilla/dom/Promise.h"
+
 #if defined(XP_WIN)
 #include "nsIWindowMediator.h"
 #include "nsIBaseWindow.h"
 #include "windows.h"
 #include "winbase.h"
 #endif
 
 #include "npapi.h"
 
 using namespace mozilla;
 using mozilla::TimeStamp;
 using mozilla::plugins::FakePluginTag;
 using mozilla::plugins::PluginTag;
 using mozilla::dom::FakePluginTagInit;
 using mozilla::dom::FakePluginMimeEntry;
+using mozilla::dom::Promise;
 
 // Null out a strong ref to a linked list iteratively to avoid
 // exhausting the stack (bug 486349).
 #define NS_ITERATIVE_UNREF_LIST(type_, list_, mNext_)                \
   {                                                                  \
     while (list_) {                                                  \
       type_ temp = list_->mNext_;                                    \
       list_->mNext_ = nullptr;                                       \
@@ -242,16 +245,120 @@ bool ReadSectionHeader(nsPluginManifestL
   return false;
 }
 
 static bool UnloadPluginsASAP()
 {
   return (Preferences::GetUint(kPrefUnloadPluginTimeoutSecs, kDefaultPluginUnloadingTimeout) == 0);
 }
 
+namespace mozilla {
+namespace plugins {
+class BlocklistPromiseHandler final : public mozilla::dom::PromiseNativeHandler
+{
+  public:
+    NS_DECL_ISUPPORTS
+
+    BlocklistPromiseHandler(nsPluginTag *aTag, const bool aShouldSoftblock)
+      : mTag(aTag)
+      , mShouldDisableWhenSoftblocked(aShouldSoftblock)
+    {
+      MOZ_ASSERT(mTag, "Should always be passed a plugin tag");
+      sPendingBlocklistStateRequests++;
+    }
+
+    void
+    MaybeWriteBlocklistChanges()
+    {
+      // We're called immediately when the promise resolves/rejects, and (as a backup)
+      // when the handler is destroyed. To ensure we only run once, we use mTag as a
+      // sentinel, setting it to nullptr when we run.
+      if (!mTag) {
+        return;
+      }
+      mTag = nullptr;
+      sPendingBlocklistStateRequests--;
+      // If this was the only remaining pending request, check if we need to write
+      // state and if so update the child processes.
+      if (!sPendingBlocklistStateRequests &&
+          sPluginBlocklistStatesChangedSinceLastWrite) {
+        sPluginBlocklistStatesChangedSinceLastWrite = false;
+
+        RefPtr<nsPluginHost> host = nsPluginHost::GetInst();
+        // Write the changed list to disk:
+        host->WritePluginInfo();
+
+        // We update blocklist info in content processes asynchronously
+        // by just sending a new plugin list to content.
+        host->IncrementChromeEpoch();
+        host->SendPluginsToContent();
+      }
+    }
+
+    void
+    ResolvedCallback(JSContext *aCx, JS::Handle<JS::Value> aValue) override
+    {
+      if (!aValue.isInt32()) {
+        MOZ_ASSERT(false, "Blocklist should always return int32");
+        return;
+      }
+      int32_t newState = aValue.toInt32();
+      MOZ_ASSERT(newState >= 0 && newState < nsIBlocklistService::STATE_MAX,
+        "Shouldn't get an out of bounds blocklist state");
+
+      // Check the old and new state and see if there was a change:
+      uint32_t oldState = nsIBlocklistService::STATE_NOT_BLOCKED;
+      MOZ_ALWAYS_SUCCEEDS(mTag->GetBlocklistState(&oldState));
+      bool changed = oldState != (uint32_t)newState;
+      mTag->SetBlocklistState(newState);
+
+      if (newState == nsIBlocklistService::STATE_SOFTBLOCKED && mShouldDisableWhenSoftblocked) {
+        mTag->SetEnabledState(nsIPluginTag::STATE_DISABLED);
+        changed = true;
+      }
+      sPluginBlocklistStatesChangedSinceLastWrite |= changed;
+
+      MaybeWriteBlocklistChanges();
+    }
+    void
+    RejectedCallback(JSContext *aCx, JS::Handle<JS::Value> aValue) override
+    {
+      MOZ_ASSERT(false, "Shouldn't reject plugin blocklist state request");
+      MaybeWriteBlocklistChanges();
+    }
+
+  private:
+    ~BlocklistPromiseHandler() {
+      // If we have multiple plugins and the last pending request is GC'd
+      // and so never resolves/rejects, ensure we still write the blocklist.
+      MaybeWriteBlocklistChanges();
+    }
+
+    RefPtr<nsPluginTag> mTag;
+    bool mShouldDisableWhenSoftblocked;
+
+    // Whether we changed any of the plugins' blocklist states since
+    // we last started fetching them (async). This is reset to false
+    // every time we finish fetching plugin blocklist information.
+    // When this happens, if the previous value was true, we store the
+    // updated list on disk and send it to child processes.
+    static bool sPluginBlocklistStatesChangedSinceLastWrite;
+    // How many pending blocklist state requests we've got
+    static uint32_t sPendingBlocklistStateRequests;
+};
+
+NS_IMPL_ISUPPORTS0(BlocklistPromiseHandler)
+
+
+bool BlocklistPromiseHandler::sPluginBlocklistStatesChangedSinceLastWrite = false;
+uint32_t BlocklistPromiseHandler::sPendingBlocklistStateRequests = 0;
+} // namespace plugins
+} // namespace mozilla
+
+
 nsPluginHost::nsPluginHost()
   : mPluginsLoaded(false)
   , mOverrideInternalTypes(false)
   , mPluginsDisabled(false)
   , mPluginEpoch(0)
 {
   // check to see if pref is set at startup to let plugins take over in
   // full page mode for certain image mime types that we handle internally
@@ -263,17 +370,16 @@ nsPluginHost::nsPluginHost()
   Preferences::AddStrongObserver(this, "plugin.disable");
 
   nsCOMPtr<nsIObserverService> obsService =
     mozilla::services::GetObserverService();
   if (obsService) {
     obsService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
     if (XRE_IsParentProcess()) {
       obsService->AddObserver(this, "blocklist-updated", false);
-      obsService->AddObserver(this, "blocklist-loaded", false);
     }
   }
 
 #ifdef PLUGIN_LOGGING
   MOZ_LOG(nsPluginLogging::gNPNLog, PLUGIN_LOG_ALWAYS,("NPN Logging Active!\n"));
   MOZ_LOG(nsPluginLogging::gPluginLog, PLUGIN_LOG_ALWAYS,("General Plugin Logging Active! (nsPluginHost::ctor)\n"));
   MOZ_LOG(nsPluginLogging::gNPPLog, PLUGIN_LOG_ALWAYS,("NPP Logging Active!\n"));
 
@@ -1997,23 +2103,16 @@ nsresult nsPluginHost::ScanPluginsDirect
     }
   }
 
   pluginFiles.Sort(CompareFilesByTime());
 
   nsCOMArray<nsIFile> extensionDirs;
   GetExtensionDirectories(extensionDirs);
 
-  nsCOMPtr<nsIBlocklistService> blocklist =
-    do_GetService("@mozilla.org/extensions/blocklist;1");
-
-  bool isBlocklistLoaded = false;
-  if (blocklist && NS_FAILED(blocklist->GetIsLoaded(&isBlocklistLoaded))) {
-    isBlocklistLoaded = false;
-  }
   for (int32_t i = (pluginFiles.Length() - 1); i >= 0; i--) {
     nsCOMPtr<nsIFile>& localfile = pluginFiles[i];
 
     nsString utf16FilePath;
     rv = localfile->GetPath(utf16FilePath);
     if (NS_FAILED(rv))
       continue;
 
@@ -2098,30 +2197,20 @@ nsresult nsPluginHost::ScanPluginsDirect
         // Mark aPluginsChanged so pluginreg is rewritten
         *aPluginsChanged = true;
         continue;
       }
 
       uint32_t state = nsIBlocklistService::STATE_NOT_BLOCKED;
       pluginTag = new nsPluginTag(&info, fileModTime, fromExtension, state);
       pluginTag->mLibrary = library;
-      // If the blocklist is loaded, get the blocklist state now.
-      // If it isn't loaded yet, we'll update it once it loads.
-      if (isBlocklistLoaded &&
-          NS_SUCCEEDED(blocklist->GetPluginBlocklistState(pluginTag, EmptyString(),
-                                                          EmptyString(), &state))) {
-        pluginTag->SetBlocklistState(state);
-      }
       pluginFile.FreePluginInfo(info);
-
-      // If the blocklist says it is risky and we have never seen this
-      // plugin before, then disable it.
-      if (state == nsIBlocklistService::STATE_SOFTBLOCKED && !seenBefore) {
-        pluginTag->SetEnabledState(nsIPluginTag::STATE_DISABLED);
-      }
+      // Pass whether we've seen this plugin before. If the plugin is
+      // softblocked and new (not seen before), it will be disabled.
+      UpdatePluginBlocklistState(pluginTag, !seenBefore);
 
       // Plugin unloading is tag-based. If we created a new tag and loaded
       // the library in the process then we want to attempt to unload it here.
       // Only do this if the pref is set for aggressive unloading.
       if (UnloadPluginsASAP()) {
         pluginTag->TryUnloadPlugin(false);
       }
     }
@@ -2146,16 +2235,36 @@ nsresult nsPluginHost::ScanPluginsDirect
     }
 
     AddPluginTag(pluginTag);
   }
 
   return NS_OK;
 }
 
+void
+nsPluginHost::UpdatePluginBlocklistState(nsPluginTag* aPluginTag, bool aShouldSoftblock)
+{
+  nsCOMPtr<nsIBlocklistService> blocklist =
+    do_GetService("@mozilla.org/extensions/blocklist;1");
+  MOZ_ASSERT(blocklist, "Should be able to access the blocklist");
+  if (!blocklist) {
+    return;
+  }
+  // Asynchronously get the blocklist state.
+  nsCOMPtr<nsISupports> result;
+  blocklist->GetPluginBlocklistState(aPluginTag, EmptyString(),
+                                     EmptyString(), getter_AddRefs(result));
+  RefPtr<Promise> promise = do_QueryObject(result);
+  MOZ_ASSERT(promise, "Should always get a promise for plugin blocklist state.");
+  if (promise) {
+    promise->AppendNativeHandler(new mozilla::plugins::BlocklistPromiseHandler(aPluginTag, aShouldSoftblock));
+  }
+}
+
 nsresult nsPluginHost::ScanPluginsDirectoryList(nsISimpleEnumerator *dirEnum,
                                                 bool aCreatePluginList,
                                                 bool *aPluginsChanged)
 {
   MOZ_ASSERT(XRE_IsParentProcess());
 
     bool hasMore;
     while (NS_SUCCEEDED(dirEnum->HasMoreElements(&hasMore)) && hasMore) {
@@ -3402,46 +3511,25 @@ NS_IMETHODIMP nsPluginHost::Observe(nsIS
     mPluginsDisabled = Preferences::GetBool("plugin.disable", false);
     // Unload or load plugins as needed
     if (mPluginsDisabled) {
       UnloadPlugins();
     } else {
       LoadPlugins();
     }
   }
-  if (XRE_IsParentProcess() &&
-      (!strcmp("blocklist-updated", aTopic) || !strcmp("blocklist-loaded", aTopic))) {
-    nsCOMPtr<nsIBlocklistService> blocklist =
-      do_GetService("@mozilla.org/extensions/blocklist;1");
-    if (!blocklist) {
-      return NS_OK;
-    }
+  if (XRE_IsParentProcess() && !strcmp("blocklist-updated", aTopic)) {
+    // The blocklist has updated. Asynchronously get blocklist state for all items.
+    // The promise resolution handler takes care of checking if anything changed,
+    // and writing an updated state to file, as well as sending data to child processes.
     nsPluginTag* plugin = mPlugins;
-    bool blocklistAlteredPlugins = false;
     while (plugin) {
-      uint32_t blocklistState = nsIBlocklistService::STATE_NOT_BLOCKED;
-      nsresult rv = blocklist->GetPluginBlocklistState(plugin, EmptyString(),
-                                                       EmptyString(), &blocklistState);
-      NS_ENSURE_SUCCESS(rv, rv);
-      uint32_t oldBlocklistState;
-      plugin->GetBlocklistState(&oldBlocklistState);
-      plugin->SetBlocklistState(blocklistState);
-      blocklistAlteredPlugins |= (oldBlocklistState != blocklistState);
+      UpdatePluginBlocklistState(plugin);
       plugin = plugin->mNext;
     }
-    if (blocklistAlteredPlugins) {
-      // Write the changed list to disk:
-      WritePluginInfo();
-
-      // We update blocklists asynchronously by just sending a new plugin list to
-      // content.
-      // We'll need to repack our tags and send them to content again.
-      IncrementChromeEpoch();
-      SendPluginsToContent();
-    }
   }
   return NS_OK;
 }
 
 nsresult
 nsPluginHost::ParsePostBufferToFixHeaders(const char *inPostData, uint32_t inPostDataLen,
                                           char **outPostData, uint32_t *outPostDataLen)
 {
--- a/dom/plugins/base/nsPluginHost.h
+++ b/dom/plugins/base/nsPluginHost.h
@@ -23,26 +23,28 @@
 #include "nsTArray.h"
 #include "nsINamed.h"
 #include "nsTObserverArray.h"
 #include "nsITimer.h"
 #include "nsPluginTags.h"
 #include "nsIEffectiveTLDService.h"
 #include "nsIIDNService.h"
 #include "nsCRT.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
 
 #ifdef XP_WIN
 #include <minwindef.h>
 #include "nsIWindowsRegKey.h"
 #endif
 
 namespace mozilla {
 namespace plugins {
 class FakePluginTag;
 class PluginTag;
+class BlocklistPromiseHandler;
 } // namespace plugins
 } // namespace mozilla
 
 class nsNPAPIPlugin;
 class nsIFile;
 class nsIChannel;
 class nsPluginNativeWindow;
 class nsObjectLoadingContent;
@@ -251,16 +253,17 @@ public:
                              bool firstMatchOnly);
 
   nsresult SendPluginsToContent();
   nsresult SetPluginsInContent(uint32_t aPluginEpoch,
                                nsTArray<mozilla::plugins::PluginTag>& aPlugins,
                                nsTArray<mozilla::plugins::FakePluginTag>& aFakePlugins);
 private:
   friend class nsPluginUnloadRunnable;
+  friend class mozilla::plugins::BlocklistPromiseHandler;
 
   void DestroyRunningInstances(nsPluginTag* aPluginTag);
 
   // Writes updated plugins settings to disk and unloads the plugin
   // if it is now disabled. Should only be called by the plugin tag in question
   void UpdatePluginInfo(nsPluginTag* aPluginTag);
 
   nsresult TrySetUpPluginInstance(const nsACString &aMimeType, nsIURI *aURL,
@@ -312,16 +315,19 @@ private:
                         ePluginUnregister,
                         // Checks if this type should still be registered first
                         ePluginMaybeUnregister };
   void RegisterWithCategoryManager(const nsCString& aMimeType,
                                    nsRegisterType aType);
 
   void AddPluginTag(nsPluginTag* aPluginTag);
 
+  void UpdatePluginBlocklistState(nsPluginTag* aPluginTag,
+                                  bool aShouldSoftblock = false);
+
   nsresult
   ScanPluginsDirectory(nsIFile *pluginsDir,
                        bool aCreatePluginList,
                        bool *aPluginsChanged);
 
   nsresult
   ScanPluginsDirectoryList(nsISimpleEnumerator *dirEnum,
                            bool aCreatePluginList,
--- a/dom/security/nsCSPParser.cpp
+++ b/dom/security/nsCSPParser.cpp
@@ -1166,16 +1166,20 @@ nsCSPParser::directive()
   // one directive and one src.
   if (mCurDir.Length() < 1) {
     const char16_t* params[] = { u"directive missing" };
     logWarningErrorToConsole(nsIScriptError::warningFlag, "failedToParseUnrecognizedSource",
                              params, ArrayLength(params));
     return;
   }
 
+  if (CSP_IsEmptyDirective(mCurValue, mCurToken)) {
+    return;
+  }
+
   // Try to create a new CSPDirective
   nsCSPDirective* cspDir = directiveName();
   if (!cspDir) {
     // if we can not create a CSPDirective, we can skip parsing the srcs for that array
     return;
   }
 
   // special case handling for block-all-mixed-content, which is only specified
--- a/dom/security/nsCSPUtils.cpp
+++ b/dom/security/nsCSPUtils.cpp
@@ -303,16 +303,22 @@ CSP_CreateHostSrcFromSelfURI(nsIURI* aSe
     nsAutoString portStr;
     portStr.AppendInt(port);
     hostsrc->setPort(portStr);
   }
   return hostsrc;
 }
 
 bool
+CSP_IsEmptyDirective(const nsAString& aValue, const nsAString& aDir)
+{
+  return (aDir.Length() == 0 &&
+          aValue.Length() == 0);
+}
+bool
 CSP_IsValidDirective(const nsAString& aDir)
 {
   uint32_t numDirs = (sizeof(CSPStrDirectives) / sizeof(CSPStrDirectives[0]));
 
   for (uint32_t i = 0; i < numDirs; i++) {
     if (aDir.LowerCaseEqualsASCII(CSPStrDirectives[i])) {
       return true;
     }
--- a/dom/security/nsCSPUtils.h
+++ b/dom/security/nsCSPUtils.h
@@ -206,16 +206,17 @@ nsresult CSP_AppendCSPFromHeader(nsICont
                                  const nsAString& aHeaderValue,
                                  bool aReportOnly);
 
 /* =============== Helpers ================== */
 
 class nsCSPHostSrc;
 
 nsCSPHostSrc* CSP_CreateHostSrcFromSelfURI(nsIURI* aSelfURI);
+bool CSP_IsEmptyDirective(const nsAString& aValue, const nsAString& aDir);
 bool CSP_IsValidDirective(const nsAString& aDir);
 bool CSP_IsDirective(const nsAString& aValue, CSPDirective aDir);
 bool CSP_IsKeyword(const nsAString& aValue, enum CSPKeyword aKey);
 bool CSP_IsQuotelessKeyword(const nsAString& aKey);
 CSPDirective CSP_ContentTypeToDirective(nsContentPolicyType aType);
 
 class nsCSPSrcVisitor;
 
copy from dom/security/test/csp/file_self_none_as_hostname_confusion.html
copy to dom/security/test/csp/file_empty_directive.html
copy from dom/security/test/csp/file_self_none_as_hostname_confusion.html^headers^
copy to dom/security/test/csp/file_empty_directive.html^headers^
--- a/dom/security/test/csp/file_self_none_as_hostname_confusion.html^headers^
+++ b/dom/security/test/csp/file_empty_directive.html^headers^
@@ -1,1 +1,1 @@
-Content-Security-Policy: default-src 'self' SELF;
+Content-Security-Policy: ;
--- a/dom/security/test/csp/mochitest.ini
+++ b/dom/security/test/csp/mochitest.ini
@@ -103,16 +103,18 @@ support-files =
   file_dual_header_testserver.sjs
   file_hash_source.html^headers^
   file_scheme_relative_sources.js
   file_scheme_relative_sources.sjs
   file_ignore_unsafe_inline.html
   file_ignore_unsafe_inline_multiple_policies_server.sjs
   file_self_none_as_hostname_confusion.html
   file_self_none_as_hostname_confusion.html^headers^
+  file_empty_directive.html
+  file_empty_directive.html^headers^
   file_path_matching.html
   file_path_matching_incl_query.html
   file_path_matching.js
   file_path_matching_redirect.html
   file_path_matching_redirect_server.sjs
   file_testserver.sjs
   file_report_uri_missing_in_report_only_header.html
   file_report_uri_missing_in_report_only_header.html^headers^
@@ -263,16 +265,17 @@ skip-if = toolkit == 'android' # Times o
 [test_policyuri_regression_from_multipolicy.html]
 [test_nonce_source.html]
 [test_bug941404.html]
 [test_form-action.html]
 [test_hash_source.html]
 [test_scheme_relative_sources.html]
 [test_ignore_unsafe_inline.html]
 [test_self_none_as_hostname_confusion.html]
+[test_empty_directive.html]
 [test_path_matching.html]
 [test_path_matching_redirect.html]
 [test_report_uri_missing_in_report_only_header.html]
 [test_report.html]
 [test_301_redirect.html]
 [test_302_redirect.html]
 [test_303_redirect.html]
 [test_307_redirect.html]
copy from dom/security/test/csp/test_self_none_as_hostname_confusion.html
copy to dom/security/test/csp/test_empty_directive.html
--- a/dom/security/test/csp/test_self_none_as_hostname_confusion.html
+++ b/dom/security/test/csp/test_empty_directive.html
@@ -1,55 +1,51 @@
 <!DOCTYPE HTML>
 <html>
 <!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=587377
+https://bugzilla.mozilla.org/show_bug.cgi?id=1439425
 -->
 <head>
   <meta charset="utf-8">
-  <title>Test for Bug 587377</title>
+  <title>Test for Bug 1439425</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=587377">Mozilla Bug 587377</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1439425">Mozilla Bug 1439425</a>
 <p id="display"></p>
 
 <iframe id="cspframe"></iframe>
 
 <pre id="test">
 
 <script class="testbody" type="text/javascript">
-// Load locale string during mochitest
-var stringBundleService = SpecialPowers.Cc["@mozilla.org/intl/stringbundle;1"]
-                          .getService(SpecialPowers.Ci.nsIStringBundleService);
-var localizer = stringBundleService.createBundle("chrome://global/locale/security/csp.properties");
-var confusionMsg = localizer.formatStringFromName("hostNameMightBeKeyword", ["SELF", "self"], 2);
+let consoleCount = 0;
 
 function cleanup() {
   SpecialPowers.postConsoleSentinel();
-  SimpleTest.finish();
-};
+}
 
-// To prevent the test from asserting twice and calling SimpleTest.finish() twice,
-// startTest will be marked false as soon as the confusionMsg is detected.
-startTest = false;
+function finish() {
+  SimpleTest.finish();
+}
+
 SpecialPowers.registerConsoleListener(function ConsoleMsgListener(aMsg) {
-  if (startTest) {
-    if (aMsg.message.indexOf(confusionMsg) > -1) {
-      startTest = false;
-      ok(true, "CSP header with a hostname similar to keyword should be warned");
-      SimpleTest.executeSoon(cleanup);
-    } else {
-      // don't see the warning yet? wait.
-      return;
-    }
+  if (aMsg.message == "SENTINEL") {
+    is(consoleCount, 0);
+    SimpleTest.executeSoon(finish);
+  } else {
+    ++consoleCount;
+    ok(false, "Must never see a console warning here");
   }
 });
 
 // set up and start testing
 SimpleTest.waitForExplicitFinish();
-document.getElementById('cspframe').src = 'file_self_none_as_hostname_confusion.html';
-startTest = true;
+let frame = document.getElementById('cspframe');
+frame.onload = () => {
+  SimpleTest.executeSoon(cleanup);
+};
+frame.src = 'file_empty_directive.html';
 </script>
 </pre>
 </body>
 </html>
--- a/dom/serviceworkers/test/test_serviceworker_interfaces.js
+++ b/dom/serviceworkers/test/test_serviceworker_interfaces.js
@@ -184,16 +184,18 @@ var interfaceNamesInGlobalScope =
     "PerformanceMeasure",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PerformanceObserver",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PerformanceObserverEntryList",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PerformanceResourceTiming",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    "PerformanceServerTiming",
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "ProgressEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PushEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PushManager",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PushMessageData",
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/svg/crashtests/crashtests.list
+++ b/dom/svg/crashtests/crashtests.list
@@ -26,17 +26,17 @@ load 398926-both-same.svg
 load 398926-fill.svg
 load 398926-stroke.svg
 load 405639-1.svg
 load 406361-1.html
 load 409811-1.html
 load 410659-1.svg
 load 410659-2.svg
 load 410659-3.svg
-asserts(3-4) load 412104-1.svg # bug 903785
+load 412104-1.svg
 load 413174-1.svg
 load 414188-1.svg
 load 427325-1.svg
 load 428228-1.svg
 load 428841-1.svg
 load 436418-mpathRoot-1.svg
 load 448244-1.svg
 load 466576-1.xhtml
--- a/dom/tests/mochitest/general/test_interfaces.js
+++ b/dom/tests/mochitest/general/test_interfaces.js
@@ -764,16 +764,18 @@ var interfaceNamesInGlobalScope =
     {name: "PerformanceNavigationTiming", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PerformanceObserver", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PerformanceObserverEntryList", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PerformanceResourceTiming", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    {name: "PerformanceServerTiming", insecureContext: false},
+// IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PerformanceTiming", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PeriodicWave", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "Permissions", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PermissionStatus", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/webidl/MediaList.webidl
+++ b/dom/webidl/MediaList.webidl
@@ -1,16 +1,15 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 // http://dev.w3.org/csswg/cssom/#the-medialist-interface
 
-[ArrayClass]
 interface MediaList {
   // Bug 824857: no support for stringifier attributes yet.
   //   [TreatNullAs=EmptyString]
   // stringifier attribute DOMString        mediaText;
 
   // Bug 824857 should remove this.
   stringifier;
 
--- a/dom/webidl/PerformanceResourceTiming.webidl
+++ b/dom/webidl/PerformanceResourceTiming.webidl
@@ -44,10 +44,15 @@ interface PerformanceResourceTiming : Pe
 
   [NeedsSubjectPrincipal]
   readonly attribute unsigned long long transferSize;
   [NeedsSubjectPrincipal]
   readonly attribute unsigned long long encodedBodySize;
   [NeedsSubjectPrincipal]
   readonly attribute unsigned long long decodedBodySize;
 
+  // TODO: Use FrozenArray once available. (Bug 1236777)
+  // readonly attribute FrozenArray<PerformanceServerTiming> serverTiming;
+  [SecureContext, Frozen, Cached, Pure, NeedsSubjectPrincipal]
+  readonly attribute sequence<PerformanceServerTiming> serverTiming;
+
   jsonifier;
 };
new file mode 100644
--- /dev/null
+++ b/dom/webidl/PerformanceServerTiming.webidl
@@ -0,0 +1,20 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ *
+ * The origin of this IDL file is
+ * https://w3c.github.io/server-timing/
+ *
+ * Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
+ * liability, trademark and document use rules apply.
+ */
+
+[SecureContext,Exposed=(Window,Worker)]
+interface PerformanceServerTiming {
+  readonly attribute DOMString           name;
+  readonly attribute DOMHighResTimeStamp duration;
+  readonly attribute DOMString           description;
+
+  jsonifier;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -708,16 +708,17 @@ WEBIDL_FILES = [
     'PerformanceEntry.webidl',
     'PerformanceMark.webidl',
     'PerformanceMeasure.webidl',
     'PerformanceNavigation.webidl',
     'PerformanceNavigationTiming.webidl',
     'PerformanceObserver.webidl',
     'PerformanceObserverEntryList.webidl',
     'PerformanceResourceTiming.webidl',
+    'PerformanceServerTiming.webidl',
     'PerformanceTiming.webidl',
     'PeriodicWave.webidl',
     'Permissions.webidl',
     'PermissionStatus.webidl',
     'Plugin.webidl',
     'PluginArray.webidl',
     'PointerEvent.webidl',
     'PopupBoxObject.webidl',
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -3335,19 +3335,17 @@ WorkerPrivate::DoRunLoop(JSContext* aCx)
         debuggerRunnablesPending = !mDebuggerQueue.IsEmpty();
       }
 
       MOZ_ASSERT(runnable);
       static_cast<nsIRunnable*>(runnable)->Run();
       runnable->Release();
 
       CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get();
-      if (ccjs) {
-        ccjs->PerformDebuggerMicroTaskCheckpoint();
-      }
+      ccjs->PerformDebuggerMicroTaskCheckpoint();
 
       if (debuggerRunnablesPending) {
         WorkerDebuggerGlobalScope* globalScope = DebuggerGlobalScope();
         MOZ_ASSERT(globalScope);
 
         // Now *might* be a good time to GC. Let the JS engine make the decision.
         JSAutoCompartment ac(aCx, globalScope->GetGlobalJSObject());
         JS_MaybeGC(aCx);
@@ -4399,16 +4397,17 @@ WorkerPrivate::PostMessageToParent(
 
 void
 WorkerPrivate::EnterDebuggerEventLoop()
 {
   AssertIsOnWorkerThread();
 
   JSContext* cx = GetJSContext();
   MOZ_ASSERT(cx);
+  CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get();
 
   uint32_t currentEventLoopLevel = ++mDebuggerEventLoopLevel;
 
   while (currentEventLoopLevel <= mDebuggerEventLoopLevel) {
 
     bool debuggerRunnablesPending = false;
 
     {
@@ -4421,53 +4420,46 @@ WorkerPrivate::EnterDebuggerEventLoop()
     if (!debuggerRunnablesPending) {
       SetGCTimerMode(IdleTimer);
     }
 
     // Wait for something to do
     {
       MutexAutoLock lock(mMutex);
 
-      CycleCollectedJSContext* context = CycleCollectedJSContext::Get();
       std::queue<RefPtr<MicroTaskRunnable>>& debuggerMtQueue =
-        context->GetDebuggerMicroTaskQueue();
+        ccjscx->GetDebuggerMicroTaskQueue();
       while (mControlQueue.IsEmpty() &&
              !(debuggerRunnablesPending = !mDebuggerQueue.IsEmpty()) &&
              debuggerMtQueue.empty()) {
         WaitForWorkerEvents();
       }
 
       ProcessAllControlRunnablesLocked();
 
       // XXXkhuey should we abort JS on the stack here if we got Abort above?
     }
-    CycleCollectedJSContext* context = CycleCollectedJSContext::Get();
-    if (context) {
-      context->PerformDebuggerMicroTaskCheckpoint();
-    }
+    ccjscx->PerformDebuggerMicroTaskCheckpoint();
     if (debuggerRunnablesPending) {
       // Start the periodic GC timer if it is not already running.
       SetGCTimerMode(PeriodicTimer);
 
       WorkerRunnable* runnable = nullptr;
 
       {
         MutexAutoLock lock(mMutex);
 
         mDebuggerQueue.Pop(runnable);
       }
 
       MOZ_ASSERT(runnable);
       static_cast<nsIRunnable*>(runnable)->Run();
       runnable->Release();
 
-      CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get();
-      if (ccjs) {
-        ccjs->PerformDebuggerMicroTaskCheckpoint();
-      }
+      ccjscx->PerformDebuggerMicroTaskCheckpoint();
 
       // Now *might* be a good time to GC. Let the JS engine make the decision.
       if (JS::CurrentGlobalOrNull(cx)) {
         JS_MaybeGC(cx);
       }
     }
   }
 }
--- a/dom/workers/test/test_worker_interfaces.js
+++ b/dom/workers/test/test_worker_interfaces.js
@@ -184,16 +184,18 @@ var interfaceNamesInGlobalScope =
     {name: "PerformanceMeasure", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PerformanceObserver", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PerformanceObserverEntryList", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PerformanceResourceTiming", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    {name: "PerformanceServerTiming", insecureContext: false},
+// IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "ProgressEvent", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PushManager", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PushSubscription", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PushSubscriptionOptions", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/editor/libeditor/EditorBase.cpp
+++ b/editor/libeditor/EditorBase.cpp
@@ -2330,75 +2330,16 @@ EditorBase::RestorePreservedSelection(Se
 
 void
 EditorBase::StopPreservingSelection()
 {
   mRangeUpdater.DropSelectionState(mSavedSel);
   mSavedSel.MakeEmpty();
 }
 
-bool
-EditorBase::EnsureComposition(WidgetCompositionEvent* aCompositionEvent)
-{
-  if (mComposition) {
-    return true;
-  }
-  // The compositionstart event must cause creating new TextComposition
-  // instance at being dispatched by IMEStateManager.
-  mComposition = IMEStateManager::GetTextCompositionFor(aCompositionEvent);
-  if (!mComposition) {
-    // However, TextComposition may be committed before the composition
-    // event comes here.
-    return false;
-  }
-  mComposition->StartHandlingComposition(this);
-  return true;
-}
-
-nsresult
-EditorBase::BeginIMEComposition(WidgetCompositionEvent* aCompositionEvent)
-{
-  MOZ_ASSERT(!mComposition, "There is composition already");
-  if (!EnsureComposition(aCompositionEvent)) {
-    return NS_OK;
-  }
-  return NS_OK;
-}
-
-void
-EditorBase::EndIMEComposition()
-{
-  NS_ENSURE_TRUE_VOID(mComposition); // nothing to do
-
-  // commit the IME transaction..we can get at it via the transaction mgr.
-  // Note that this means IME won't work without an undo stack!
-  if (mTransactionManager) {
-    nsCOMPtr<nsITransaction> txn = mTransactionManager->PeekUndoStack();
-    nsCOMPtr<nsIAbsorbingTransaction> plcTxn = do_QueryInterface(txn);
-    if (plcTxn) {
-      DebugOnly<nsresult> rv = plcTxn->Commit();
-      NS_ASSERTION(NS_SUCCEEDED(rv),
-                   "nsIAbsorbingTransaction::Commit() failed");
-    }
-  }
-
-  // Composition string may have hidden the caret.  Therefore, we need to
-  // cancel it here.
-  HideCaret(false);
-
-  // FYI: mComposition still keeps storing container text node of committed
-  //      string, its offset and length.  However, they will be invalidated
-  //      soon since its Destroy() will be called by IMEStateManager.
-  mComposition->EndHandlingComposition(this);
-  mComposition = nullptr;
-
-  // notify editor observers of action
-  NotifyEditorObservers(eNotifyEditorObserversOfEnd);
-}
-
 NS_IMETHODIMP
 EditorBase::ForceCompositionEnd()
 {
   return CommitComposition();
 }
 
 nsresult
 EditorBase::CommitComposition()
@@ -2553,17 +2494,17 @@ EditorBase::CloneAttributesWithTransacti
   // Use transaction system for undo only if destination is already in the
   // document
   Element* rootElement = GetRoot();
   if (NS_WARN_IF(!rootElement)) {
     return;
   }
 
   OwningNonNull<Element> destElement(aDestElement);
-  OwningNonNull<Element> sourceElement(aDestElement);
+  OwningNonNull<Element> sourceElement(aSourceElement);
   bool isDestElementInBody = rootElement->Contains(destElement);
 
   // Clear existing attributes
   RefPtr<nsDOMAttributeMap> destAttributes = destElement->Attributes();
   while (RefPtr<Attr> attr = destAttributes->Item(0)) {
     if (isDestElementInBody) {
       RemoveAttributeWithTransaction(destElement,
                                      *attr->NodeInfo()->NameAtom());
--- a/editor/libeditor/EditorBase.h
+++ b/editor/libeditor/EditorBase.h
@@ -653,24 +653,16 @@ public:
 
   /**
    * Creates text node which is marked as "maybe modified frequently".
    */
   static already_AddRefed<nsTextNode> CreateTextNode(nsIDocument& aDocument,
                                                      const nsAString& aData);
 
   /**
-   * IME event handlers.
-   */
-  virtual nsresult BeginIMEComposition(WidgetCompositionEvent* aEvent);
-  virtual nsresult UpdateIMEComposition(
-                     WidgetCompositionEvent* aCompositionChangeEvet) = 0;
-  void EndIMEComposition();
-
-  /**
    * Get preferred IME status of current widget.
    */
   virtual nsresult GetPreferredIMEState(widget::IMEState* aState);
 
   /**
    * Commit composition if there is.
    * Note that when there is a composition, this requests to commit composition
    * to native IME.  Therefore, when there is composition, this can do anything.
@@ -939,28 +931,16 @@ protected:
   {
     // Check for password/readonly/disabled, which are not spellchecked
     // regardless of DOM. Also, check to see if spell check should be skipped
     // or not.
     return !IsPasswordEditor() && !IsReadonly() && !IsDisabled() &&
            !ShouldSkipSpellCheck();
   }
 
-  /**
-   * EnsureComposition() should be called by composition event handlers.  This
-   * tries to get the composition for the event and set it to mComposition.
-   * However, this may fail because the composition may be committed before
-   * the event comes to the editor.
-   *
-   * @return            true if there is a composition.  Otherwise, for example,
-   *                    a composition event handler in web contents moved focus
-   *                    for committing the composition, returns false.
-   */
-  bool EnsureComposition(WidgetCompositionEvent* aCompositionEvent);
-
   nsresult GetSelection(SelectionType aSelectionType,
                         nsISelection** aSelection);
 
   /**
    * (Begin|End)PlaceholderTransaction() are called by AutoPlaceholderBatch.
    * This set of methods are similar to the (Begin|End)Transaction(), but do
    * not use the transaction managers batching feature.  Instead we use a
    * placeholder transaction to wrap up any further transaction while the
--- a/editor/libeditor/EditorEventListener.cpp
+++ b/editor/libeditor/EditorEventListener.cpp
@@ -757,36 +757,16 @@ EditorEventListener::MouseDown(MouseEven
   //      when the event is not acceptable for committing composition.
   if (DetachedFromEditor()) {
     return NS_OK;
   }
   Unused << EnsureCommitCompoisition();
   return NS_OK;
 }
 
-nsresult
-EditorEventListener::HandleChangeComposition(
-                       WidgetCompositionEvent* aCompositionChangeEvent)
-{
-  MOZ_ASSERT(!aCompositionChangeEvent->DefaultPrevented(),
-             "eCompositionChange event shouldn't be cancelable");
-  RefPtr<EditorBase> editorBase(mEditorBase);
-  if (DetachedFromEditor() ||
-      !editorBase->IsAcceptableInputEvent(aCompositionChangeEvent)) {
-    return NS_OK;
-  }
-
-  // if we are readonly or disabled, then do nothing.
-  if (editorBase->IsReadonly() || editorBase->IsDisabled()) {
-    return NS_OK;
-  }
-
-  return editorBase->UpdateIMEComposition(aCompositionChangeEvent);
-}
-
 /**
  * Drag event implementation
  */
 
 nsresult
 EditorEventListener::DragEnter(DragEvent* aDragEvent)
 {
   if (NS_WARN_IF(!aDragEvent) || DetachedFromEditor()) {
@@ -1013,40 +993,73 @@ EditorEventListener::CanDrop(DragEvent* 
   }
   return true;
 }
 
 nsresult
 EditorEventListener::HandleStartComposition(
                        WidgetCompositionEvent* aCompositionStartEvent)
 {
+  if (NS_WARN_IF(!aCompositionStartEvent)) {
+    return NS_ERROR_FAILURE;
+  }
   RefPtr<EditorBase> editorBase(mEditorBase);
   if (DetachedFromEditor() ||
       !editorBase->IsAcceptableInputEvent(aCompositionStartEvent)) {
     return NS_OK;
   }
   // Although, "compositionstart" should be cancelable, but currently,
   // eCompositionStart event coming from widget is not cancelable.
   MOZ_ASSERT(!aCompositionStartEvent->DefaultPrevented(),
              "eCompositionStart shouldn't be cancelable");
-  return editorBase->BeginIMEComposition(aCompositionStartEvent);
+  TextEditor* textEditor = editorBase->AsTextEditor();
+  return textEditor->OnCompositionStart(*aCompositionStartEvent);
+}
+
+nsresult
+EditorEventListener::HandleChangeComposition(
+                       WidgetCompositionEvent* aCompositionChangeEvent)
+{
+  if (NS_WARN_IF(!aCompositionChangeEvent)) {
+    return NS_ERROR_FAILURE;
+  }
+  MOZ_ASSERT(!aCompositionChangeEvent->DefaultPrevented(),
+             "eCompositionChange event shouldn't be cancelable");
+  RefPtr<EditorBase> editorBase(mEditorBase);
+  if (DetachedFromEditor() ||
+      !editorBase->IsAcceptableInputEvent(aCompositionChangeEvent)) {
+    return NS_OK;
+  }
+
+  // if we are readonly or disabled, then do nothing.
+  if (editorBase->IsReadonly() || editorBase->IsDisabled()) {
+    return NS_OK;
+  }
+
+  TextEditor* textEditor = editorBase->AsTextEditor();
+  return textEditor->OnCompositionChange(*aCompositionChangeEvent);
 }
 
 void
 EditorEventListener::HandleEndComposition(
                        WidgetCompositionEvent* aCompositionEndEvent)
 {
+  if (NS_WARN_IF(!aCompositionEndEvent)) {
+    return;
+  }
   RefPtr<EditorBase> editorBase(mEditorBase);
   if (DetachedFromEditor() ||
       !editorBase->IsAcceptableInputEvent(aCompositionEndEvent)) {
     return;
   }
   MOZ_ASSERT(!aCompositionEndEvent->DefaultPrevented(),
              "eCompositionEnd shouldn't be cancelable");
-  editorBase->EndIMEComposition();
+
+  TextEditor* textEditor = editorBase->AsTextEditor();
+  textEditor->OnCompositionEnd(*aCompositionEndEvent);
 }
 
 nsresult
 EditorEventListener::Focus(InternalFocusEvent* aFocusEvent)
 {
   if (NS_WARN_IF(!aFocusEvent) || DetachedFromEditor()) {
     return NS_OK;
   }
--- a/editor/libeditor/TextEditor.cpp
+++ b/editor/libeditor/TextEditor.cpp
@@ -10,16 +10,17 @@
 #include "InternetCiter.h"
 #include "TextEditUtils.h"
 #include "gfxFontUtils.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/EditAction.h"
 #include "mozilla/EditorDOMPoint.h"
 #include "mozilla/EditorUtils.h" // AutoPlaceholderBatch, AutoRules
 #include "mozilla/HTMLEditor.h"
+#include "mozilla/IMEStateManager.h"
 #include "mozilla/mozalloc.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/TextEditRules.h"
 #include "mozilla/TextComposition.h"
 #include "mozilla/TextEvents.h"
 #include "mozilla/TextServicesDocument.h"
 #include "mozilla/dom/Selection.h"
 #include "mozilla/dom/Event.h"
@@ -31,16 +32,17 @@
 #include "nsComponentManagerUtils.h"
 #include "nsContentCID.h"
 #include "nsContentList.h"
 #include "nsCopySupport.h"
 #include "nsDebug.h"
 #include "nsDependentSubstring.h"
 #include "nsError.h"
 #include "nsGkAtoms.h"
+#include "nsIAbsorbingTransaction.h"
 #include "nsIClipboard.h"
 #include "nsIContent.h"
 #include "nsIContentIterator.h"
 #include "nsIDOMNode.h"
 #include "nsIDocumentEncoder.h"
 #include "nsINode.h"
 #include "nsIPresShell.h"
 #include "nsISelectionController.h"
@@ -1130,96 +1132,150 @@ TextEditor::SetText(const nsAString& aSt
         NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new text");
       }
     }
   }
   // post-process
   return rules->DidDoAction(selection, &ruleInfo, rv);
 }
 
+bool
+TextEditor::EnsureComposition(WidgetCompositionEvent& aCompositionEvent)
+{
+  if (mComposition) {
+    return true;
+  }
+  // The compositionstart event must cause creating new TextComposition
+  // instance at being dispatched by IMEStateManager.
+  mComposition = IMEStateManager::GetTextCompositionFor(&aCompositionEvent);
+  if (!mComposition) {
+    // However, TextComposition may be committed before the composition
+    // event comes here.
+    return false;
+  }
+  mComposition->StartHandlingComposition(this);
+  return true;
+}
+
 nsresult
-TextEditor::BeginIMEComposition(WidgetCompositionEvent* aEvent)
+TextEditor::OnCompositionStart(WidgetCompositionEvent& aCompositionStartEvent)
 {
-  NS_ENSURE_TRUE(!mComposition, NS_OK);
+  if (NS_WARN_IF(mComposition)) {
+    return NS_OK;
+  }
 
   if (IsPasswordEditor()) {
-    NS_ENSURE_TRUE(mRules, NS_ERROR_NULL_POINTER);
+    if (NS_WARN_IF(!mRules)) {
+      return NS_ERROR_FAILURE;
+    }
     // Protect the edit rules object from dying
     RefPtr<TextEditRules> rules(mRules);
     rules->ResetIMETextPWBuf();
   }
 
-  return EditorBase::BeginIMEComposition(aEvent);
+  EnsureComposition(aCompositionStartEvent);
+  NS_WARNING_ASSERTION(mComposition, "Failed to get TextComposition instance?");
+  return NS_OK;
 }
 
 nsresult
-TextEditor::UpdateIMEComposition(WidgetCompositionEvent* aCompsitionChangeEvent)
+TextEditor::OnCompositionChange(WidgetCompositionEvent& aCompsitionChangeEvent)
 {
-  MOZ_ASSERT(aCompsitionChangeEvent,
-             "aCompsitionChangeEvent must not be nullptr");
-
-  if (NS_WARN_IF(!aCompsitionChangeEvent)) {
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  MOZ_ASSERT(aCompsitionChangeEvent->mMessage == eCompositionChange,
+  MOZ_ASSERT(aCompsitionChangeEvent.mMessage == eCompositionChange,
              "The event should be eCompositionChange");
 
   if (!EnsureComposition(aCompsitionChangeEvent)) {
     return NS_OK;
   }
 
-  nsCOMPtr<nsIPresShell> ps = GetPresShell();
-  NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED);
+  nsIPresShell* presShell = GetPresShell();
+  if (NS_WARN_IF(!presShell)) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
 
   RefPtr<Selection> selection = GetSelection();
-  NS_ENSURE_STATE(selection);
+  if (NS_WARN_IF(!selection)) {
+    return NS_ERROR_FAILURE;
+  }
 
   // NOTE: TextComposition should receive selection change notification before
   //       CompositionChangeEventHandlingMarker notifies TextComposition of the
   //       end of handling compositionchange event because TextComposition may
   //       need to ignore selection changes caused by composition.  Therefore,
   //       CompositionChangeEventHandlingMarker must be destroyed after a call
   //       of NotifiyEditorObservers(eNotifyEditorObserversOfEnd) or
   //       NotifiyEditorObservers(eNotifyEditorObserversOfCancel) which notifies
   //       TextComposition of a selection change.
   MOZ_ASSERT(!mPlaceholderBatch,
     "UpdateIMEComposition() must be called without place holder batch");
   TextComposition::CompositionChangeEventHandlingMarker
-    compositionChangeEventHandlingMarker(mComposition, aCompsitionChangeEvent);
+    compositionChangeEventHandlingMarker(mComposition, &aCompsitionChangeEvent);
 
-  RefPtr<nsCaret> caretP = ps->GetCaret();
+  RefPtr<nsCaret> caretP = presShell->GetCaret();
 
   nsresult rv;
   {
     AutoPlaceholderBatch batch(this, nsGkAtoms::IMETxnName);
 
     MOZ_ASSERT(mIsInEditAction,
       "AutoPlaceholderBatch should've notified the observes of before-edit");
-    rv = InsertTextAsAction(aCompsitionChangeEvent->mData);
+    rv = InsertTextAsAction(aCompsitionChangeEvent.mData);
     NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
       "Failed to insert new composition string");
 
     if (caretP) {
       caretP->SetSelection(selection);
     }
   }
 
   // If still composing, we should fire input event via observer.
   // Note that if the composition will be committed by the following
   // compositionend event, we don't need to notify editor observes of this
   // change.
   // NOTE: We must notify after the auto batch will be gone.
-  if (!aCompsitionChangeEvent->IsFollowedByCompositionEnd()) {
+  if (!aCompsitionChangeEvent.IsFollowedByCompositionEnd()) {
     NotifyEditorObservers(eNotifyEditorObserversOfEnd);
   }
 
   return rv;
 }
 
+void
+TextEditor::OnCompositionEnd(WidgetCompositionEvent& aCompositionEndEvent)
+{
+  if (NS_WARN_IF(!mComposition)) {
+    return;
+  }
+
+  // commit the IME transaction..we can get at it via the transaction mgr.
+  // Note that this means IME won't work without an undo stack!
+  if (mTransactionManager) {
+    nsCOMPtr<nsITransaction> txn = mTransactionManager->PeekUndoStack();
+    nsCOMPtr<nsIAbsorbingTransaction> plcTxn = do_QueryInterface(txn);
+    if (plcTxn) {
+      DebugOnly<nsresult> rv = plcTxn->Commit();
+      NS_ASSERTION(NS_SUCCEEDED(rv),
+                   "nsIAbsorbingTransaction::Commit() failed");
+    }
+  }
+
+  // Composition string may have hidden the caret.  Therefore, we need to
+  // cancel it here.
+  HideCaret(false);
+
+  // FYI: mComposition still keeps storing container text node of committed
+  //      string, its offset and length.  However, they will be invalidated
+  //      soon since its Destroy() will be called by IMEStateManager.
+  mComposition->EndHandlingComposition(this);
+  mComposition = nullptr;
+
+  // notify editor observers of action
+  NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+}
+
 already_AddRefed<nsIContent>
 TextEditor::GetInputEventTargetContent()
 {
   nsCOMPtr<nsIContent> target = do_QueryInterface(mEventTarget);
   return target.forget();
 }
 
 nsresult
--- a/editor/libeditor/TextEditor.h
+++ b/editor/libeditor/TextEditor.h
@@ -116,20 +116,16 @@ public:
    */
   virtual nsresult SelectEntireDocument(Selection* aSelection) override;
 
   virtual nsresult HandleKeyPressEvent(
                      WidgetKeyboardEvent* aKeyboardEvent) override;
 
   virtual dom::EventTarget* GetDOMEventTarget() override;
 
-  virtual nsresult BeginIMEComposition(WidgetCompositionEvent* aEvent) override;
-  virtual nsresult UpdateIMEComposition(
-                     WidgetCompositionEvent* aCompositionChangeEvet) override;
-
   virtual already_AddRefed<nsIContent> GetInputEventTargetContent() override;
 
   /**
    * DeleteSelectionAsAction() removes selection content or content around
    * caret with transactions.  This should be used for handling it as an
    * edit action.
    *
    * @param aDirection          How much range should be removed.
@@ -224,16 +220,39 @@ public:
   /**
    * Replace existed string with a string.
    * This is fast path to replace all string when using single line control.
    *
    * @ param aString   the string to be set
    */
   nsresult SetText(const nsAString& aString);
 
+  /**
+   * OnCompositionStart() is called when editor receives eCompositionStart
+   * event which should be handled in this editor.
+   */
+  nsresult OnCompositionStart(WidgetCompositionEvent& aCompositionStartEvent);
+
+  /**
+   * OnCompositionChange() is called when editor receives an eCompositioChange
+   * event which should be handled in this editor.
+   *
+   * @param aCompositionChangeEvent     eCompositionChange event which should
+   *                                    be handled in this editor.
+   */
+  nsresult
+  OnCompositionChange(WidgetCompositionEvent& aCompositionChangeEvent);
+
+  /**
+   * OnCompositionEnd() is called when editor receives an eCompositionChange
+   * event and it's followed by eCompositionEnd event and after
+   * OnCompositionChange() is called.
+   */
+  void OnCompositionEnd(WidgetCompositionEvent& aCompositionEndEvent);
+
 protected:
   virtual ~TextEditor();
 
   NS_IMETHOD InitRules();
   void BeginEditorInit();
   nsresult EndEditorInit();
 
   already_AddRefed<nsIDocumentEncoder> GetAndInitDocEncoder(
@@ -304,16 +323,28 @@ protected:
   bool CanCutOrCopy(PasswordFieldAllowed aPasswordFieldAllowed);
   bool FireClipboardEvent(EventMessage aEventMessage,
                           int32_t aSelectionType,
                           bool* aActionTaken = nullptr);
 
   bool UpdateMetaCharset(nsIDocument& aDocument,
                          const nsACString& aCharacterSet);
 
+  /**
+   * EnsureComposition() should be called by composition event handlers.  This
+   * tries to get the composition for the event and set it to mComposition.
+   * However, this may fail because the composition may be committed before
+   * the event comes to the editor.
+   *
+   * @return            true if there is a composition.  Otherwise, for example,
+   *                    a composition event handler in web contents moved focus
+   *                    for committing the composition, returns false.
+   */
+  bool EnsureComposition(WidgetCompositionEvent& aCompositionEvent);
+
 protected:
   nsCOMPtr<nsIDocumentEncoder> mCachedDocumentEncoder;
   nsString mCachedDocumentEncoderType;
   int32_t mWrapColumn;
   int32_t mMaxTextLength;
   int32_t mInitTriggerCounter;
   int32_t mNewlineHandling;
   int32_t mCaretStyle;
--- a/gfx/layers/AnimationHelper.cpp
+++ b/gfx/layers/AnimationHelper.cpp
@@ -130,26 +130,26 @@ void
 CompositorAnimationStorage::SetAnimations(uint64_t aId, const AnimationArray& aValue)
 {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
   AnimationArray* value = new AnimationArray(aValue);
   mAnimations.Put(aId, value);
 }
 
 
-void
+AnimationHelper::SampleResult
 AnimationHelper::SampleAnimationForEachNode(
   TimeStamp aTime,
   AnimationArray& aAnimations,
   InfallibleTArray<AnimData>& aAnimationData,
-  RefPtr<RawServoAnimationValue>& aAnimationValue,
-  bool& aHasInEffectAnimations)
+  RefPtr<RawServoAnimationValue>& aAnimationValue)
 {
   MOZ_ASSERT(!aAnimations.IsEmpty(), "Should be called with animations");
 
+  bool hasInEffectAnimations = false;
   // Process in order, since later aAnimations override earlier ones.
   for (size_t i = 0, iEnd = aAnimations.Length(); i < iEnd; ++i) {
     Animation& animation = aAnimations[i];
     AnimData& animData = aAnimationData[i];
 
     MOZ_ASSERT((!animation.originTime().IsNull() &&
                 animation.startTime().type() ==
                   MaybeTimeDuration::TTimeDuration) ||
@@ -160,37 +160,47 @@ AnimationHelper::SampleAnimationForEachN
     // finished, then use the hold time to stay at the same position.
     TimeDuration elapsedDuration =
       animation.isNotPlaying() ||
       animation.startTime().type() != MaybeTimeDuration::TTimeDuration
       ? animation.holdTime()
       : (aTime - animation.originTime() -
          animation.startTime().get_TimeDuration())
         .MultDouble(animation.playbackRate());
-    TimingParams timing {
-      animation.duration(),
-      animation.delay(),
-      animation.endDelay(),
-      animation.iterations(),
-      animation.iterationStart(),
-      static_cast<dom::PlaybackDirection>(animation.direction()),
-      static_cast<dom::FillMode>(animation.fillMode()),
-      Move(AnimationUtils::TimingFunctionToComputedTimingFunction(
-           animation.easingFunction()))
-    };
 
     ComputedTiming computedTiming =
       dom::AnimationEffectReadOnly::GetComputedTimingAt(
-        dom::Nullable<TimeDuration>(elapsedDuration), timing,
+        dom::Nullable<TimeDuration>(elapsedDuration), animData.mTiming,
         animation.playbackRate());
 
     if (computedTiming.mProgress.IsNull()) {
       continue;
     }
 
+    dom::IterationCompositeOperation iterCompositeOperation =
+        static_cast<dom::IterationCompositeOperation>(
+          animation.iterationComposite());
+
+    // Skip caluculation if the progress hasn't changed since the last
+    // calculation.
+    // Note that we don't skip calculate this animation if there is another
+    // animation since the other animation might be 'accumulate' or 'add', or
+    // might have a missing keyframe (i.e. this animation value will be used in
+    // the missing keyframe).
+    // FIXME Bug 1455476: We should do this optimizations for the case where
+    // the layer has multiple animations.
+    if (iEnd == 1 &&
+        !dom::KeyframeEffectReadOnly::HasComputedTimingChanged(
+          computedTiming,
+          iterCompositeOperation,
+          animData.mProgressOnLastCompose,
+          animData.mCurrentIterationOnLastCompose)) {
+      return SampleResult::Skipped;
+    }
+
     uint32_t segmentIndex = 0;
     size_t segmentSize = animation.segments().Length();
     AnimationSegment* segment = animation.segments().Elements();
     while (segment->endPortion() < computedTiming.mProgress.Value() &&
            segmentIndex < segmentSize - 1) {
       ++segment;
       ++segmentIndex;
     }
@@ -199,42 +209,56 @@ AnimationHelper::SampleAnimationForEachN
       (computedTiming.mProgress.Value() - segment->startPortion()) /
       (segment->endPortion() - segment->startPortion());
 
     double portion =
       ComputedTimingFunction::GetPortion(animData.mFunctions[segmentIndex],
                                          positionInSegment,
                                      computedTiming.mBeforeFlag);
 
+    // Like above optimization, skip caluculation if the target segment isn't
+    // changed and if the portion in the segment isn't changed.
+    // This optimization is needed for CSS animations/transitions with step
+    // timing functions (e.g. the throbber animation on tab or frame based
+    // animations).
+    // FIXME Bug 1455476: Like the above optimization, we should apply this
+    // optimizations for multiple animation cases as well.
+    if (iEnd == 1 &&
+        animData.mSegmentIndexOnLastCompose == segmentIndex &&
+        !animData.mPortionInSegmentOnLastCompose.IsNull() &&
+        animData.mPortionInSegmentOnLastCompose.Value() == portion) {
+      return SampleResult::Skipped;
+    }
+
     AnimationPropertySegment animSegment;
     animSegment.mFromKey = 0.0;
     animSegment.mToKey = 1.0;
     animSegment.mFromValue =
       AnimationValue(animData.mStartValues[segmentIndex]);
     animSegment.mToValue =
       AnimationValue(animData.mEndValues[segmentIndex]);
     animSegment.mFromComposite =
       static_cast<dom::CompositeOperation>(segment->startComposite());
     animSegment.mToComposite =
       static_cast<dom::CompositeOperation>(segment->endComposite());
 
     // interpolate the property
-    dom::IterationCompositeOperation iterCompositeOperation =
-        static_cast<dom::IterationCompositeOperation>(
-          animation.iterationComposite());
-
     aAnimationValue =
       Servo_ComposeAnimationSegment(
         &animSegment,
         aAnimationValue,
         animData.mEndValues.LastElement(),
         iterCompositeOperation,
         portion,
         computedTiming.mCurrentIteration).Consume();
-    aHasInEffectAnimations = true;
+    hasInEffectAnimations = true;
+    animData.mProgressOnLastCompose = computedTiming.mProgress;
+    animData.mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
+    animData.mSegmentIndexOnLastCompose = segmentIndex;
+    animData.mPortionInSegmentOnLastCompose.SetValue(portion);
   }
 
 #ifdef DEBUG
   // Sanity check that all of animation data are the same.
   const AnimationData& lastData = aAnimations.LastElement().data();
   for (const Animation& animation : aAnimations) {
     const AnimationData& data = animation.data();
     MOZ_ASSERT(data.type() == lastData.type(),
@@ -250,16 +274,18 @@ AnimationHelper::SampleAnimationForEachN
                transformData.transformOrigin() ==
                  lastTransformData.transformOrigin() &&
                transformData.bounds() == lastTransformData.bounds() &&
                transformData.appUnitsPerDevPixel() ==
                  lastTransformData.appUnitsPerDevPixel(),
                "All of members of TransformData should be the same");
   }
 #endif
+
+  return hasInEffectAnimations ? SampleResult::Sampled : SampleResult::None;
 }
 
 struct BogusAnimation {};
 
 static inline Result<Ok, BogusAnimation>
 SetCSSAngle(const CSSAngle& aAngle, nsCSSValue& aValue)
 {
   aValue.SetFloatValue(aAngle.value(), nsCSSUnit(aAngle.unit()));
@@ -460,16 +486,28 @@ AnimationHelper::SetAnimations(
         break;
     }
 
     if (animation.baseStyle().type() != Animatable::Tnull_t) {
       aBaseAnimationStyle = ToAnimationValue(animation.baseStyle());
     }
 
     AnimData* data = aAnimData.AppendElement();
+
+    data->mTiming = TimingParams {
+      animation.duration(),
+      animation.delay(),
+      animation.endDelay(),
+      animation.iterations(),
+      animation.iterationStart(),
+      static_cast<dom::PlaybackDirection>(animation.direction()),
+      static_cast<dom::FillMode>(animation.fillMode()),
+      Move(AnimationUtils::TimingFunctionToComputedTimingFunction(
+           animation.easingFunction()))
+    };
     InfallibleTArray<Maybe<ComputedTimingFunction>>& functions =
       data->mFunctions;
     InfallibleTArray<RefPtr<RawServoAnimationValue>>& startValues =
       data->mStartValues;
     InfallibleTArray<RefPtr<RawServoAnimationValue>>& endValues =
       data->mEndValues;
 
     const InfallibleTArray<AnimationSegment>& segments = animation.segments();
@@ -506,34 +544,33 @@ AnimationHelper::SampleAnimations(Compos
   // Do nothing if there are no compositor animations
   if (!aStorage->AnimationsCount()) {
     return;
   }
 
   //Sample the animations in CompositorAnimationStorage
   for (auto iter = aStorage->ConstAnimationsTableIter();
        !iter.Done(); iter.Next()) {
-    bool hasInEffectAnimations = false;
     AnimationArray* animations = iter.UserData();
     if (animations->IsEmpty()) {
       continue;
     }
 
     RefPtr<RawServoAnimationValue> animationValue;
     InfallibleTArray<AnimData> animationData;
     AnimationHelper::SetAnimations(*animations,
                                    animationData,
                                    animationValue);
-    AnimationHelper::SampleAnimationForEachNode(aTime,
-                                                *animations,
-                                                animationData,
-                                                animationValue,
-                                                hasInEffectAnimations);
+    AnimationHelper::SampleResult sampleResult =
+      AnimationHelper::SampleAnimationForEachNode(aTime,
+                                                  *animations,
+                                                  animationData,
+                                                  animationValue);
 
-    if (!hasInEffectAnimations) {
+    if (sampleResult != AnimationHelper::SampleResult::Sampled) {
       continue;
     }
 
     // Store the AnimatedValue
     Animation& animation = animations->LastElement();
     switch (animation.property()) {
       case eCSSProperty_opacity: {
         aStorage->SetAnimatedValue(
--- a/gfx/layers/AnimationHelper.h
+++ b/gfx/layers/AnimationHelper.h
@@ -2,32 +2,44 @@
 /* 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 mozilla_layers_AnimationHelper_h
 #define mozilla_layers_AnimationHelper_h
 
+#include "mozilla/dom/Nullable.h"
 #include "mozilla/ComputedTimingFunction.h" // for ComputedTimingFunction
 #include "mozilla/layers/LayersMessages.h" // for TransformData, etc
 #include "mozilla/TimeStamp.h"          // for TimeStamp
-
+#include "mozilla/TimingParams.h"
+#include "X11UndefineNone.h"
 
 namespace mozilla {
 struct AnimationValue;
 namespace layers {
 class Animation;
 
 typedef InfallibleTArray<layers::Animation> AnimationArray;
 
 struct AnimData {
   InfallibleTArray<RefPtr<RawServoAnimationValue>> mStartValues;
   InfallibleTArray<RefPtr<RawServoAnimationValue>> mEndValues;
   InfallibleTArray<Maybe<mozilla::ComputedTimingFunction>> mFunctions;
+  TimingParams mTiming;
+  // These two variables correspond to the variables of the same name in
+  // KeyframeEffectReadOnly and are used for the same purpose: to skip composing
+  // animations whose progress has not changed.
+  dom::Nullable<double> mProgressOnLastCompose;
+  uint64_t mCurrentIterationOnLastCompose = 0;
+  // These two variables are used for a similar optimization above but are
+  // applied to the timing function in each keyframe.
+  uint32_t mSegmentIndexOnLastCompose = 0;
+  dom::Nullable<double> mPortionInSegmentOnLastCompose;
 };
 
 struct AnimationTransform {
   /*
    * This transform is calculated from sampleanimation in device pixel
    * and used by compositor.
    */
   gfx::Matrix4x4 mTransformInDevSpace;
@@ -188,26 +200,37 @@ private:
  * This utility class allows reusing code between the webrender and
  * non-webrender compositor-side implementations. It provides
  * utility functions for sampling animations at particular timestamps.
  */
 class AnimationHelper
 {
 public:
 
+  enum class SampleResult {
+    None,
+    Skipped,
+    Sampled
+  };
+
   /**
    * Sample animations based on a given time stamp for a element(layer) with
    * its animation data.
+   *
+   * Returns SampleResult::None if none of the animations are producing a result
+   * (e.g. they are in the delay phase with no backwards fill),
+   * SampleResult::Skipped if the animation output did not change since the last
+   * call of this function,
+   * SampleResult::Sampled if the animation output was updated.
    */
-  static void
+  static SampleResult
   SampleAnimationForEachNode(TimeStamp aTime,
                              AnimationArray& aAnimations,
                              InfallibleTArray<AnimData>& aAnimationData,
-                             RefPtr<RawServoAnimationValue>& aAnimationValue,
-                             bool& aHasInEffectAnimations);
+                             RefPtr<RawServoAnimationValue>& aAnimationValue);
   /**
    * Populates AnimData stuctures into |aAnimData| and |aBaseAnimationStyle|
    * based on |aAnimations|.
    */
   static void
   SetAnimations(AnimationArray& aAnimations,
                 InfallibleTArray<AnimData>& aAnimData,
                 RefPtr<RawServoAnimationValue>& aBaseAnimationStyle);
--- a/gfx/layers/Layers.cpp
+++ b/gfx/layers/Layers.cpp
@@ -208,16 +208,27 @@ Layer::SetCompositorAnimations(const Com
     this, ("Layer::Mutated(%p) SetCompositorAnimations with id=%" PRIu64, this, mAnimationInfo.GetCompositorAnimationsId()));
 
   mAnimationInfo.SetCompositorAnimations(aCompositorAnimations);
 
   Mutated();
 }
 
 void
+Layer::ClearCompositorAnimations()
+{
+  MOZ_LAYERS_LOG_IF_SHADOWABLE(
+    this, ("Layer::Mutated(%p) ClearCompositorAnimations with id=%" PRIu64, this, mAnimationInfo.GetCompositorAnimationsId()));
+
+  mAnimationInfo.ClearAnimations();
+
+  Mutated();
+}
+
+void
 Layer::StartPendingAnimations(const TimeStamp& aReadyTime)
 {
   ForEachNode<ForwardIterator>(
       this,
       [&aReadyTime](Layer *layer)
       {
         if (layer->mAnimationInfo.StartPendingAnimations(aReadyTime)) {
           layer->Mutated();
--- a/gfx/layers/Layers.h
+++ b/gfx/layers/Layers.h
@@ -1204,16 +1204,18 @@ public:
   // layout code.  To add an animation to this layer, use AddAnimation.
   void SetCompositorAnimations(const CompositorAnimations& aCompositorAnimations);
   // Go through all animations in this layer and its children and, for
   // any animations with a null start time, update their start time such
   // that at |aReadyTime| the animation's current time corresponds to its
   // 'initial current time' value.
   void StartPendingAnimations(const TimeStamp& aReadyTime);
 
+  void ClearCompositorAnimations();
+
   /**
    * CONSTRUCTION PHASE ONLY
    * If a layer represents a fixed position element, this data is stored on the
    * layer for use by the compositor.
    *
    *   - |aScrollId| identifies the scroll frame that this element is fixed
    *     with respect to.
    *
--- a/gfx/layers/composite/AsyncCompositionManager.cpp
+++ b/gfx/layers/composite/AsyncCompositionManager.cpp
@@ -590,16 +590,18 @@ ApplyAnimatedValue(Layer* aLayer,
   HostLayer* layerCompositor = aLayer->AsHostLayer();
   switch (aProperty) {
     case eCSSProperty_opacity: {
       float opacity = Servo_AnimationValue_GetOpacity(aValue);
       layerCompositor->SetShadowOpacity(opacity);
       layerCompositor->SetShadowOpacitySetByAnimation(true);
       aStorage->SetAnimatedValue(aLayer->GetCompositorAnimationsId(), opacity);
 
+      layerCompositor->SetShadowBaseTransform(aLayer->GetBaseTransform());
+      layerCompositor->SetShadowTransformSetByAnimation(false);
       break;
     }
     case eCSSProperty_transform: {
       RefPtr<nsCSSValueSharedList> list;
       Servo_AnimationValue_GetTransform(aValue, &list);
       const TransformData& transformData = aAnimationData.get_TransformData();
       nsPoint origin = transformData.origin();
       // we expect all our transform data to arrive in device pixels
@@ -627,16 +629,19 @@ ApplyAnimatedValue(Layer* aLayer,
         transform.PostScale(c->GetInheritedXScale(), c->GetInheritedYScale(), 1);
       }
 
       layerCompositor->SetShadowBaseTransform(transform);
       layerCompositor->SetShadowTransformSetByAnimation(true);
       aStorage->SetAnimatedValue(aLayer->GetCompositorAnimationsId(),
                                  Move(transform), Move(frameTransform),
                                  transformData);
+
+      layerCompositor->SetShadowOpacity(aLayer->GetOpacity());
+      layerCompositor->SetShadowOpacitySetByAnimation(false);
       break;
     }
     default:
       MOZ_ASSERT_UNREACHABLE("Unhandled animated property");
   }
 }
 
 static bool
@@ -650,31 +655,68 @@ SampleAnimations(Layer* aLayer,
       aLayer,
       [&] (Layer* layer)
       {
         AnimationArray& animations = layer->GetAnimations();
         if (animations.IsEmpty()) {
           return;
         }
         isAnimating = true;
-        bool hasInEffectAnimations = false;
         RefPtr<RawServoAnimationValue> animationValue =
           layer->GetBaseAnimationStyle();
-        AnimationHelper::SampleAnimationForEachNode(aTime,
-                                                    animations,
-                                                    layer->GetAnimationData(),
-                                                    animationValue,
-                                                    hasInEffectAnimations);
-        if (hasInEffectAnimations) {
-          Animation& animation = animations.LastElement();
-          ApplyAnimatedValue(layer,
-                             aStorage,
-                             animation.property(),
-                             animation.data(),
-                             animationValue);
+        AnimationHelper::SampleResult sampleResult =
+          AnimationHelper::SampleAnimationForEachNode(aTime,
+                                                      animations,
+                                                      layer->GetAnimationData(),
+                                                      animationValue);
+        switch (sampleResult) {
+          case AnimationHelper::SampleResult::Sampled: {
+            Animation& animation = animations.LastElement();
+            ApplyAnimatedValue(layer,
+                               aStorage,
+                               animation.property(),
+                               animation.data(),
+                               animationValue);
+            break;
+          }
+          case AnimationHelper::SampleResult::Skipped:
+            // We don't need to update animation values for this layer since
+            // the values haven't changed.
+#ifdef DEBUG
+            // Sanity check that the animation value is surely unchanged.
+            switch (animations[0].property()) {
+              case eCSSProperty_opacity:
+                MOZ_ASSERT(FuzzyEqualsMultiplicative(
+                  layer->AsHostLayer()->GetShadowOpacity(),
+                  *(aStorage->GetAnimationOpacity(layer->GetCompositorAnimationsId()))));
+                break;
+              case eCSSProperty_transform: {
+                AnimatedValue* transform =
+                  aStorage->GetAnimatedValue(layer->GetCompositorAnimationsId());
+                MOZ_ASSERT(
+                  transform->mTransform.mTransformInDevSpace.FuzzyEqualsMultiplicative(
+                    (layer->AsHostLayer()->GetShadowBaseTransform())));
+                break;
+              }
+              default:
+                MOZ_ASSERT_UNREACHABLE("Unsupported properties");
+                break;
+            }
+#endif
+            break;
+          case AnimationHelper::SampleResult::None: {
+            HostLayer* layerCompositor = layer->AsHostLayer();
+            layerCompositor->SetShadowBaseTransform(layer->GetBaseTransform());
+            layerCompositor->SetShadowTransformSetByAnimation(false);
+            layerCompositor->SetShadowOpacity(layer->GetOpacity());
+            layerCompositor->SetShadowOpacitySetByAnimation(false);
+            break;
+          }
+          default:
+            break;
         }
       });
 
   return isAnimating;
 }
 
 void
 AsyncCompositionManager::RecordShadowTransforms(Layer* aLayer)
--- a/gfx/layers/ipc/CompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CompositorBridgeParent.cpp
@@ -915,22 +915,28 @@ CompositorBridgeParent::SetShadowPropert
         }
         for (size_t i = 0; i < layer->GetAncestorMaskLayerCount(); i++) {
           SetShadowProperties(layer->GetAncestorMaskLayerAt(i));
         }
 
         // FIXME: Bug 717688 -- Do these updates in LayerTransactionParent::RecvUpdate.
         HostLayer* layerCompositor = layer->AsHostLayer();
         // Set the layerComposite's base transform to the layer's base transform.
-        layerCompositor->SetShadowBaseTransform(layer->GetBaseTransform());
-        layerCompositor->SetShadowTransformSetByAnimation(false);
+        AnimationArray& animations = layer->GetAnimations();
+        // If there is any animation, the animation value will override
+        // non-animated value later, so we don't need to set the non-animated
+        // value here.
+        if (animations.IsEmpty()) {
+          layerCompositor->SetShadowBaseTransform(layer->GetBaseTransform());
+          layerCompositor->SetShadowTransformSetByAnimation(false);
+          layerCompositor->SetShadowOpacity(layer->GetOpacity());
+          layerCompositor->SetShadowOpacitySetByAnimation(false);
+        }
         layerCompositor->SetShadowVisibleRegion(layer->GetVisibleRegion());
         layerCompositor->SetShadowClipRect(layer->GetClipRect());
-        layerCompositor->SetShadowOpacity(layer->GetOpacity());
-        layerCompositor->SetShadowOpacitySetByAnimation(false);
       }
     );
 }
 
 void
 CompositorBridgeParent::CompositeToTarget(DrawTarget* aTarget, const gfx::IntRect* aRect)
 {
   AUTO_PROFILER_TRACING("Paint", "Composite");
--- a/gfx/layers/ipc/LayerTransactionParent.cpp
+++ b/gfx/layers/ipc/LayerTransactionParent.cpp
@@ -974,16 +974,17 @@ LayerTransactionParent::RecvReleaseLayer
 {
   RefPtr<Layer> layer;
   if (!aHandle || !mLayerMap.Remove(aHandle.Value(), getter_AddRefs(layer))) {
     return IPC_FAIL_NO_REASON(this);
   }
   if (mAnimStorage &&
       layer->GetCompositorAnimationsId()) {
     mAnimStorage->ClearById(layer->GetCompositorAnimationsId());
+    layer->ClearCompositorAnimations();
   }
   layer->Disconnect();
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
 LayerTransactionParent::RecvReleaseCompositable(const CompositableHandle& aHandle)
 {
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -3431,30 +3431,38 @@ FindAncestorWithGeneratedContentPseudo(n
 }
 
 #define SIMPLE_FCDATA(_func) FCDATA_DECL(0, _func)
 #define FULL_CTOR_FCDATA(_flags, _func)                             \
   { _flags | FCDATA_FUNC_IS_FULL_CTOR, { nullptr }, _func, nullptr }
 
 /* static */
 const nsCSSFrameConstructor::FrameConstructionData*
-nsCSSFrameConstructor::FindTextData(nsIFrame* aParentFrame)
-{
+nsCSSFrameConstructor::FindTextData(nsIFrame* aParentFrame,
+                                    nsIContent* aTextContent)
+{
+  MOZ_ASSERT(aTextContent, "How?");
   if (aParentFrame && IsFrameForSVG(aParentFrame)) {
-    nsIFrame *ancestorFrame =
+    nsIFrame* ancestorFrame =
       nsSVGUtils::GetFirstNonAAncestorFrame(aParentFrame);
-    if (ancestorFrame) {
-      static const FrameConstructionData sSVGTextData =
-        FCDATA_DECL(FCDATA_IS_LINE_PARTICIPANT | FCDATA_IS_SVG_TEXT,
-                    NS_NewTextFrame);
-      if (nsSVGUtils::IsInSVGTextSubtree(ancestorFrame)) {
-        return &sSVGTextData;
-      }
-    }
-    return nullptr;
+    if (!ancestorFrame || !nsSVGUtils::IsInSVGTextSubtree(ancestorFrame)) {
+      return nullptr;
+    }
+
+    // Don't render stuff in display: contents / Shadow DOM subtrees, because
+    // TextCorrespondenceRecorder in the SVG text code doesn't really know how
+    // to deal with it. This kinda sucks. :(
+    if (aParentFrame->GetContent() != aTextContent->GetParent()) {
+      return nullptr;
+    }
+
+    static const FrameConstructionData sSVGTextData =
+      FCDATA_DECL(FCDATA_IS_LINE_PARTICIPANT | FCDATA_IS_SVG_TEXT,
+                  NS_NewTextFrame);
+    return &sSVGTextData;
   }
 
   static const FrameConstructionData sTextData =
     FCDATA_DECL(FCDATA_IS_LINE_PARTICIPANT, NS_NewTextFrame);
   return &sTextData;
 }
 
 void
@@ -5765,17 +5773,17 @@ nsCSSFrameConstructor::AddFrameConstruct
     return;
   }
 
   bool isPopup = false;
   const bool isText = !aContent->IsElement();
   // Try to find frame construction data for this content
   const FrameConstructionData* data;
   if (isText) {
-    data = FindTextData(aParentFrame);
+    data = FindTextData(aParentFrame, aContent);
     if (!data) {
       // Nothing to do here; suppressed text inside SVG
       return;
     }
   } else {
     Element* element = aContent->AsElement();
 
     // Don't create frames for non-SVG element children of SVG elements.
--- a/layout/base/nsCSSFrameConstructor.h
+++ b/layout/base/nsCSSFrameConstructor.h
@@ -1417,17 +1417,18 @@ private:
   nsIFrame* ConstructDetailsFrame(nsFrameConstructorState& aState,
                                   FrameConstructionItem& aItem,
                                   nsContainerFrame* aParentFrame,
                                   const nsStyleDisplay* aStyleDisplay,
                                   nsFrameItems& aFrameItems);
 
   // aParentFrame might be null.  If it is, that means it was an
   // inline frame.
-  static const FrameConstructionData* FindTextData(nsIFrame* aParentFrame);
+  static const FrameConstructionData* FindTextData(nsIFrame* aParentFrame,
+                                                   nsIContent* aTextContent);
 
   void ConstructTextFrame(const FrameConstructionData* aData,
                           nsFrameConstructorState& aState,
                           nsIContent*              aContent,
                           nsContainerFrame*        aParentFrame,
                           ComputedStyle*          aComputedStyle,
                           nsFrameItems&            aFrameItems);
 
--- a/layout/svg/SVGTextFrame.cpp
+++ b/layout/svg/SVGTextFrame.cpp
@@ -4577,16 +4577,18 @@ SVGTextFrame::ResolvePositionsForNode(ns
         mPositions[aIndex].mPosition.x = 0.0;
       }
       if (!mPositions[aIndex].IsYSpecified()) {
         mPositions[aIndex].mPosition.y = 0.0;
       }
       mPositions[aIndex].mStartOfChunk = true;
     }
   } else if (!aContent->IsSVGElement(nsGkAtoms::a)) {
+    MOZ_ASSERT(aContent->IsSVGElement());
+
     // We have a text content element that can have x/y/dx/dy/rotate attributes.
     nsSVGElement* element = static_cast<nsSVGElement*>(aContent);
 
     // Get x, y, dx, dy.
     SVGUserUnitList x, y, dx, dy;
     element->GetAnimatedLengthListValues(&x, &y, &dx, &dy, nullptr);
 
     // Get rotate.
new file mode 100644
--- /dev/null
+++ b/layout/svg/crashtests/1421807-1.html
@@ -0,0 +1,5 @@
+<body onload=b.appendChild(a)>
+<div id="a" style="display: contents">
+</div>
+<svg>
+<text id="b">
new file mode 100644
--- /dev/null
+++ b/layout/svg/crashtests/1421807-2.html
@@ -0,0 +1,15 @@
+<style>
+.c1 { display: contents; }
+</style>
+<script>
+function go() {
+  a.attachShadow({mode: "open"}).innerHTML = `<slot> </slot> `;
+  b.appendChild(a);
+}
+</script>
+<body onload=go()>
+<div id="a" class="c1">
+  <span></span>
+</div>
+<svg>
+<text id="b">
--- a/layout/svg/crashtests/crashtests.list
+++ b/layout/svg/crashtests/crashtests.list
@@ -203,8 +203,10 @@ load 1322852.html
 load 1348564.svg
 load 1402109.html
 load 1402124.html
 load 1402486.html
 load conditional-outer-svg-nondirty-reflow-assert.xhtml
 load extref-test-1.xhtml
 load blob-merging-and-retained-display-list.html
 load grouping-empty-bounds.html
+load 1421807-1.html
+pref(dom.webcomponents.shadowdom.enabled,true) load 1421807-2.html
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -538,17 +538,17 @@
 <!ENTITY pref_tab_queue_summary4 "Save links until the next time you open &brandShortName;">
 
 <!-- Localization note (pref_compact_tabs): Label for setting that controls whether 1 wide column or
      2 narrower (compact) columns are used for tabs in the tabs tray in portrait mode on phones. -->
 <!ENTITY pref_compact_tabs "Compact tabs">
 <!ENTITY pref_compact_tabs_summary2 "Arrange tabs in two columns in portrait mode">
 
 <!-- Localization note (page_removed): This string appears in a toast message when
-     any page is removed frome about:home. This includes pages that are in history,
+     any page is removed from about:home. This includes pages that are in history,
      bookmarks, or reading list. -->
 <!ENTITY page_removed "Page removed">
 <!ENTITY folder_removed "Folder removed">
 
 <!ENTITY bookmark_edit_title "Edit Bookmark">
 <!ENTITY bookmark_edit_folder_title "Edit Folder">
 <!ENTITY bookmark_edit_name "Name">
 <!ENTITY bookmark_edit_location "Location">
@@ -756,17 +756,17 @@ just addresses the organization to follo
 
 <!-- Localization note (remote_tabs_last_synced): the variable is replaced by a
      "relative time span string" produced by Android.  This string describes the
      time the tabs were last synced relative to the current time; examples
      include "42 minutes ago", "4 days ago", "last week", etc. The subject of
      "Last synced" is one of the user's other Sync clients, typically Firefox on
      their desktop or laptop.-->
 <!ENTITY remote_tabs_last_synced "Last synced: &formatS;">
-<!-- Localization note: Used when the sync has not happend yet, showed in place of a date -->
+<!-- Localization note: Used when the sync has not happened yet, showed in place of a date -->
 <!ENTITY remote_tabs_never_synced "Last synced: never">
 
 <!-- LOCALIZATION NOTE (intent_uri_private_browsing_prompt): This string will
      appear in an alert when a user, who is currently in private browsing,
      clicks a link that will open an external Android application. "&formatS;"
      will be replaced with the name of the application that will be opened. -->
 <!ENTITY intent_uri_private_browsing_prompt "This link will open in &formatS;. Are you sure you want to exit Private Browsing?">
 <!-- LOCALIZATION NOTE (intent_uri_private_browsing_multiple_match_title): This
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -371,17 +371,17 @@ clickToPlayPlugins.dontActivate=Don't ac
 # checkbox to indicate whether or not the user wants to make a permanent decision.
 clickToPlayPlugins.dontAskAgain=Don't ask again for this site
 # LOCALIZATION NOTE (clickToPlayPlugins.plugins): Label that
 # will be used in site settings dialog.
 clickToPlayPlugins.plugins=Plugins
 
 # Site settings dialog
 # LOCALIZATION NOTE (siteSettings.labelToValue): This string will be used to
-# dislay a list of current permissions settings for a site.
+# display a list of current permissions settings for a site.
 # Example: "Store Offline Data: Allow"
 siteSettings.labelToValue=%S: %S
 
 masterPassword.incorrect=Incorrect password
 
 # Debugger
 # LOCALIZATION NOTE (remoteIncomingPromptTitle): The title displayed on the
 # dialog that prompts the user to allow the incoming connection.
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -233,17 +233,17 @@ pref("dom.keyboardevent.dispatch_during_
 pref("dom.keyboardevent.keypress.dispatch_non_printable_keys_only_system_group_in_content", true);
 // Blacklist of domains of web apps which are not aware of strict keypress
 // dispatching behavior.  This is comma separated list.  If you need to match
 // all sub-domains, you can specify it as "*.example.com".  Additionally, you
 // can limit the path.  E.g., "example.com/foo" means "example.com/foo*".  So,
 // if you need to limit under a directory, the path should end with "/" like
 // "example.com/foo/".  Note that this cannot limit port number for now.
 pref("dom.keyboardevent.keypress.hack.dispatch_non_printable_keys",
-     "docs.google.com,mail.google.com,hangouts.google.com,keep.google.com,*.etherpad.org/p/,etherpad.wikimedia.org/p/,board.net/p/,pad.riseup.net/p/,*.sandstorm.io,factor.cc/pad/,*.etherpad.fr/p/,piratenpad.de/p/,notes.typo3.org/p/,etherpad.net/p/,mensuel.framapad.org/p/,pad.ouvaton.coop/,pad.systemli.org/p/,pad.lqdn.fr/p/,public.etherpad-mozilla.org/p/,*.cloudron.me/p/,pad.aquilenet.fr/p/,free.primarypad.com/p/,pad.ondesk.work/p/,demo.maadix.org/etherpad/pads/");
+     "docs.google.com,mail.google.com,hangouts.google.com,keep.google.com,inbox.google.com,*.etherpad.org/p/,etherpad.wikimedia.org/p/,board.net/p/,pad.riseup.net/p/,*.sandstorm.io,factor.cc/pad/,*.etherpad.fr/p/,piratenpad.de/p/,notes.typo3.org/p/,etherpad.net/p/,mensuel.framapad.org/p/,pad.ouvaton.coop/,pad.systemli.org/p/,pad.lqdn.fr/p/,public.etherpad-mozilla.org/p/,*.cloudron.me/p/,pad.aquilenet.fr/p/,free.primarypad.com/p/,pad.ondesk.work/p/,demo.maadix.org/etherpad/pads/");
 #else
 pref("dom.keyboardevent.keypress.dispatch_non_printable_keys_only_system_group_in_content", false);
 #endif
 
 // Whether the WebMIDI API is enabled
 pref("dom.webmidi.enabled", false);
 
 // Whether to enable the JavaScript start-up cache. This causes one of the first
@@ -2508,17 +2508,17 @@ pref("security.view-source.reachable-fro
 // If set to true, in some limited circumstances it may be possible to load
 // privileged content in frames inside unprivileged content.
 pref("security.allow_chrome_frames_inside_content", false);
 
 // Services security settings
 pref("services.settings.server", "https://firefox.settings.services.mozilla.com/v1");
 pref("services.settings.changes.path", "/buckets/monitor/collections/changes/records");
 pref("services.settings.default_bucket", "main");
-pref("services.settings.default_signer", "");
+pref("services.settings.default_signer", "remote-settings.content-signature.mozilla.org");
 
 // Blocklist preferences
 pref("extensions.blocklist.enabled", true);
 // OneCRL freshness checking depends on this value, so if you change it,
 // please also update security.onecrl.maximum_staleness_in_seconds.
 pref("extensions.blocklist.interval", 86400);
 // Required blocklist freshness for OneCRL OCSP bypass
 // (default is 1.25x extensions.blocklist.interval, or 30 hours)
--- a/netwerk/base/nsITimedChannel.idl
+++ b/netwerk/base/nsITimedChannel.idl
@@ -4,27 +4,31 @@
 
 #include "nsISupports.idl"
 interface nsIArray;
 interface nsIPrincipal;
 %{C++
 namespace mozilla {
 class TimeStamp;
 }
+#include "nsTArrayForwardDeclare.h"
+#include "nsCOMPtr.h"
 %}
 
 native TimeStamp(mozilla::TimeStamp);
 
 [scriptable, uuid(c2d9e95b-9cc9-4f47-9ef6-1de0cf7ebc75)]
 interface nsIServerTiming : nsISupports {
   [must_use] readonly attribute ACString name;
   [must_use] readonly attribute double duration;
   [must_use] readonly attribute ACString description;
 };
 
+[ref] native nsServerTimingArrayRef(nsTArray<nsCOMPtr<nsIServerTiming>>);
+
 // All properties return zero if the value is not available
 [scriptable, uuid(ca63784d-959c-4c3a-9a59-234a2a520de0)]
 interface nsITimedChannel : nsISupports {
   // Set this attribute to true to enable collection of timing data.
   // channelCreationTime will be available even with this attribute set to
   // false.
   attribute boolean timingEnabled;
 
@@ -105,9 +109,10 @@ interface nsITimedChannel : nsISupports 
   readonly attribute PRTime cacheReadEndTime;
   readonly attribute PRTime redirectStartTime;
   readonly attribute PRTime redirectEndTime;
 
   // If this attribute is false, this resource MUST NOT be reported in resource timing.
   [noscript] attribute boolean reportResourceTiming;
 
   readonly attribute nsIArray serverTiming;
+  nsServerTimingArrayRef getNativeServerTiming();
 };
--- a/netwerk/dns/TRR.cpp
+++ b/netwerk/dns/TRR.cpp
@@ -814,17 +814,17 @@ TRR::FailData()
 
 nsresult
 TRR::On200Response()
 {
   // decode body and create an AddrInfo struct for the response
   nsresult rv = DohDecode();
 
   if (NS_SUCCEEDED(rv)) {
-    if (!mCname.IsEmpty()) {
+    if (!mDNS.mAddresses.getFirst() && !mCname.IsEmpty()) {
       if (!--mCnameLoop) {
         LOG(("TRR::On200Response CNAME loop, eject!\n"));
       } else  {
         LOG(("TRR::On200Response CNAME %s => %s (%u)\n", mHost.get(), mCname.get(),
              mCnameLoop));
         RefPtr<TRR> trr = new TRR(mHostResolver, mRec, mCname,
                                   mType, mCnameLoop, mPB);
         rv = NS_DispatchToMainThread(trr);
--- a/netwerk/protocol/http/HttpBaseChannel.cpp
+++ b/netwerk/protocol/http/HttpBaseChannel.cpp
@@ -4245,21 +4245,16 @@ HttpBaseChannel::GetPerformanceStorage()
   }
 
   // If a custom performance storage is set, let's use it.
   mozilla::dom::PerformanceStorage* performanceStorage = mLoadInfo->GetPerformanceStorage();
   if (performanceStorage) {
     return performanceStorage;
   }
 
-  // We don't need to report the resource timing entry for a TYPE_DOCUMENT load.
-  if (mLoadInfo->GetExternalContentPolicyType() == nsIContentPolicy::TYPE_DOCUMENT) {
-    return nullptr;
-  }
-
   nsCOMPtr<nsIDOMDocument> domDocument;
   mLoadInfo->GetLoadingDocument(getter_AddRefs(domDocument));
   if (!domDocument) {
     return nullptr;
   }
 
   nsCOMPtr<nsIDocument> loadingDocument = do_QueryInterface(domDocument);
   if (!loadingDocument) {
@@ -4282,16 +4277,41 @@ HttpBaseChannel::GetPerformanceStorage()
   mozilla::dom::Performance* performance = innerWindow->GetPerformance();
   if (!performance) {
     return nullptr;
   }
 
   return performance->AsPerformanceStorage();
 }
 
+void
+HttpBaseChannel::MaybeReportTimingData()
+{
+  // We don't need to report the resource timing entry for a TYPE_DOCUMENT load.
+  // But for the case that Server-Timing headers are existed for
+  // a document load, we have to create the document entry early
+  // with the timed channel. This is the only way to make
+  // server timing data availeble in the document entry.
+  if (mLoadInfo->GetExternalContentPolicyType() == nsIContentPolicy::TYPE_DOCUMENT) {
+    if ((mResponseHead && mResponseHead->HasHeader(nsHttp::Server_Timing)) ||
+        (mResponseTrailers && mResponseTrailers->HasHeader(nsHttp::Server_Timing))) {
+      mozilla::dom::PerformanceStorage* documentPerformance = GetPerformanceStorage();
+      if (documentPerformance) {
+        documentPerformance->CreateDocumentEntry(this);
+      }
+    }
+    return;
+  }
+
+  mozilla::dom::PerformanceStorage* documentPerformance = GetPerformanceStorage();
+  if (documentPerformance) {
+      documentPerformance->AddEntry(this, this);
+  }
+}
+
 NS_IMETHODIMP
 HttpBaseChannel::SetReportResourceTiming(bool enabled) {
   mReportTiming = enabled;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 HttpBaseChannel::GetReportResourceTiming(bool* _retval) {
@@ -4541,52 +4561,64 @@ HttpBaseChannel::CallTypeSniffers(void *
   if (!newType.IsEmpty()) {
     chan->SetContentType(newType);
   }
 }
 
 template <class T>
 static void
 ParseServerTimingHeader(const nsAutoPtr<T> &aHeader,
-                        nsIMutableArray* aOutput)
+                        nsTArray<nsCOMPtr<nsIServerTiming>>& aOutput)
 {
   if (!aHeader) {
     return;
   }
 
   nsAutoCString serverTimingHeader;
   Unused << aHeader->GetHeader(nsHttp::Server_Timing, serverTimingHeader);
   if (serverTimingHeader.IsEmpty()) {
     return;
   }
 
   ServerTimingParser parser(serverTimingHeader);
   parser.Parse();
 
   nsTArray<nsCOMPtr<nsIServerTiming>> array = parser.TakeServerTimingHeaders();
-  for (const auto &data : array) {
-    aOutput->AppendElement(data);
-  }
+  aOutput.AppendElements(array);
 }
 
 NS_IMETHODIMP
 HttpBaseChannel::GetServerTiming(nsIArray **aServerTiming)
 {
+  nsresult rv;
   NS_ENSURE_ARG_POINTER(aServerTiming);
 
+  nsCOMPtr<nsIMutableArray> array = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsTArray<nsCOMPtr<nsIServerTiming>> data;
+  rv = GetNativeServerTiming(data);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  for (const auto &entry : data) {
+    array->AppendElement(entry);
+  }
+
+  array.forget(aServerTiming);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+HttpBaseChannel::GetNativeServerTiming(nsTArray<nsCOMPtr<nsIServerTiming>>& aServerTiming)
+{
+  aServerTiming.Clear();
+
   bool isHTTPS = false;
   if (NS_SUCCEEDED(mURI->SchemeIs("https", &isHTTPS)) && isHTTPS) {
-    nsTArray<nsCOMPtr<nsIServerTiming>> data;
-    nsresult rv = NS_OK;
-    nsCOMPtr<nsIMutableArray> array = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    ParseServerTimingHeader(mResponseHead, array);
-    ParseServerTimingHeader(mResponseTrailers, array);
-
-    array.forget(aServerTiming);
+    ParseServerTimingHeader(mResponseHead, aServerTiming);
+    ParseServerTimingHeader(mResponseTrailers, aServerTiming);
   }
 
   return NS_OK;
 }
 
 } // namespace net
 } // namespace mozilla
--- a/netwerk/protocol/http/HttpBaseChannel.h
+++ b/netwerk/protocol/http/HttpBaseChannel.h
@@ -424,16 +424,17 @@ protected:
 
   // This is fired only when a cookie is created due to the presence of
   // Set-Cookie header in the response header of any network request.
   // This notification will come only after the "http-on-examine-response"
   // was fired.
   void NotifySetCookie(char const *aCookie);
 
   mozilla::dom::PerformanceStorage* GetPerformanceStorage();
+  void MaybeReportTimingData();
   nsIURI* GetReferringPage();
   nsPIDOMWindowInner* GetInnerDOMWindow();
 
   void AddCookiesToRequest();
   virtual MOZ_MUST_USE nsresult
   SetupReplacementChannel(nsIURI *, nsIChannel *, bool preserveMethod,
                           uint32_t redirectFlags);
 
--- a/netwerk/protocol/http/HttpChannelChild.cpp
+++ b/netwerk/protocol/http/HttpChannelChild.cpp
@@ -1208,20 +1208,17 @@ void
 HttpChannelChild::DoPreOnStopRequest(nsresult aStatus)
 {
   LOG(("HttpChannelChild::DoPreOnStopRequest [this=%p status=%" PRIx32 "]\n",
        this, static_cast<uint32_t>(aStatus)));
   mIsPending = false;
 
   MaybeCallSynthesizedCallback();
 
-  PerformanceStorage* performanceStorage = GetPerformanceStorage();
-  if (performanceStorage) {
-      performanceStorage->AddEntry(this, this);
-  }
+  MaybeReportTimingData();
 
   if (!mCanceled && NS_SUCCEEDED(mStatus)) {
     mStatus = aStatus;
   }
 
   CollectOMTTelemetry();
 }
 
--- a/netwerk/protocol/http/InterceptedHttpChannel.cpp
+++ b/netwerk/protocol/http/InterceptedHttpChannel.cpp
@@ -1079,20 +1079,17 @@ InterceptedHttpChannel::OnStopRequest(ns
   // progress when OnStopRequest() is triggered.  Report any left over
   // progress immediately.  The extra runnable will then do nothing thanks
   // to the ReleaseListeners() call below.
   MaybeCallStatusAndProgress();
 
   mIsPending = false;
 
   // Register entry to the PerformanceStorage resource timing
-  mozilla::dom::PerformanceStorage* performanceStorage = GetPerformanceStorage();
-  if (performanceStorage) {
-    performanceStorage->AddEntry(this, this);
-  }
+  MaybeReportTimingData();
 
   if (mListener) {
     mListener->OnStopRequest(this, mListenerContext, mStatus);
   }
 
   gHttpHandler->OnStopRequest(this);
 
   ReleaseListeners();
--- a/netwerk/protocol/http/NullHttpChannel.cpp
+++ b/netwerk/protocol/http/NullHttpChannel.cpp
@@ -896,16 +896,22 @@ NullHttpChannel::GetReportResourceTiming
 }
 
 NS_IMETHODIMP
 NullHttpChannel::GetServerTiming(nsIArray **aServerTiming)
 {
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
+NS_IMETHODIMP
+NullHttpChannel::GetNativeServerTiming(nsTArray<nsCOMPtr<nsIServerTiming>>& aServerTiming)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
 #define IMPL_TIMING_ATTR(name)                                 \
 NS_IMETHODIMP                                                  \
 NullHttpChannel::Get##name##Time(PRTime* _retval) {            \
     TimeStamp stamp;                                           \
     Get##name(&stamp);                                         \
     if (stamp.IsNull()) {                                      \
         *_retval = 0;                                          \
         return NS_OK;                                          \
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -7426,20 +7426,17 @@ nsHttpChannel::OnStopRequest(nsIRequest 
                      static_cast<uint32_t>(rv)));
             }
         }
     }
 
     ReportRcwnStats(isFromNet);
 
     // Register entry to the PerformanceStorage resource timing
-    mozilla::dom::PerformanceStorage* performanceStorage = GetPerformanceStorage();
-    if (performanceStorage) {
-        performanceStorage->AddEntry(this, this);
-    }
+    MaybeReportTimingData();
 
     if (mListener) {
         LOG(("nsHttpChannel %p calling OnStopRequest\n", this));
         MOZ_ASSERT(mOnStartRequestCalled,
                    "OnStartRequest should be called before OnStopRequest");
         MOZ_ASSERT(!mOnStopRequestCalled,
                    "We should not call OnStopRequest twice");
         mListener->OnStopRequest(this, mListenerContext, status);
--- a/netwerk/protocol/http/nsServerTiming.h
+++ b/netwerk/protocol/http/nsServerTiming.h
@@ -9,17 +9,17 @@
 
 #include "nsITimedChannel.h"
 #include "nsString.h"
 #include "nsTArray.h"
 
 class nsServerTiming final : public nsIServerTiming
 {
 public:
-  NS_DECL_ISUPPORTS
+  NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSISERVERTIMING
 
   nsServerTiming() = default;
 
   void SetName(const nsACString &aName)
   {
     mName = aName;
   }
--- a/python/docs/index.rst
+++ b/python/docs/index.rst
@@ -32,17 +32,17 @@ Using a Python package index
 If the Python package is not used in the building of Firefox then it can be
 installed from a package index. Some tasks are not permitted to use external
 resources, and for those we can publish packages to an internal PyPI mirror.
 See `how to upload to internal PyPI <https://wiki.mozilla.org/ReleaseEngineering/How_To/Upload_to_internal_Pypi>`_
 for more details. If you are not restricted, you can install packages from PyPI
 or another package index.
 
 All packages installed from a package index **MUST** specify hashes to ensure
-compatibiliy and protect against remote tampering. Hash-checking mode can be
+compatibility and protect against remote tampering. Hash-checking mode can be
 forced on when using ``pip`` be specifying the ``--require-hashes``
 command-line option. See `hash-checking mode <https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode>`_ for
 more details.
 
 Note that when using a Python package index there is a risk that the service
 could be unavailable, or packages may be updated or even pulled without notice.
 These issues are less likely with our internal PyPI mirror, but still possible.
 If this is undesirable, then consider vendoring the package.
--- a/security/manager/locales/en-US/chrome/pipnss/pipnss.properties
+++ b/security/manager/locales/en-US/chrome/pipnss/pipnss.properties
@@ -12,17 +12,17 @@ CertPassPromptDefault=Please enter your 
 #
 # It's possible to verify the length of a translation using the Browser Console
 # in Firefox and evaluating the following code:
 #
 # (new TextEncoder('utf-8').encode('YOURSTRING')).length
 #
 # Simply replace YOURSTRING with your translation.
 #
-# If it's not possible to produce an understandable translation withing these
+# If it's not possible to produce an understandable translation within these
 # limits, keeping the English text is an acceptable workaround.
 
 # LOCALIZATION NOTE (RootCertModuleName): string limit is 64 bytes after
 # conversion to UTF-8.
 # length_limit = 64 bytes
 RootCertModuleName=Builtin Roots Module
 # LOCALIZATION NOTE (ManufacturerID): string limit is 32 bytes after conversion
 # to UTF-8.
--- a/servo/components/style/applicable_declarations.rs
+++ b/servo/components/style/applicable_declarations.rs
@@ -5,138 +5,157 @@
 //! Applicable declarations management.
 
 use properties::PropertyDeclarationBlock;
 use rule_tree::{CascadeLevel, ShadowCascadeOrder, StyleSource};
 use servo_arc::Arc;
 use shared_lock::Locked;
 use smallvec::SmallVec;
 use std::fmt::{self, Debug};
-use std::mem;
 
 /// List of applicable declarations. This is a transient structure that shuttles
 /// declarations between selector matching and inserting into the rule tree, and
 /// therefore we want to avoid heap-allocation where possible.
 ///
 /// In measurements on wikipedia, we pretty much never have more than 8 applicable
 /// declarations, so we could consider making this 8 entries instead of 16.
 /// However, it may depend a lot on workload, and stack space is cheap.
 pub type ApplicableDeclarationList = SmallVec<[ApplicableDeclarationBlock; 16]>;
 
 /// Blink uses 18 bits to store source order, and does not check overflow [1].
 /// That's a limit that could be reached in realistic webpages, so we use
 /// 24 bits and enforce defined behavior in the overflow case.
 ///
-/// Note that the value of 24 is also hard-coded into the level() accessor,
-/// which does a byte-aligned load of the 4th byte. If you change this value
-/// you'll need to change that as well.
-///
 /// [1] https://cs.chromium.org/chromium/src/third_party/WebKit/Source/core/css/
 ///     RuleSet.h?l=128&rcl=90140ab80b84d0f889abc253410f44ed54ae04f3
+const SOURCE_ORDER_SHIFT: usize = 0;
 const SOURCE_ORDER_BITS: usize = 24;
-const SOURCE_ORDER_MASK: u32 = (1 << SOURCE_ORDER_BITS) - 1;
-const SOURCE_ORDER_MAX: u32 = SOURCE_ORDER_MASK;
+const SOURCE_ORDER_MAX: u32 = (1 << SOURCE_ORDER_BITS) - 1;
+const SOURCE_ORDER_MASK: u32 = SOURCE_ORDER_MAX << SOURCE_ORDER_SHIFT;
 
-/// Stores the source order of a block and the cascade level it belongs to.
+/// We store up-to-15 shadow order levels.
+///
+/// You'd need an element slotted across 16 components with ::slotted rules to
+/// trigger this as of this writing, which looks... Unlikely.
+const SHADOW_CASCADE_ORDER_SHIFT: usize = SOURCE_ORDER_BITS;
+const SHADOW_CASCADE_ORDER_BITS: usize = 4;
+const SHADOW_CASCADE_ORDER_MAX: u8 = (1 << SHADOW_CASCADE_ORDER_BITS) - 1;
+const SHADOW_CASCADE_ORDER_MASK: u32 = (SHADOW_CASCADE_ORDER_MAX as u32) << SHADOW_CASCADE_ORDER_SHIFT;
+
+const CASCADE_LEVEL_SHIFT: usize = SOURCE_ORDER_BITS + SHADOW_CASCADE_ORDER_BITS;
+const CASCADE_LEVEL_BITS: usize = 4;
+const CASCADE_LEVEL_MAX: u8 = (1 << CASCADE_LEVEL_BITS) - 1;
+const CASCADE_LEVEL_MASK: u32 = (CASCADE_LEVEL_MAX as u32) << CASCADE_LEVEL_SHIFT;
+
+/// Stores the source order of a block, the cascade level it belongs to, and the
+/// counter needed to handle Shadow DOM cascade order properly.
 #[derive(Clone, Copy, Eq, MallocSizeOf, PartialEq)]
-struct SourceOrderAndCascadeLevel(u32);
+struct ApplicableDeclarationBits(u32);
 
-impl SourceOrderAndCascadeLevel {
-    fn new(source_order: u32, cascade_level: CascadeLevel) -> SourceOrderAndCascadeLevel {
+impl ApplicableDeclarationBits {
+    fn new(
+        source_order: u32,
+        cascade_level: CascadeLevel,
+        shadow_cascade_order: ShadowCascadeOrder,
+    ) -> Self {
+        debug_assert!(
+            cascade_level as u8 <= CASCADE_LEVEL_MAX,
+            "Gotta find more bits!"
+        );
         let mut bits = ::std::cmp::min(source_order, SOURCE_ORDER_MAX);
-        bits |= (cascade_level as u8 as u32) << SOURCE_ORDER_BITS;
-        SourceOrderAndCascadeLevel(bits)
+        bits |= ((shadow_cascade_order & SHADOW_CASCADE_ORDER_MAX) as u32) << SHADOW_CASCADE_ORDER_SHIFT;
+        bits |= (cascade_level as u8 as u32) << CASCADE_LEVEL_SHIFT;
+        ApplicableDeclarationBits(bits)
     }
 
-    fn order(&self) -> u32 {
-        self.0 & SOURCE_ORDER_MASK
+    fn source_order(&self) -> u32 {
+        (self.0 & SOURCE_ORDER_MASK) >> SOURCE_ORDER_SHIFT
+    }
+
+    fn shadow_cascade_order(&self) -> ShadowCascadeOrder {
+        ((self.0 & SHADOW_CASCADE_ORDER_MASK) >> SHADOW_CASCADE_ORDER_SHIFT) as ShadowCascadeOrder
     }
 
     fn level(&self) -> CascadeLevel {
-        unsafe {
-            // Transmute rather than shifting so that we're sure the compiler
-            // emits a simple byte-aligned load.
-            let as_bytes: [u8; 4] = mem::transmute(self.0);
-            CascadeLevel::from_byte(as_bytes[3])
-        }
+        let byte = ((self.0 & CASCADE_LEVEL_MASK) >> CASCADE_LEVEL_SHIFT) as u8;
+        unsafe { CascadeLevel::from_byte(byte) }
     }
 }
 
-impl Debug for SourceOrderAndCascadeLevel {
+impl Debug for ApplicableDeclarationBits {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        f.debug_struct("SourceOrderAndCascadeLevel")
-            .field("order", &self.order())
+        f.debug_struct("ApplicableDeclarationBits")
+            .field("source_order", &self.source_order())
+            .field("shadow_cascade_order", &self.shadow_cascade_order())
             .field("level", &self.level())
             .finish()
     }
 }
 
 /// A property declaration together with its precedence among rules of equal
 /// specificity so that we can sort them.
 ///
 /// This represents the declarations in a given declaration block for a given
 /// importance.
 #[derive(Clone, Debug, MallocSizeOf, PartialEq)]
 pub struct ApplicableDeclarationBlock {
     /// The style source, either a style rule, or a property declaration block.
     #[ignore_malloc_size_of = "Arc"]
     pub source: StyleSource,
-    /// The source order of the block, and the cascade level it belongs to.
-    order_and_level: SourceOrderAndCascadeLevel,
+    /// The bits containing the source order, cascade level, and shadow cascade
+    /// order.
+    bits: ApplicableDeclarationBits,
     /// The specificity of the selector this block is represented by.
     pub specificity: u32,
-    /// The order in the tree of trees we carry on.
-    pub shadow_cascade_order: ShadowCascadeOrder,
 }
 
 impl ApplicableDeclarationBlock {
     /// Constructs an applicable declaration block from a given property
     /// declaration block and importance.
     #[inline]
     pub fn from_declarations(
         declarations: Arc<Locked<PropertyDeclarationBlock>>,
         level: CascadeLevel,
     ) -> Self {
         ApplicableDeclarationBlock {
             source: StyleSource::Declarations(declarations),
-            order_and_level: SourceOrderAndCascadeLevel::new(0, level),
+            bits: ApplicableDeclarationBits::new(0, level, 0),
             specificity: 0,
-            shadow_cascade_order: 0,
         }
     }
 
     /// Constructs an applicable declaration block from the given components
     #[inline]
     pub fn new(
         source: StyleSource,
         order: u32,
         level: CascadeLevel,
         specificity: u32,
-        shadow_cascade_order: u32,
+        shadow_cascade_order: ShadowCascadeOrder,
     ) -> Self {
         ApplicableDeclarationBlock {
             source,
-            order_and_level: SourceOrderAndCascadeLevel::new(order, level),
+            bits: ApplicableDeclarationBits::new(order, level, shadow_cascade_order),
             specificity,
-            shadow_cascade_order,
         }
     }
 
     /// Returns the source order of the block.
     #[inline]
     pub fn source_order(&self) -> u32 {
-        self.order_and_level.order()
+        self.bits.source_order()
     }
 
     /// Returns the cascade level of the block.
     #[inline]
     pub fn level(&self) -> CascadeLevel {
-        self.order_and_level.level()
+        self.bits.level()
     }
 
     /// Convenience method to consume self and return the right thing for the
     /// rule tree to iterate over.
     #[inline]
     pub fn for_rule_tree(self) -> (StyleSource, CascadeLevel, ShadowCascadeOrder) {
         let level = self.level();
-        (self.source, level, self.shadow_cascade_order)
+        let cascade_order = self.bits.shadow_cascade_order();
+        (self.source, level, cascade_order)
     }
 }
--- a/servo/components/style/rule_tree/mod.rs
+++ b/servo/components/style/rule_tree/mod.rs
@@ -165,17 +165,17 @@ const FREE_LIST_LOCKED: *mut RuleNode = 
 /// A counter to track how many inner shadow roots rules deep we are.
 ///
 /// This is used to handle:
 ///
 /// https://drafts.csswg.org/css-scoping/#shadow-cascading
 ///
 /// In particular, it'd be `0` for the innermost shadow host, `1` for the next,
 /// and so on.
-pub type ShadowCascadeOrder = u32;
+pub type ShadowCascadeOrder = u8;
 
 impl RuleTree {
     /// Construct a new rule tree.
     pub fn new() -> Self {
         RuleTree {
             root: StrongRuleNode::new(Box::new(RuleNode::root())),
         }
     }
--- a/servo/ports/geckolib/tests/size_of.rs
+++ b/servo/ports/geckolib/tests/size_of.rs
@@ -30,17 +30,17 @@ size_of_test!(test_size_of_cv, ComputedV
 size_of_test!(test_size_of_option_arc_cv, Option<Arc<ComputedValues>>, 8);
 size_of_test!(test_size_of_option_rule_node, Option<StrongRuleNode>, 8);
 
 size_of_test!(test_size_of_element_styles, ElementStyles, 16);
 size_of_test!(test_size_of_element_data, ElementData, 24);
 
 size_of_test!(test_size_of_property_declaration, style::properties::PropertyDeclaration, 32);
 
-size_of_test!(test_size_of_application_declaration_block, ApplicableDeclarationBlock, 32);
+size_of_test!(test_size_of_application_declaration_block, ApplicableDeclarationBlock, 24);
 size_of_test!(test_size_of_rule_node, RuleNode, 80);
 
 // This is huge, but we allocate it on the stack and then never move it,
 // we only pass `&mut SourcePropertyDeclaration` references around.
 size_of_test!(test_size_of_parsed_declaration, style::properties::SourcePropertyDeclaration, 608);
 
 size_of_test!(test_size_of_computed_image, computed::image::Image, 32);
 size_of_test!(test_size_of_specified_image, specified::image::Image, 32);
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -1,17 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 /* global XPCNativeWrapper */
 
 ChromeUtils.import("resource://gre/modules/Log.jsm");
-ChromeUtils.import("resource://gre/modules/Preferences.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.import("chrome://marionette/content/accessibility.js");
 const {Addon} = ChromeUtils.import("chrome://marionette/content/addon.js", {});
 ChromeUtils.import("chrome://marionette/content/assert.js");
 ChromeUtils.import("chrome://marionette/content/atom.js");
 const {
@@ -44,16 +43,17 @@ const {
   WebDriverError,
 } = ChromeUtils.import("chrome://marionette/content/error.js", {});
 ChromeUtils.import("chrome://marionette/content/evaluate.js");
 const {pprint} = ChromeUtils.import("chrome://marionette/content/format.js", {});
 ChromeUtils.import("chrome://marionette/content/interaction.js");
 ChromeUtils.import("chrome://marionette/content/l10n.js");
 ChromeUtils.import("chrome://marionette/content/legacyaction.js");
 ChromeUtils.import("chrome://marionette/content/modal.js");
+const {MarionettePrefs} = ChromeUtils.import("chrome://marionette/content/prefs.js", {});
 ChromeUtils.import("chrome://marionette/content/proxy.js");
 ChromeUtils.import("chrome://marionette/content/reftest.js");
 ChromeUtils.import("chrome://marionette/content/session.js");
 const {
   PollPromise,
   TimedPromise,
 } = ChromeUtils.import("chrome://marionette/content/sync.js", {});
 
@@ -61,19 +61,16 @@ Cu.importGlobalProperties(["URL"]);
 
 this.EXPORTED_SYMBOLS = ["GeckoDriver"];
 
 const APP_ID_FIREFOX = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
 
 const FRAME_SCRIPT = "chrome://marionette/content/listener.js";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
-const CLICK_TO_START_PREF = "marionette.debugging.clicktostart";
-const CONTENT_LISTENER_PREF = "marionette.contentListener";
-
 const SUPPORTED_STRATEGIES = new Set([
   element.Strategy.ClassName,
   element.Strategy.Selector,
   element.Strategy.ID,
   element.Strategy.TagName,
   element.Strategy.XPath,
   element.Strategy.Anon,
   element.Strategy.AnonAttribute,
@@ -472,22 +469,22 @@ GeckoDriver.prototype.whenBrowserStarted
         for (let i = 0; i < mm.childCount; i++) {
           if (mm.getChildAt(i).childCount !== 0) {
             this.curBrowser.frameRegsPending += 1;
           }
         }
       }
     }
 
-    if (!Preferences.get(CONTENT_LISTENER_PREF) || !isNewSession) {
+    if (!MarionettePrefs.contentListener || !isNewSession) {
       // load listener into the remote frame
       // and any applicable new frames
       // opened after this call
       mm.loadFrameScript(FRAME_SCRIPT, true);
-      Preferences.set(CONTENT_LISTENER_PREF, true);
+      MarionettePrefs.contentListener = true;
     }
   } else {
     logger.error("Unable to load content frame script");
   }
 };
 
 /**
  * Recursively get all labeled text.
@@ -731,25 +728,24 @@ GeckoDriver.prototype.newSession = async
         if (ev.target != win.document) {
           return;
         }
         win.removeEventListener("load", listener);
         waitForWindow.call(this);
       };
       win.addEventListener("load", listener, true);
     } else {
-      let clickToStart = Preferences.get(CLICK_TO_START_PREF);
-      if (clickToStart) {
+      if (MarionettePrefs.clickToStart) {
         Services.prompt.alert(win, "", "Click to start execution of marionette tests");
       }
       this.startBrowser(win, true);
     }
   };
 
-  if (!Preferences.get(CONTENT_LISTENER_PREF)) {
+  if (!MarionettePrefs.contentListener) {
     waitForWindow.call(this);
   } else if (this.appId != APP_ID_FIREFOX && this.curBrowser === null) {
     // if there is a content listener, then we just wake it up
     let win = this.getCurrentWindow();
     this.addBrowser(win);
     this.whenBrowserStarted(win, false);
   } else {
     throw new WebDriverError("Session already running");
@@ -2778,17 +2774,17 @@ GeckoDriver.prototype.closeChromeWindow 
   await this.curBrowser.closeWindow();
   return this.chromeWindowHandles.map(String);
 };
 
 /** Delete Marionette session. */
 GeckoDriver.prototype.deleteSession = function() {
   if (this.curBrowser !== null) {
     // frame scripts can be safely reused
-    Preferences.set(CONTENT_LISTENER_PREF, false);
+    MarionettePrefs.contentListener = false;
 
     globalMessageManager.broadcastAsyncMessage("Marionette:Session:Delete");
     globalMessageManager.broadcastAsyncMessage("Marionette:Deregister");
 
     for (let win of this.windows) {
       if (win.messageManager) {
         win.messageManager.removeDelayedFrameScript(FRAME_SCRIPT);
       } else {
--- a/testing/marionette/prefs.js
+++ b/testing/marionette/prefs.js
@@ -150,16 +150,41 @@ class MarionetteBranch extends Branch {
     return this.get("enabled", false);
   }
 
   set enabled(isEnabled) {
     this.set("enabled", isEnabled);
   }
 
   /**
+   * The `marionette.debugging.clicktostart` preference delays
+   * server startup until a modal dialogue has been clicked to allow
+   * time for user to set breakpoints in the Browser Toolbox.
+   *
+   * @return {boolean}
+   */
+  get clickToStart() {
+    return this.get("debugging.clicktostart", false);
+  }
+
+  /**
+   * Whether content scripts can be safely reused.
+   *
+   * @deprecated
+   * @return {boolean}
+   */
+  get contentListener() {
+    return this.get("contentListener", false);
+  }
+
+  set contentListener(value) {
+    this.set("contentListener", value);
+  }
+
+  /**
    * The `marionette.port` preference, detailing which port
    * the TCP server should listen on.
    *
    * @return {number}
    */
   get port() {
     return this.get("port", 2828);
   }
--- a/testing/marionette/prefs/marionette.js
+++ b/testing/marionette/prefs/marionette.js
@@ -6,17 +6,17 @@
 
 // Marionette is the remote protocol that lets OOP programs
 // communicate with, instrument, and control Gecko.
 
 // Starts and stops the Marionette server.
 pref("marionette.enabled", false);
 
 // Delay server startup until a modal dialogue has been clicked to
-// allow time for user to set breakpoints in Browser Toolbox.
+// allow time for user to set breakpoints in the Browser Toolbox.
 pref("marionette.debugging.clicktostart", false);
 
 // Marionette logging verbosity.  Allowed values are "fatal", "error",
 // "warn", "info", "config", "debug", and "trace".
 pref("marionette.log.level", "info");
 
 // Port to start Marionette server on.
 pref("marionette.port", 2828);
--- a/testing/marionette/server.js
+++ b/testing/marionette/server.js
@@ -7,50 +7,47 @@
 const CC = Components.Constructor;
 
 const ServerSocket = CC(
     "@mozilla.org/network/server-socket;1",
     "nsIServerSocket",
     "initSpecialConnection");
 
 ChromeUtils.import("resource://gre/modules/Log.jsm");
-ChromeUtils.import("resource://gre/modules/Preferences.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 ChromeUtils.import("chrome://marionette/content/assert.js");
 const {GeckoDriver} = ChromeUtils.import("chrome://marionette/content/driver.js", {});
 const {WebElement} = ChromeUtils.import("chrome://marionette/content/element.js", {});
 const {
   error,
   UnknownCommandError,
 } = ChromeUtils.import("chrome://marionette/content/error.js", {});
 const {
   Command,
   Message,
   Response,
 } = ChromeUtils.import("chrome://marionette/content/message.js", {});
+const {MarionettePrefs} = ChromeUtils.import("chrome://marionette/content/prefs.js", {});
 const {DebuggerTransport} = ChromeUtils.import("chrome://marionette/content/transport.js", {});
 
 const logger = Log.repository.getLogger("Marionette");
 
 const {KeepWhenOffline, LoopbackOnly} = Ci.nsIServerSocket;
 
 this.EXPORTED_SYMBOLS = [
   "TCPConnection",
   "TCPListener",
 ];
 
 /** @namespace */
 this.server = {};
 
 const PROTOCOL_VERSION = 3;
 
-const PREF_CONTENT_LISTENER = "marionette.contentListener";
-const PREF_PORT = "marionette.port";
-
 /**
  * Bootstraps Marionette and handles incoming client connections.
  *
  * Starting the Marionette server will open a TCP socket sporting the
  * debugger transport interface on the provided `port`.  For every
  * new connection, a {@link TCPConnection} is created.
  */
 class TCPListener {
@@ -70,17 +67,17 @@ class TCPListener {
    * Function produces a {@link GeckoDriver}.
    *
    * Determines the application to initialise the driver with.
    *
    * @return {GeckoDriver}
    *     A driver instance.
    */
   driverFactory() {
-    Preferences.set(PREF_CONTENT_LISTENER, false);
+    MarionettePrefs.contentListener = false;
     return new GeckoDriver(Services.appinfo.ID, this);
   }
 
   set acceptConnections(value) {
     if (value) {
       if (!this.socket) {
         try {
           const flags = KeepWhenOffline | LoopbackOnly;
@@ -112,17 +109,17 @@ class TCPListener {
    */
   start() {
     if (this.alive) {
       return;
     }
 
     // Start socket server and listening for connection attempts
     this.acceptConnections = true;
-    Preferences.set(PREF_PORT, this.port);
+    MarionettePrefs.port = this.port;
     this.alive = true;
   }
 
   stop() {
     if (!this.alive) {
       return;
     }
 
--- a/testing/marionette/test/unit/test_prefs.js
+++ b/testing/marionette/test/unit/test_prefs.js
@@ -98,28 +98,33 @@ add_test(function test_EnvironmentPrefs_
     env.set("FOO", null);
   }
 
   run_next_test();
 });
 
 add_test(function test_MarionettePrefs_getters() {
   equal(false, MarionettePrefs.enabled);
+  equal(false, MarionettePrefs.clickToStart);
+  equal(false, MarionettePrefs.contentListener);
   equal(2828, MarionettePrefs.port);
   equal(Log.Level.Info, MarionettePrefs.logLevel);
   equal(true, MarionettePrefs.recommendedPrefs);
 
   run_next_test();
 });
 
 add_test(function test_MarionettePrefs_setters() {
   try {
     MarionettePrefs.enabled = true;
+    MarionettePrefs.contentListener = true;
     MarionettePrefs.port = 777;
     equal(true, MarionettePrefs.enabled);
+    equal(true, MarionettePrefs.contentListener);
     equal(777, MarionettePrefs.port);
   } finally {
     Services.prefs.clearUserPref("marionette.enabled");
+    Services.prefs.clearUserPref("marionette.contentListener");
     Services.prefs.clearUserPref("marionette.port");
   }
 
   run_next_test();
 });
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -394372,16 +394372,28 @@
    "webdriver/tests/actions/special_keys.py": [
     [
      "/webdriver/tests/actions/special_keys.py",
      {
       "timeout": "long"
      }
     ]
    ],
+   "webdriver/tests/close_window/close.py": [
+    [
+     "/webdriver/tests/close_window/close.py",
+     {}
+    ]
+   ],
+   "webdriver/tests/close_window/user_prompts.py": [
+    [
+     "/webdriver/tests/close_window/user_prompts.py",
+     {}
+    ]
+   ],
    "webdriver/tests/contexts/json_serialize_windowproxy.py": [
     [
      "/webdriver/tests/contexts/json_serialize_windowproxy.py",
      {
       "timeout": "long"
      }
     ]
    ],
@@ -607403,16 +607415,24 @@
   "webdriver/tests/actions/support/refine.py": [
    "0d244bffe67ef57be68aad99f1cbc7440ff80e27",
    "support"
   ],
   "webdriver/tests/actions/support/test_actions_wdspec.html": [
    "95203777fcc012ab64465287737a89a4ba2c31dc",
    "support"
   ],
+  "webdriver/tests/close_window/close.py": [
+   "4138394adbccb06712b22a2f982e8638aa03629a",
+   "wdspec"
+  ],
+  "webdriver/tests/close_window/user_prompts.py": [
+   "6151fb105fafcfb462a580fc725b207eae725f6c",
+   "wdspec"
+  ],
   "webdriver/tests/conftest.py": [
    "c812269d034c9ca1b8c4f136dd5d0cea52f4d0f0",
    "support"
   ],
   "webdriver/tests/contexts/json_serialize_windowproxy.py": [
    "d29c82c48b3bd1e2b07c40798a774eb77d6178a5",
    "wdspec"
   ],
@@ -607636,17 +607656,17 @@
    "5a31a3917a5157516c10951a3b3d5ffb43b992d9",
    "support"
   ],
   "webdriver/tests/support/asserts.py": [
    "1b839404daaca1d059cba98377edb91691ef7e82",
    "support"
   ],
   "webdriver/tests/support/fixtures.py": [
-   "e787c0ef06fcdfc86f337be5af61e492be08ec9c",
+   "126e82c42568c9ecb240e735a2b1361882ecd9ba",
    "support"
   ],
   "webdriver/tests/support/http_request.py": [
    "cb40c781fea2280b98135522def5e6a116d7b946",
    "support"
   ],
   "webdriver/tests/support/inline.py": [
    "ffabd6a12d6e7928176fa00702214e0c8e0a25d7",
--- a/testing/web-platform/meta/css/cssom/interfaces.html.ini
+++ b/testing/web-platform/meta/css/cssom/interfaces.html.ini
@@ -3,22 +3,16 @@
     expected: FAIL
 
   [Stringification of document]
     expected: FAIL
 
   [ProcessingInstruction interface: attribute sheet]
     expected: FAIL
 
-  [StyleSheetList interface: existence and properties of interface prototype object]
-    expected: FAIL
-
-  [CSSRuleList interface: existence and properties of interface prototype object]
-    expected: FAIL
-
   [CSSRule interface: constant MARGIN_RULE on interface object]
     expected: FAIL
 
   [CSSRule interface: constant MARGIN_RULE on interface prototype object]
     expected: FAIL
 
   [CSSRule interface: style_element.sheet.cssRules[0\] must inherit property "MARGIN_RULE" with the proper type (6)]
     expected: FAIL
--- a/testing/web-platform/meta/server-timing/cross_origin.html.ini
+++ b/testing/web-platform/meta/server-timing/cross_origin.html.ini
@@ -1,4 +1,6 @@
 [cross_origin.html]
   [Untitled]
     expected: FAIL
 
+  [Expected entry count for http://web-platform.test:8000/server-timing/resources/blue.png: 1]
+    expected: FAIL
--- a/testing/web-platform/meta/server-timing/server_timing_header-parsing.html.ini
+++ b/testing/web-platform/meta/server-timing/server_timing_header-parsing.html.ini
@@ -1,4 +1,213 @@
 [server_timing_header-parsing.html]
   [Untitled]
     expected: FAIL
 
+  [1.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [2.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [3.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [4.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [5.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [7.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [6.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [8.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [9.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [10.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [11.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [12.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [13.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [14.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [15.js - count (0 ?== 3)]
+    expected: FAIL
+
+  [16.js - count (0 ?== 5)]
+    expected: FAIL
+
+  [17.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [18.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [19.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [20.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [21.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [22.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [23.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [24.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [25.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [26.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [28.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [27.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [29.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [30.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [31.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [33.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [32.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [34.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [35.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [37.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [36.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [38.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [39.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [40.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [42.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [41.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [43.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [44.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [45.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [46.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [47.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [48.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [50.js - count (0 ?== 2)]
+    expected: FAIL
+
+  [51.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [49.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [54.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [52.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [53.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [55.js - count (0 ?== 2)]
+    expected: FAIL
+
+  [56.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [57.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [59.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [58.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [60.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [61.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [63.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [65.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [62.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [64.js - count (0 ?== 2)]
+    expected: FAIL
+
+  [68.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [66.js - count (0 ?== 2)]
+    expected: FAIL
+
+  [67.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [69.js - count (0 ?== 1)]
+    expected: FAIL
+
+  [70.js - count (0 ?== 1)]
+    expected: FAIL
--- a/testing/web-platform/meta/server-timing/test_server_timing.html.ini
+++ b/testing/web-platform/meta/server-timing/test_server_timing.html.ini
@@ -3,8 +3,19 @@
     expected: FAIL
 
   [Untitled 1]
     expected: FAIL
 
   [Untitled 2]
     expected: FAIL
 
+  [Entry {"duration":1.1,"name":"metric1","description":"document"} could not be found.]
+    expected: FAIL
+
+  [Entry {"duration":1.2,"name":"metric1","description":"document"} could not be found.]
+    expected: FAIL
+
+  [Entry {"duration":2.1,"name":"metric2","description":"blue.png"} could not be found.]
+    expected: FAIL
+
+  [Entry {"duration":3.1,"name":"metric3","description":"green.png"} could not be found.]
+    expected: FAIL
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/webdriver/tests/close_window/user_prompts.py.ini
@@ -0,0 +1,3 @@
+[user_prompts.py]
+  [test_handle_prompt_accept]
+    expected: FAIL
--- a/testing/web-platform/mozilla/meta/MANIFEST.json
+++ b/testing/web-platform/mozilla/meta/MANIFEST.json
@@ -464,16 +464,22 @@
     ]
    ],
    "dom/throttling/throttling-ws.window.js": [
     [
      "/_mozilla/dom/throttling/throttling-ws.window.html",
      {}
     ]
    ],
+   "editor/cloning_attributes.html": [
+    [
+     "/_mozilla/editor/cloning_attributes.html",
+     {}
+    ]
+   ],
    "editor/initial_selection_on_focus.html": [
     [
      "/_mozilla/editor/initial_selection_on_focus.html",
      {}
     ]
    ],
    "editor/joining_nodes.html": [
     [
@@ -1029,16 +1035,20 @@
   "dom/throttling/throttling-webrtc.window.js": [
    "02e6acec2ff275e0e935cb6d903d348f98d5d437",
    "testharness"
   ],
   "dom/throttling/throttling-ws.window.js": [
    "67a981ba2a4d08b684947ed42aba6648dcd262b4",
    "testharness"
   ],
+  "editor/cloning_attributes.html": [
+   "257023d742dd84a93816e05d24f380e27405db66",
+   "testharness"
+  ],
   "editor/initial_selection_on_focus.html": [
    "06948dbf72160a7de5a0baaa2f6cf1bb54fbeb8f",
    "testharness"
   ],
   "editor/joining_nodes.html": [
    "048cf7d99acdecb927f97c4554c4d04ca8b15a8a",
    "testharness"
   ],
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/mozilla/meta/editor/cloning_attributes.html.ini
@@ -0,0 +1,68 @@
+[cloning_attributes.html]
+  type: testharness
+  [Should clone attributes of <span> element in <p> to <span> in new <p>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <span> element at end of <p> to <span> in new <p>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <li> element at splitting at middle of the <li>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <li> element at splitting at end of the <li>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <span> element in <li> to <span> in new <li>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <span> element at end of <li> to <span> in new <li>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <dt> element to new <dt> element at splitting at middle of the <dt>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <span> element in <dt> to <span> in new <dd>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <dd> element to new <dd> element at splitting at middle of the <dd>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <span> element in <dd> to <span> in new <dd>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <h1> element to new <h1> at splitting it at middle of the <h1>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <span> element in <h1> to <span> in new <h1>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <h2> element to new <h2> at splitting it at middle of the <h2>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <span> element in <h2> to <span> in new <h2>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <h3> element to new <h3> at splitting it at middle of the <h3>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <span> element in <h3> to <span> in new <h3>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <h4> element to new <h4> at splitting it at middle of the <h4>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <span> element in <h4> to <span> in new <h4>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <h5> element to new <h5> at splitting it at middle of the <h5>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <span> element in <h5> to <span> in new <h5>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <h6> element to new <h6> at splitting it at middle of the <h6>, right element, id attribute]
+    expected: FAIL
+
+  [Should clone attributes of <span> element in <h6> to <span> in new <h6>, right element, id attribute]
+    expected: FAIL
+
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/mozilla/tests/editor/cloning_attributes.html
@@ -0,0 +1,728 @@
+<!doctype html>
+<html>
+<head>
+<meta charset=utf-8>
+<title>Cloning attributes</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+</head>
+<body>
+<script>
+"use strict";
+
+(function() {
+  // These tests assume that id attribute should be only in the left element.
+  // However, this is not standardized and other browsers may behave
+  // differently.  So, it's okay to change the behavior for id attribute
+  // for compatibility with the other browsers.
+  const kTests = [
+    { description: "Should clone attributes of <p> element at splitting at middle of the <p>",
+      innerHTML: "<p foo=\"bar\" id=\"original\">foobar</p>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild, 3);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <p> element at splitting at end of the <p>",
+      innerHTML: "<p foo=\"bar\" id=\"original\">foobar</p>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild, 6);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <span> element in <p> to <span> in new <p>",
+      innerHTML: "<p>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span>ar</p>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.nextSibling.firstChild, 1);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <span> element at end of <p> to <span> in new <p>",
+      innerHTML: "<p>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span></p>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.nextSibling.firstChild, 2);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <li> element at splitting at middle of the <li>",
+      innerHTML: "<ul><li foo=\"bar\" id=\"original\">foobar</li></ul>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.firstChild, 3);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <li> element at splitting at end of the <li>",
+      innerHTML: "<ul><li foo=\"bar\" id=\"original\">foobar</li></ul>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.firstChild, 6);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <span> element in <li> to <span> in new <li>",
+      innerHTML: "<ul><li>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span>ar</li></ul>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.firstChild.nextSibling.firstChild, 1);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <span> element at end of <li> to <span> in new <li>",
+      innerHTML: "<ul><li>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span></li></ul>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.firstChild.nextSibling.firstChild, 2);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <dt> element to new <dt> element at splitting at middle of the <dt>",
+      innerHTML: "<dl><dt foo=\"bar\" id=\"original\">foobar</dt></dl>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.firstChild, 3);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <dt> element to new <dd> element at splitting at end of the <dt>",
+      innerHTML: "<dl><dt foo=\"bar\" id=\"original\">foobar</dt></dl>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.firstChild, 6);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <span> element in <dt> to <span> in new <dd>",
+      innerHTML: "<dl><dt>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span>ar</dt></dl>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.firstChild.nextSibling.firstChild, 1);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <span> element at end of <dt> to new <dd>",
+      innerHTML: "<dl><dt>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span></dt></dl>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.firstChild.nextSibling.firstChild, 2);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <dd> element to new <dd> element at splitting at middle of the <dd>",
+      innerHTML: "<dl><dd foo=\"bar\" id=\"original\">foobar</dd></dl>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.firstChild, 3);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <dd> element to new <dt> element at splitting at end of the <dd>",
+      innerHTML: "<dl><dd foo=\"bar\" id=\"original\">foobar</dd></dl>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.firstChild, 6);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <span> element in <dd> to <span> in new <dd>",
+      innerHTML: "<dl><dd>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span>ar</dd></dl>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.firstChild.nextSibling.firstChild, 1);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <span> element at end of <dd> to new <dt>",
+      innerHTML: "<dl><dd>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span></dd></dl>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.firstChild.nextSibling.firstChild, 2);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <h1> element to new <h1> at splitting it at middle of the <h1>",
+      innerHTML: "<h1 foo=\"bar\" id=\"original\">foobar</h1>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild, 3);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <h1> element to new <div> at splitting it at end of the <h1>",
+      innerHTML: "<h1 foo=\"bar\" id=\"original\">foobar</h1>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild, 6);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <span> element in <h1> to <span> in new <h1>",
+      innerHTML: "<h1>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span>ar</h1>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.nextSibling.firstChild, 1);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <span> element at end of <h1> to new <div>",
+      innerHTML: "<h1>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span></h1>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.nextSibling.firstChild, 2);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <h2> element to new <h2> at splitting it at middle of the <h2>",
+      innerHTML: "<h2 foo=\"bar\" id=\"original\">foobar</h2>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild, 3);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <h2> element to new <div> at splitting it at end of the <h2>",
+      innerHTML: "<h2 foo=\"bar\" id=\"original\">foobar</h2>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild, 6);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <span> element in <h2> to <span> in new <h2>",
+      innerHTML: "<h2>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span>ar</h2>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.nextSibling.firstChild, 1);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <span> element at end of <h2> to new <div>",
+      innerHTML: "<h2>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span></h2>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.nextSibling.firstChild, 2);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <h3> element to new <h3> at splitting it at middle of the <h3>",
+      innerHTML: "<h3 foo=\"bar\" id=\"original\">foobar</h3>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild, 3);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <h3> element to new <div> at splitting it at end of the <h3>",
+      innerHTML: "<h3 foo=\"bar\" id=\"original\">foobar</h3>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild, 6);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <span> element in <h3> to <span> in new <h3>",
+      innerHTML: "<h3>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span>ar</h3>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.nextSibling.firstChild, 1);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <span> element at end of <h3> to new <div>",
+      innerHTML: "<h3>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span></h3>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.nextSibling.firstChild, 2);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <h4> element to new <h4> at splitting it at middle of the <h4>",
+      innerHTML: "<h4 foo=\"bar\" id=\"original\">foobar</h4>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild, 3);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <h4> element to new <div> at splitting it at end of the <h4>",
+      innerHTML: "<h4 foo=\"bar\" id=\"original\">foobar</h4>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild, 6);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <span> element in <h4> to <span> in new <h4>",
+      innerHTML: "<h4>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span>ar</h4>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.nextSibling.firstChild, 1);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <span> element at end of <h4> to new <div>",
+      innerHTML: "<h4>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span></h4>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.nextSibling.firstChild, 2);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <h5> element to new <h5> at splitting it at middle of the <h5>",
+      innerHTML: "<h5 foo=\"bar\" id=\"original\">foobar</h5>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild, 3);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <h5> element to new <div> at splitting it at end of the <h5>",
+      innerHTML: "<h5 foo=\"bar\" id=\"original\">foobar</h5>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild, 6);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <span> element in <h5> to <span> in new <h5>",
+      innerHTML: "<h5>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span>ar</h5>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.nextSibling.firstChild, 1);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <span> element at end of <h5> to new <div>",
+      innerHTML: "<h5>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span></h5>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.nextSibling.firstChild, 2);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <h6> element to new <h6> at splitting it at middle of the <h6>",
+      innerHTML: "<h6 foo=\"bar\" id=\"original\">foobar</h6>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild, 3);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <h6> element to new <div> at splitting it at end of the <h6>",
+      innerHTML: "<h6 foo=\"bar\" id=\"original\">foobar</h6>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        foo: "bar",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild, 6);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should clone attributes of <span> element in <h6> to <span> in new <h6>",
+      innerHTML: "<h6>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span>ar</h6>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: true,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.nextSibling.firstChild, 1);
+        document.execCommand("insertParagraph", false);
+      }, },
+    { description: "Should NOT clone attributes of <span> element at end of <h6> to new <div>",
+      innerHTML: "<h6>fo<span id=\"original\" style=\"font-weight: bold;\">ob</span></h6>",
+      getLeftElement: function (aEditor) {
+        return aEditor.firstChild.firstChild.nextSibling;
+      },
+      getRightElement: function (aEditor) {
+        return aEditor.firstChild.nextSibling.firstChild;;
+      },
+      expectedToCloneAttributes: false,
+      expectedAttributes: {
+        style: "font-weight: bold;",
+        id: "original",
+      },
+      doIt: function (aEditor) {
+        window.getSelection().collapse(aEditor.firstChild.firstChild.nextSibling.firstChild, 2);
+        document.execCommand("insertParagraph", false);
+      }, },
+  ];
+
+  document.body.innerHTML = "<div id=\"editor\" contenteditable></div>";
+  let editor = document.getElementById("editor");
+  editor.focus();
+  let selection = document.getSelection();
+
+  for (const kTest of kTests) {
+    editor.innerHTML = kTest.innerHTML;
+    editor.focus();
+    kTest.doIt(editor);
+    let leftElement = kTest.getLeftElement(editor);
+    for (const kAttr in kTest.expectedAttributes) {
+      test(function () {
+        assert_equals(leftElement.getAttribute(kAttr),
+                      kTest.expectedAttributes[kAttr]);
+      }, kTest.description + "left Element, " + kAttr + " attribute");
+    }
+    let rightElement = kTest.getRightElement(editor);
+    for (const kAttr in kTest.expectedAttributes) {
+      test(function () {
+        assert_equals(rightElement.getAttribute(kAttr),
+                      !kTest.expectedToCloneAttributes || kAttr === "id"  ? null : kTest.expectedAttributes[kAttr]);
+      }, kTest.description + ", right element, " + kAttr + " attribute");
+    }
+  }
+})();
+</script>
+</body>
+</html>
--- a/testing/web-platform/tests/interfaces/cssom.idl
+++ b/testing/web-platform/tests/interfaces/cssom.idl
@@ -1,16 +1,15 @@
 // GENERATED CONTENT - DO NOT EDIT
 // Content of this file was automatically extracted from the CSS Object Model (CSSOM) spec.
 // See https://drafts.csswg.org/cssom/
 
 typedef USVString CSSOMString;
 
-[Exposed=Window,
- LegacyArrayClass]
+[Exposed=Window]
 interface MediaList {
   stringifier attribute [TreatNullAs=EmptyString] CSSOMString mediaText;
   readonly attribute unsigned long length;
   getter CSSOMString? item(unsigned long index);
   void appendMedium(CSSOMString medium);
   void deleteMedium(CSSOMString medium);
 };
 
@@ -28,18 +27,17 @@ interface StyleSheet {
 [Exposed=Window]
 interface CSSStyleSheet : StyleSheet {
   readonly attribute CSSRule? ownerRule;
   [SameObject] readonly attribute CSSRuleList cssRules;
   unsigned long insertRule(CSSOMString rule, optional unsigned long index = 0);
   void deleteRule(unsigned long index);
 };
 
-[Exposed=Window,
- LegacyArrayClass]
+[Exposed=Window]
 interface StyleSheetList {
   getter StyleSheet? item(unsigned long index);
   readonly attribute unsigned long length;
 };
 
 partial interface Document {
   [SameObject] readonly attribute StyleSheetList styleSheets;
 };
@@ -47,18 +45,17 @@ partial interface Document {
 [Exposed=Window,
  NoInterfaceObject]
 interface LinkStyle {
   readonly attribute StyleSheet? sheet;
 };
 
 ProcessingInstruction implements LinkStyle;
 
-[Exposed=Window,
- LegacyArrayClass]
+[Exposed=Window]
 interface CSSRuleList {
   getter CSSRule? item(unsigned long index);
   readonly attribute unsigned long length;
 };
 
 [Exposed=Window]
 interface CSSRule {
   const unsigned short STYLE_RULE = 1;
--- a/testing/web-platform/tests/tools/webdriver/webdriver/client.py
+++ b/testing/web-platform/tests/tools/webdriver/webdriver/client.py
@@ -531,17 +531,22 @@ class Session(object):
         else:
             url = "frame"
             body = {"id": frame}
 
         return self.send_session_command("POST", url, body)
 
     @command
     def close(self):
-        return self.send_session_command("DELETE", "window")
+        handles = self.send_session_command("DELETE", "window")
+        if len(handles) == 0:
+            # With no more open top-level browsing contexts, the session is closed.
+            self.session_id = None
+
+        return handles
 
     @property
     @command
     def handles(self):
         return self.send_session_command("GET", "window/handles")
 
     @property
     @command
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/close_window/close.py
@@ -0,0 +1,38 @@
+from tests.support.asserts import assert_error, assert_success
+
+
+def close(session):
+    return session.transport.send("DELETE", "session/%s/window" % session.session_id)
+
+
+def test_no_browsing_context(session, create_window):
+    new_handle = create_window()
+
+    session.window_handle = new_handle
+    session.close()
+    assert new_handle not in session.handles
+
+    response = close(session)
+    assert_error(response, "no such window")
+
+
+def test_close_browsing_context(session, create_window):
+    handles = session.handles
+
+    new_handle = create_window()
+    session.window_handle = new_handle
+
+    response = close(session)
+    value = assert_success(response, handles)
+    assert session.handles == handles
+    assert new_handle not in value
+
+
+def test_close_last_browsing_context(session):
+    assert len(session.handles) == 1
+    response = close(session)
+
+    assert_success(response, [])
+
+    # With no more open top-level browsing contexts, the session is closed.
+    session.session_id = None
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/close_window/user_prompts.py
@@ -0,0 +1,66 @@
+from tests.support.asserts import assert_error, assert_dialog_handled
+from tests.support.fixtures import create_dialog, create_window
+from tests.support.inline import inline
+
+
+def close(session):
+    return session.transport.send("DELETE", "session/%s/window" % session.session_id)
+
+
+def test_handle_prompt_dismiss_and_notify():
+    """TODO"""
+
+
+def test_handle_prompt_accept_and_notify():
+    """TODO"""
+
+
+def test_handle_prompt_ignore():
+    """TODO"""
+
+
+def test_handle_prompt_accept(new_session, add_browser_capabilites):
+    _, session = new_session({"capabilities": {
+        "alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "accept"})}})
+    session.window_handle = create_window(session)()
+
+    session.url = inline("<title>WD doc title</title>")
+
+    create_dialog(session)("alert", text="dismiss #1", result_var="dismiss1")
+    response = close(session)
+    assert response.status == 200
+    assert_dialog_handled(session, "dismiss #1")
+
+    create_dialog(session)("confirm", text="dismiss #2", result_var="dismiss2")
+    response = close(session)
+    assert response.status == 200
+    assert_dialog_handled(session, "dismiss #2")
+
+    create_dialog(session)("prompt", text="dismiss #3", result_var="dismiss3")
+    response = close(session)
+    assert response.status == 200
+    assert_dialog_handled(session, "dismiss #3")
+
+
+def test_handle_prompt_missing_value(session, create_dialog, create_window):
+    session.window_handle = create_window()
+
+    session.url = inline("<title>WD doc title</title>")
+    create_dialog("alert", text="dismiss #1", result_var="dismiss1")
+
+    response = close(session)
+
+    assert_error(response, "unexpected alert open")
+    assert_dialog_handled(session, "dismiss #1")
+
+    create_dialog("confirm", text="dismiss #2", result_var="dismiss2")
+
+    response = close(session)
+    assert_error(response, "unexpected alert open")
+    assert_dialog_handled(session, "dismiss #2")
+
+    create_dialog("prompt", text="dismiss #3", result_var="dismiss3")
+
+    response = close(session)
+    assert_error(response, "unexpected alert open")
+    assert_dialog_handled(session, "dismiss #3")
--- a/testing/web-platform/tests/webdriver/tests/support/fixtures.py
+++ b/testing/web-platform/tests/webdriver/tests/support/fixtures.py
@@ -71,16 +71,17 @@ def _restore_windows(session):
     for window in _windows(session, exclude=[current_window]):
         session.window_handle = window
         if len(session.handles) > 1:
             session.close()
 
     session.window_handle = current_window
 
 
+@ignore_exceptions
 def _switch_to_top_level_browsing_context(session):
     """If the current browsing context selected by WebDriver is a
     `<frame>` or an `<iframe>`, switch it back to the top-level
     browsing context.
     """
     session.switch_frame(None)
 
 
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -183,8 +183,48 @@ devtools.main:
     notification_emails: ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"]
     record_in_processes: ["main"]
     description: User closes devtools toolbox.
     release_channel_collection: opt-out
     expiry_version: never
     extra_keys:
       host: "Toolbox host (positioning): bottom, side, window or other."
       width: Toolbox width rounded up to the nearest 50px.
+  enter:
+    objects: ["webconsole", "inspector", "jsdebugger", "styleeditor", "netmonitor", "storage", "other"]
+    bug_numbers: [1441070]
+    notification_emails: ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"]
+    record_in_processes: ["main"]
+    description: User opens a tool in the devtools toolbox.
+    release_channel_collection: opt-out
+    expiry_version: never
+    extra_keys:
+      host: "Toolbox host (positioning): bottom, side, window or other."
+      width: Toolbox width rounded up to the nearest 50px.
+      message_count: The number of cached console messages.
+      start_state: debuggerStatement, breakpoint, exception, tab_switch, toolbox_show, initial_panel, toggle_settings_off, toggle_settings_on, key_shortcut, select_next_key, select_prev_key, tool_unloaded, inspect_dom, unknown etc.
+      panel_name: The name of the panel opened, webconsole, inspector, jsdebugger, styleeditor, netmonitor, storage or other
+      cold: Is this the first time the current panel has been opened in this toolbox?
+  exit:
+    objects: ["webconsole", "inspector", "jsdebugger", "styleeditor", "netmonitor", "storage", "other"]
+    bug_numbers: [1455270]
+    notification_emails: ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"]
+    record_in_processes: ["main"]
+    description: User closes a tool in the devtools toolbox.
+    release_channel_collection: opt-out
+    expiry_version: never
+    extra_keys:
+      host: "Toolbox host (positioning): bottom, side, window or other."
+      width: Toolbox width rounded up to the nearest 50px.
+      next_panel: The name of the panel closed, webconsole, inspector, jsdebugger, styleeditor, netmonitor, storage or other.
+      panel_name: The name of the panel opened, webconsole, inspector, jsdebugger, styleeditor, netmonitor, storage or other
+      reason: debuggerStatement, breakpoint, exception, tab_switch, toolbox_show, initial_panel, toggle_settings_off, toggle_settings_on, key_shortcut, select_next_key, select_prev_key, tool_unloaded, inspect_dom, toolbox_closed, unknown etc.
+  activate:
+    objects: ["responsive_design", "split_console"]
+    bug_numbers: [1455273]
+    notification_emails: ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"]
+    record_in_processes: ["main"]
+    description: User activates the responsive_design or split_console in the devtools toolbox.
+    release_channel_collection: opt-out
+    expiry_version: never
+    extra_keys:
+      host: "Toolbox host (positioning): bottom, side, window or other."
+      width: Toolbox width rounded up to the nearest 50px.
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -11486,144 +11486,180 @@
     "alert_emails": ["firefox-dev@mozilla.org"],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 30000,
     "n_buckets": 20,
     "description": "Sanitize: Time it takes to sanitize the open windows list (ms). On Android, this is the time it takes to close all open tabs (ms)."
   },
   "PWMGR_BLOCKLIST_NUM_SITES": {
-    "record_in_processes": ["main", "content"],
+    "record_in_processes": ["main"],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 100,
     "n_buckets" : 10,
-    "description": "The number of sites for which the user has explicitly rejected saving logins"
+    "description": "The number of sites for which the user has explicitly rejected saving logins",
+    "alert_emails": ["loines@mozilla.com"],
+    "bug_numbers": [1454733],
+    "releaseChannelCollection": "opt-out"
   },
   "PWMGR_FORM_AUTOFILL_RESULT": {
     "record_in_processes": ["main", "content"],
     "alert_emails": ["seceng-telemetry@mozilla.com"],
     "bug_numbers": [1340021],
     "releaseChannelCollection": "opt-out",
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values" : 20,
     "description": "The result of auto-filling a login form. See http://mzl.la/1Mbs6jL for bucket descriptions."
   },
   "PWMGR_LOGIN_LAST_USED_DAYS": {
-    "record_in_processes": ["main", "content"],
+    "record_in_processes": ["main"],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 750,
     "n_buckets" : 40,
-    "description": "Time in days each saved login was last used"
+    "description": "Time in days each saved login was last used",
+    "alert_emails": ["loines@mozilla.com"],
+    "bug_numbers": [1454733],
+    "releaseChannelCollection": "opt-out"
   },
   "PWMGR_LOGIN_PAGE_SAFETY": {
     "record_in_processes": ["main", "content"],
     "alert_emails": ["seceng-telemetry@mozilla.com"],
     "bug_numbers": [1340021],
     "releaseChannelCollection": "opt-out",
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 8,
     "description": "The safety of a page where we see a password field. (0: safe page & safe submit; 1: safe page & unsafe submit; 2: safe page & unknown submit; 3: unsafe page & safe submit; 4: unsafe page & unsafe submit; 5: unsafe page & unknown submit)"
   },
   "PWMGR_MANAGE_COPIED_PASSWORD": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "never",
     "kind": "count",
-    "description": "Count of passwords copied from the password management interface"
+    "description": "Count of passwords copied from the password management interface",
+    "alert_emails": ["loines@mozilla.com"],
+    "bug_numbers": [1454733],
+    "releaseChannelCollection": "opt-out"
   },
   "PWMGR_MANAGE_COPIED_USERNAME": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "never",
     "kind": "count",
-    "description": "Count of usernames copied from the password management interface"
+    "description": "Count of usernames copied from the password management interface",
+    "alert_emails": ["loines@mozilla.com"],
+    "bug_numbers": [1454733],
+    "releaseChannelCollection": "opt-out"
   },
   "PWMGR_MANAGE_DELETED": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "never",
     "kind": "count",
-    "description": "Count of passwords deleted from the password management interface (including via Remove All)"
+    "description": "Count of passwords deleted from the password management interface (including via Remove All)",
+    "alert_emails": ["loines@mozilla.com"],
+    "bug_numbers": [1454733],
+    "releaseChannelCollection": "opt-out"
   },
   "PWMGR_MANAGE_DELETED_ALL": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "never",
     "kind": "count",
     "description": "Count of times that Remove All was used from the password management interface"
   },
   "PWMGR_MANAGE_OPENED": {
-    "record_in_processes": ["main", "content"],
+    "record_in_processes": ["main"],
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values" : 5,
-    "description": "Accumulates how the password management interface was opened. (0=Preferences, 1=Page Info)"
+    "description": "Accumulates how the password management interface was opened. (0=Preferences, 1=Page Info)",
+    "alert_emails": ["loines@mozilla.com"],
+    "bug_numbers": [1454733],
+    "releaseChannelCollection": "opt-out"
   },
   "PWMGR_MANAGE_SORTED": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "never",
     "keyed": true,
     "kind": "count",
     "description": "Reports the column that logins are sorted by"
   },
   "PWMGR_MANAGE_VISIBILITY_TOGGLED": {
-    "record_in_processes": ["main", "content"],
-    "expires_in_version": "never",
-    "kind": "boolean",
-    "description": "Whether the visibility of passwords was toggled (0=Hide, 1=Show)"
+    "record_in_processes": ["main"],
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Whether the visibility of passwords was toggled (0=Hide, 1=Show)",
+    "alert_emails": ["loines@mozilla.com"],
+    "bug_numbers": [1454733],
+    "releaseChannelCollection": "opt-out"
   },
   "PWMGR_NUM_PASSWORDS_PER_HOSTNAME": {
-    "record_in_processes": ["main", "content"],
+    "record_in_processes": ["main"],
     "expires_in_version": "never",
     "kind": "linear",
     "high": 21,
     "n_buckets" : 20,
-    "description": "The number of passwords per hostname"
+    "description": "The number of passwords per hostname",
+    "alert_emails": ["loines@mozilla.com"],
+    "bug_numbers": [1454733],
+    "releaseChannelCollection": "opt-out"
   },
   "PWMGR_NUM_SAVED_PASSWORDS": {
-    "record_in_processes": ["main", "content"],
+    "record_in_processes": ["main"],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 750,
     "n_buckets" : 50,
-    "description": "Total number of saved logins, including those that cannot be decrypted"
+    "description": "Total number of saved logins, including those that cannot be decrypted",
+    "alert_emails": ["loines@mozilla.com"],
+    "bug_numbers": [1454733],
+    "releaseChannelCollection": "opt-out"
   },
   "PWMGR_NUM_HTTPAUTH_PASSWORDS": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 750,
     "n_buckets" : 50,
     "description": "Number of HTTP Auth logins"
   },
   "PWMGR_PASSWORD_INPUT_IN_FORM": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Whether an <input type=password> is associated with a <form> when it is added to a document"
   },
   "PWMGR_PROMPT_REMEMBER_ACTION" : {
-    "record_in_processes": ["main", "content"],
+    "record_in_processes": ["main"],
+    "expires_in_version": "never",
+    "kind": "enumerated",
+    "n_values": 5,
+    "description": "Action taken by user through prompt for creating a login. (0=Prompt displayed [always recorded], 1=Add login, 2=Don't save now, 3=Never save)",
+    "alert_emails": ["loines@mozilla.com"],
+    "bug_numbers": [1454733],
+    "releaseChannelCollection": "opt-out"
+  },
+  "PWMGR_PROMPT_UPDATE_ACTION" : {
+    "record_in_processes": ["main"],
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 5,
-    "description": "Action taken by user through prompt for creating a login. (0=Prompt displayed [always recorded], 1=Add login, 2=Don't save now, 3=Never save)"
-  },
-  "PWMGR_PROMPT_UPDATE_ACTION" : {
-    "record_in_processes": ["main", "content"],
-    "expires_in_version": "never",
-    "kind": "enumerated",
-    "n_values": 5,
-    "description": "Action taken by user through prompt for modifying a login. (0=Prompt displayed [always recorded], 1=Update login)"
+    "description": "Action taken by user through prompt for modifying a login. (0=Prompt displayed [always recorded], 1=Update login)",
+    "alert_emails": ["loines@mozilla.com"],
+    "bug_numbers": [1454733],
+    "releaseChannelCollection": "opt-out"
   },
   "PWMGR_SAVING_ENABLED": {
-    "record_in_processes": ["main", "content"],
-    "expires_in_version": "never",
-    "kind": "boolean",
-    "description": "Number of users who have password saving on globally"
+    "record_in_processes": ["main"],
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Number of users who have password saving on globally",
+    "alert_emails": ["loines@mozilla.com"],
+    "bug_numbers": [1454733],
+    "releaseChannelCollection": "opt-out"
   },
   "PWMGR_USERNAME_PRESENT": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Whether a saved login has a username"
   },
   "FENNEC_SYNC11_MIGRATION_SENTINELS_SEEN": {
--- a/toolkit/components/telemetry/histogram-whitelists.json
+++ b/toolkit/components/telemetry/histogram-whitelists.json
@@ -400,32 +400,20 @@
     "PRCONNECTCONTINUE_BLOCKING_TIME_SHUTDOWN",
     "PRCONNECT_BLOCKING_TIME_CONNECTIVITY_CHANGE",
     "PRCONNECT_BLOCKING_TIME_LINK_CHANGE",
     "PRCONNECT_BLOCKING_TIME_NORMAL",
     "PRCONNECT_BLOCKING_TIME_OFFLINE",
     "PRCONNECT_BLOCKING_TIME_SHUTDOWN",
     "PROCESS_CRASH_SUBMIT_ATTEMPT",
     "PROCESS_CRASH_SUBMIT_SUCCESS",
-    "PWMGR_BLOCKLIST_NUM_SITES",
-    "PWMGR_LOGIN_LAST_USED_DAYS",
-    "PWMGR_MANAGE_COPIED_PASSWORD",
-    "PWMGR_MANAGE_COPIED_USERNAME",
-    "PWMGR_MANAGE_DELETED",
     "PWMGR_MANAGE_DELETED_ALL",
-    "PWMGR_MANAGE_OPENED",
     "PWMGR_MANAGE_SORTED",
-    "PWMGR_MANAGE_VISIBILITY_TOGGLED",
     "PWMGR_NUM_HTTPAUTH_PASSWORDS",
-    "PWMGR_NUM_PASSWORDS_PER_HOSTNAME",
-    "PWMGR_NUM_SAVED_PASSWORDS",
     "PWMGR_PASSWORD_INPUT_IN_FORM",
-    "PWMGR_PROMPT_REMEMBER_ACTION",
-    "PWMGR_PROMPT_UPDATE_ACTION",
-    "PWMGR_SAVING_ENABLED",
     "PWMGR_USERNAME_PRESENT",
     "REFRESH_DRIVER_TICK",
     "REQUESTS_OF_ORIGINAL_CONTENT",
     "SAFE_MODE_USAGE",
     "SEARCH_COUNTS",
     "SEARCH_SERVICE_INIT_MS",
     "SERVICE_WORKER_CONTROLLED_DOCUMENTS",
     "SERVICE_WORKER_LIFE_TIME",
@@ -1023,32 +1011,20 @@
     "PRCONNECTCONTINUE_BLOCKING_TIME_SHUTDOWN",
     "PRCONNECT_BLOCKING_TIME_CONNECTIVITY_CHANGE",
     "PRCONNECT_BLOCKING_TIME_LINK_CHANGE",
     "PRCONNECT_BLOCKING_TIME_NORMAL",
     "PRCONNECT_BLOCKING_TIME_OFFLINE",
     "PRCONNECT_BLOCKING_TIME_SHUTDOWN",
     "PROCESS_CRASH_SUBMIT_ATTEMPT",
     "PROCESS_CRASH_SUBMIT_SUCCESS",
-    "PWMGR_BLOCKLIST_NUM_SITES",
-    "PWMGR_LOGIN_LAST_USED_DAYS",
-    "PWMGR_MANAGE_COPIED_PASSWORD",
-    "PWMGR_MANAGE_COPIED_USERNAME",
-    "PWMGR_MANAGE_DELETED",
     "PWMGR_MANAGE_DELETED_ALL",
-    "PWMGR_MANAGE_OPENED",
     "PWMGR_MANAGE_SORTED",
-    "PWMGR_MANAGE_VISIBILITY_TOGGLED",
     "PWMGR_NUM_HTTPAUTH_PASSWORDS",
-    "PWMGR_NUM_PASSWORDS_PER_HOSTNAME",
-    "PWMGR_NUM_SAVED_PASSWORDS",
     "PWMGR_PASSWORD_INPUT_IN_FORM",
-    "PWMGR_PROMPT_REMEMBER_ACTION",
-    "PWMGR_PROMPT_UPDATE_ACTION",
-    "PWMGR_SAVING_ENABLED",
     "PWMGR_USERNAME_PRESENT",
     "RANGE_CHECKSUM_ERRORS",
     "READER_MODE_DOWNLOAD_RESULT",
     "READER_MODE_PARSE_RESULT",
     "REFRESH_DRIVER_TICK",
     "REQUESTS_OF_ORIGINAL_CONTENT",
     "SAFE_MODE_USAGE",
     "SEARCH_COUNTS",
--- a/toolkit/components/url-classifier/ProtocolParser.cpp
+++ b/toolkit/components/url-classifier/ProtocolParser.cpp
@@ -85,16 +85,35 @@ void
 ProtocolParser::CleanupUpdates()
 {
   for (uint32_t i = 0; i < mTableUpdates.Length(); i++) {
     delete mTableUpdates[i];
   }
   mTableUpdates.Clear();
 }
 
+nsresult
+ProtocolParser::Begin(const nsACString& aTable,
+                      const nsTArray<nsCString>& aUpdateTables)
+{
+  // ProtocolParser objects should never be reused.
+  MOZ_ASSERT(mPending.IsEmpty());
+  MOZ_ASSERT(mTableUpdates.IsEmpty());
+  MOZ_ASSERT(mForwards.IsEmpty());
+  MOZ_ASSERT(mRequestedTables.IsEmpty());
+  MOZ_ASSERT(mTablesToReset.IsEmpty());
+
+  if (!aTable.IsEmpty()) {
+    SetCurrentTable(aTable);
+  }
+  SetRequestedTables(aUpdateTables);
+
+  return NS_OK;
+}
+
 TableUpdate *
 ProtocolParser::GetTableUpdate(const nsACString& aTable)
 {
   for (uint32_t i = 0; i < mTableUpdates.Length(); i++) {
     if (aTable.Equals(mTableUpdates[i]->TableName())) {
       return mTableUpdates[i];
     }
   }
@@ -108,17 +127,16 @@ ProtocolParser::GetTableUpdate(const nsA
   return update;
 }
 
 ///////////////////////////////////////////////////////////////////////
 // ProtocolParserV2
 
 ProtocolParserV2::ProtocolParserV2()
   : mState(PROTOCOL_STATE_CONTROL)
-  , mResetRequested(false)
   , mTableUpdate(nullptr)
 {
 }
 
 ProtocolParserV2::~ProtocolParserV2()
 {
 }
 
@@ -183,17 +201,18 @@ ProtocolParserV2::ProcessControl(bool* a
       // Set the table name from the table header line.
       SetCurrentTable(Substring(line, 2));
     } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("n:"))) {
       if (PR_sscanf(line.get(), "n:%d", &mUpdateWaitSec) != 1) {
         PARSER_LOG(("Error parsing n: '%s' (%d)", line.get(), mUpdateWaitSec));
         return NS_ERROR_FAILURE;
       }
     } else if (line.EqualsLiteral("r:pleasereset")) {
-      mResetRequested = true;
+      PARSER_LOG(("All tables will be reset."));
+      mTablesToReset = mRequestedTables;
     } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("u:"))) {
       rv = ProcessForward(line);
       NS_ENSURE_SUCCESS(rv, rv);
     } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("a:")) ||
                StringBeginsWith(line, NS_LITERAL_CSTRING("s:"))) {
       rv = ProcessChunkControl(line);
       NS_ENSURE_SUCCESS(rv, rv);
       *aDone = false;
@@ -775,31 +794,39 @@ ProtocolParserProtobuf::End()
   }
 
   auto minWaitDuration = response.minimum_wait_duration();
   mUpdateWaitSec = minWaitDuration.seconds() +
                    minWaitDuration.nanos() / 1000000000;
 
   for (int i = 0; i < response.list_update_responses_size(); i++) {
     auto r = response.list_update_responses(i);
-    nsresult rv = ProcessOneResponse(r);
+    nsAutoCString listName;
+    nsresult rv = ProcessOneResponse(r, listName);
     if (NS_SUCCEEDED(rv)) {
       mUpdateStatus = rv;
     } else {
       nsAutoCString errorName;
       mozilla::GetErrorName(rv, errorName);
-      NS_WARNING(nsPrintfCString("Failed to process one response: %s",
-                                 errorName.get()).get());
+      NS_WARNING(nsPrintfCString("Failed to process one response for '%s': %s",
+                                 listName.get(), errorName.get()).get());
+      if (!listName.IsEmpty()) {
+        PARSER_LOG(("Table %s will be reset.", listName.get()));
+        mTablesToReset.AppendElement(listName);
+      }
     }
   }
 }
 
 nsresult
-ProtocolParserProtobuf::ProcessOneResponse(const ListUpdateResponse& aResponse)
+ProtocolParserProtobuf::ProcessOneResponse(const ListUpdateResponse& aResponse,
+                                           nsACString& aListName)
 {
+  MOZ_ASSERT(aListName.IsEmpty());
+
   // A response must have a threat type.
   if (!aResponse.has_threat_type()) {
     NS_WARNING("Threat type not initialized. This seems to be an invalid response.");
     return NS_ERROR_UC_PARSER_MISSING_PARAM;
   }
 
   // Convert threat type to list name.
   nsCOMPtr<nsIUrlClassifierUtils> urlUtil =
@@ -811,27 +838,26 @@ ProtocolParserProtobuf::ProcessOneRespon
     PARSER_LOG(("Threat type to list name conversion error: %d",
                 aResponse.threat_type()));
     return NS_ERROR_UC_PARSER_UNKNOWN_THREAT;
   }
 
   // Match the table name we received with one of the ones we requested.
   // We ignore the case where a threat type matches more than one list
   // per provider and return the first one. See bug 1287059."
-  nsCString listName;
   nsTArray<nsCString> possibleListNameArray;
   Classifier::SplitTables(possibleListNames, possibleListNameArray);
   for (auto possibleName : possibleListNameArray) {
     if (mRequestedTables.Contains(possibleName)) {
-      listName = possibleName;
+      aListName = possibleName;
       break;
     }
   }
 
-  if (listName.IsEmpty()) {
+  if (aListName.IsEmpty()) {
     PARSER_LOG(("We received an update for a list we didn't ask for. Ignoring it."));
     return NS_ERROR_FAILURE;
   }
 
   // Test if this is a full update.
   bool isFullUpdate = false;
   if (aResponse.has_response_type()) {
     isFullUpdate =
@@ -842,30 +868,30 @@ ProtocolParserProtobuf::ProcessOneRespon
   }
 
   // Warn if there's no new state.
   if (!aResponse.has_new_client_state()) {
     NS_WARNING("New state not initialized.");
     return NS_ERROR_UC_PARSER_MISSING_PARAM;
   }
 
-  auto tu = GetTableUpdate(nsCString(listName.get()));
+  auto tu = GetTableUpdate(aListName);
   auto tuV4 = TableUpdate::Cast<TableUpdateV4>(tu);
   NS_ENSURE_TRUE(tuV4, NS_ERROR_FAILURE);
 
   nsCString state(aResponse.new_client_state().c_str(),
                   aResponse.new_client_state().size());
   tuV4->SetNewClientState(state);
 
   if (aResponse.has_checksum()) {
     tuV4->NewChecksum(aResponse.checksum().sha256());
   }
 
   PARSER_LOG(("==== Update for threat type '%d' ====", aResponse.threat_type()));
-  PARSER_LOG(("* listName: %s\n", listName.get()));
+  PARSER_LOG(("* aListName: %s\n", PromiseFlatCString(aListName).get()));
   PARSER_LOG(("* newState: %s\n", aResponse.new_client_state().c_str()));
   PARSER_LOG(("* isFullUpdate: %s\n", (isFullUpdate ? "yes" : "no")));
   PARSER_LOG(("* hasChecksum: %s\n", (aResponse.has_checksum() ? "yes" : "no")));
   PARSER_LOG(("* additions: %d\n", aResponse.additions().size()));
   PARSER_LOG(("* removals: %d\n", aResponse.removals().size()));
 
   tuV4->SetFullUpdate(isFullUpdate);
 
--- a/toolkit/components/url-classifier/ProtocolParser.h
+++ b/toolkit/components/url-classifier/ProtocolParser.h
@@ -33,17 +33,18 @@ public:
 
   virtual void SetCurrentTable(const nsACString& aTable) = 0;
 
   void SetRequestedTables(const nsTArray<nsCString>& aRequestTables)
   {
     mRequestedTables = aRequestTables;
   }
 
-  nsresult Begin();
+  nsresult Begin(const nsACString& aTable,
+                 const nsTArray<nsCString>& aUpdateTables);
   virtual nsresult AppendStream(const nsACString& aData) = 0;
 
   uint32_t UpdateWaitSec() { return mUpdateWaitSec; }
 
   // Notify that the inbound data is ready for parsing if progressive
   // parsing is not supported, for example in V4.
   virtual void End() = 0;
 
@@ -53,32 +54,36 @@ public:
   void ForgetTableUpdates() { mTableUpdates.Clear(); }
   nsTArray<TableUpdate*> &GetTableUpdates() { return mTableUpdates; }
 
   // These are only meaningful to V2. Since they were originally public,
   // moving them to ProtocolParserV2 requires a dymamic cast in the call
   // sites. As a result, we will leave them until we remove support
   // for V2 entirely..
   virtual const nsTArray<ForwardedUpdate> &Forwards() const { return mForwards; }
-  virtual bool ResetRequested() { return false; }
+  bool ResetRequested() const { return !mTablesToReset.IsEmpty(); }
+  const nsTArray<nsCString>& TablesToReset() const { return mTablesToReset; }
 
 protected:
   virtual TableUpdate* CreateTableUpdate(const nsACString& aTableName) const = 0;
 
   nsCString mPending;
   nsresult mUpdateStatus;
 
   // Keep track of updates to apply before passing them to the DBServiceWorkers.
   nsTArray<TableUpdate*> mTableUpdates;
 
   nsTArray<ForwardedUpdate> mForwards;
 
   // The table names that were requested from the client.
   nsTArray<nsCString> mRequestedTables;
 
+  // The table names that failed to update and need to be reset.
+  nsTArray<nsCString> mTablesToReset;
+
   // How long we should wait until the next update.
   uint32_t mUpdateWaitSec;
 
 private:
   void CleanupUpdates();
 };
 
 /**
@@ -90,17 +95,16 @@ public:
   virtual ~ProtocolParserV2();
 
   virtual void SetCurrentTable(const nsACString& aTable) override;
   virtual nsresult AppendStream(const nsACString& aData) override;
   virtual void End() override;
 
   // Update information.
   virtual const nsTArray<ForwardedUpdate> &Forwards() const override { return mForwards; }
-  virtual bool ResetRequested() override { return mResetRequested; }
 
 #ifdef MOZ_SAFEBROWSING_DUMP_FAILED_UPDATES
   // Unfortunately we have to override to return mRawUpdate which
   // will not be modified during the parsing, unlike mPending.
   virtual nsCString GetRawTableUpdates() const override { return mRawUpdate; }
 #endif
 
 private:
@@ -151,18 +155,16 @@ private:
     ChunkType type;
     uint32_t num;
     uint32_t hashSize;
     uint32_t length;
     void Clear() { num = 0; hashSize = 0; length = 0; }
   };
   ChunkState mChunkState;
 
-  bool mResetRequested;
-
   // Updates to apply to the current table being parsed.
   TableUpdateV2 *mTableUpdate;
 
 #ifdef MOZ_SAFEBROWSING_DUMP_FAILED_UPDATES
   nsCString mRawUpdate; // Keep a copy of mPending before it's processed.
 #endif
 };
 
@@ -180,17 +182,18 @@ public:
   virtual void End() override;
 
 private:
   virtual ~ProtocolParserProtobuf();
 
   virtual TableUpdate* CreateTableUpdate(const nsACString& aTableName) const override;
 
   // For parsing update info.
-  nsresult ProcessOneResponse(const ListUpdateResponse& aResponse);
+  nsresult ProcessOneResponse(const ListUpdateResponse& aResponse,
+                              nsACString& aListName);
 
   nsresult ProcessAdditionOrRemoval(TableUpdateV4& aTableUpdate,
                                     const ThreatEntrySetList& aUpdate,
                                     bool aIsAddition);
 
   nsresult ProcessRawAddition(TableUpdateV4& aTableUpdate,
                               const ThreatEntrySet& aAddition);
 
--- a/toolkit/components/url-classifier/nsUrlClassifierDBService.cpp
+++ b/toolkit/components/url-classifier/nsUrlClassifierDBService.cpp
@@ -467,23 +467,17 @@ nsUrlClassifierDBServiceWorker::BeginStr
   mProtocolParser = (useProtobuf ? static_cast<ProtocolParser*>(new (fallible)
                                      ProtocolParserProtobuf())
                                  : static_cast<ProtocolParser*>(new (fallible)
                                      ProtocolParserV2()));
   if (!mProtocolParser) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
-  if (!table.IsEmpty()) {
-    mProtocolParser->SetCurrentTable(table);
-  }
-
-  mProtocolParser->SetRequestedTables(mUpdateTables);
-
-  return NS_OK;
+  return mProtocolParser->Begin(table, mUpdateTables);
 }
 
 /**
  * Updating the database:
  *
  * The Update() method takes a series of chunks separated with control data,
  * as described in
  * http://code.google.com/p/google-safe-browsing/wiki/Protocolv2Spec
@@ -514,32 +508,36 @@ nsUrlClassifierDBServiceWorker::BeginStr
  */
 NS_IMETHODIMP
 nsUrlClassifierDBServiceWorker::UpdateStream(const nsACString& chunk)
 {
   if (gShuttingDownThread) {
     return NS_ERROR_NOT_INITIALIZED;
   }
 
+  MOZ_ASSERT(mProtocolParser);
+
   NS_ENSURE_STATE(mInStream);
 
   HandlePendingLookups();
 
   // Feed the chunk to the parser.
   return mProtocolParser->AppendStream(chunk);
 }
 
 NS_IMETHODIMP
 nsUrlClassifierDBServiceWorker::FinishStream()
 {
   if (gShuttingDownThread) {
     LOG(("shutting down"));
     return NS_ERROR_NOT_INITIALIZED;
   }
 
+  MOZ_ASSERT(mProtocolParser);
+
   NS_ENSURE_STATE(mInStream);
   NS_ENSURE_STATE(mUpdateObserver);
 
   mInStream = false;
 
   mProtocolParser->End();
 
   if (NS_SUCCEEDED(mProtocolParser->Status())) {
@@ -566,17 +564,18 @@ nsUrlClassifierDBServiceWorker::FinishSt
     LOG(("nsUrlClassifierDBService::FinishStream Failed to parse the stream "
          "using mProtocolParser."));
     mUpdateStatus = mProtocolParser->Status();
   }
   mUpdateObserver->StreamFinished(mProtocolParser->Status(), 0);
 
   if (NS_SUCCEEDED(mUpdateStatus)) {
     if (mProtocolParser->ResetRequested()) {
-      mClassifier->ResetTables(Classifier::Clear_All, mUpdateTables);
+      mClassifier->ResetTables(Classifier::Clear_All,
+                               mProtocolParser->TablesToReset());
     }
   }
 
   mProtocolParser = nullptr;
 
   return mUpdateStatus;
 }
 
@@ -687,21 +686,16 @@ nsUrlClassifierDBServiceWorker::NotifyUp
   // Null out mUpdateObserver before notifying so that BeginUpdate()
   // becomes available prior to callback.
   nsCOMPtr<nsIUrlClassifierUpdateObserver> updateObserver = nullptr;
   updateObserver.swap(mUpdateObserver);
 
   if (NS_SUCCEEDED(mUpdateStatus)) {
     LOG(("Notifying success: %d", mUpdateWaitSec));
     updateObserver->UpdateSuccess(mUpdateWaitSec);
-  } else if (NS_ERROR_NOT_IMPLEMENTED == mUpdateStatus) {
-    LOG(("Treating NS_ERROR_NOT_IMPLEMENTED a successful update "
-         "but still mark it spoiled."));
-    updateObserver->UpdateSuccess(0);
-    mClassifier->ResetTables(Classifier::Clear_Cache, mUpdateTables);
   } else {
     if (LOG_ENABLED()) {
       nsAutoCString errorName;
       mozilla::GetErrorName(mUpdateStatus, errorName);
       LOG(("Notifying error: %s (%" PRIu32 ")", errorName.get(),
            static_cast<uint32_t>(mUpdateStatus)));
     }
 
--- a/toolkit/locales/en-US/chrome/global/keys.properties
+++ b/toolkit/locales/en-US/chrome/global/keys.properties
@@ -1,15 +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/.
 
 # LOCALIZATION NOTE : FILE This file contains the application's labels for keys on the keyboard.
 #                     If you decide to translate this file, you should translate it based on
-#                     the prevelant kind of keyboard for your target user.
+#                     the prevalent kind of keyboard for your target user.
 # LOCALIZATION NOTE : There are two types of keys, those w/ text on their labels
 #                     and those w/ glyphs.
 # LOCALIZATION NOTE : VK_<…> represents a key on the keyboard.
 #
 # For more information please see bugzilla bug 90888.
 
 # F1..F10 should probably not be translated unless there are keyboards that actually have other labels
 # F11..F20 might be something else, but are really keyboard specific and not region/language specific
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -133,32 +133,33 @@ class MockBlocklist {
   }
 
   unregister() {
     MockRegistrar.unregister(this.originalCID);
     this._reLazifyService();
   }
 
   async getAddonBlocklistState(addon, appVersion, toolkitVersion) {
-    await new Promise(r => setTimeout(r, 0));
+    await new Promise(r => setTimeout(r, 150));
     return this.addons.get(addon.id) || Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
   }
 
   async getAddonBlocklistEntry(addon, appVersion, toolkitVersion) {
     let state = await this.getAddonBlocklistState(addon, appVersion, toolkitVersion);
     if (state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
       return {
         state,
         url: "http://example.com/",
       };
     }
     return null;
   }
 
-  getPluginBlocklistState(plugin, version, appVersion, toolkitVersion) {
+  async getPluginBlocklistState(plugin, version, appVersion, toolkitVersion) {
+    await new Promise(r => setTimeout(r, 150));
     return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
   }
 }
 
 MockBlocklist.prototype.QueryInterface = XPCOMUtils.generateQI(["nsIBlocklistService"]);
 
 
 /**
--- a/toolkit/mozapps/extensions/nsBlocklistService.js
+++ b/toolkit/mozapps/extensions/nsBlocklistService.js
@@ -855,18 +855,19 @@ Blocklist.prototype = {
     }
     if (!this._preloadPromise) {
       this._preloadPromise = this._loadBlocklistAsyncInternal();
     }
     await this._preloadPromise;
   },
 
   async _loadBlocklistAsyncInternal() {
-    let profPath = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
     try {
+      // Get the path inside the try...catch because there's no profileDir in e.g. xpcshell tests.
+      let profPath = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
       await this._preloadBlocklistFile(profPath);
       return;
     } catch (e) {
       LOG("Blocklist::loadBlocklistAsync: Failed to load XML file " + e);
     }
 
     var appFile = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
     try {
@@ -1138,22 +1139,21 @@ Blocklist.prototype = {
       if (value) {
         blockEntry[matchElement.localName] = value;
       }
     }
     result.push(blockEntry);
   },
 
   /* See nsIBlocklistService */
-  getPluginBlocklistState(plugin, appVersion, toolkitVersion) {
+  async getPluginBlocklistState(plugin, appVersion, toolkitVersion) {
     if (AppConstants.platform == "android") {
       return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
     }
-    if (!this.isLoaded)
-      this._loadBlocklist();
+    await this.loadBlocklistAsync();
     return this._getPluginBlocklistState(plugin, this._pluginEntries,
                                          appVersion, toolkitVersion);
   },
 
   /**
    * Private helper to get the blocklist entry for a plugin given a set of
    * blocklist entries and versions.
    *
@@ -1305,17 +1305,16 @@ Blocklist.prototype = {
         return `${key}:${value}`;
       }).join("\t");
     }).join("\n");
     Services.obs.notifyObservers(null, "blocklist-data-gfxItems", payload);
   },
 
   _notifyObserversBlocklistUpdated() {
     Services.obs.notifyObservers(this, "blocklist-updated");
-    Services.ppmm.broadcastAsyncMessage("Blocklist:blocklistInvalidated", {});
   },
 
   async _blocklistUpdated(oldAddonEntries, oldPluginEntries) {
     var addonList = [];
 
     // A helper function that reverts the prefs passed to default values.
     function resetPrefs(prefs) {
       for (let pref of prefs)
@@ -1325,17 +1324,17 @@ Blocklist.prototype = {
     let addons = await AddonManager.getAddonsByTypes(types);
     for (let addon of addons) {
       let oldState = addon.blocklistState;
       if (addon.updateBlocklistState) {
         await addon.updateBlocklistState(false);
       } else if (oldAddonEntries) {
         oldState = this._getAddonBlocklistState(addon, oldAddonEntries);
       } else {
-        oldState = Ci.nsIBlocklistService.STATE_NOTBLOCKED;
+        oldState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
       }
       let state = addon.blocklistState;
 
       LOG("Blocklist state for " + addon.id + " changed from " +
           oldState + " to " + state);
 
       // We don't want to re-warn about add-ons
       if (state == oldState)
@@ -1392,17 +1391,17 @@ Blocklist.prototype = {
     var phs = Cc["@mozilla.org/plugin/host;1"].
               getService(Ci.nsIPluginHost);
     var plugins = phs.getPluginTags();
 
     for (let plugin of plugins) {
       let oldState = -1;
       if (oldPluginEntries)
         oldState = this._getPluginBlocklistState(plugin, oldPluginEntries);
-      let state = this.getPluginBlocklistState(plugin);
+      let state = this._getPluginBlocklistState(plugin, this._pluginEntries);
       LOG("Blocklist state for " + plugin.name + " changed from " +
           oldState + " to " + state);
       // We don't want to re-warn about items
       if (state == oldState)
         continue;
 
       if (oldState == Ci.nsIBlocklistService.STATE_BLOCKED) {
         if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_appversion.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_appversion.js
@@ -187,21 +187,22 @@ var ADDONS = [{
 
 function MockPluginTag(name, version, start, appBlocks, toolkitBlocks) {
   this.name = name;
   this.version = version;
   this.start = start;
   this.appBlocks = appBlocks;
   this.toolkitBlocks = toolkitBlocks;
 }
-Object.defineProperty(MockPluginTag.prototype, "blocklisted", {
-  get: function MockPluginTag_getBlocklisted() {
-    return Services.blocklist.getPluginBlocklistState(this) == Services.blocklist.STATE_BLOCKED;
+MockPluginTag.prototype = {
+  async isBlocklisted() {
+    let state = await Services.blocklist.getPluginBlocklistState(this);
+    return state == Services.blocklist.STATE_BLOCKED;
   }
-});
+};
 
 var PLUGINS = [
   new MockPluginTag("test_bug449027_1", "5", false, false, false),
   new MockPluginTag("test_bug449027_2", "5", false, true, false),
   new MockPluginTag("test_bug449027_3", "5", false, true, false),
   new MockPluginTag("test_bug449027_4", "5", false, false, false),
   new MockPluginTag("test_bug449027_5", "5", false, false, false),
   new MockPluginTag("test_bug449027_6", "5", false, true, false),
@@ -283,32 +284,35 @@ function createAddon(addon) {
  * Checks that items are blocklisted correctly according to the current test.
  * If a lastTest is provided checks that the notification dialog got passed
  * the newly blocked items compared to the previous test.
  */
 async function checkState(test, lastTest, callback) {
   let addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id));
 
   for (var i = 0; i < ADDONS.length; i++) {
+    await TestUtils.waitForCondition(() => {
+      return (addons[i].blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) == ADDONS[i][test];
+    }).catch(() => { /* ignore exceptions; the following test will fail anyway. */ });
     var blocked = addons[i].blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED;
     equal(blocked, ADDONS[i][test],
-          `Blocklist state should match expected for extension ${i + 1}, test ${test}`);
+          `Blocklist state should match expected for extension ${addons[i].id}, test ${test}`);
   }
 
   for (i = 0; i < PLUGINS.length; i++) {
-    equal(PLUGINS[i].blocklisted, PLUGINS[i][test],
-          `Blocklist state should match expected for plugin ${i + 1}, test ${test}`);
+    equal(await PLUGINS[i].isBlocklisted(), PLUGINS[i][test],
+          `Blocklist state should match expected for plugin ${PLUGINS[i].name}, test ${test}`);
   }
 
   if (lastTest) {
     var expected = 0;
     for (i = 0; i < PLUGINS.length; i++) {
       if (PLUGINS[i][test] && !PLUGINS[i][lastTest]) {
         ok(gNewBlocks.includes(`${PLUGINS[i].name} ${PLUGINS[i].version}`),
-           `Plugin ${i + 1} should have been listed in the blocklist notification for test ${test}`);
+           `Plugin ${PLUGINS[i].name} should have been listed in the blocklist notification for test ${test}`);
         expected++;
       }
     }
 
     Assert.equal(expected, gNewBlocks.length);
   }
 }
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_flashonly.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_flashonly.js
@@ -10,26 +10,25 @@ function get_test_plugintag() {
   var tags = host.getPluginTags();
   for (let tag of tags) {
     if (tag.name == "Test Plug-in")
       return tag;
   }
   return null;
 }
 
-function run_test() {
+add_task(async function checkFlashOnlyPluginState() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
 
   copyBlocklistToProfile(do_get_file("data/test_bug514327_2.xml"));
 
   Services.prefs.setBoolPref("plugin.load_flash_only", false);
 
   var plugin = get_test_plugintag();
   if (!plugin)
     do_throw("Plugin tag not found");
 
   // run the code after the blocklist is closed
   Services.obs.notifyObservers(null, "addon-blocklist-closed");
-  executeSoon(function() {
-    // should be marked as outdated by the blocklist
-    Assert.ok(Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9") == nsIBLS.STATE_OUTDATED);
-  });
-}
+  await new Promise(executeSoon);
+  // should be marked as outdated by the blocklist
+  Assert.equal(await Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9"), nsIBLS.STATE_OUTDATED);
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_outdated.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_outdated.js
@@ -84,27 +84,27 @@ add_task(async function setup() {
   // initialize the blocklist with no entries
   copyBlocklistToProfile(do_get_file("data/test_bug514327_3_empty.xml"));
 
   await promiseStartupManager();
 
   gBlocklist = Services.blocklist;
 
   // should NOT be marked as outdated by the blocklist
-  Assert.ok(gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == nsIBLS.STATE_NOT_BLOCKED);
+  Assert.equal(await gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9"), nsIBLS.STATE_NOT_BLOCKED);
 });
 
 add_task(async function test_part_1() {
   // update blocklist with data that marks the plugin as outdated
   await loadBlocklist("test_bug514327_3_outdated_1.xml");
 
   // plugin should now be marked as outdated
-  Assert.ok(gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == nsIBLS.STATE_OUTDATED);
+  Assert.equal(await gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9"), nsIBLS.STATE_OUTDATED);
 
 });
 
 add_task(async function test_part_2() {
   // update blocklist with data that marks the plugin as outdated
   await loadBlocklist("test_bug514327_3_outdated_2.xml");
 
   // plugin should still be marked as outdated
-  Assert.ok(gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == nsIBLS.STATE_OUTDATED);
+  Assert.equal(await gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9"), nsIBLS.STATE_OUTDATED);
 });
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_regexp.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_regexp.js
@@ -1,55 +1,54 @@
 /* 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/. */
 
 var PLUGINS = [{
-  // Normal blacklisted plugin, before an invalid regexp
+  // Normal blocklisted plugin, before an invalid regexp
   name: "test_bug468528_1",
   version: "5",
   disabled: false,
   blocklisted: false
 },
 {
-  // Normal blacklisted plugin, with an invalid regexp
+  // Normal blocklisted plugin, with an invalid regexp
   name: "test_bug468528_2",
   version: "5",
   disabled: false,
   blocklisted: false
 },
 {
-  // Normal blacklisted plugin, after an invalid regexp
+  // Normal blocklisted plugin, after an invalid regexp
   name: "test_bug468528_3",
   version: "5",
   disabled: false,
   blocklisted: false
 },
 {
   // Non-blocklisted plugin
   name: "test_bug468528_4",
   version: "5",
   disabled: false,
   blocklisted: false
 }];
 
 
-function run_test() {
+add_task(async function checkBlocklistForRegexes() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
 
   // We cannot force the blocklist to update so just copy our test list to the profile
   copyBlocklistToProfile(do_get_file("data/test_bug468528.xml"));
 
   var {blocklist} = Services;
 
   // blocked (sanity check)
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == blocklist.STATE_BLOCKED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9"), blocklist.STATE_BLOCKED);
 
   // not blocked - won't match due to invalid regexp
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9") == blocklist.STATE_NOT_BLOCKED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9"), blocklist.STATE_NOT_BLOCKED);
 
   // blocked - the invalid regexp for the previous item shouldn't affect this one
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9") == blocklist.STATE_BLOCKED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9"), blocklist.STATE_BLOCKED);
 
   // not blocked - the previous invalid regexp shouldn't act as a wildcard
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9") == blocklist.STATE_NOT_BLOCKED);
-
-}
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9"), blocklist.STATE_NOT_BLOCKED);
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_severities.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_plugin_severities.js
@@ -28,27 +28,27 @@ var PLUGINS = [{
   name: "test_bug514327_4",
   version: "5",
   disabled: false,
   blocklisted: false,
   outdated: false
 }];
 
 
-function run_test() {
+add_task(async function checkBlocklistSeverities() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
 
   copyBlocklistToProfile(do_get_file("data/test_bug514327_1.xml"));
 
   var {blocklist} = Services;
 
   // blocked (sanity check)
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == blocklist.STATE_BLOCKED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9"), blocklist.STATE_BLOCKED);
 
   // outdated
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9") == blocklist.STATE_OUTDATED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9"), blocklist.STATE_OUTDATED);
 
   // outdated
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9") == blocklist.STATE_OUTDATED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9"), blocklist.STATE_OUTDATED);
 
   // not blocked
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9") == blocklist.STATE_NOT_BLOCKED);
-}
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9"), blocklist.STATE_NOT_BLOCKED);
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_severities.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_severities.js
@@ -76,19 +76,16 @@ var ADDONS = [{
 }];
 
 class MockPlugin {
   constructor(name, version, enabledState) {
     this.name = name;
     this.version = version;
     this.enabledState = enabledState;
   }
-  get blocklisted() {
-    return Services.blocklist.getPluginBlocklistState(this) == Services.blocklist.STATE_BLOCKED;
-  }
   get disabled() {
     return this.enabledState == Ci.nsIPluginTag.STATE_DISABLED;
   }
 }
 
 var PLUGINS = [
   // Tests how the blocklist affects a disabled plugin
   new MockPlugin("test_bug455906_1", "5", Ci.nsIPluginTag.STATE_DISABLED),
@@ -157,18 +154,19 @@ async function loadBlocklist(file, callb
 
   Services.prefs.setCharPref("extensions.blocklist.url",
                              "http://example.com/data/" + file);
   Services.blocklist.QueryInterface(Ci.nsITimerCallback).notify(null);
 
   await blocklistUpdated;
 }
 
-function check_plugin_state(plugin) {
-  return plugin.disabled + "," + plugin.blocklisted;
+async function check_plugin_state(plugin) {
+  let blocklistState = await Services.blocklist.getPluginBlocklistState(plugin);
+  return `${plugin.disabled},${blocklistState == Services.blocklist.STATE_BLOCKED}`;
 }
 
 function create_blocklistURL(blockID) {
   let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ITEM_URL);
   url = url.replace(/%blockID%/g, blockID);
   return url;
 }
 
@@ -179,22 +177,22 @@ async function checkInitialState() {
   checkAddonState(addons[0], {userDisabled: true, softDisabled: false, appDisabled: false});
   checkAddonState(addons[1], {userDisabled: false, softDisabled: false, appDisabled: false});
   checkAddonState(addons[2], {userDisabled: false, softDisabled: false, appDisabled: false});
   checkAddonState(addons[3], {userDisabled: true, softDisabled: true, appDisabled: false});
   checkAddonState(addons[4], {userDisabled: false, softDisabled: false, appDisabled: false});
   checkAddonState(addons[5], {userDisabled: false, softDisabled: false, appDisabled: true});
   checkAddonState(addons[6], {userDisabled: false, softDisabled: false, appDisabled: true});
 
-  equal(check_plugin_state(PLUGINS[0]), "true,false");
-  equal(check_plugin_state(PLUGINS[1]), "false,false");
-  equal(check_plugin_state(PLUGINS[2]), "false,false");
-  equal(check_plugin_state(PLUGINS[3]), "true,false");
-  equal(check_plugin_state(PLUGINS[4]), "false,false");
-  equal(check_plugin_state(PLUGINS[5]), "false,true");
+  equal(await check_plugin_state(PLUGINS[0]), "true,false");
+  equal(await check_plugin_state(PLUGINS[1]), "false,false");
+  equal(await check_plugin_state(PLUGINS[2]), "false,false");
+  equal(await check_plugin_state(PLUGINS[3]), "true,false");
+  equal(await check_plugin_state(PLUGINS[4]), "false,false");
+  equal(await check_plugin_state(PLUGINS[5]), "false,true");
 }
 
 function checkAddonState(addon, state) {
   return checkAddon(addon.id, addon, state);
 }
 
 add_task(async function setup() {
   // Copy the initial blocklist into the profile to check add-ons start in the
@@ -273,32 +271,32 @@ add_task(async function test_1() {
 
   await promiseRestartManager();
   dump("Checking results pt 2\n");
 
   addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id));
 
   // Should have disabled this add-on as requested
   checkAddonState(addons[2], {userDisabled: true, softDisabled: true, appDisabled: false});
-  equal(check_plugin_state(PLUGINS[2]), "true,false");
+  equal(await check_plugin_state(PLUGINS[2]), "true,false");
 
   // The blocked add-on should have changed to soft disabled
   checkAddonState(addons[5], {userDisabled: true, softDisabled: true, appDisabled: false});
   checkAddonState(addons[6], {userDisabled: true, softDisabled: true, appDisabled: true});
-  equal(check_plugin_state(PLUGINS[5]), "true,false");
+  equal(await check_plugin_state(PLUGINS[5]), "true,false");
 
   // These should have been unchanged
   checkAddonState(addons[0], {userDisabled: true, softDisabled: false, appDisabled: false});
   checkAddonState(addons[1], {userDisabled: false, softDisabled: false, appDisabled: false});
   checkAddonState(addons[3], {userDisabled: true, softDisabled: true, appDisabled: false});
   checkAddonState(addons[4], {userDisabled: false, softDisabled: false, appDisabled: false});
-  equal(check_plugin_state(PLUGINS[0]), "true,false");
-  equal(check_plugin_state(PLUGINS[1]), "false,false");
-  equal(check_plugin_state(PLUGINS[3]), "true,false");
-  equal(check_plugin_state(PLUGINS[4]), "false,false");
+  equal(await check_plugin_state(PLUGINS[0]), "true,false");
+  equal(await check_plugin_state(PLUGINS[1]), "false,false");
+  equal(await check_plugin_state(PLUGINS[3]), "true,false");
+  equal(await check_plugin_state(PLUGINS[4]), "false,false");
 
   // Back to starting state
   addons[2].userDisabled = false;
   addons[5].userDisabled = false;
   PLUGINS[2].enabledState = Ci.nsIPluginTag.STATE_ENABLED;
   PLUGINS[5].enabledState = Ci.nsIPluginTag.STATE_ENABLED;
 
   await promiseRestartManager();
@@ -351,21 +349,21 @@ add_task(async function test_pt3() {
 
   let addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id));
 
   // All should have gained the blocklist state, user disabled as previously
   checkAddonState(addons[0], {userDisabled: true, softDisabled: false, appDisabled: true});
   checkAddonState(addons[1], {userDisabled: false, softDisabled: false, appDisabled: true});
   checkAddonState(addons[2], {userDisabled: false, softDisabled: false, appDisabled: true});
   checkAddonState(addons[4], {userDisabled: false, softDisabled: false, appDisabled: true});
-  equal(check_plugin_state(PLUGINS[0]), "true,true");
-  equal(check_plugin_state(PLUGINS[1]), "false,true");
-  equal(check_plugin_state(PLUGINS[2]), "false,true");
-  equal(check_plugin_state(PLUGINS[3]), "true,true");
-  equal(check_plugin_state(PLUGINS[4]), "false,true");
+  equal(await check_plugin_state(PLUGINS[0]), "true,true");
+  equal(await check_plugin_state(PLUGINS[1]), "false,true");
+  equal(await check_plugin_state(PLUGINS[2]), "false,true");
+  equal(await check_plugin_state(PLUGINS[3]), "true,true");
+  equal(await check_plugin_state(PLUGINS[4]), "false,true");
 
   // Should have gained the blocklist state but no longer be soft disabled
   checkAddonState(addons[3], {userDisabled: false, softDisabled: false, appDisabled: true});
 
   // Check blockIDs are correct
   equal(await getAddonBlocklistURL(addons[0]), create_blocklistURL(addons[0].id));
   equal(await getAddonBlocklistURL(addons[1]), create_blocklistURL(addons[1].id));
   equal(await getAddonBlocklistURL(addons[2]), create_blocklistURL(addons[2].id));
@@ -377,17 +375,17 @@ add_task(async function test_pt3() {
   equal(Services.blocklist.getPluginBlocklistURL(PLUGINS[1]), create_blocklistURL("test_bug455906_plugin"));
   equal(Services.blocklist.getPluginBlocklistURL(PLUGINS[2]), create_blocklistURL("test_bug455906_plugin"));
   equal(Services.blocklist.getPluginBlocklistURL(PLUGINS[3]), create_blocklistURL("test_bug455906_plugin"));
   equal(Services.blocklist.getPluginBlocklistURL(PLUGINS[4]), create_blocklistURL("test_bug455906_plugin"));
 
   // Shouldn't be changed
   checkAddonState(addons[5], {userDisabled: false, softDisabled: false, appDisabled: true});
   checkAddonState(addons[6], {userDisabled: false, softDisabled: false, appDisabled: true});
-  equal(check_plugin_state(PLUGINS[5]), "false,true");
+  equal(await check_plugin_state(PLUGINS[5]), "false,true");
 
   // Back to starting state
   await loadBlocklist("bug455906_start.xml");
 });
 
 add_task(async function test_pt4() {
   let addon = await AddonManager.getAddonByID(ADDONS[4].id);
   addon.userDisabled = false;
@@ -406,25 +404,25 @@ add_task(async function test_pt4() {
   });
 
   await promiseRestartManager();
   dump("Checking results pt 4\n");
 
   let addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id));
   // This should have become unblocked
   checkAddonState(addons[5], {userDisabled: false, softDisabled: false, appDisabled: false});
-  equal(check_plugin_state(PLUGINS[5]), "false,false");
+  equal(await check_plugin_state(PLUGINS[5]), "false,false");
 
   // Should get re-enabled
   checkAddonState(addons[3], {userDisabled: false, softDisabled: false, appDisabled: false});
 
   // No change for anything else
   checkAddonState(addons[0], {userDisabled: true, softDisabled: false, appDisabled: false});
   checkAddonState(addons[1], {userDisabled: false, softDisabled: false, appDisabled: false});
   checkAddonState(addons[2], {userDisabled: false, softDisabled: false, appDisabled: false});
   checkAddonState(addons[4], {userDisabled: false, softDisabled: false, appDisabled: false});
   checkAddonState(addons[6], {userDisabled: false, softDisabled: false, appDisabled: true});
-  equal(check_plugin_state(PLUGINS[0]), "true,false");
-  equal(check_plugin_state(PLUGINS[1]), "false,false");
-  equal(check_plugin_state(PLUGINS[2]), "false,false");
-  equal(check_plugin_state(PLUGINS[3]), "true,false");
-  equal(check_plugin_state(PLUGINS[4]), "false,false");
+  equal(await check_plugin_state(PLUGINS[0]), "true,false");
+  equal(await check_plugin_state(PLUGINS[1]), "false,false");
+  equal(await check_plugin_state(PLUGINS[2]), "false,false");
+  equal(await check_plugin_state(PLUGINS[3]), "true,false");
+  equal(await check_plugin_state(PLUGINS[4]), "false,false");
 });
--- a/toolkit/mozapps/extensions/test/xpcshell/test_pluginBlocklistCtp.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_pluginBlocklistCtp.js
@@ -1,16 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const nsIBLS = Ci.nsIBlocklistService;
 
 var gNotifier = null;
-var gNextTest = null;
 var gPluginHost = null;
 
 var gTestserver = AddonTestUtils.createHttpServer({hosts: ["example.com"]});
 gTestserver.registerDirectory("/data/", do_get_file("data"));
 
 var PLUGINS = [{
   // severity=0, vulnerabilitystatus=0 -> outdated
   name: "test_plugin_0",
@@ -49,128 +48,119 @@ var PLUGINS = [{
 {
   // not in the blocklist -> not blocked
   name: "test_plugin_5",
   version: "5",
   disabled: false,
   blocklisted: false
 }];
 
-function test_basic() {
+async function updateBlocklist(blocklistURL) {
+  if (blocklistURL) {
+    Services.prefs.setCharPref("extensions.blocklist.url", blocklistURL);
+  }
+  let blocklistUpdated = TestUtils.topicObserved("blocklist-updated");
+  gNotifier.notify(null);
+  return blocklistUpdated;
+}
+
+add_task(async function setup() {
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+  Services.prefs.setCharPref("extensions.blocklist.url", "http://example.com/data/test_pluginBlocklistCtp.xml");
+  Services.prefs.setBoolPref("plugin.load_flash_only", false);
+  await promiseStartupManager();
+
+  gPluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+  gNotifier = Cc["@mozilla.org/extensions/blocklist;1"].getService(Ci.nsITimerCallback);
+
+  registerCleanupFunction(function() {
+    Services.prefs.clearUserPref("extensions.blocklist.url");
+    Services.prefs.clearUserPref("extensions.blocklist.enabled");
+    Services.prefs.clearUserPref("plugins.click_to_play");
+  });
+});
+
+add_task(async function basic() {
+  await updateBlocklist();
   var {blocklist} = Services;
 
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == nsIBLS.STATE_OUTDATED);
-
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9") == nsIBLS.STATE_VULNERABLE_UPDATE_AVAILABLE);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9"),
+               nsIBLS.STATE_OUTDATED);
 
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9") == nsIBLS.STATE_VULNERABLE_NO_UPDATE);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9"),
+               nsIBLS.STATE_VULNERABLE_UPDATE_AVAILABLE);
 
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9") == nsIBLS.STATE_BLOCKED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9"),
+               nsIBLS.STATE_VULNERABLE_NO_UPDATE);
 
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[4], "1", "1.9") == nsIBLS.STATE_SOFTBLOCKED);
-
-  Assert.ok(blocklist.getPluginBlocklistState(PLUGINS[5], "1", "1.9") == nsIBLS.STATE_NOT_BLOCKED);
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9"),
+               nsIBLS.STATE_BLOCKED);
 
-  gNextTest = test_is_not_clicktoplay;
-  executeSoon(gNextTest);
-}
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[4], "1", "1.9"),
+               nsIBLS.STATE_SOFTBLOCKED);
+
+  Assert.equal(await blocklist.getPluginBlocklistState(PLUGINS[5], "1", "1.9"),
+               nsIBLS.STATE_NOT_BLOCKED);
+
+});
 
 function get_test_plugin() {
-  var pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
-  for (var plugin of pluginHost.getPluginTags()) {
+  for (var plugin of gPluginHost.getPluginTags()) {
     if (plugin.name == "Test Plug-in")
       return plugin;
   }
   Assert.ok(false);
   return null;
 }
 
 // At this time, the blocklist does not have an entry for the test plugin,
 // so it shouldn't be click-to-play.
-function test_is_not_clicktoplay() {
+add_task(async function test_is_not_clicktoplay() {
   var plugin = get_test_plugin();
-  var blocklistState = Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
+  var blocklistState = await Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
   Assert.notEqual(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE);
   Assert.notEqual(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
-
-  Services.prefs.setCharPref("extensions.blocklist.url", "http://example.com/data/test_pluginBlocklistCtpUndo.xml");
-  gNextTest = test_is_clicktoplay;
-  gNotifier.notify(null);
-}
+});
 
 // Here, we've updated the blocklist to have a block for the test plugin,
 // so it should be click-to-play.
-function test_is_clicktoplay() {
+add_task(async function test_is_clicktoplay() {
+  await updateBlocklist("http://example.com/data/test_pluginBlocklistCtpUndo.xml");
   var plugin = get_test_plugin();
-  var blocklistState = Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
+  var blocklistState = await Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
   Assert.equal(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
-
-  Services.prefs.setCharPref("extensions.blocklist.url", "http://example.com/data/test_pluginBlocklistCtp.xml");
-  gNextTest = test_is_not_clicktoplay2;
-  gNotifier.notify(null);
-}
+});
 
 // But now we've removed that entry from the blocklist (really we've gone back
 // to the old one), so the plugin shouldn't be click-to-play any more.
-function test_is_not_clicktoplay2() {
+add_task(async function test_is_not_clicktoplay2() {
+  await updateBlocklist("http://example.com/data/test_pluginBlocklistCtp.xml");
   var plugin = get_test_plugin();
-  var blocklistState = Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
+  var blocklistState = await Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
   Assert.notEqual(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE);
   Assert.notEqual(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
 
-  Services.prefs.setCharPref("extensions.blocklist.url", "http://example.com/data/test_pluginBlocklistCtpUndo.xml");
-  gNextTest = test_disable_blocklist;
-  gNotifier.notify(null);
-}
+});
 
 // Test that disabling the blocklist when a plugin is ctp-blocklisted will
 // result in the plugin not being click-to-play.
-function test_disable_blocklist() {
+add_task(async function test_disable_blocklist() {
+  await updateBlocklist("http://example.com/data/test_pluginBlocklistCtpUndo.xml");
   var plugin = get_test_plugin();
-  var blocklistState = Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
+  var blocklistState = await Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
   Assert.equal(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
 
-  gNextTest = null;
   Services.prefs.setBoolPref("extensions.blocklist.enabled", false);
-  blocklistState = Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
+  blocklistState = await Services.blocklist.getPluginBlocklistState(plugin, "1", "1.9");
   Assert.notEqual(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
   Assert.notEqual(blocklistState, Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE);
 
   // it should still be possible to make a plugin click-to-play via the pref
   // and setting that plugin's enabled state to click-to-play
   Services.prefs.setBoolPref("plugins.click_to_play", true);
   let previousEnabledState = plugin.enabledState;
   plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
   Assert.equal(gPluginHost.getStateForType("application/x-test"), Ci.nsIPluginTag.STATE_CLICKTOPLAY);
   // clean up plugin state
   plugin.enabledState = previousEnabledState;
-
-  do_test_finished();
-}
-
-// Observe "blocklist-updated" so we know when to advance to the next test
-function observer() {
-  if (gNextTest)
-    executeSoon(gNextTest);
-}
-
-function run_test() {
-  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+});
 
-  Services.prefs.setCharPref("extensions.blocklist.url", "http://example.com/data/test_pluginBlocklistCtp.xml");
-  Services.prefs.setBoolPref("plugin.load_flash_only", false);
-  startupManager();
-
-  gPluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
-  gNotifier = Cc["@mozilla.org/extensions/blocklist;1"].getService(Ci.nsITimerCallback);
-  Services.obs.addObserver(observer, "blocklist-updated");
-
-  registerCleanupFunction(function() {
-    Services.prefs.clearUserPref("extensions.blocklist.url");
-    Services.prefs.clearUserPref("extensions.blocklist.enabled");
-    Services.prefs.clearUserPref("plugins.click_to_play");
-    Services.obs.removeObserver(observer, "blocklist-updated");
-  });
-
-  gNextTest = test_basic;
-  do_test_pending();
-  gNotifier.notify(null);
-}
--- a/toolkit/mozapps/extensions/test/xpcshell/test_pluginInfoURL.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_pluginInfoURL.js
@@ -10,21 +10,16 @@ ChromeUtils.import("resource://gre/modul
  */
 function MockPlugin(name, version, enabledState) {
   this.name = name;
   this.version = version;
   this.enabledState = enabledState;
 }
 
 MockPlugin.prototype = {
-  get blocklisted() {
-    let bls = Services.blocklist;
-    return bls.getPluginBlocklistState(this) == bls.STATE_BLOCKED;
-  },
-
   get disabled() {
     return this.enabledState == Ci.nsIPluginTag.STATE_DISABLED;
   }
 };
 
 // The mocked blocked plugin used to test the blocklist.
 const PLUGINS = [
   new MockPlugin("test_with_infoURL", "5", Ci.nsIPluginTag.STATE_ENABLED),
--- a/tools/lint/codespell.yml
+++ b/tools/lint/codespell.yml
@@ -1,23 +1,42 @@
 ---
 codespell:
     description: Check code for common misspellings
     include:
         - browser/base/content/docs/
-        - build/docs
-        - mobile/android/docs
-        - python/docs
-        - python/mozlint
-        - taskcluster/docs
-        - testing/mozbase/docs
-        - toolkit/components/extensions/docs
+        - browser/branding/
+        - browser/extensions/activity-stream/prerendered/locales/en-US/
+        - browser/extensions/formautofill/locales/en-US/
+        - browser/extensions/onboarding/locales/en-US/
+        - browser/extensions/pocket/locale/en-US/
+        - browser/extensions/webcompat-reporter/locales/en-US/
+        - browser/locales/en-US/
+        - build/docs/
+        - devtools/client/locales/en-US/
+        - devtools/shared/locales/en-US/
+        - devtools/startup/locales/en-US/
+        - dom/locales/en-US/
+        - intl/locales/en-US/
+        - layout/tools/layout-debug/ui/locale/en-US/
+        - mobile/android/base/locales/en-US/
+        - mobile/android/branding/
+        - mobile/android/docs/
+        - mobile/android/locales/en-US/
+        - mobile/locales/en-US/
+        - netwerk/locales/en-US/
+        - python/docs/
+        - python/mozlint/
+        - services/sync/locales/en-US/
+        - taskcluster/docs/
+        - testing/mozbase/docs/
+        - toolkit/components/extensions/docs/
         - toolkit/components/telemetry/docs/
-        - toolkit/crashreporter/docs
-        - tools/lint
+        - toolkit/crashreporter/docs/
+        - tools/lint/
     exclude:
         - third_party
     # List of extensions coming from:
     # tools/lint/{flake8,eslint}.yml
     # tools/mach_commands.py (clang-format)
     # + documentation
     # + localization files
     extensions:
--- a/tools/lint/spell/exclude-list.txt
+++ b/tools/lint/spell/exclude-list.txt
@@ -1,6 +1,6 @@
 cas
 optin
 aparent
 acount
 te
-
+wasn
--- a/widget/cocoa/nsMacSharingService.mm
+++ b/widget/cocoa/nsMacSharingService.mm
@@ -4,16 +4,21 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #import <Cocoa/Cocoa.h>
 
 #include "nsMacSharingService.h"
 #include "nsCocoaUtils.h"
 #include "mozilla/MacStringHelpers.h"
 
+// List of sharingProviders that we do not want to expose to
+// the user, because they are duplicates or do not work correctly
+// within the context
+NSArray *filteredProviderTitles = @[@"Add to Reading List", @"Mail"];
+
 NS_IMPL_ISUPPORTS(nsMacSharingService, nsIMacSharingService)
 
 static NSString*
 NSImageToBase64(const NSImage* aImage)
 {
   [aImage lockFocus];
   NSBitmapImageRep* bitmapRep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:NSMakeRect(0, 0, aImage.size.width, aImage.size.height)];
   [aImage unlockFocus];
@@ -45,25 +50,26 @@ nsMacSharingService::GetSharingProviders
   JS::Rooted<JSObject*> array(aCx, JS_NewArrayObject(aCx, 0));
   NSURL* url = [NSURL URLWithString:nsCocoaUtils::ToNSString(aUrlToShare)];
 
   NSArray* sharingService = [NSSharingService
                              sharingServicesForItems:[NSArray arrayWithObject:url]];
   int32_t serviceCount = 0;
 
   for (NSSharingService *currentService in sharingService) {
-
-    JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+    if (![filteredProviderTitles containsObject:currentService.title]) {
+      JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
 
-    SetStrAttribute(aCx, obj, "title", currentService.title);
-    SetStrAttribute(aCx, obj, "menuItemTitle", currentService.menuItemTitle);
-    SetStrAttribute(aCx, obj, "image", NSImageToBase64(currentService.image));
+      SetStrAttribute(aCx, obj, "title", currentService.title);
+      SetStrAttribute(aCx, obj, "menuItemTitle", currentService.menuItemTitle);
+      SetStrAttribute(aCx, obj, "image", NSImageToBase64(currentService.image));
 
-    JS::Rooted<JS::Value> element(aCx, JS::ObjectValue(*obj));
-    JS_SetElement(aCx, array, serviceCount++, element);
+      JS::Rooted<JS::Value> element(aCx, JS::ObjectValue(*obj));
+      JS_SetElement(aCx, array, serviceCount++, element);
+    }
   }
 
   aResult.setObject(*array);
 
   return NS_OK;
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
--- a/widget/gtk/WidgetStyleCache.cpp
+++ b/widget/gtk/WidgetStyleCache.cpp
@@ -1280,16 +1280,24 @@ GetCssNodeStyleInternal(WidgetNodeType a
       return gtk_widget_get_style_context(widget);
     }
     case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE:
     {
       NS_ASSERTION(false,
           "MOZ_GTK_HEADER_BAR_BUTTON_RESTORE is used as an icon only!");
       return nullptr;
     }
+    case MOZ_GTK_WINDOW_DECORATION:
+    {
+      GtkStyleContext* parentStyle =
+          CreateSubStyleWithClass(MOZ_GTK_WINDOW, "csd");
+      style = CreateCSSNode("decoration", parentStyle);
+      g_object_unref(parentStyle);
+      break;
+    }
     default:
       return GetWidgetRootStyle(aNodeType);
   }
 
   MOZ_ASSERT(style, "missing style context for node type");
   sStyleStorage[aNodeType] = style;
   return style;
 }
--- a/widget/gtk/gtk3drawing.cpp
+++ b/widget/gtk/gtk3drawing.cpp
@@ -19,30 +19,62 @@
 
 #include <math.h>
 #include <dlfcn.h>
 
 static gboolean checkbox_check_state;
 static gboolean notebook_has_tab_gap;
 
 static ScrollbarGTKMetrics sScrollbarMetrics[2];
-static ScrollbarGTKMetrics sScrollbarMetricsActive[2];
+static ScrollbarGTKMetrics sActiveScrollbarMetrics[2];
 static ToggleGTKMetrics sCheckboxMetrics;
 static ToggleGTKMetrics sRadioMetrics;
 static ToolbarGTKMetrics sToolbarMetrics;
 
 #define ARROW_UP      0
 #define ARROW_DOWN    G_PI
 #define ARROW_RIGHT   G_PI_2
 #define ARROW_LEFT    (G_PI+G_PI_2)
 
 #if !GTK_CHECK_VERSION(3,14,0)
 #define GTK_STATE_FLAG_CHECKED (1 << 11)
 #endif
 
+static GtkBorder
+operator-(const GtkBorder& first, const GtkBorder& second)
+{
+    GtkBorder result;
+    result.left = first.left - second.left;
+    result.right = first.right - second.right;
+    result.top = first.top - second.top;
+    result.bottom = first.bottom - second.bottom;
+    return result;
+}
+
+static GtkBorder
+operator+(const GtkBorder& first, const GtkBorder& second)
+{
+    GtkBorder result;
+    result.left = first.left + second.left;
+    result.right = first.right + second.right;
+    result.top = first.top + second.top;
+    result.bottom = first.bottom + second.bottom;
+    return result;
+}
+
+static GtkBorder
+operator+=(GtkBorder& first, const GtkBorder& second)
+{
+    first.left += second.left;
+    first.right += second.right;
+    first.top += second.top;
+    first.bottom += second.bottom;
+    return first;
+}
+
 static gint
 moz_gtk_get_tab_thickness(GtkStyleContext *style);
 
 static gint
 moz_gtk_menu_item_paint(WidgetNodeType widget, cairo_t *cr, GdkRectangle* rect,
                         GtkWidgetState* state, GtkTextDirection direction);
 
 static GtkBorder
@@ -69,31 +101,31 @@ moz_gtk_add_style_margin(GtkStyleContext
 }
 
 static void
 moz_gtk_add_style_border(GtkStyleContext* style,
                          gint* left, gint* top, gint* right, gint* bottom)
 {
     GtkBorder border;
 
-    gtk_style_context_get_border(style, GTK_STATE_FLAG_NORMAL, &border);
+    gtk_style_context_get_border(style, gtk_style_context_get_state(style), &border);
 
     *left += border.left;
     *right += border.right;
     *top += border.top;
     *bottom += border.bottom;
 }
 
 static void
 moz_gtk_add_style_padding(GtkStyleContext* style,
                           gint* left, gint* top, gint* right, gint* bottom)
 {
     GtkBorder padding;
 
-    gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding);
+    gtk_style_context_get_padding(style, gtk_style_context_get_state(style), &padding);
 
     *left += padding.left;
     *right += padding.right;
     *top += padding.top;
     *bottom += padding.bottom;
 }
 
 static void
@@ -178,18 +210,18 @@ moz_gtk_refresh()
                                     "has-tab-gap", &notebook_has_tab_gap, NULL);
     }
     else {
         notebook_has_tab_gap = true;
     }
 
     sScrollbarMetrics[GTK_ORIENTATION_HORIZONTAL].initialized = false;
     sScrollbarMetrics[GTK_ORIENTATION_VERTICAL].initialized = false;
-    sScrollbarMetricsActive[GTK_ORIENTATION_HORIZONTAL].initialized = false;
-    sScrollbarMetricsActive[GTK_ORIENTATION_VERTICAL].initialized = false;
+    sActiveScrollbarMetrics[GTK_ORIENTATION_HORIZONTAL].initialized = false;
+    sActiveScrollbarMetrics[GTK_ORIENTATION_VERTICAL].initialized = false;
     sCheckboxMetrics.initialized = false;
     sRadioMetrics.initialized = false;
     sToolbarMetrics.initialized = false;
 
     /* This will destroy all of our widgets */
     ResetWidgetCache();
 }
 
@@ -214,18 +246,18 @@ moz_gtk_radio_get_metrics(gint* indicato
 }
 
 static gint
 moz_gtk_get_focus_outline_size(GtkStyleContext* style,
                                gint* focus_h_width, gint* focus_v_width)
 {
     GtkBorder border;
     GtkBorder padding;
-    gtk_style_context_get_border(style, GTK_STATE_FLAG_NORMAL, &border);
-    gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding);
+    gtk_style_context_get_border(style, gtk_style_context_get_state(style), &border);
+    gtk_style_context_get_padding(style, gtk_style_context_get_state(style), &padding);
     *focus_h_width = border.left + padding.left;
     *focus_v_width = border.top + padding.top;
     return MOZ_GTK_SUCCESS;
 }
 
 gint
 moz_gtk_get_focus_outline_size(gint* focus_h_width, gint* focus_v_width)
 {
@@ -673,18 +705,18 @@ calculate_button_inner_rect(GtkWidget* b
 {
     GtkStyleContext* style;
     GtkBorder border;
     GtkBorder padding = {0, 0, 0, 0};
 
     style = gtk_widget_get_style_context(button);
 
     /* This mirrors gtkbutton's child positioning */
-    gtk_style_context_get_border(style, GTK_STATE_FLAG_NORMAL, &border);
-    gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding);
+    gtk_style_context_get_border(style, gtk_style_context_get_state(style), &border);
+    gtk_style_context_get_padding(style, gtk_style_context_get_state(style), &padding);
 
     inner_rect->x = rect->x + border.left + padding.left;
     inner_rect->y = rect->y + padding.top + border.top;
     inner_rect->width = MAX(1, rect->width - padding.left -
        padding.right - border.left * 2);
     inner_rect->height = MAX(1, rect->height - padding.top -
        padding.bottom - border.top * 2);
 
@@ -995,29 +1027,31 @@ moz_gtk_scrollbar_paint(WidgetNodeType w
 
 static gint
 moz_gtk_scrollbar_thumb_paint(WidgetNodeType widget,
                               cairo_t *cr, const GdkRectangle* aRect,
                               GtkWidgetState* state,
                               GtkTextDirection direction)
 {
     GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state);
+    GtkStyleContext* style = GetStyleContext(widget, direction, state_flags);
+
+    GtkOrientation orientation = (widget == MOZ_GTK_SCROLLBAR_THUMB_HORIZONTAL) ?
+        GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL;
 
     GdkRectangle rect = *aRect;
-    GtkStyleContext* style = GetStyleContext(widget, direction, state_flags);
-    InsetByMargin(&rect, style);
-
-    gtk_render_slider(style, cr,
-                      rect.x,
-                      rect.y,
-                      rect.width,
-                      rect.height,
-                     (widget == MOZ_GTK_SCROLLBAR_THUMB_HORIZONTAL) ?
-                     GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL);
-
+
+    const ScrollbarGTKMetrics* metrics =
+        (state->depressed || state->active || state->inHover) ?
+        GetActiveScrollbarMetrics(orientation) :
+        GetScrollbarMetrics(orientation);
+    Inset(&rect, metrics->margin.thumb);
+
+    gtk_render_slider(style, cr, rect.x, rect.y, rect.width, rect.height,
+                      orientation);
 
     return MOZ_GTK_SUCCESS;
 }
 
 static gint
 moz_gtk_inner_spin_paint(cairo_t *cr, GdkRectangle* rect,
                          GtkWidgetState* state,
                          GtkTextDirection direction)
@@ -1601,17 +1635,17 @@ moz_gtk_toolbar_separator_paint(cairo_t 
 
         gtk_render_frame(style, cr,
                           rect->x + (rect->width - separator_width) / 2,
                           rect->y + rect->height * start_fraction,
                           separator_width,
                           rect->height * (end_fraction - start_fraction));
     } else {
         GtkBorder padding;
-        gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding);
+        gtk_style_context_get_padding(style, gtk_style_context_get_state(style), &padding);
 
         paint_width = padding.left;
         if (paint_width > rect->width)
             paint_width = rect->width;
 
         gtk_render_line(style, cr,
                         rect->x + (rect->width - paint_width) / 2,
                         rect->y + rect->height * start_fraction,
@@ -1770,17 +1804,17 @@ moz_gtk_progress_chunk_paint(cairo_t *cr
 
 static gint
 moz_gtk_get_tab_thickness(GtkStyleContext *style)
 {
     if (!notebook_has_tab_gap)
       return 0; /* tabs do not overdraw the tabpanel border with "no gap" style */
 
     GtkBorder border;
-    gtk_style_context_get_border(style, GTK_STATE_FLAG_NORMAL, &border);
+    gtk_style_context_get_border(style, gtk_style_context_get_state(style), &border);
     if (border.top < 2)
         return 2; /* some themes don't set ythickness correctly */
 
     return border.top;
 }
 
 gint
 moz_gtk_get_tab_thickness(WidgetNodeType aNodeType)
@@ -2107,17 +2141,17 @@ moz_gtk_menu_separator_paint(cairo_t *cr
 
     GtkStyleContext* style;
     gboolean wide_separators;
     gint separator_height;
     gint x, y, w;
     GtkBorder padding;
 
     style = GetStyleContext(MOZ_GTK_MENUSEPARATOR, direction);
-    gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding);
+    gtk_style_context_get_padding(style, gtk_style_context_get_state(style), &padding);
 
     x = rect->x;
     y = rect->y;
     w = rect->width;
 
     gtk_style_context_save(style);
     gtk_style_context_add_class(style, GTK_STYLE_CLASS_SEPARATOR);
 
@@ -2413,17 +2447,17 @@ moz_gtk_get_widget_border(WidgetNodeType
             if (comboBoxSeparator) {
                 style = gtk_widget_get_style_context(comboBoxSeparator);
                 gtk_style_context_get_style(style,
                                             "wide-separators", &wide_separators,
                                             "separator-width", &separator_width,
                                             NULL);
 
                 if (!wide_separators) {
-                    gtk_style_context_get_border(style, GTK_STATE_FLAG_NORMAL,
+                    gtk_style_context_get_border(style, gtk_style_context_get_state(style),
                                                  &border);
                     separator_width = border.left;
                 }
             }
 
             gtk_widget_get_preferred_size(GetWidget(MOZ_GTK_COMBOBOX_ARROW),
                                           NULL, &arrow_req);
 
@@ -2586,23 +2620,23 @@ moz_gtk_get_tab_border(gint* left, gint*
             if (direction == GTK_TEXT_DIR_RTL)
                 *right += initial_gap;
             else
                 *left += initial_gap;
         }
     } else {
         GtkBorder margin;
 
-        gtk_style_context_get_margin(style, GTK_STATE_FLAG_NORMAL, &margin);
+        gtk_style_context_get_margin(style, gtk_style_context_get_state(style), &margin);
         *left += margin.left;
         *right += margin.right;
 
         if (flags & MOZ_GTK_TAB_FIRST) {
             style = GetStyleContext(MOZ_GTK_NOTEBOOK_HEADER, direction);
-            gtk_style_context_get_margin(style, GTK_STATE_FLAG_NORMAL, &margin);
+            gtk_style_context_get_margin(style, gtk_style_context_get_state(style), &margin);
             *left += margin.left;
             *right += margin.right;
         }
     }
 
     return MOZ_GTK_SUCCESS;
 }
 
@@ -2667,17 +2701,17 @@ moz_gtk_get_toolbar_separator_width(gint
 
     GtkStyleContext* style = GetStyleContext(MOZ_GTK_TOOLBAR);
     gtk_style_context_get_style(style,
                                 "space-size", size,
                                 "wide-separators",  &wide_separators,
                                 "separator-width", &separator_width,
                                 NULL);
     /* Just in case... */
-    gtk_style_context_get_border(style, GTK_STATE_FLAG_NORMAL, &border);
+    gtk_style_context_get_border(style, gtk_style_context_get_state(style), &border);
     *size = MAX(*size, (wide_separators ? separator_width : border.left));
     return MOZ_GTK_SUCCESS;
 }
 
 gint
 moz_gtk_get_expander_size(gint* size)
 {
     GtkStyleContext* style = GetStyleContext(MOZ_GTK_EXPANDER);
@@ -2698,17 +2732,17 @@ moz_gtk_get_treeview_expander_size(gint*
 // See gtk_menu_item_draw() for reference.
 gint
 moz_gtk_get_menu_separator_height(gint *size)
 {
     gboolean  wide_separators;
     gint      separator_height;
     GtkBorder padding;
     GtkStyleContext* style = GetStyleContext(MOZ_GTK_MENUSEPARATOR);
-    gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding);
+    gtk_style_context_get_padding(style, gtk_style_context_get_state(style), &padding);
 
     gtk_style_context_save(style);
     gtk_style_context_add_class(style, GTK_STYLE_CLASS_SEPARATOR);
 
     gtk_style_context_get_style(style,
                                 "wide-separators",  &wide_separators,
                                 "separator-height", &separator_height,
                                 NULL);
@@ -2730,18 +2764,18 @@ moz_gtk_get_entry_min_height(gint* heigh
                               "min-height", height,
                               nullptr);
     } else {
         *height = 0;
     }
 
     GtkBorder border;
     GtkBorder padding;
-    gtk_style_context_get_border(style, GTK_STATE_FLAG_NORMAL, &border);
-    gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding);
+    gtk_style_context_get_border(style, gtk_style_context_get_state(style), &border);
+    gtk_style_context_get_padding(style, gtk_style_context_get_state(style), &padding);
 
     *height += (border.top + border.bottom + padding.top + padding.bottom);
 }
 
 void
 moz_gtk_get_scale_metrics(GtkOrientation orient, gint* scale_width,
                           gint* scale_height)
 {
@@ -2881,33 +2915,27 @@ GetToggleMetrics(bool isRadio)
                              "indicator_spacing", &indicator_spacing,
                              nullptr);
         metrics->minSizeWithBorder.width =
             metrics->minSizeWithBorder.height = indicator_size;
     }
     return metrics;
 }
 
-const ScrollbarGTKMetrics*
-GetScrollbarMetrics(GtkOrientation aOrientation, bool aActive)
+static void
+InitScrollbarMetrics(ScrollbarGTKMetrics* aMetrics,
+                     GtkOrientation aOrientation,
+                     GtkStateFlags aStateFlags)
 {
-    auto metrics = aActive ? &sScrollbarMetricsActive[aOrientation] :
-                             &sScrollbarMetrics[aOrientation];
-    if (metrics->initialized)
-        return metrics;
-
-    metrics->initialized = true;
-
     WidgetNodeType scrollbar = aOrientation == GTK_ORIENTATION_HORIZONTAL ?
         MOZ_GTK_SCROLLBAR_HORIZONTAL : MOZ_GTK_SCROLLBAR_VERTICAL;
 
     gboolean backward, forward, secondary_backward, secondary_forward;
     GtkStyleContext* style = GetStyleContext(scrollbar, GTK_TEXT_DIR_NONE,
-                             aActive ? GTK_STATE_FLAG_PRELIGHT :
-                                       GTK_STATE_FLAG_NORMAL);
+                                             aStateFlags);
     gtk_style_context_get_style(style,
                                 "has-backward-stepper", &backward,
                                 "has-forward-stepper", &forward,
                                 "has-secondary-backward-stepper",
                                 &secondary_backward,
                                 "has-secondary-forward-stepper",
                                 &secondary_forward, nullptr);
     bool hasButtons =
@@ -2918,51 +2946,52 @@ GetScrollbarMetrics(GtkOrientation aOrie
 
         gtk_style_context_get_style(style,
                                     "slider-width", &slider_width,
                                     "trough-border", &trough_border,
                                     "stepper-size", &stepper_size,
                                     "min-slider-length", &min_slider_size,
                                     nullptr);
 
-        metrics->size.thumb =
+        aMetrics->size.thumb =
             SizeFromLengthAndBreadth(aOrientation, min_slider_size, slider_width);
-        metrics->size.button =
+        aMetrics->size.button =
             SizeFromLengthAndBreadth(aOrientation, stepper_size, slider_width);
         // overall scrollbar
         gint breadth = slider_width + 2 * trough_border;
         // Require room for the slider in the track if we don't have buttons.
         gint length = hasButtons ? 0 : min_slider_size + 2 * trough_border;
-        metrics->size.scrollbar =
+        aMetrics->size.scrollbar =
             SizeFromLengthAndBreadth(aOrientation, length, breadth);
 
         // Borders on the major axis are set on the outermost scrollbar
         // element to correctly position the buttons when
         // trough-under-steppers is true.
         // Borders on the minor axis are set on the track element so that it
         // receives mouse events, as in GTK.
         // Other borders have been zero-initialized.
         if (aOrientation == GTK_ORIENTATION_HORIZONTAL) {
-            metrics->border.scrollbar.left =
-                metrics->border.scrollbar.right =
-                metrics->border.track.top =
-                metrics->border.track.bottom = trough_border;
+            aMetrics->border.scrollbar.left =
+                aMetrics->border.scrollbar.right =
+                aMetrics->border.track.top =
+                aMetrics->border.track.bottom = trough_border;
         } else {
-            metrics->border.scrollbar.top =
-                metrics->border.scrollbar.bottom =
-                metrics->border.track.left =
-                metrics->border.track.right = trough_border;
+            aMetrics->border.scrollbar.top =
+                aMetrics->border.scrollbar.bottom =
+                aMetrics->border.track.left =
+                aMetrics->border.track.right = trough_border;
         }
 
-        return metrics;
+        // We're done here for Gtk+ < 3.20...
+        return;
     }
 
     // GTK version > 3.20
     // scrollbar
-    metrics->border.scrollbar = GetMarginBorderPadding(style);
+    aMetrics->border.scrollbar = GetMarginBorderPadding(style);
 
     WidgetNodeType contents, track, thumb;
     if (aOrientation == GTK_ORIENTATION_HORIZONTAL) {
         contents = MOZ_GTK_SCROLLBAR_CONTENTS_HORIZONTAL;
         track = MOZ_GTK_SCROLLBAR_TROUGH_HORIZONTAL;
         thumb = MOZ_GTK_SCROLLBAR_THUMB_HORIZONTAL;
     } else {
         contents = MOZ_GTK_SCROLLBAR_CONTENTS_VERTICAL;
@@ -2983,82 +3012,174 @@ GetScrollbarMetrics(GtkOrientation aOrie
      * the Gtk+ css rule looks like:
      *
      *    "scrollbar:hover contents trough slider"
      *
      *  So we need to apply GtkStateFlags to each widgets in style path.
      */
 
     // thumb
-    style = CreateStyleContextWithStates(thumb, GTK_TEXT_DIR_NONE,
-                                         aActive ? GTK_STATE_FLAG_PRELIGHT :
-                                                   GTK_STATE_FLAG_NORMAL);
-    metrics->size.thumb = GetMinMarginBox(style);
+    style = CreateStyleContextWithStates(thumb, GTK_TEXT_DIR_NONE, aStateFlags);
+    aMetrics->size.thumb = GetMinMarginBox(style);
+    gtk_style_context_get_margin(style, gtk_style_context_get_state(style),
+                                 &aMetrics->margin.thumb);
     g_object_unref(style);
 
     // track
-    style = CreateStyleContextWithStates(track, GTK_TEXT_DIR_NONE,
-                                         aActive ? GTK_STATE_FLAG_PRELIGHT :
-                                                   GTK_STATE_FLAG_NORMAL);
-    metrics->border.track = GetMarginBorderPadding(style);
-    MozGtkSize trackMinSize = GetMinContentBox(style) + metrics->border.track;
-    MozGtkSize trackSizeForThumb = metrics->size.thumb + metrics->border.track;
+    style = CreateStyleContextWithStates(track, GTK_TEXT_DIR_NONE, aStateFlags);
+    aMetrics->border.track = GetMarginBorderPadding(style);
+    MozGtkSize trackMinSize = GetMinContentBox(style) + aMetrics->border.track;
+    MozGtkSize trackSizeForThumb = aMetrics->size.thumb + aMetrics->border.track;
     g_object_unref(style);
 
     // button
     if (hasButtons) {
         style = CreateStyleContextWithStates(MOZ_GTK_SCROLLBAR_BUTTON,
-                                             GTK_TEXT_DIR_NONE,
-                                             aActive ? GTK_STATE_FLAG_PRELIGHT :
-                                                       GTK_STATE_FLAG_NORMAL);
-        metrics->size.button = GetMinMarginBox(style);
+                                             GTK_TEXT_DIR_NONE, aStateFlags);
+        aMetrics->size.button = GetMinMarginBox(style);
         g_object_unref(style);
     } else {
-        metrics->size.button = {0, 0};
+        aMetrics->size.button = {0, 0};
     }
     if (aOrientation == GTK_ORIENTATION_HORIZONTAL) {
-        metrics->size.button.Rotate();
+        aMetrics->size.button.Rotate();
         // If the track is wider than necessary for the thumb, including when
         // the buttons will cause Gecko to expand the track to fill
         // available breadth, then add to the track border to prevent Gecko
         // from expanding the thumb to fill available breadth.
         gint extra =
             std::max(trackMinSize.height,
-                     metrics->size.button.height) - trackSizeForThumb.height;
+                     aMetrics->size.button.height) - trackSizeForThumb.height;
         if (extra > 0) {
             // If extra is odd, then the thumb is 0.5 pixels above
             // center as in gtk_range_compute_slider_position().
-            metrics->border.track.top += extra / 2;
-            metrics->border.track.bottom += extra - extra / 2;
+            aMetrics->border.track.top += extra / 2;
+            aMetrics->border.track.bottom += extra - extra / 2;
             // Update size for change in border.
             trackSizeForThumb.height += extra;
         }
     } else {
         gint extra =
             std::max(trackMinSize.width,
-                     metrics->size.button.width) - trackSizeForThumb.width;
+                     aMetrics->size.button.width) - trackSizeForThumb.width;
         if (extra > 0) {
             // If extra is odd, then the thumb is 0.5 pixels to the left
             // of center as in gtk_range_compute_slider_position().
-            metrics->border.track.left += extra / 2;
-            metrics->border.track.right += extra - extra / 2;
+            aMetrics->border.track.left += extra / 2;
+            aMetrics->border.track.right += extra - extra / 2;
             trackSizeForThumb.width += extra;
         }
     }
 
     style = CreateStyleContextWithStates(contents, GTK_TEXT_DIR_NONE,
-                                            aActive ? GTK_STATE_FLAG_PRELIGHT :
-                                                      GTK_STATE_FLAG_NORMAL);
+                                         aStateFlags);
     GtkBorder contentsBorder = GetMarginBorderPadding(style);
     g_object_unref(style);
 
-    metrics->size.scrollbar =
-        trackSizeForThumb + contentsBorder + metrics->border.scrollbar;
-
-    return metrics;
+    aMetrics->size.scrollbar =
+        trackSizeForThumb + contentsBorder + aMetrics->border.scrollbar;
+}
+
+const ScrollbarGTKMetrics*
+GetScrollbarMetrics(GtkOrientation aOrientation)
+{
+  auto metrics = &sScrollbarMetrics[aOrientation];
+  if (!metrics->initialized) {
+      InitScrollbarMetrics(metrics, aOrientation, GTK_STATE_FLAG_NORMAL);
+
+      // We calculate thumb margin here because it's composited from
+      // thumb class margin + difference margin between active and inactive
+      // scrollbars. It's a workaround which alows us to emulate
+      // overlay scrollbars for some Gtk+ themes (Ubuntu/Ambiance),
+      // when an inactive scrollbar thumb is smaller than the active one.
+      const ScrollbarGTKMetrics *metricsActive =
+          GetActiveScrollbarMetrics(aOrientation);
+
+      if (metrics->size.thumb < metricsActive->size.thumb) {
+          metrics->margin.thumb +=
+              (metrics->border.scrollbar + metrics->border.track) -
+              (metricsActive->border.scrollbar + metricsActive->border.track);
+      }
+
+      metrics->initialized = true;
+  }
+  return metrics;
+}
+
+const ScrollbarGTKMetrics*
+GetActiveScrollbarMetrics(GtkOrientation aOrientation)
+{
+  auto metrics = &sActiveScrollbarMetrics[aOrientation];
+  if (!metrics->initialized) {
+      InitScrollbarMetrics(metrics, aOrientation, GTK_STATE_FLAG_PRELIGHT);
+      metrics->initialized = true;
+  }
+  return metrics;
+}
+
+/*
+ * get_shadow_width() from gtkwindow.c is not public so we need
+ * to implement it.
+ */
+bool
+GetCSDDecorationSize(GtkBorder* aDecorationSize)
+{
+    GtkStyleContext* context = GetStyleContext(MOZ_GTK_WINDOW_DECORATION);
+
+    /* Always sum border + padding */
+    GtkBorder padding;
+    GtkStateFlags state = gtk_style_context_get_state(context);
+    gtk_style_context_get_border(context, state, aDecorationSize);
+    gtk_style_context_get_padding(context, state, &padding);
+    *aDecorationSize += padding;
+
+    // Available on GTK 3.20+.
+    static auto sGtkRenderBackgroundGetClip =
+        (void (*)(GtkStyleContext*, gdouble, gdouble, gdouble, gdouble, GdkRectangle*))
+        dlsym(RTLD_DEFAULT, "gtk_render_background_get_clip");
+
+    GtkBorder margin;
+    gtk_style_context_get_margin(context, state, &margin);
+
+    GtkBorder extents = {0, 0, 0, 0};
+    if (sGtkRenderBackgroundGetClip) {
+        /* Get shadow extents but combine with style margin; use the bigger value.
+         */
+        GdkRectangle clip;
+        sGtkRenderBackgroundGetClip(context, 0, 0, 0, 0, &clip);
+
+        extents.top = -clip.y;
+        extents.right = clip.width + clip.x;
+        extents.bottom = clip.height + clip.y;
+        extents.left = -clip.x;
+
+        extents.top = MAX(extents.top, margin.top);
+        extents.right = MAX(extents.right, margin.right);
+        extents.bottom = MAX(extents.bottom, margin.bottom);
+        extents.left = MAX(extents.left, margin.left);
+    } else {
+        /* If we can't get shadow extents use decoration-resize-handle instead
+         * as a workaround. This is inspired by update_border_windows()
+         * from gtkwindow.c although this is not 100% accurate as we emulate
+         * the extents here.
+         */
+        gint handle;
+        gtk_widget_style_get(GetWidget(MOZ_GTK_WINDOW),
+                             "decoration-resize-handle", &handle,
+                             NULL);
+
+        extents.top = handle + margin.top;
+        extents.right = handle + margin.right;
+        extents.bottom = handle + margin.bottom;
+        extents.left = handle + margin.left;
+    }
+
+    *aDecorationSize += extents;
+
+    return (sGtkRenderBackgroundGetClip != nullptr);
 }
 
 /* cairo_t *cr argument has to be a system-cairo. */
 gint
 moz_gtk_widget_paint(WidgetNodeType widget, cairo_t *cr,
                      GdkRectangle* rect,
                      GtkWidgetState* state, gint flags,
                      GtkTextDirection direction)
--- a/widget/gtk/gtkdrawing.h
+++ b/widget/gtk/gtkdrawing.h
@@ -77,16 +77,19 @@ typedef struct {
     MozGtkSize scrollbar;
     MozGtkSize thumb;
     MozGtkSize button;
   } size;
   struct {
     GtkBorder scrollbar;
     GtkBorder track;
   } border;
+  struct {
+    GtkBorder thumb;
+  } margin;
 } ScrollbarGTKMetrics;
 
 typedef struct {
   bool initialized;
   MozGtkSize minSizeWithBorder;
   GtkBorder borderAndPadding;
 } ToggleGTKMetrics;
 
@@ -329,16 +332,19 @@ typedef enum {
   MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE,
 
   /* MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE is a state of
    * MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE button and it's used as
    * an icon placeholder only.
    */
   MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE,
 
+  /* Client-side window decoration node. Available on GTK 3.20+. */
+  MOZ_GTK_WINDOW_DECORATION,
+
   MOZ_GTK_WIDGET_NODE_COUNT
 } WidgetNodeType;
 
 /*** General library functions ***/
 /**
  * Initializes the drawing library.  You must call this function
  * prior to using any other functionality.
  * returns: MOZ_GTK_SUCCESS if there were no errors
@@ -492,21 +498,27 @@ moz_gtk_get_scale_metrics(GtkOrientation
  * returns:    MOZ_GTK_SUCCESS if there was no error, an error code otherwise
  */
 gint
 moz_gtk_get_scalethumb_metrics(GtkOrientation orient, gint* thumb_length, gint* thumb_height);
 
 /**
  * Get the metrics in GTK pixels for a scrollbar.
  * aOrientation:     [IN] the scrollbar orientation
- * aActive:          [IN] Metricts for scrollbar with mouse pointer over it.
- *
  */
 const ScrollbarGTKMetrics*
-GetScrollbarMetrics(GtkOrientation aOrientation, bool aActive = false);
+GetScrollbarMetrics(GtkOrientation aOrientation);
+
+/**
+ * Get the metrics in GTK pixels for a scrollbar which is active
+ * (selected by mouse pointer).
+ * aOrientation:     [IN] the scrollbar orientation
+ */
+const ScrollbarGTKMetrics*
+GetActiveScrollbarMetrics(GtkOrientation aOrientation);
 
 /**
  * Get the desired size of a dropdown arrow button
  * width:   [OUT] the desired width
  * height:  [OUT] the desired height
  *
  * returns:    MOZ_GTK_SUCCESS if there was no error, an error code otherwise
  */
@@ -601,9 +613,21 @@ GetToolbarButtonMetrics(WidgetNodeType a
  * aMaxButtonNums: [IN] Allocated aButtonLayout entries. Must be at least
                         TOOLBAR_BUTTONS wide.
  *
  * returns:    Number of returned entries at aButtonLayout.
  */
 int
 GetGtkHeaderBarButtonLayout(WidgetNodeType* aButtonLayout, int aMaxButtonNums);
 
+/**
+ * Get size of CSD window extents.
+ *
+ * aDecorationSize [OUT] Returns calculated (or estimated) decoration
+ *                       size of CSD window.
+ *
+ * returns:    True if we have extract decoration size (for GTK 3.20+)
+ *             False if we have only an estimation (for GTK+ before  3.20+)
+ */
+bool
+GetCSDDecorationSize(GtkBorder* aDecorationSize);
+
 #endif
--- a/widget/gtk/nsLookAndFeel.cpp
+++ b/widget/gtk/nsLookAndFeel.cpp
@@ -178,17 +178,17 @@ GetBorderColors(GtkStyleContext* aContex
     gtk_style_context_get(aContext, state, GTK_STYLE_PROPERTY_BORDER_STYLE,
                           &borderStyle, nullptr);
     bool visible = borderStyle != GTK_BORDER_STYLE_NONE &&
         borderStyle != GTK_BORDER_STYLE_HIDDEN;
     if (visible) {
         // GTK has an initial value of zero for border-widths, and so themes
         // need to explicitly set border-widths to make borders visible.
         GtkBorder border;
-        gtk_style_context_get_border(aContext, GTK_STATE_FLAG_NORMAL, &border);
+        gtk_style_context_get_border(aContext, state, &border);
         visible = border.top != 0 || border.right != 0 ||
             border.bottom != 0 || border.left != 0;
     }
 
     if (visible &&
         GetUnicoBorderGradientColors(aContext, aLightColor, aDarkColor))
         return true;
 
--- a/widget/gtk/nsNativeThemeGTK.cpp
+++ b/widget/gtk/nsNativeThemeGTK.cpp
@@ -1283,32 +1283,34 @@ nsNativeThemeGTK::GetWidgetBorder(nsDevi
   aResult->top = aResult->left = aResult->right = aResult->bottom = 0;
   switch (aWidgetType) {
   case NS_THEME_SCROLLBAR_HORIZONTAL:
   case NS_THEME_SCROLLBAR_VERTICAL:
     {
       GtkOrientation orientation =
         aWidgetType == NS_THEME_SCROLLBAR_HORIZONTAL ?
         GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL;
-      const ScrollbarGTKMetrics* metrics = GetScrollbarMetrics(orientation, true);
+      const ScrollbarGTKMetrics* metrics =
+        GetActiveScrollbarMetrics(orientation);
 
       const GtkBorder& border = metrics->border.scrollbar;
       aResult->top = border.top;
       aResult->right = border.right;
       aResult->bottom = border.bottom;
       aResult->left = border.left;
     }
     break;
   case NS_THEME_SCROLLBARTRACK_HORIZONTAL:
   case NS_THEME_SCROLLBARTRACK_VERTICAL:
     {
       GtkOrientation orientation =
         aWidgetType == NS_THEME_SCROLLBARTRACK_HORIZONTAL ?
         GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL;
-      const ScrollbarGTKMetrics* metrics = GetScrollbarMetrics(orientation, true);
+      const ScrollbarGTKMetrics* metrics =
+        GetActiveScrollbarMetrics(orientation);
 
       const GtkBorder& border = metrics->border.track;
       aResult->top = border.top;
       aResult->right = border.right;
       aResult->bottom = border.bottom;
       aResult->left = border.left;
     }
     break;
@@ -1453,28 +1455,28 @@ nsNativeThemeGTK::GetMinimumWidgetSize(n
   aResult->width = aResult->height = 0;
   *aIsOverridable = true;
 
   switch (aWidgetType) {
     case NS_THEME_SCROLLBARBUTTON_UP:
     case NS_THEME_SCROLLBARBUTTON_DOWN:
       {
         const ScrollbarGTKMetrics* metrics =
-          GetScrollbarMetrics(GTK_ORIENTATION_VERTICAL, true);
+          GetActiveScrollbarMetrics(GTK_ORIENTATION_VERTICAL);
 
         aResult->width = metrics->size.button.width;
         aResult->height = metrics->size.button.height;
         *aIsOverridable = false;
       }
       break;
     case NS_THEME_SCROLLBARBUTTON_LEFT:
     case NS_THEME_SCROLLBARBUTTON_RIGHT:
       {
         const ScrollbarGTKMetrics* metrics =
-          GetScrollbarMetrics(GTK_ORIENTATION_HORIZONTAL, true);
+          GetActiveScrollbarMetrics(GTK_ORIENTATION_HORIZONTAL);
 
         aResult->width = metrics->size.button.width;
         aResult->height = metrics->size.button.height;
         *aIsOverridable = false;
       }
       break;
     case NS_THEME_SPLITTER:
     {
@@ -1497,29 +1499,31 @@ nsNativeThemeGTK::GetMinimumWidgetSize(n
       /* While we enforce a minimum size for the thumb, this is ignored
        * for the some scrollbars if buttons are hidden (bug 513006) because
        * the thumb isn't a direct child of the scrollbar, unlike the buttons
        * or track. So add a minimum size to the track as well to prevent a
        * 0-width scrollbar. */
       GtkOrientation orientation =
         aWidgetType == NS_THEME_SCROLLBAR_HORIZONTAL ?
         GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL;
-      const ScrollbarGTKMetrics* metrics = GetScrollbarMetrics(orientation, true);
+      const ScrollbarGTKMetrics* metrics =
+        GetActiveScrollbarMetrics(orientation);
 
       aResult->width = metrics->size.scrollbar.width;
       aResult->height = metrics->size.scrollbar.height;
     }
     break;
     case NS_THEME_SCROLLBARTHUMB_VERTICAL:
     case NS_THEME_SCROLLBARTHUMB_HORIZONTAL:
       {
         GtkOrientation orientation =
           aWidgetType == NS_THEME_SCROLLBARTHUMB_HORIZONTAL ?
           GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL;
-        const ScrollbarGTKMetrics* metrics = GetScrollbarMetrics(orientation, true);
+        const ScrollbarGTKMetrics* metrics =
+          GetActiveScrollbarMetrics(orientation);
 
         aResult->width = metrics->size.thumb.width;
         aResult->height = metrics->size.thumb.height;
         *aIsOverridable = false;
       }
       break;
     case NS_THEME_RANGE_THUMB:
       {
--- a/widget/gtk/nsWindow.cpp
+++ b/widget/gtk/nsWindow.cpp
@@ -124,16 +124,17 @@ using namespace mozilla::widget;
 #include "WindowSurfaceX11SHM.h"
 #include "WindowSurfaceXRender.h"
 #endif // MOZ_X11
 #ifdef MOZ_WAYLAND
 #include "nsIClipboard.h"
 #endif
 
 #include "nsShmImage.h"
+#include "gtkdrawing.h"
 
 #include "NativeKeyBindings.h"
 
 #include <dlfcn.h>
 
 using namespace mozilla;
 using namespace mozilla::gfx;
 using namespace mozilla::widget;
@@ -4202,16 +4203,18 @@ nsWindow::NativeMoveResize()
         // hides the window or shows it.  It also prevents us from
         // calling NativeShow(false) excessively on the window which
         // causes unneeded X traffic.
         if (!mNeedsShow && mIsShown) {
             mNeedsShow = true;
             NativeShow(false);
         }
         NativeMove();
+
+        return;
     }
 
     GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mBounds.Size());
     GdkPoint topLeft = DevicePixelsToGdkPointRoundDown(mBounds.TopLeft());
 
     LOG(("nsWindow::NativeMoveResize [%p] %d %d %d %d\n", (void *)this,
          topLeft.x, topLeft.y, size.width, size.height));
 
@@ -6587,16 +6590,38 @@ nsWindow::ClearCachedResources()
     for (GList* list = children; list; list = list->next) {
         nsWindow* window = get_window_for_gdk_window(GDK_WINDOW(list->data));
         if (window) {
             window->ClearCachedResources();
         }
     }
 }
 
+/* nsWindow::UpdateClientOffsetForCSDWindow() is designed to be called from
+ * paint code to update mClientOffset any time. It also propagates
+ * the mClientOffset to child tabs.
+ *
+ * It works only for CSD decorated GtkWindow.
+ */
+void
+nsWindow::UpdateClientOffsetForCSDWindow()
+{
+    // _NET_FRAME_EXTENTS is not set on client decorated windows,
+    // so we need to read offset between mContainer and toplevel mShell
+    // window.
+    GtkBorder decorationSize;
+    GetCSDDecorationSize(&decorationSize);
+    mClientOffset = nsIntPoint(decorationSize.left, decorationSize.top);
+
+    // Send a WindowMoved notification. This ensures that TabParent
+    // picks up the new client offset and sends it to the child process
+    // if appropriate.
+    NotifyWindowMoved(mBounds.x, mBounds.y);
+}
+
 nsresult
 nsWindow::SetNonClientMargins(LayoutDeviceIntMargin &aMargins)
 {
     SetDrawsInTitlebar(aMargins.top == 0);
     return NS_OK;
 }
 
 void
@@ -6661,16 +6686,18 @@ nsWindow::SetDrawsInTitlebar(bool aState
                                         &allocation.height);
         gtk_widget_size_allocate(GTK_WIDGET(mShell), &allocation);
 
         gtk_widget_realize(GTK_WIDGET(mShell));
         gtk_widget_reparent(GTK_WIDGET(mContainer), GTK_WIDGET(mShell));
         mNeedsShow = true;
         NativeResize();
 
+        UpdateClientOffsetForCSDWi