Merge mozilla-inbound to mozilla-central a=merge
authorDaniel Varga <dvarga@mozilla.com>
Sat, 08 Sep 2018 01:04:10 +0300
changeset 490965 3026c40acec365f6606c39234bba5f4e99c83dac
parent 490957 36ee80fc14aefdd00385da7fc073f28e78dfbd40 (diff)
parent 490964 d1b2141b1c454f28b8d35164c958e9ddcc7058fe (current diff)
child 490982 0e957ded091e36c8af71ec7a175274f550a14b9b
child 491026 293719637a6250a9ff98082e03b89b940c3ce761
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone64.0a1
first release with
nightly linux32
3026c40acec3 / 64.0a1 / 20180907225622 / files
nightly linux64
3026c40acec3 / 64.0a1 / 20180907225622 / files
nightly mac
3026c40acec3 / 64.0a1 / 20180907225622 / files
nightly win32
3026c40acec3 / 64.0a1 / 20180907225622 / files
nightly win64
3026c40acec3 / 64.0a1 / 20180907225622 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-inbound to mozilla-central a=merge
--- a/.taskcluster.yml
+++ b/.taskcluster.yml
@@ -114,17 +114,17 @@ tasks:
                 GECKO_HEAD_REPOSITORY: '${repoUrl}'
                 GECKO_HEAD_REF: '${push.revision}'
                 GECKO_HEAD_REV: '${push.revision}'
                 GECKO_COMMIT_MSG: {$if: 'tasks_for != "action"', then: '${push.comment}'}
                 HG_STORE_PATH: /builds/worker/checkouts/hg-store
                 TASKCLUSTER_CACHES: /builds/worker/checkouts
               - $if: 'tasks_for == "action"'
                 then:
-                  ACTION_TASK_GROUP_ID: '${taskGroupId}'     # taskGroupId of the target task
+                  ACTION_TASK_GROUP_ID: '${action.taskGroupId}'     # taskGroupId of the target task
                   ACTION_TASK_ID: {$json: {$eval: 'taskId'}} # taskId of the target task (JSON-encoded)
                   ACTION_INPUT: {$json: {$eval: 'input'}}
                   ACTION_CALLBACK: '${action.cb_name}'
                   ACTION_PARAMETERS: {$json: {$eval: 'parameters'}}
 
           cache:
             level-${repository.level}-checkouts-sparse-v2: /builds/worker/checkouts
 
--- a/browser/base/content/test/forms/head.js
+++ b/browser/base/content/test/forms/head.js
@@ -6,13 +6,13 @@ function hideSelectPopup(selectPopup, mo
     return ContentTaskUtils.waitForCondition(() => !SelectContentHelper.open);
   });
 
   if (mode == "escape") {
     EventUtils.synthesizeKey("KEY_Escape", {}, win);
   } else if (mode == "enter") {
     EventUtils.synthesizeKey("KEY_Enter", {}, win);
   } else if (mode == "click") {
-    EventUtils.synthesizeMouseAtCenter(selectPopup.lastChild, { }, win);
+    EventUtils.synthesizeMouseAtCenter(selectPopup.lastElementChild, { }, win);
   }
 
   return selectClosedPromise;
 }
--- a/browser/base/content/test/performance/browser_windowopen.js
+++ b/browser/base/content/test/performance/browser_windowopen.js
@@ -42,16 +42,27 @@ if (Services.appinfo.OS == "WINNT" || Se
         "whenWindowLayoutReady@chrome://browser/content/browser-tabsintitlebar.js",
       ],
       // These numbers should only ever go down - never up.
       maxCount: Services.appinfo.OS == "WINNT" ? 5 : 4,
     },
   );
 }
 
+// We'll assume the changes we are seeing are due to this focus change if
+// there are at least 5 areas that changed near the top of the screen, or if
+// the toolbar background is involved on OSX, but will only ignore this once.
+function isLikelyFocusChange(rects) {
+  if (rects.length > 5 && rects.every(r => r.y2 < 100))
+    return true;
+  if (Services.appinfo.OS == "Darwin" && rects.length == 2 && rects.every(r => r.y1 == 0 && r.h == 33))
+    return true;
+  return false;
+}
+
 /*
  * This test ensures that there are no unexpected
  * uninterruptible reflows or flickering areas when opening new windows.
  */
 add_task(async function() {
   // Flushing all caches helps to ensure that we get consistent
   // behaviour when opening a new window, even if windows have been
   // opened in previous tests.
@@ -66,21 +77,17 @@ add_task(async function() {
   let alreadyFocused = false;
   let inRange = (val, min, max) => min <= val && val <= max;
   let expectations = {
     expectedReflows: EXPECTED_REFLOWS,
     frames: {
       filter(rects, frame, previousFrame) {
         // The first screenshot we get in OSX / Windows shows an unfocused browser
         // window for some reason. See bug 1445161.
-        //
-        // We'll assume the changes we are seeing are due to this focus change if
-        // there are at least 5 areas that changed near the top of the screen, but
-        // will only ignore this once (hence the alreadyFocused variable).
-        if (!alreadyFocused && rects.length > 5 && rects.every(r => r.y2 < 100)) {
+        if (!alreadyFocused && isLikelyFocusChange(rects)) {
           alreadyFocused = true;
           todo(false,
                "bug 1445161 - the window should be focused at first paint, " +
                rects.toSource());
           return [];
         }
 
         return rects;
--- a/browser/base/content/test/permissions/browser_autoplay_blocked.js
+++ b/browser/base/content/test/permissions/browser_autoplay_blocked.js
@@ -22,30 +22,30 @@ function autoplayBlockedIcon() {
 }
 
 add_task(async function testMainViewVisible() {
 
   Services.prefs.setIntPref("media.autoplay.default", Ci.nsIAutoplay.ALLOWED);
 
   await BrowserTestUtils.withNewTab(AUTOPLAY_PAGE, async function() {
     let permissionsList = document.getElementById("identity-popup-permission-list");
-    let emptyLabel = permissionsList.nextSibling.nextSibling;
+    let emptyLabel = permissionsList.nextElementSibling.nextElementSibling;
 
     ok(BrowserTestUtils.is_hidden(autoplayBlockedIcon()), "Blocked icon not shown");
 
     await openIdentityPopup();
     ok(!BrowserTestUtils.is_hidden(emptyLabel), "List of permissions is empty");
     await closeIdentityPopup();
   });
 
   Services.prefs.setIntPref("media.autoplay.default", Ci.nsIAutoplay.BLOCKED);
 
   await BrowserTestUtils.withNewTab(AUTOPLAY_PAGE, async function() {
     let permissionsList = document.getElementById("identity-popup-permission-list");
-    let emptyLabel = permissionsList.nextSibling.nextSibling;
+    let emptyLabel = permissionsList.nextElementSibling.nextElementSibling;
 
     ok(!BrowserTestUtils.is_hidden(autoplayBlockedIcon()), "Blocked icon is shown");
 
     await openIdentityPopup();
     ok(BrowserTestUtils.is_hidden(emptyLabel), "List of permissions is not empty");
     let labelText = SitePermissions.getPermissionLabel("autoplay-media");
     let labels = permissionsList.querySelectorAll(".identity-popup-permission-label");
     is(labels.length, 1, "One permission visible in main view");
--- a/browser/base/content/test/popupNotifications/head.js
+++ b/browser/base/content/test/popupNotifications/head.js
@@ -284,17 +284,17 @@ function triggerSecondaryCommand(popup, 
   info("Triggering secondary command for notification " + notification.id);
 
   if (index == 0) {
     EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
     return;
   }
 
   // Extra secondary actions appear in a menu.
-  notification.secondaryButton.nextSibling.nextSibling.focus();
+  notification.secondaryButton.nextElementSibling.nextElementSibling.focus();
 
   popup.addEventListener("popupshown", function() {
     info("Command popup open for notification " + notification.id);
     // Press down until the desired command is selected. Decrease index by one
     // since the secondary action was handled above.
     for (let i = 0; i <= index - 1; i++) {
       EventUtils.synthesizeKey("KEY_ArrowDown");
     }
--- a/browser/base/content/test/urlbar/head.js
+++ b/browser/base/content/test/urlbar/head.js
@@ -273,17 +273,17 @@ function promisePageActionViewShown() {
   return BrowserTestUtils.waitForEvent(BrowserPageActions.panelNode, "ViewShown").then(async event => {
     let panelViewNode = event.originalTarget;
     await promisePageActionViewChildrenVisible(panelViewNode);
     return panelViewNode;
   });
 }
 
 function promisePageActionViewChildrenVisible(panelViewNode) {
-  return promiseNodeVisible(panelViewNode.firstChild.firstChild);
+  return promiseNodeVisible(panelViewNode.firstElementChild.firstElementChild);
 }
 
 function promiseNodeVisible(node) {
   info(`promiseNodeVisible waiting, node.id=${node.id} node.localeName=${node.localName}\n`);
   let dwu = window.windowUtils;
   return BrowserTestUtils.waitForCondition(() => {
     let bounds = dwu.getBoundsWithoutFlushing(node);
     if (bounds.width > 0 && bounds.height > 0) {
--- a/browser/base/content/test/webextensions/head.js
+++ b/browser/base/content/test/webextensions/head.js
@@ -26,17 +26,17 @@ function promisePopupNotificationShown(n
     function popupshown() {
       let notification = PopupNotifications.getNotification(name);
       if (!notification) { return; }
 
       ok(notification, `${name} notification shown`);
       ok(PopupNotifications.isPanelOpen, "notification panel open");
 
       PopupNotifications.panel.removeEventListener("popupshown", popupshown);
-      resolve(PopupNotifications.panel.firstChild);
+      resolve(PopupNotifications.panel.firstElementChild);
     }
 
     PopupNotifications.panel.addEventListener("popupshown", popupshown);
   });
 }
 
 /**
  * Wait for a specific install event to fire for a given addon
--- a/browser/base/content/test/webrtc/head.js
+++ b/browser/base/content/test/webrtc/head.js
@@ -293,17 +293,17 @@ function promiseMessage(aMessage, aActio
 }
 
 function promisePopupNotificationShown(aName, aAction) {
   return new Promise(resolve => {
 
     PopupNotifications.panel.addEventListener("popupshown", function() {
       ok(!!PopupNotifications.getNotification(aName), aName + " notification shown");
       ok(PopupNotifications.isPanelOpen, "notification panel open");
-      ok(!!PopupNotifications.panel.firstChild, "notification panel populated");
+      ok(!!PopupNotifications.panel.firstElementChild, "notification panel populated");
 
       executeSoon(resolve);
     }, {once: true});
 
     if (aAction)
       aAction();
 
   });
@@ -336,17 +336,17 @@ function promiseNoPopupNotification(aNam
   });
 }
 
 const kActionAlways = 1;
 const kActionDeny = 2;
 const kActionNever = 3;
 
 function activateSecondaryAction(aAction) {
-  let notification = PopupNotifications.panel.firstChild;
+  let notification = PopupNotifications.panel.firstElementChild;
   switch (aAction) {
     case kActionNever:
       notification.checkbox.setAttribute("checked", true); // fallthrough
     case kActionDeny:
       notification.secondaryButton.click();
       break;
     case kActionAlways:
       notification.checkbox.setAttribute("checked", true);
--- a/browser/components/customizableui/DragPositionManager.jsm
+++ b/browser/components/customizableui/DragPositionManager.jsm
@@ -278,17 +278,17 @@ AreaPositionManager.prototype = {
       rv = prev;
     }
     return rv;
   },
 
   _getVisibleSiblingForDirection(aNode, aDirection) {
     let rv = aNode;
     do {
-      rv = rv[aDirection + "Sibling"];
+      rv = rv[aDirection + "ElementSibling"];
     } while (rv && rv.getAttribute("hidden") == "true");
     return rv;
   },
 };
 
 var DragPositionManager = {
   start(aWindow) {
     let areas = [aWindow.document.getElementById(kPaletteId)];
--- a/browser/components/extensions/parent/ext-browserAction.js
+++ b/browser/components/extensions/parent/ext-browserAction.js
@@ -1,25 +1,26 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 ChromeUtils.defineModuleGetter(this, "CustomizableUI",
                                "resource:///modules/CustomizableUI.jsm");
 ChromeUtils.defineModuleGetter(this, "clearTimeout",
                                "resource://gre/modules/Timer.jsm");
+ChromeUtils.defineModuleGetter(this, "ExtensionTelemetry",
+                               "resource://gre/modules/ExtensionTelemetry.jsm");
 ChromeUtils.defineModuleGetter(this, "setTimeout",
                                "resource://gre/modules/Timer.jsm");
 ChromeUtils.defineModuleGetter(this, "ViewPopup",
                                "resource:///modules/ExtensionPopups.jsm");
 
 var {
   DefaultWeakMap,
   ExtensionError,
-  ExtensionTelemetry,
 } = ExtensionUtils;
 
 ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 
 var {
   IconDetails,
   StartupCache,
 } = ExtensionParent;
--- a/browser/components/extensions/parent/ext-pageAction.js
+++ b/browser/components/extensions/parent/ext-pageAction.js
@@ -1,27 +1,28 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+ChromeUtils.defineModuleGetter(this, "ExtensionTelemetry",
+                               "resource://gre/modules/ExtensionTelemetry.jsm");
 ChromeUtils.defineModuleGetter(this, "PageActions",
                                "resource:///modules/PageActions.jsm");
 ChromeUtils.defineModuleGetter(this, "PanelPopup",
                                "resource:///modules/ExtensionPopups.jsm");
 
 ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 
 var {
   IconDetails,
   StartupCache,
 } = ExtensionParent;
 
 var {
   DefaultWeakMap,
-  ExtensionTelemetry,
 } = ExtensionUtils;
 
 // WeakMap[Extension -> PageAction]
 let pageActionMap = new WeakMap();
 
 this.pageAction = class extends ExtensionAPI {
   static for(extension) {
     return pageActionMap.get(extension);
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -74,16 +74,17 @@ skip-if = (verify && debug && (os == 'ma
 [browser_ext_commands_execute_page_action.js]
 skip-if = (verify && (os == 'linux' || os == 'mac'))
 [browser_ext_commands_execute_sidebar_action.js]
 [browser_ext_commands_getAll.js]
 [browser_ext_commands_onCommand.js]
 [browser_ext_commands_update.js]
 [browser_ext_connect_and_move_tabs.js]
 [browser_ext_contentscript_connect.js]
+[browser_ext_contentscript_nontab_connect.js]
 [browser_ext_contextMenus.js]
 [browser_ext_contextMenus_checkboxes.js]
 [browser_ext_contextMenus_commands.js]
 [browser_ext_contextMenus_icons.js]
 [browser_ext_contextMenus_onclick.js]
 [browser_ext_contextMenus_radioGroups.js]
 [browser_ext_contextMenus_targetUrlPatterns.js]
 [browser_ext_contextMenus_uninstall.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contentscript_nontab_connect.js
@@ -0,0 +1,107 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// This script is loaded in a non-tab extension context, and starts the test by
+// loading an iframe that runs contentScript as a content script.
+function extensionScript() {
+  let FRAME_URL = browser.runtime.getManifest().content_scripts[0].matches[0];
+  // Cannot use :8888 in the manifest because of bug 1468162.
+  FRAME_URL = FRAME_URL.replace("mochi.test", "mochi.test:8888");
+
+  browser.runtime.onConnect.addListener((port) => {
+    browser.test.assertEq(port.sender.tab, undefined, "Sender is not a tab");
+    browser.test.assertEq(port.sender.url, FRAME_URL, "Expected sender URL");
+    port.onMessage.addListener(msg => {
+      browser.test.assertEq("pong", msg, "Reply from content script");
+      port.disconnect();
+    });
+    port.postMessage("ping");
+  });
+
+  browser.test.log(`Going to open ${FRAME_URL} at ${location.pathname}`);
+  let f = document.createElement("iframe");
+  f.src = FRAME_URL;
+  document.body.appendChild(f);
+}
+
+function contentScript() {
+  browser.test.log(`Running content script at ${document.URL}`);
+
+  let port = browser.runtime.connect();
+  port.onMessage.addListener(msg => {
+    browser.test.assertEq("ping", msg, "Expected message to content script");
+    port.postMessage("pong");
+  });
+  port.onDisconnect.addListener(() => {
+    browser.test.sendMessage("disconnected_in_content_script");
+  });
+}
+
+add_task(async function connect_from_background_frame() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      content_scripts: [{
+        matches: ["http://mochi.test/?background"],
+        js: ["contentscript.js"],
+        all_frames: true,
+      }],
+    },
+    files: {
+      "contentscript.js": contentScript,
+    },
+    background: extensionScript,
+  });
+  await extension.startup();
+  await extension.awaitMessage("disconnected_in_content_script");
+  await extension.unload();
+});
+
+add_task(async function connect_from_sidebar_panel() {
+  let extension = ExtensionTestUtils.loadExtension({
+    useAddonManager: "temporary", // To automatically show sidebar on load.
+    manifest: {
+      content_scripts: [{
+        matches: ["http://mochi.test/?sidebar"],
+        js: ["contentscript.js"],
+        all_frames: true,
+      }],
+      sidebar_action: {
+        default_panel: "sidebar.html",
+      },
+    },
+    files: {
+      "contentscript.js": contentScript,
+      "sidebar.html": `<!DOCTYPE html><meta charset="utf-8"><body><script src="sidebar.js"></script></body>`,
+      "sidebar.js": extensionScript,
+    },
+  });
+  await extension.startup();
+  await extension.awaitMessage("disconnected_in_content_script");
+  await extension.unload();
+});
+
+add_task(async function connect_from_browser_action_popup() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      content_scripts: [{
+        matches: ["http://mochi.test/?browser_action_popup"],
+        js: ["contentscript.js"],
+        all_frames: true,
+      }],
+      browser_action: {
+        default_popup: "popup.html",
+      },
+    },
+    files: {
+      "contentscript.js": contentScript,
+      "popup.html": `<!DOCTYPE html><meta charset="utf-8"><body><script src="popup.js"></script></body>`,
+      "popup.js": extensionScript,
+    },
+  });
+  await extension.startup();
+  await clickBrowserAction(extension);
+  await extension.awaitMessage("disconnected_in_content_script");
+  await closeBrowserAction(extension);
+  await extension.unload();
+});
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -393,17 +393,17 @@ async function openChromeContextMenu(men
   const menu = win.document.getElementById(menuId);
   const shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
   EventUtils.synthesizeMouseAtCenter(node, {type: "contextmenu"}, win);
   await shown;
   return menu;
 }
 
 async function openSubmenu(submenuItem, win = window) {
-  const submenu = submenuItem.firstChild;
+  const submenu = submenuItem.firstElementChild;
   const shown = BrowserTestUtils.waitForEvent(submenu, "popupshown");
   EventUtils.synthesizeMouseAtCenter(submenuItem, {}, win);
   await shown;
   return submenu;
 }
 
 function closeChromeContextMenu(menuId, itemToSelect, win = window) {
   const menu = win.document.getElementById(menuId);
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent.js
@@ -21,68 +21,75 @@ add_task(async function test() {
   let prefix = "http://mochi.test:8888/browser/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent_page.html";
 
   function getElts(browser) {
     return browser.contentTitle.split("|");
   }
 
   // Step 1
   let non_private_browser = gBrowser.selectedBrowser;
-  non_private_browser.loadURI(prefix + "?action=set&name=test&value=value&initial=true");
-  await BrowserTestUtils.browserLoaded(non_private_browser);
+  let url = prefix + "?action=set&name=test&value=value&initial=true";
+  non_private_browser.loadURI(url);
+  await BrowserTestUtils.browserLoaded(non_private_browser, false, url);
 
 
   // Step 2
   let private_window = await BrowserTestUtils.openNewBrowserWindow({ private: true });
   let private_browser = private_window.getBrowser().selectedBrowser;
-  private_browser.loadURI(prefix + "?action=set&name=test2&value=value2");
-  await BrowserTestUtils.browserLoaded(private_browser);
+  url = prefix + "?action=set&name=test2&value=value2";
+  private_browser.loadURI(url);
+  await BrowserTestUtils.browserLoaded(private_browser, false, url);
 
 
   // Step 3
-  non_private_browser.loadURI(prefix + "?action=get&name=test2");
-  await BrowserTestUtils.browserLoaded(non_private_browser);
+  url = prefix + "?action=get&name=test2";
+  non_private_browser.loadURI(url);
+  await BrowserTestUtils.browserLoaded(non_private_browser, false, url);
   let elts = await getElts(non_private_browser);
   isnot(elts[0], "value2", "public window shouldn't see private storage");
   is(elts[1], "1", "public window should only see public items");
 
 
   // Step 4
-  private_browser.loadURI(prefix + "?action=get&name=test");
-  await BrowserTestUtils.browserLoaded(private_browser);
+  url = prefix + "?action=get&name=test";
+  private_browser.loadURI(url);
+  await BrowserTestUtils.browserLoaded(private_browser, false, url);
   elts = await getElts(private_browser);
   isnot(elts[0], "value", "private window shouldn't see public storage");
   is(elts[1], "1", "private window should only see private items");
 
 
   // Reopen the private window again, without privateBrowsing, which should clear the
   // the private storage.
   private_window.close();
   private_window = await BrowserTestUtils.openNewBrowserWindow({ private: false });
   private_browser = null;
   await new Promise(resolve => Cu.schedulePreciseGC(resolve));
   private_browser = private_window.getBrowser().selectedBrowser;
 
-  private_browser.loadURI(prefix + "?action=get&name=test2");
-  await BrowserTestUtils.browserLoaded(private_browser, false, prefix + "?action=get&name=test2");
+  url = prefix + "?action=get&name=test2";
+  private_browser.loadURI(url);
+  await BrowserTestUtils.browserLoaded(private_browser, false, url);
   elts = await getElts(private_browser);
   isnot(elts[0], "value2", "public window shouldn't see cleared private storage");
   is(elts[1], "1", "public window should only see public items");
 
 
   // Making it private again should clear the storage and it shouldn't
   // be able to see the old private storage as well.
   private_window.close();
   private_window = await BrowserTestUtils.openNewBrowserWindow({ private: true });
   private_browser = null;
   await new Promise(resolve => Cu.schedulePreciseGC(resolve));
   private_browser = private_window.getBrowser().selectedBrowser;
 
-  private_browser.loadURI(prefix + "?action=set&name=test3&value=value3");
-  await BrowserTestUtils.browserLoaded(private_browser);
+  url = prefix + "?action=set&name=test3&value=value3";
+  private_browser.loadURI(url);
+  await BrowserTestUtils.browserLoaded(private_browser, false, url);
   elts = await getElts(private_browser);
   is(elts[1], "1", "private window should only see new private items");
 
   // Cleanup.
-  non_private_browser.loadURI(prefix + "?final=true");
-  await BrowserTestUtils.browserLoaded(non_private_browser);
+  url = prefix + "?final=true";
+  non_private_browser.loadURI(url);
+  await BrowserTestUtils.browserLoaded(non_private_browser, false, url);
   private_window.close();
 });
--- a/browser/extensions/webcompat-reporter/test/browser/head.js
+++ b/browser/extensions/webcompat-reporter/test/browser/head.js
@@ -51,18 +51,18 @@ function promisePageActionPanelShown() {
     }, { once: true });
   });
 }
 
 function promisePageActionViewChildrenVisible(panelViewNode) {
   info("promisePageActionViewChildrenVisible waiting for a child node to be visible");
   let dwu = window.windowUtils;
   return BrowserTestUtils.waitForCondition(() => {
-    let bodyNode = panelViewNode.firstChild;
-    for (let childNode of bodyNode.childNodes) {
+    let bodyNode = panelViewNode.firstElementChild;
+    for (let childNode of bodyNode.children) {
       let bounds = dwu.getBoundsWithoutFlushing(childNode);
       if (bounds.width > 0 && bounds.height > 0) {
         return true;
       }
     }
     return false;
   });
 }
--- a/browser/themes/osx/syncedtabs/sidebar.css
+++ b/browser/themes/osx/syncedtabs/sidebar.css
@@ -13,22 +13,22 @@ body:not([lwt-sidebar]) .content-contain
 
 .item-title-container {
   box-sizing: border-box;
   align-items: center;
   height: 24px;
   font-size: 12px;
 }
 
-.item.selected > .item-title-container {
+body:not([lwt-sidebar]) .item.selected > .item-title-container {
   -moz-appearance: -moz-mac-source-list-selection;
   -moz-font-smoothing-background-color: -moz-mac-source-list-selection;
 }
 
-.item.selected:focus > .item-title-container {
+body:not([lwt-sidebar-highlight]) .item.selected:focus > .item-title-container {
   -moz-appearance: -moz-mac-active-source-list-selection;
   -moz-font-smoothing-background-color: -moz-mac-active-source-list-selection;
 }
 
 @media (-moz-mac-yosemite-theme: 0) {
   .item.selected > .item-title-container {
     color: #fff;
     font-weight: bold;
--- a/browser/themes/shared/syncedtabs/sidebar.inc.css
+++ b/browser/themes/shared/syncedtabs/sidebar.inc.css
@@ -191,17 +191,16 @@ body {
      never actually hit this minimum
   */
   min-width: 0px;
 }
 
 .sync-state > p {
   padding-inline-end: 10px;
   padding-inline-start: 10px;
-  color: #888;
 }
 
 .text-link {
   color: rgb(0, 149, 221);
   cursor: pointer;
 }
 
 .text-link:hover {
@@ -250,16 +249,21 @@ body {
 .deck .instructions {
   text-align: center;
   color: GrayText;
   padding: 0 11px;
   max-width: 15em;
   margin: 0 auto;
 }
 
+body[lwt-sidebar] .deck .instructions {
+  color: inherit;
+  opacity: .6;
+}
+
 .deck .button {
   display: block;
   background-color: #0060df;
   color: white;
   border: 0;
   border-radius: 2px;
   margin: 15px auto;
   padding: 8px;
@@ -304,17 +308,15 @@ body[lwt-sidebar] {
 }
 
 body[lwt-sidebar] .item.selected > .item-title-container {
   background-color: hsla(0,0%,80%,.3);
   color: inherit;
 }
 
 body[lwt-sidebar-brighttext] .item.selected > .item-title-container {
-  -moz-appearance: none;
   background-color: rgba(249,249,250,.1);
 }
 
 body[lwt-sidebar-highlight] .item.selected:focus > .item-title-container {
-  -moz-appearance: none;
   background-color: var(--lwt-sidebar-highlight-background-color);
   color: var(--lwt-sidebar-highlight-text-color);
 }
--- a/dom/html/test/browser_content_contextmenu_userinput.js
+++ b/dom/html/test/browser_content_contextmenu_userinput.js
@@ -19,21 +19,21 @@ add_task(async function() {
                                {type: "contextmenu", button: 2}, window);
     await popupShownPromise;
     is(contextMenu.state, "open", "Should have opened context menu");
 
     let pageMenuSep = document.getElementById("page-menu-separator");
     ok(pageMenuSep && !pageMenuSep.hidden,
        "Page menu separator should be shown");
 
-    let testMenuSep = pageMenuSep.previousSibling;
+    let testMenuSep = pageMenuSep.previousElementSibling;
     ok(testMenuSep && !testMenuSep.hidden,
        "User-added menu separator should be shown");
 
-    let testMenuItem = testMenuSep.previousSibling;
+    let testMenuItem = testMenuSep.previousElementSibling;
     is(testMenuItem.label, "Test Context Menu Click", "Got context menu item");
 
     let promiseCtxMenuClick = ContentTask.spawn(aBrowser, null, async function() {
       await new Promise(resolve => {
         let windowUtils = content.windowUtils;
         let menuitem = content.document.getElementById("menuitem");
         menuitem.addEventListener("click", function() {
           Assert.ok(windowUtils.isHandlingUserInput,
--- a/dom/ipc/ProcessPriorityManager.cpp
+++ b/dom/ipc/ProcessPriorityManager.cpp
@@ -829,17 +829,18 @@ ProcessPriority
 ParticularProcessPriorityManager::CurrentPriority()
 {
   return mPriority;
 }
 
 ProcessPriority
 ParticularProcessPriorityManager::ComputePriority()
 {
-  if (!mActiveTabParents.IsEmpty()) {
+  if (!mActiveTabParents.IsEmpty() ||
+      mContentParent->GetRemoteType().EqualsLiteral(EXTENSION_REMOTE_TYPE)) {
     return PROCESS_PRIORITY_FOREGROUND;
   }
 
   if (mHoldsCPUWakeLock || mHoldsHighPriorityWakeLock) {
     return PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE;
   }
 
   return PROCESS_PRIORITY_BACKGROUND;
--- a/dom/webauthn/tests/browser/browser_webauthn_prompts.js
+++ b/dom/webauthn/tests/browser/browser_webauthn_prompts.js
@@ -113,17 +113,17 @@ add_task(async function test_register() 
   let request = promiseWebAuthnRegister(tab)
     .then(arrivingHereIsBad)
     .catch(expectAbortError)
     .then(() => active = false);
   await promiseNotification("webauthn-prompt-register");
 
   // Cancel the request.
   ok(active, "request should still be active");
-  PopupNotifications.panel.firstChild.button.click();
+  PopupNotifications.panel.firstElementChild.button.click();
   await request;
 
   // Close tab.
   await BrowserTestUtils.removeTab(tab);
 });
 
 add_task(async function test_sign() {
   // Open a new tab.
@@ -134,17 +134,17 @@ add_task(async function test_sign() {
   let request = promiseWebAuthnSign(tab)
     .then(arrivingHereIsBad)
     .catch(expectAbortError)
     .then(() => active = false);
   await promiseNotification("webauthn-prompt-sign");
 
   // Cancel the request.
   ok(active, "request should still be active");
-  PopupNotifications.panel.firstChild.button.click();
+  PopupNotifications.panel.firstElementChild.button.click();
   await request;
 
   // Close tab.
   await BrowserTestUtils.removeTab(tab);
 });
 
 add_task(async function test_register_direct_cancel() {
   // Open a new tab.
@@ -154,17 +154,17 @@ add_task(async function test_register_di
   let active = true;
   let promise = promiseWebAuthnRegister(tab, "direct")
     .then(arrivingHereIsBad).catch(expectAbortError)
     .then(() => active = false);
   await promiseNotification("webauthn-prompt-register-direct");
 
   // Cancel the request.
   ok(active, "request should still be active");
-  PopupNotifications.panel.firstChild.secondaryButton.click();
+  PopupNotifications.panel.firstElementChild.secondaryButton.click();
   await promise;
 
   // Close tab.
   await BrowserTestUtils.removeTab(tab);
 });
 
 add_task(async function test_setup_softtoken() {
   await SpecialPowers.pushPrefEnv({
@@ -181,17 +181,17 @@ add_task(async function test_register_di
   // Open a new tab.
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
 
   // Request a new credential with direct attestation and wait for the prompt.
   let request = promiseWebAuthnRegister(tab, "direct");
   await promiseNotification("webauthn-prompt-register-direct");
 
   // Proceed.
-  PopupNotifications.panel.firstChild.button.click();
+  PopupNotifications.panel.firstElementChild.button.click();
 
   // Ensure we got "direct" attestation.
   await request.then(verifyDirectCertificate);
 
   // Close tab.
   await BrowserTestUtils.removeTab(tab);
 });
 
@@ -199,17 +199,17 @@ add_task(async function test_register_di
   // Open a new tab.
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
 
   // Request a new credential with direct attestation and wait for the prompt.
   let request = promiseWebAuthnRegister(tab, "direct");
   await promiseNotification("webauthn-prompt-register-direct");
 
   // Check "anonymize anyway" and proceed.
-  PopupNotifications.panel.firstChild.checkbox.checked = true;
-  PopupNotifications.panel.firstChild.button.click();
+  PopupNotifications.panel.firstElementChild.checkbox.checked = true;
+  PopupNotifications.panel.firstElementChild.button.click();
 
   // Ensure we got "none" attestation.
   await request.then(verifyAnonymizedCertificate);
 
   // Close tab.
   await BrowserTestUtils.removeTab(tab);
 });
--- a/js/src/jit/arm64/vixl/MozSimulator-vixl.cpp
+++ b/js/src/jit/arm64/vixl/MozSimulator-vixl.cpp
@@ -526,33 +526,33 @@ typedef int64_t (*Prototype_General4)(in
 typedef int64_t (*Prototype_General5)(int64_t arg0, int64_t arg1, int64_t arg2, int64_t arg3,
                                       int64_t arg4);
 typedef int64_t (*Prototype_General6)(int64_t arg0, int64_t arg1, int64_t arg2, int64_t arg3,
                                       int64_t arg4, int64_t arg5);
 typedef int64_t (*Prototype_General7)(int64_t arg0, int64_t arg1, int64_t arg2, int64_t arg3,
                                       int64_t arg4, int64_t arg5, int64_t arg6);
 typedef int64_t (*Prototype_General8)(int64_t arg0, int64_t arg1, int64_t arg2, int64_t arg3,
                                       int64_t arg4, int64_t arg5, int64_t arg6, int64_t arg7);
-typedef int64_t (*Prototype_GeneralGeneralGeneralInt64)(int64_t arg0, int32_t arg1, int32_t arg2,
+typedef int64_t (*Prototype_GeneralGeneralGeneralInt64)(int64_t arg0, int64_t arg1, int64_t arg2,
                                                         int64_t arg3);
-typedef int64_t (*Prototype_GeneralGeneralInt64Int64)(int64_t arg0, int32_t arg1, int64_t arg2,
+typedef int64_t (*Prototype_GeneralGeneralInt64Int64)(int64_t arg0, int64_t arg1, int64_t arg2,
                                                       int64_t arg3);
 
 typedef int64_t (*Prototype_Int_Double)(double arg0);
-typedef int64_t (*Prototype_Int_IntDouble)(int32_t arg0, double arg1);
+typedef int64_t (*Prototype_Int_IntDouble)(int64_t arg0, double arg1);
 typedef int64_t (*Prototype_Int_DoubleIntInt)(double arg0, uint64_t arg1, uint64_t arg2);
 typedef int64_t (*Prototype_Int_IntDoubleIntInt)(uint64_t arg0, double arg1,
                                                  uint64_t arg2, uint64_t arg3);
 
 typedef float (*Prototype_Float32_Float32)(float arg0);
 typedef float (*Prototype_Float32_Float32Float32)(float arg0, float arg1);
 
 typedef double (*Prototype_Double_None)();
 typedef double (*Prototype_Double_Double)(double arg0);
-typedef double (*Prototype_Double_Int)(int32_t arg0);
+typedef double (*Prototype_Double_Int)(int64_t arg0);
 typedef double (*Prototype_Double_DoubleInt)(double arg0, int64_t arg1);
 typedef double (*Prototype_Double_IntDouble)(int64_t arg0, double arg1);
 typedef double (*Prototype_Double_DoubleDouble)(double arg0, double arg1);
 typedef double (*Prototype_Double_DoubleDoubleDouble)(double arg0, double arg1, double arg2);
 typedef double (*Prototype_Double_DoubleDoubleDoubleDouble)(double arg0, double arg1,
                                                             double arg2, double arg3);
 
 
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -4521,16 +4521,22 @@ nsresult
 ScrollFrameHelper::FireScrollPortEvent()
 {
   mAsyncScrollPortEvent.Forget();
 
   // Keep this in sync with PostOverflowEvent().
   nsSize scrollportSize = mScrollPort.Size();
   nsSize childSize = GetScrolledRect().Size();
 
+  // TODO(emilio): why do we need the whole WillPaintObserver infrastructure and
+  // can't use AddScriptRunner & co? I guess it made sense when we used
+  // WillPaintObserver for scroll events too, or when this used to flush.
+  //
+  // Should we remove this?
+
   bool newVerticalOverflow = childSize.height > scrollportSize.height;
   bool vertChanged = mVerticalOverflow != newVerticalOverflow;
 
   bool newHorizontalOverflow = childSize.width > scrollportSize.width;
   bool horizChanged = mHorizontalOverflow != newHorizontalOverflow;
 
   if (!vertChanged && !horizChanged) {
     return NS_OK;
@@ -5082,20 +5088,16 @@ ScrollFrameHelper::PostScrollEvent()
 
   // The ScrollEvent constructor registers itself with the refresh driver.
   mScrollEvent = new ScrollEvent(this);
 }
 
 NS_IMETHODIMP
 ScrollFrameHelper::AsyncScrollPortEvent::Run()
 {
-  if (mHelper) {
-    mHelper->mOuter->PresContext()->Document()->
-      FlushPendingNotifications(FlushType::InterruptibleLayout);
-  }
   return mHelper ? mHelper->FireScrollPortEvent() : NS_OK;
 }
 
 bool
 nsXULScrollFrame::AddHorizontalScrollbar(nsBoxLayoutState& aState, bool aOnBottom)
 {
   if (!mHelper.mHScrollbarBox) {
     return true;
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java
@@ -22,27 +22,29 @@ import android.support.v4.util.SimpleArr
 import android.util.Log;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoActivityMonitor;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.mozglue.SafeIntent;
+import org.mozilla.gecko.updater.UpdateServiceHelper;
 import org.mozilla.gecko.util.BitmapUtils;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.StrictModeContext;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.io.File;
 import java.io.UnsupportedEncodingException;
 import java.net.URLConnection;
 import java.net.URLDecoder;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 public final class NotificationHelper implements BundleEventListener {
     public static final String HELPER_BROADCAST_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".helperBroadcastAction";
 
@@ -113,17 +115,20 @@ public final class NotificationHelper im
         /**
          * Leanplum notification channel - use only when <code>AppConstants.MOZ_ANDROID_MMA</code> is true.
          */
         LP_DEFAULT,
     }
 
     // Holds the mapping between the Channel enum used by the rest of our codebase and the
     // channel ID used for communication with the system NotificationManager.
-    private final Map<Channel, String> mDefinedNotificationChannels = new HashMap<Channel, String>(7) {{
+    // How to determine the initialCapacity: Count all channels (including the Updater, which is
+    // only added further down in initNotificationChannels), multiply by 4/3 for a maximum load
+    // factor of 75 % and round up to the next multiple of two.
+    private final Map<Channel, String> mDefinedNotificationChannels = new HashMap<Channel, String>(16) {{
         final String DEFAULT_CHANNEL_TAG = "default2-notification-channel";
         put(Channel.DEFAULT, DEFAULT_CHANNEL_TAG);
 
         final String MLS_CHANNEL_TAG = "mls-notification-channel";
         put(Channel.MLS, MLS_CHANNEL_TAG);
 
         final String DOWNLOAD_NOTIFICATION_TAG = "download-notification-channel";
         put(Channel.DOWNLOAD, DOWNLOAD_NOTIFICATION_TAG);
@@ -131,30 +136,25 @@ public final class NotificationHelper im
         final String MEDIA_CHANNEL_TAG = "media-notification-channel";
         put(Channel.MEDIA, MEDIA_CHANNEL_TAG);
 
         if (AppConstants.MOZ_ANDROID_MMA) {
             final String LP_DEFAULT_CHANNEL_TAG = "lp-default-notification-channel";
             put(Channel.LP_DEFAULT, LP_DEFAULT_CHANNEL_TAG);
         }
 
-        if (AppConstants.MOZ_UPDATER) {
-            final String UPDATER_CHANNEL_TAG = "updater-notification-channel";
-            put(Channel.UPDATER, UPDATER_CHANNEL_TAG);
-        }
-
         final String SYNCED_TABS_CHANNEL_TAG = "synced-tabs-notification-channel";
         put(Channel.SYNCED_TABS, SYNCED_TABS_CHANNEL_TAG);
     }};
 
     // These are channels we no longer require and want to retire from Android's settings UI.
-    private final List<String> mDeprecatedNotificationChannels = Arrays.asList(
+    private final List<String> mDeprecatedNotificationChannels = new ArrayList<>(Arrays.asList(
             "default-notification-channel",
             null
-    );
+    ));
 
     // Holds a list of notifications that should be cleared if the Fennec Activity is shut down.
     // Will not include ongoing or persistent notifications that are tied to Gecko's lifecycle.
     private SimpleArrayMap<String, GeckoBundle> mClearableNotifications;
 
     private boolean mInitialized;
     private static NotificationHelper sInstance;
 
@@ -187,16 +187,23 @@ public final class NotificationHelper im
 
         if (sInstance == null) {
             sInstance = new NotificationHelper(context.getApplicationContext());
         }
         return sInstance;
     }
 
     private void initNotificationChannels() {
+        final String UPDATER_CHANNEL_TAG = "updater-notification-channel";
+        if (UpdateServiceHelper.isUpdaterEnabled(mContext)) {
+            mDefinedNotificationChannels.put(Channel.UPDATER, UPDATER_CHANNEL_TAG);
+        } else {
+            mDeprecatedNotificationChannels.add(UPDATER_CHANNEL_TAG);
+        }
+
         final NotificationManager notificationManager =
                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
 
         for (String channelId : mDeprecatedNotificationChannels) {
             removeChannel(notificationManager, channelId);
         }
 
         for (Channel mozChannel : mDefinedNotificationChannels.keySet()) {
--- a/netwerk/test/httpserver/httpd.js
+++ b/netwerk/test/httpserver/httpd.js
@@ -2792,17 +2792,22 @@ ServerHandler.prototype =
         try
         {
           // Alas, the line number in errors dumped to console when calling the
           // request handler is simply an offset from where we load the SJS file.
           // Work around this in a reasonably non-fragile way by dynamically
           // getting the line number where we evaluate the SJS file.  Don't
           // separate these two lines!
           var line = new Error().lineNumber;
-          Cu.evalInSandbox(sis.read(file.fileSize), s, "latest");
+          let uri = Cc["@mozilla.org/network/io-service;1"]
+                      .getService(Ci.nsIIOService)
+                      .newFileURI(file);
+          let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
+                               .getService(Ci.mozIJSSubScriptLoader);
+          scriptLoader.loadSubScript(uri.spec, s);
         }
         catch (e)
         {
           dumpn("*** syntax error in SJS at " + file.path + ": " + e);
           throw HTTP_500;
         }
 
         try
--- a/taskcluster/ci/release-version-bump/kind.yml
+++ b/taskcluster/ci/release-version-bump/kind.yml
@@ -20,16 +20,17 @@ job-defaults:
         by-project:
             mozilla-(beta|release|esr.*): scriptworker-prov-v1/treescript-v1
             maple: scriptworker-prov-v1/treescript-v1
             birch: scriptworker-prov-v1/treescript-v1
             jamun: scriptworker-prov-v1/treescript-v1
             default: scriptworker-prov-v1/treescript-dev
     worker:
         implementation: treescript
+        dontbuild: true
         tags: ['release']
         bump: true
         bump-files:
             by-project:
                 default: ["browser/config/version_display.txt"]
                 mozilla-release:
                     - "browser/config/version.txt"
                     - "browser/config/version_display.txt"
--- a/taskcluster/taskgraph/files_changed.py
+++ b/taskcluster/taskgraph/files_changed.py
@@ -12,45 +12,42 @@ import logging
 import requests
 from redo import retry
 from mozpack.path import match as mozpackmatch, join as join_path
 from mozversioncontrol import get_repository_object, InvalidRepoPath
 from subprocess import CalledProcessError
 from mozbuild.util import memoize
 
 logger = logging.getLogger(__name__)
-_cache = {}
 
 
+@memoize
 def get_changed_files(repository, revision):
     """
     Get the set of files changed in the push headed by the given revision.
     Responses are cached, so multiple calls with the same arguments are OK.
     """
-    key = repository, revision
-    if key not in _cache:
-        url = '%s/json-automationrelevance/%s' % (repository.rstrip('/'), revision)
-        logger.debug("Querying version control for metadata: %s", url)
+    url = '%s/json-automationrelevance/%s' % (repository.rstrip('/'), revision)
+    logger.debug("Querying version control for metadata: %s", url)
 
-        def get_automationrelevance():
-            response = requests.get(url, timeout=5)
-            return response.json()
-        contents = retry(get_automationrelevance, attempts=10, sleeptime=10)
+    def get_automationrelevance():
+        response = requests.get(url, timeout=5)
+        return response.json()
+    contents = retry(get_automationrelevance, attempts=10, sleeptime=10)
 
-        logger.debug('{} commits influencing task scheduling:'
-                     .format(len(contents['changesets'])))
-        changed_files = set()
-        for c in contents['changesets']:
-            logger.debug(" {cset} {desc}".format(
-                cset=c['node'][0:12],
-                desc=c['desc'].splitlines()[0].encode('ascii', 'ignore')))
-            changed_files |= set(c['files'])
+    logger.debug('{} commits influencing task scheduling:'
+                 .format(len(contents['changesets'])))
+    changed_files = set()
+    for c in contents['changesets']:
+        logger.debug(" {cset} {desc}".format(
+            cset=c['node'][0:12],
+            desc=c['desc'].splitlines()[0].encode('ascii', 'ignore')))
+        changed_files |= set(c['files'])
 
-        _cache[key] = changed_files
-    return _cache[key]
+    return changed_files
 
 
 def check(params, file_patterns):
     """Determine whether any of the files changed in the indicated push to
     https://hg.mozilla.org match any of the given file patterns."""
     repository = params.get('head_repository')
     revision = params.get('head_rev')
     if not repository or not revision:
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -553,17 +553,17 @@ Tester.prototype = {
     // eslint-disable-next-line no-nested-ternary
     let baseMsg = timedOut ? "Found a {elt} after previous test timed out"
                            : this.currentTest ? "Found an unexpected {elt} at the end of test run"
                                               : "Found an unexpected {elt}";
 
     // Remove stale tabs
     if (this.currentTest && window.gBrowser && gBrowser.tabs.length > 1) {
       while (gBrowser.tabs.length > 1) {
-        let lastTab = gBrowser.tabContainer.lastChild;
+        let lastTab = gBrowser.tabContainer.lastElementChild;
         if (!lastTab.closing) {
           // Report the stale tab as an error only when they're not closing.
           // Tests can finish without waiting for the closing tabs.
           this.currentTest.addResult(new testResult({
             name: baseMsg.replace("{elt}", "tab") + ": " +
               lastTab.linkedBrowser.currentURI.spec,
             allowFailure: this.currentTest.allowFailure,
           }));
--- a/testing/raptor/raptor/playback/mitmproxy.py
+++ b/testing/raptor/raptor/playback/mitmproxy.py
@@ -30,16 +30,19 @@ else:
     mozharness_dir = os.path.join(here, '../../../mozharness')
 sys.path.insert(0, mozharness_dir)
 
 # required for using a python3 virtualenv on win for mitmproxy
 from mozharness.base.python import Python3Virtualenv
 from mozharness.mozilla.testing.testbase import TestingMixin
 from mozharness.base.vcs.vcsbase import MercurialScript
 
+# import mozharness write_autoconfig_files
+from mozharness.mozilla.firefox.autoconfig import write_autoconfig_files
+
 raptor_dir = os.path.join(here, '..')
 sys.path.insert(0, raptor_dir)
 
 from utils import transform_platform
 
 external_tools_path = os.environ.get('EXTERNALTOOLSPATH', None)
 
 if external_tools_path is not None:
@@ -53,17 +56,17 @@ else:
 # on local machine it is 'HOME', however it is different on production machines
 try:
     DEFAULT_CERT_PATH = os.path.join(os.getenv('HOME'),
                                      '.mitmproxy', 'mitmproxy-ca-cert.cer')
 except Exception:
     DEFAULT_CERT_PATH = os.path.join(os.getenv('HOMEDRIVE'), os.getenv('HOMEPATH'),
                                      '.mitmproxy', 'mitmproxy-ca-cert.cer')
 
-MITMPROXY_SETTINGS = '''// Start with a comment
+MITMPROXY_ON_SETTINGS = '''// Start with a comment
 // Load up mitmproxy cert
 var certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(Ci.nsIX509CertDB);
 var certdb2 = certdb;
 
 try {
 certdb2 = Cc["@mozilla.org/security/x509certdb;1"].getService(Ci.nsIX509CertDB2);
 } catch (e) {}
 
@@ -74,16 +77,21 @@ certdb2.addCertFromBase64(cert, "C,C,C",
 // Manual proxy configuration
 pref("network.proxy.type", 1);
 pref("network.proxy.http", "127.0.0.1");
 pref("network.proxy.http_port", 8080);
 pref("network.proxy.ssl", "127.0.0.1");
 pref("network.proxy.ssl_port", 8080);
 '''
 
+MITMPROXY_OFF_SETTINGS = '''// Start with a comment
+// Turn off proxy
+pref("network.proxy.type", 0);
+'''
+
 
 class Mitmproxy(Playback, Python3Virtualenv, TestingMixin, MercurialScript):
 
     def __init__(self, config):
         self.config = config
         self.mitmproxy_proc = None
         self.mitmdump_path = None
         self.recordings = config.get('playback_recordings', None)
@@ -186,53 +194,43 @@ class Mitmproxy(Playback, Python3Virtual
         # add py3 executables path to system path
         sys.path.insert(1, self.py3_path_to_executables())
         # install mitmproxy itself
         self.py3_install_modules(modules=['mitmproxy'])
         self.mitmdump_path = os.path.join(self.py3_path_to_executables(), 'mitmdump')
 
     def setup(self):
         # install the generated CA certificate into Firefox
-        # mitmproxy cert setup needs path to mozharness install; mozharness has set this
-        # value in the SCRIPTSPATH env var for us in mozharness/mozilla/testing/talos.py
-        scripts_path = os.environ.get('SCRIPTSPATH')
-        LOG.info('scripts_path: %s' % str(scripts_path))
         self.install_mitmproxy_cert(self.mitmproxy_proc,
-                                    self.browser_path,
-                                    str(scripts_path))
+                                    self.browser_path)
         return
 
     def start(self):
         # if on windows, the mitmdump_path was already set when creating py3 env
         if self.mitmdump_path is None:
             self.mitmdump_path = os.path.join(self.raptor_dir, 'mitmdump')
 
         recordings_list = self.recordings.split()
         self.mitmproxy_proc = self.start_mitmproxy_playback(self.mitmdump_path,
                                                             self.recordings_path,
                                                             recordings_list,
                                                             self.browser_path)
         return
 
     def stop(self):
         self.stop_mitmproxy_playback()
+        self.turn_off_browser_proxy()
         return
 
     def configure_mitmproxy(self,
                             fx_install_dir,
-                            scripts_path,
                             certificate_path=DEFAULT_CERT_PATH):
-        # scripts_path is path to mozharness on test machine; needed so can import
-        if scripts_path is not False:
-            sys.path.insert(1, scripts_path)
-            sys.path.insert(1, os.path.join(scripts_path, 'mozharness'))
-        from mozharness.mozilla.firefox.autoconfig import write_autoconfig_files
         certificate = self._read_certificate(certificate_path)
         write_autoconfig_files(fx_install_dir=fx_install_dir,
-                               cfg_contents=MITMPROXY_SETTINGS % {
+                               cfg_contents=MITMPROXY_ON_SETTINGS % {
                                   'cert': certificate})
 
     def _read_certificate(self, certificate_path):
         ''' Return the certificate's hash from the certificate file.'''
         # NOTE: mitmproxy's certificates do not exist until one of its binaries
         #       has been executed once on the host
         with open(certificate_path, 'r') as fd:
             contents = fd.read()
@@ -240,40 +238,40 @@ class Mitmproxy(Playback, Python3Virtual
 
     def is_mitmproxy_cert_installed(self, browser_install):
         """Verify mitmxproy CA cert was added to Firefox"""
         from mozharness.mozilla.firefox.autoconfig import read_autoconfig_file
         try:
             # read autoconfig file, confirm mitmproxy cert is in there
             certificate = self._read_certificate(DEFAULT_CERT_PATH)
             contents = read_autoconfig_file(browser_install)
-            if (MITMPROXY_SETTINGS % {'cert': certificate}) in contents:
+            if (MITMPROXY_ON_SETTINGS % {'cert': certificate}) in contents:
                 LOG.info("Verified mitmproxy CA certificate is installed in Firefox")
             else:
                 LOG.info("Firefox autoconfig file contents:")
                 LOG.info(contents)
                 return False
         except Exception:
             LOG.info("Failed to read Firefox autoconfig file, when verifying CA cert install")
             return False
         return True
 
-    def install_mitmproxy_cert(self, mitmproxy_proc, browser_path, scripts_path):
+    def install_mitmproxy_cert(self, mitmproxy_proc, browser_path):
         """Install the CA certificate generated by mitmproxy, into Firefox"""
         LOG.info("Installing mitmxproxy CA certficate into Firefox")
         # browser_path is exe, we want install dir
-        browser_install = os.path.dirname(browser_path)
+        self.browser_install = os.path.dirname(browser_path)
         # on macosx we need to remove the last folders 'Content/MacOS'
         if mozinfo.os == 'mac':
-            browser_install = browser_install[:-14]
+            self.browser_install = self.browser_install[:-14]
 
-        LOG.info('Calling configure_mitmproxy with browser folder: %s' % browser_install)
-        self.configure_mitmproxy(browser_install, scripts_path)
+        LOG.info('Calling configure_mitmproxy with browser folder: %s' % self.browser_install)
+        self.configure_mitmproxy(self.browser_install)
         # cannot continue if failed to add CA cert to Firefox, need to check
-        if not self.is_mitmproxy_cert_installed(browser_install):
+        if not self.is_mitmproxy_cert_installed(self.browser_install):
             LOG.error('Aborting: failed to install mitmproxy CA cert into Firefox')
             self.stop_mitmproxy_playback(mitmproxy_proc)
             sys.exit()
 
     def start_mitmproxy_playback(self,
                                  mitmdump_path,
                                  mitmproxy_recording_path,
                                  mitmproxy_recordings_list,
@@ -337,8 +335,15 @@ class Mitmproxy(Playback, Python3Virtual
         if status is None:  # None value indicates process hasn't terminated
             # I *think* we can still continue, as process will be automatically
             # killed anyway when mozharness is done (?) if not, we won't be able
             # to startup mitmxproy next time if it is already running
             LOG.error("Failed to kill the mitmproxy playback process")
             LOG.info(str(status))
         else:
             LOG.info("Successfully killed the mitmproxy playback process")
+
+    def turn_off_browser_proxy(self):
+        """Turn off the browser proxy that was used for mitmproxy playback"""
+        LOG.info("Turning off the browser proxy")
+
+        write_autoconfig_files(fx_install_dir=self.browser_install,
+                               cfg_contents=MITMPROXY_OFF_SETTINGS)
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -40,16 +40,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
   AddonSettings: "resource://gre/modules/addons/AddonSettings.jsm",
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
   ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm",
   ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm",
   ExtensionStorageIDB: "resource://gre/modules/ExtensionStorageIDB.jsm",
+  ExtensionTelemetry: "resource://gre/modules/ExtensionTelemetry.jsm",
   ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.jsm",
   FileSource: "resource://gre/modules/L10nRegistry.jsm",
   L10nRegistry: "resource://gre/modules/L10nRegistry.jsm",
   Log: "resource://gre/modules/Log.jsm",
   MessageChannel: "resource://gre/modules/MessageChannel.jsm",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   PluralForm: "resource://gre/modules/PluralForm.jsm",
@@ -93,17 +94,16 @@ var {
   ParentAPIManager,
   StartupCache,
   apiManager: Management,
 } = ExtensionParent;
 
 const {
   getUniqueId,
   promiseTimeout,
-  ExtensionTelemetry,
 } = ExtensionUtils;
 
 const {
   EventEmitter,
 } = ExtensionCommon;
 
 XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole);
 
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -8,16 +8,17 @@
 var EXPORTED_SYMBOLS = ["ExtensionContent"];
 
 /* globals ExtensionContent */
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
+  ExtensionTelemetry: "resource://gre/modules/ExtensionTelemetry.jsm",
   LanguageDetector: "resource:///modules/translation/LanguageDetector.jsm",
   MessageChannel: "resource://gre/modules/MessageChannel.jsm",
   Schemas: "resource://gre/modules/Schemas.jsm",
   WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
                                    "@mozilla.org/content/style-sheet-service;1",
@@ -36,17 +37,16 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["crypto", "TextEncoder"]);
 
 const {
   DefaultMap,
   DefaultWeakMap,
-  ExtensionTelemetry,
   getInnerWindowID,
   getWinUtils,
   promiseDocumentIdle,
   promiseDocumentLoaded,
   promiseDocumentReady,
 } = ExtensionUtils;
 
 const {
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -476,16 +476,25 @@ ProxyMessenger = {
         if (optionsBrowser) {
           browser = optionsBrowser;
         }
       }
 
       return {messageManager: browser.messageManager, xulBrowser: browser};
     }
 
+    // port.postMessage / port.disconnect to non-tab contexts.
+    if (recipient.envType === "content_child") {
+      let childId = `${recipient.extensionId}.${recipient.contextId}`;
+      let context = ParentAPIManager.proxyContexts.get(childId);
+      if (context) {
+        return {messageManager: context.parentMessageManager, xulBrowser: context.xulBrowser};
+      }
+    }
+
     // runtime.sendMessage / runtime.connect
     let extension = GlobalManager.extensionMap.get(recipient.extensionId);
     if (extension) {
       // A process message manager
       return {messageManager: extension.parentMessageManager, xulBrowser: null};
     }
 
     return {messageManager: null, xulBrowser: null};
--- a/toolkit/components/extensions/ExtensionStorageIDB.jsm
+++ b/toolkit/components/extensions/ExtensionStorageIDB.jsm
@@ -3,36 +3,32 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["ExtensionStorageIDB"];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/IndexedDB.jsm");
-ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
   ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm",
+  getTrimmedString: "resource://gre/modules/ExtensionTelemetry.jsm",
   Services: "resource://gre/modules/Services.jsm",
   OS: "resource://gre/modules/osfile.jsm",
 });
 
 // The userContextID reserved for the extension storage (its purpose is ensuring that the IndexedDB
 // storage used by the browser.storage.local API is not directly accessible from the extension code).
 XPCOMUtils.defineLazyGetter(this, "WEBEXT_STORAGE_USER_CONTEXT_ID", () => {
   return ContextualIdentityService.getDefaultPrivateIdentity(
     "userContextIdInternal.webextStorageLocal").userContextId;
 });
 
-var {
-  getTrimmedString,
-} = ExtensionUtils;
-
 const IDB_NAME = "webExtensions-storage-local";
 const IDB_DATA_STORENAME = "storage-local-data";
 const IDB_VERSION = 1;
 const IDB_MIGRATE_RESULT_HISTOGRAM = "WEBEXT_STORAGE_LOCAL_IDB_MIGRATE_RESULT_COUNT";
 
 // Whether or not the installed extensions should be migrated to the storage.local IndexedDB backend.
 const BACKEND_ENABLED_PREF = "extensions.webextensions.ExtensionStorageIDB.enabled";
 const IDB_MIGRATED_PREF_BRANCH = "extensions.webextensions.ExtensionStorageIDB.migrated";
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionTelemetry.jsm
@@ -0,0 +1,180 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var EXPORTED_SYMBOLS = ["ExtensionTelemetry", "getTrimmedString"];
+
+ChromeUtils.defineModuleGetter(this, "Services",
+                               "resource://gre/modules/Services.jsm");
+ChromeUtils.defineModuleGetter(this, "TelemetryStopwatch",
+                               "resource://gre/modules/TelemetryStopwatch.jsm");
+
+// Map of the base histogram ids for the metrics recorded for the extensions.
+const histograms = {
+  "extensionStartup": "WEBEXT_EXTENSION_STARTUP_MS",
+  "backgroundPageLoad": "WEBEXT_BACKGROUND_PAGE_LOAD_MS",
+  "browserActionPopupOpen": "WEBEXT_BROWSERACTION_POPUP_OPEN_MS",
+  "browserActionPreloadResult": "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT",
+  "contentScriptInjection": "WEBEXT_CONTENT_SCRIPT_INJECTION_MS",
+  "pageActionPopupOpen": "WEBEXT_PAGEACTION_POPUP_OPEN_MS",
+  "storageLocalGetJSON": "WEBEXT_STORAGE_LOCAL_GET_MS",
+  "storageLocalSetJSON": "WEBEXT_STORAGE_LOCAL_SET_MS",
+  "storageLocalGetIDB": "WEBEXT_STORAGE_LOCAL_IDB_GET_MS",
+  "storageLocalSetIDB": "WEBEXT_STORAGE_LOCAL_IDB_SET_MS",
+};
+
+/**
+ * Get a trimmed version of the given string if it is longer than 80 chars (used in telemetry
+ * when a string may be longer than allowed).
+ *
+ * @param {string} str
+ *        The original string content.
+ *
+ * @returns {string}
+ *          The trimmed version of the string when longer than 80 chars, or the given string
+ *          unmodified otherwise.
+ */
+function getTrimmedString(str) {
+  if (str.length <= 80) {
+    return str;
+  }
+
+  const length = str.length;
+
+  // Trim the string to prevent a flood of warnings messages logged internally by recordEvent,
+  // the trimmed version is going to be composed by the first 40 chars and the last 37 and 3 dots
+  // that joins the two parts, to visually indicate that the string has been trimmed.
+  return `${str.slice(0, 40)}...${str.slice(length - 37, length)}`;
+}
+
+/**
+ * This is a internal helper object which contains a collection of helpers used to make it easier
+ * to collect extension telemetry (in both the general histogram and in the one keyed by addon id).
+ *
+ * This helper object is not exported from ExtensionUtils, it is used by the ExtensionTelemetry
+ * Proxy which is exported and used by the callers to record telemetry data for one of the
+ * supported metrics.
+ */
+class ExtensionTelemetryMetric {
+  constructor(metric) {
+    this.metric = metric;
+  }
+
+  // Stopwatch methods.
+  stopwatchStart(extension, obj = extension) {
+    this._wrappedStopwatchMethod("start", this.metric, extension, obj);
+  }
+
+  stopwatchFinish(extension, obj = extension) {
+    this._wrappedStopwatchMethod("finish", this.metric, extension, obj);
+  }
+
+  stopwatchCancel(extension, obj = extension) {
+    this._wrappedStopwatchMethod("cancel", this.metric, extension, obj);
+  }
+
+  // Histogram counters methods.
+  histogramAdd(opts) {
+    this._histogramAdd(this.metric, opts);
+  }
+
+  /**
+   * Wraps a call to a TelemetryStopwatch method for a given metric and extension.
+   *
+   * @param {string} method
+   *        The stopwatch method to call ("start", "finish" or "cancel").
+   * @param {string} metric
+   *        The stopwatch metric to record (used to retrieve the base histogram id from the _histogram object).
+   * @param {Extension | BrowserExtensionContent} extension
+   *        The extension to record the telemetry for.
+   * @param {any | undefined} [obj = extension]
+   *        An optional telemetry stopwatch object (which defaults to the extension parameter when missing).
+   */
+  _wrappedStopwatchMethod(method, metric, extension, obj = extension) {
+    if (!extension) {
+      throw new Error(`Mandatory extension parameter is undefined`);
+    }
+
+    const baseId = histograms[metric];
+    if (!baseId) {
+      throw new Error(`Unknown metric ${metric}`);
+    }
+
+    // Record metric in the general histogram.
+    TelemetryStopwatch[method](baseId, obj);
+
+    // Record metric in the histogram keyed by addon id.
+    let extensionId = getTrimmedString(extension.id);
+    TelemetryStopwatch[`${method}Keyed`](`${baseId}_BY_ADDONID`, extensionId, obj);
+  }
+
+  /**
+   * Record a telemetry category and/or value for a given metric.
+   *
+   * @param {string} metric
+   *        The metric to record (used to retrieve the base histogram id from the _histogram object).
+   * @param {Object}                              options
+   * @param {Extension | BrowserExtensionContent} options.extension
+   *        The extension to record the telemetry for.
+   * @param {string | undefined}                  [options.category]
+   *        An optional histogram category.
+   * @param {number | undefined}                  [options.value]
+   *        An optional value to record.
+   */
+  _histogramAdd(metric, {category, extension, value}) {
+    if (!extension) {
+      throw new Error(`Mandatory extension parameter is undefined`);
+    }
+
+    const baseId = histograms[metric];
+    if (!baseId) {
+      throw new Error(`Unknown metric ${metric}`);
+    }
+
+    const histogram = Services.telemetry.getHistogramById(baseId);
+    if (typeof category === "string") {
+      histogram.add(category, value);
+    } else {
+      histogram.add(value);
+    }
+
+    const keyedHistogram = Services.telemetry.getKeyedHistogramById(`${baseId}_BY_ADDONID`);
+    const extensionId = getTrimmedString(extension.id);
+
+    if (typeof category === "string") {
+      keyedHistogram.add(extensionId, category, value);
+    } else {
+      keyedHistogram.add(extensionId, value);
+    }
+  }
+}
+
+// Cache of the ExtensionTelemetryMetric instances that has been lazily created by the
+// Extension Telemetry Proxy.
+const metricsCache = new Map();
+
+/**
+ * This proxy object provides the telemetry helpers for the currently supported metrics (the ones listed in
+ * ExtensionTelemetryHelpers._histograms), the telemetry helpers for a particular metric are lazily created
+ * when the related property is being accessed on this object for the first time, e.g.:
+ *
+ *      ExtensionTelemetry.extensionStartup.stopwatchStart(extension);
+ *      ExtensionTelemetry.browserActionPreloadResult.histogramAdd({category: "Shown", extension});
+ */
+var ExtensionTelemetry = new Proxy(metricsCache, {
+  get(target, prop, receiver) {
+    if (!(prop in histograms)) {
+      throw new Error(`Unknown metric ${prop}`);
+    }
+
+    // Lazily create and cache the metric result object.
+    if (!target.has(prop)) {
+      target.set(prop, new ExtensionTelemetryMetric(prop));
+    }
+
+    return target.get(prop);
+  },
+});
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -7,18 +7,16 @@
 
 var EXPORTED_SYMBOLS = ["ExtensionUtils"];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "setTimeout",
                                "resource://gre/modules/Timer.jsm");
-ChromeUtils.defineModuleGetter(this, "TelemetryStopwatch",
-                               "resource://gre/modules/TelemetryStopwatch.jsm");
 
 // xpcshell doesn't handle idle callbacks well.
 XPCOMUtils.defineLazyGetter(this, "idleTimeout",
                             () => Services.appinfo.name === "XPCShell" ? 500 : undefined);
 
 // It would be nicer to go through `Services.appinfo`, but some tests need to be
 // able to replace that field with a custom implementation before it is first
 // called.
@@ -264,182 +262,28 @@ function flushJarCache(jarPath) {
 const chromeModifierKeyMap = {
   "Alt": "alt",
   "Command": "accel",
   "Ctrl": "accel",
   "MacCtrl": "control",
   "Shift": "shift",
 };
 
-/**
- * Get a trimmed version of the given string if it is longer than 80 chars (used in telemetry
- * when a string may be longer than allowed).
- *
- * @param {string} str
- *        The original string content.
- *
- * @returns {string}
- *          The trimmed version of the string when longer than 80 chars, or the given string
- *          unmodified otherwise.
- */
-function getTrimmedString(str) {
-  if (str.length <= 80) {
-    return str;
-  }
-
-  const length = str.length;
-
-  // Trim the string to prevent a flood of warnings messages logged internally by recordEvent,
-  // the trimmed version is going to be composed by the first 40 chars and the last 37 and 3 dots
-  // that joins the two parts, to visually indicate that the string has been trimmed.
-  return `${str.slice(0, 40)}...${str.slice(length - 37, length)}`;
-}
-
-/**
- * This is a internal helper object which contains a collection of helpers used to make it easier
- * to collect extension telemetry (in both the general histogram and in the one keyed by addon id).
- *
- * This helper object is not exported from ExtensionUtils, it is used by the ExtensionTelemetry
- * Proxy which is exported and used by the callers to record telemetry data for one of the
- * supported metrics.
- */
-const ExtensionTelemetryHelpers = {
-  // Allow callers to refer to the existing metrics by accessing it as properties of the
-  // ExtensionTelemetry.metrics (e.g. ExtensionTelemetry.metrics.extensionStartup).
-
-  // Cache of the metrics helper lazily created by the ExtensionTelemetry Proxy.
-  _metricsMap: new Map(),
-
-  // Map of the base histogram ids for the metrics recorded for the extensions.
-  _histograms: {
-    "extensionStartup": "WEBEXT_EXTENSION_STARTUP_MS",
-    "backgroundPageLoad": "WEBEXT_BACKGROUND_PAGE_LOAD_MS",
-    "browserActionPopupOpen": "WEBEXT_BROWSERACTION_POPUP_OPEN_MS",
-    "browserActionPreloadResult": "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT",
-    "contentScriptInjection": "WEBEXT_CONTENT_SCRIPT_INJECTION_MS",
-    "pageActionPopupOpen": "WEBEXT_PAGEACTION_POPUP_OPEN_MS",
-    "storageLocalGetJSON": "WEBEXT_STORAGE_LOCAL_GET_MS",
-    "storageLocalSetJSON": "WEBEXT_STORAGE_LOCAL_SET_MS",
-    "storageLocalGetIDB": "WEBEXT_STORAGE_LOCAL_IDB_GET_MS",
-    "storageLocalSetIDB": "WEBEXT_STORAGE_LOCAL_IDB_SET_MS",
-  },
-  // Wraps a call to a TelemetryStopwatch method.
-  /**
-   * Wraps a call to a TelemetryStopwatch method for a given metric and extension.
-   *
-   * @param {string} method
-   *        The stopwatch method to call ("start", "finish" or "cancel").
-   * @param {string} metric
-   *        The stopwatch metric to record (used to retrieve the base histogram id from the _histogram object).
-   * @param {Extension | BrowserExtensionContent} extension
-   *        The extension to record the telemetry for.
-   * @param {any | undefined} [obj = extension]
-   *        An optional telemetry stopwatch object (which defaults to the extension parameter when missing).
-   */
-  _wrappedStopwatchMethod(method, metric, extension, obj = extension) {
-    if (!extension) {
-      throw new Error(`Mandatory extension parameter is undefined`);
-    }
-
-    const baseId = this._histograms[metric];
-    if (!baseId) {
-      throw new Error(`Unknown metric ${metric}`);
-    }
-
-    // Record metric in the general histogram.
-    TelemetryStopwatch[method](baseId, obj);
-
-    // Record metric in the histogram keyed by addon id.
-    let extensionId = getTrimmedString(extension.id);
-    TelemetryStopwatch[`${method}Keyed`](`${baseId}_BY_ADDONID`, extensionId, obj);
-  },
-  /**
-   * Record a telemetry category and/or value for a given metric.
-   *
-   * @param {string} metric
-   *        The metric to record (used to retrieve the base histogram id from the _histogram object).
-   * @param {Object}                              options
-   * @param {Extension | BrowserExtensionContent} options.extension
-   *        The extension to record the telemetry for.
-   * @param {string | undefined}                  [options.category]
-   *        An optional histogram category.
-   * @param {number | undefined}                  [options.value]
-   *        An optional value to record.
-   */
-  _histogramAdd(metric, {category, extension, value}) {
-    if (!extension) {
-      throw new Error(`Mandatory extension parameter is undefined`);
-    }
-
-    const baseId = this._histograms[metric];
-    if (!baseId) {
-      throw new Error(`Unknown metric ${metric}`);
-    }
-
-    const histogram = Services.telemetry.getHistogramById(baseId);
-    if (typeof category === "string") {
-      histogram.add(category, value);
-    } else {
-      histogram.add(value);
-    }
-
-    const keyedHistogram = Services.telemetry.getKeyedHistogramById(`${baseId}_BY_ADDONID`);
-    const extensionId = getTrimmedString(extension.id);
-
-    if (typeof category === "string") {
-      keyedHistogram.add(extensionId, category, value);
-    } else {
-      keyedHistogram.add(extensionId, value);
-    }
-  },
-};
-
-/**
- * This proxy object provides the telemetry helpers for the currently supported metrics (the ones listed in
- * ExtensionTelemetryHelpers._histograms), the telemetry helpers for a particular metric are lazily created
- * when the related property is being accessed on this object for the first time, e.g.:
- *
- *      ExtensionTelemetry.extensionStartup.stopwatchStart(extension);
- *      ExtensionTelemetry.browserActionPreloadResult.histogramAdd({category: "Shown", extension});
- */
-const ExtensionTelemetry = new Proxy(ExtensionTelemetryHelpers, {
-  get(target, prop, receiver) {
-    if (!(prop in target._histograms)) {
-      throw new Error(`Unknown metric ${prop}`);
-    }
-
-    // Lazily create and cache the metric result object.
-    if (!target._metricsMap.has(prop)) {
-      target._metricsMap.set(prop, {
-        // Stopwatch histogram helpers.
-        stopwatchStart: target._wrappedStopwatchMethod.bind(target, "start", prop),
-        stopwatchFinish: target._wrappedStopwatchMethod.bind(target, "finish", prop),
-        stopwatchCancel: target._wrappedStopwatchMethod.bind(target, "cancel", prop),
-        // Result histogram helpers.
-        histogramAdd: target._histogramAdd.bind(target, prop),
-      });
-    }
-
-    return target._metricsMap.get(prop);
-  },
-});
 
 var ExtensionUtils = {
   chromeModifierKeyMap,
   flushJarCache,
   getInnerWindowID,
   getMessageManager,
-  getTrimmedString,
   getUniqueId,
   filterStack,
   getWinUtils,
   promiseDocumentIdle,
   promiseDocumentLoaded,
   promiseDocumentReady,
   promiseEvent,
   promiseObserved,
   promiseTimeout,
   DefaultMap,
   DefaultWeakMap,
   ExtensionError,
-  ExtensionTelemetry,
   LimitedSet,
 };
--- a/toolkit/components/extensions/child/ext-storage.js
+++ b/toolkit/components/extensions/child/ext-storage.js
@@ -1,21 +1,19 @@
 "use strict";
 
 ChromeUtils.defineModuleGetter(this, "ExtensionStorage",
                                "resource://gre/modules/ExtensionStorage.jsm");
 ChromeUtils.defineModuleGetter(this, "ExtensionStorageIDB",
                                "resource://gre/modules/ExtensionStorageIDB.jsm");
+ChromeUtils.defineModuleGetter(this, "ExtensionTelemetry",
+                               "resource://gre/modules/ExtensionTelemetry.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
                                "resource://gre/modules/Services.jsm");
 
-var {
-  ExtensionTelemetry,
-} = ExtensionUtils;
-
 // Wrap a storage operation in a TelemetryStopWatch.
 async function measureOp(telemetryMetric, extension, fn) {
   const stopwatchKey = {};
   telemetryMetric.stopwatchStart(extension, stopwatchKey);
   try {
     let result = await fn();
     telemetryMetric.stopwatchFinish(extension, stopwatchKey);
     return result;
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -17,16 +17,17 @@ EXTRA_JS_MODULES += [
     'ExtensionPageChild.jsm',
     'ExtensionParent.jsm',
     'ExtensionPermissions.jsm',
     'ExtensionPreferencesManager.jsm',
     'ExtensionSettingsStore.jsm',
     'ExtensionStorage.jsm',
     'ExtensionStorageIDB.jsm',
     'ExtensionStorageSync.jsm',
+    'ExtensionTelemetry.jsm',
     'ExtensionUtils.jsm',
     'FindContent.jsm',
     'LegacyExtensionsUtils.jsm',
     'MessageChannel.jsm',
     'MessageManagerProxy.jsm',
     'NativeManifests.jsm',
     'NativeMessaging.jsm',
     'ProxyScriptContext.jsm',
--- a/toolkit/components/extensions/parent/ext-backgroundPage.js
+++ b/toolkit/components/extensions/parent/ext-backgroundPage.js
@@ -1,20 +1,19 @@
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 var {
   HiddenExtensionPage,
   promiseExtensionViewLoaded,
 } = ExtensionParent;
 
-ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
-var {
-  ExtensionTelemetry,
-} = ExtensionUtils;
+
+ChromeUtils.defineModuleGetter(this, "ExtensionTelemetry",
+                               "resource://gre/modules/ExtensionTelemetry.jsm");
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "DELAYED_STARTUP",
                                       "extensions.webextensions.background-delayed-startup");
 
 // Responsible for the background_page section of the manifest.
 class BackgroundPage extends HiddenExtensionPage {
   constructor(extension, options) {
     super(extension, "background");
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_in_background.js
@@ -0,0 +1,61 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const server = createHttpServer({hosts: ["example.com"]});
+server.registerPathHandler("/dummyFrame", (request, response) => {
+  response.setStatusLine(request.httpVersion, 200, "OK");
+  response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+  response.write("");
+});
+
+add_task(async function connect_from_background_frame() {
+  async function background() {
+    const FRAME_URL = "http://example.com:8888/dummyFrame";
+    browser.runtime.onConnect.addListener((port) => {
+      browser.test.assertEq(port.sender.tab, undefined, "Sender is not a tab");
+      browser.test.assertEq(port.sender.url, FRAME_URL, "Expected sender URL");
+      port.onMessage.addListener(msg => {
+        browser.test.assertEq("pong", msg, "Reply from content script");
+        port.disconnect();
+      });
+      port.postMessage("ping");
+    });
+
+    await browser.contentScripts.register({
+      matches: ["http://example.com/dummyFrame"],
+      js: [{file: "contentscript.js"}],
+      allFrames: true,
+    });
+
+    let f = document.createElement("iframe");
+    f.src = FRAME_URL;
+    document.body.appendChild(f);
+  }
+
+  function contentScript() {
+    browser.test.log(`Running content script at ${document.URL}`);
+
+    let port = browser.runtime.connect();
+    port.onMessage.addListener(msg => {
+      browser.test.assertEq("ping", msg, "Expected message to content script");
+      port.postMessage("pong");
+    });
+    port.onDisconnect.addListener(() => {
+      browser.test.sendMessage("disconnected_in_content_script");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["http://example.com/*"],
+    },
+    files: {
+      "contentscript.js": contentScript,
+    },
+    background,
+  });
+  await extension.startup();
+  await extension.awaitMessage("disconnected_in_content_script");
+  await extension.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/test_ext_startup_perf.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_startup_perf.js
@@ -10,16 +10,17 @@ const STARTUP_MODULES = [
   "resource://gre/modules/Extension.jsm",
   "resource://gre/modules/ExtensionCommon.jsm",
   "resource://gre/modules/ExtensionParent.jsm",
   // FIXME: This is only loaded at startup for new extension installs.
   // Otherwise the data comes from the startup cache. We should test for
   // this.
   "resource://gre/modules/ExtensionPermissions.jsm",
   "resource://gre/modules/ExtensionUtils.jsm",
+  "resource://gre/modules/ExtensionTelemetry.jsm",
 ];
 
 if (!Services.prefs.getBoolPref("extensions.webextensions.remote")) {
   STARTUP_MODULES.push(
     "resource://gre/modules/ExtensionChild.jsm",
     "resource://gre/modules/ExtensionPageChild.jsm");
 }
 
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_idb_data_migration.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_idb_data_migration.js
@@ -5,23 +5,26 @@
 // This test file verifies various scenarios related to the data migration
 // from the JSONFile backend to the IDB backend.
 
 AddonTestUtils.init(this);
 AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionStorage.jsm");
-ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 ChromeUtils.import("resource://gre/modules/TelemetryController.jsm");
 
 const {
   ExtensionStorageIDB,
 } = ChromeUtils.import("resource://gre/modules/ExtensionStorageIDB.jsm", {});
 
+const {
+  getTrimmedString,
+} = ChromeUtils.import("resource://gre/modules/ExtensionTelemetry.jsm", {});
+
 XPCOMUtils.defineLazyModuleGetters(this, {
   OS: "resource://gre/modules/osfile.jsm",
 });
 
 const {
   promiseShutdownManager,
   promiseStartupManager,
 } = AddonTestUtils;
@@ -247,17 +250,17 @@ add_task(async function test_extensionId
     },
     background,
   });
 
   await extension.startup();
 
   await extension.awaitMessage("storage-local-data-migrated");
 
-  const expectedTrimmedExtensionId = ExtensionUtils.getTrimmedString(EXTENSION_ID);
+  const expectedTrimmedExtensionId = getTrimmedString(EXTENSION_ID);
 
   equal(expectedTrimmedExtensionId.length, 80, "The trimmed version of the extensionId should be 80 chars long");
 
   assertTelemetryEvents(expectedTrimmedExtensionId, [
     {
       method: "migrateResult",
       extra: {
         backend: "IndexedDB",
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -24,16 +24,17 @@ skip-if = appname == "thunderbird" || os
 [test_ext_contentscript_api_injection.js]
 [test_ext_contentscript_async_loading.js]
 skip-if = os == 'android' && debug # The generated script takes too long to load on Android debug
 [test_ext_contentscript_context.js]
 [test_ext_contentscript_context_isolation.js]
 [test_ext_contentscript_create_iframe.js]
 [test_ext_contentscript_css.js]
 [test_ext_contentscript_exporthelpers.js]
+[test_ext_contentscript_in_background.js]
 [test_ext_contentscript_restrictSchemes.js]
 [test_ext_contentscript_teardown.js]
 [test_ext_contextual_identities.js]
 skip-if = appname == "thunderbird" || os == "android" # Containers are not exposed to android.
 [test_ext_debugging_utils.js]
 [test_ext_dns.js]
 [test_ext_downloads.js]
 skip-if = appname == "thunderbird"
--- a/toolkit/components/normandy/content/AboutPages.jsm
+++ b/toolkit/components/normandy/content/AboutPages.jsm
@@ -1,25 +1,20 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-ChromeUtils.defineModuleGetter(
-  this, "CleanupManager", "resource://normandy/lib/CleanupManager.jsm",
-);
-ChromeUtils.defineModuleGetter(
-  this, "AddonStudies", "resource://normandy/lib/AddonStudies.jsm",
-);
-ChromeUtils.defineModuleGetter(
-  this, "RecipeRunner", "resource://normandy/lib/RecipeRunner.jsm",
-);
+ChromeUtils.defineModuleGetter(this, "AddonStudyAction", "resource://normandy/actions/AddonStudyAction.jsm");
+ChromeUtils.defineModuleGetter(this, "CleanupManager", "resource://normandy/lib/CleanupManager.jsm");
+ChromeUtils.defineModuleGetter(this, "AddonStudies", "resource://normandy/lib/AddonStudies.jsm");
+ChromeUtils.defineModuleGetter(this, "RecipeRunner", "resource://normandy/lib/RecipeRunner.jsm");
 
 var EXPORTED_SYMBOLS = ["AboutPages"];
 
 const SHIELD_LEARN_MORE_URL_PREF = "app.normandy.shieldLearnMoreUrl";
 
 
 /**
  * Class for managing an about: page that Normandy provides. Adapted from
@@ -178,17 +173,18 @@ XPCOMUtils.defineLazyGetter(this.AboutPa
       }
     },
 
     /**
      * Disable an active study and remove its add-on.
      * @param {String} studyName
      */
     async removeStudy(recipeId, reason) {
-      await AddonStudies.stop(recipeId, reason);
+      const action = new AddonStudyAction();
+      await action.unenroll(recipeId, reason);
 
       // Update any open tabs with the new study list now that it has changed.
       Services.mm.broadcastAsyncMessage("Shield:ReceiveStudyList", {
         studies: await AddonStudies.getAll(),
       });
     },
 
     openDataPreferences() {
--- a/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js
@@ -61,17 +61,17 @@ add_task(async function test_backButton_
     await BrowserTestUtils.browserLoaded(aBrowser, false,
                                          "https://example.com" + DIRECTORY_PATH +
                                          "formless_basic.html?second");
     await fillTestPage(aBrowser);
 
     let forwardButton = document.getElementById("forward-button");
 
     let forwardTransitionPromise;
-    if (forwardButton.nextSibling == gURLBar) {
+    if (forwardButton.nextElementSibling == gURLBar) {
       // We need to wait for the forward button transition to complete before we
       // can click it, so we hook up a listener to wait for it to be ready.
       forwardTransitionPromise = BrowserTestUtils.waitForEvent(forwardButton, "transitionend");
     }
 
     let backPromise = BrowserTestUtils.browserStopped(aBrowser);
     EventUtils.synthesizeMouseAtCenter(document.getElementById("back-button"), {});
     await backPromise;
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5761,41 +5761,41 @@
     "record_in_processes": ["main", "content"],
     "expires_in_version": "50",
     "kind": "exponential",
     "high": 1000,
     "n_buckets": 30,
     "description": "Firefox: Time taken to kick off image compression of the canvas that will be used during swiping through history (ms)."
   },
   "FX_TAB_CLOSE_TIME_ANIM_MS": {
-    "record_in_processes": ["main", "content"],
-    "alert_emails": ["mconley@mozilla.com", "hkirschner@mozilla.com", "sphilp@mozilla.com"],
-    "bug_numbers": [1340842],
-    "expires_in_version": "65",
+    "record_in_processes": ["main"],
+    "alert_emails": ["mconley@mozilla.com", "dolske@mozilla.com"],
+    "bug_numbers": [1340842, 1488952],
+    "expires_in_version": "never",
     "releaseChannelCollection": "opt-out",
     "kind": "exponential",
     "high": 10000,
     "n_buckets": 50,
     "description": "Firefox: Time taken from the point of closing a tab (with animation), to the browser element being removed from the DOM. (ms)."
   },
   "FX_TAB_CLOSE_TIME_NO_ANIM_MS": {
-    "record_in_processes": ["main", "content"],
+    "record_in_processes": ["main"],
     "alert_emails": ["mconley@mozilla.com", "hkirschner@mozilla.com"],
     "bug_numbers": [1340842],
-    "expires_in_version": "65",
+    "expires_in_version": "68",
     "kind": "exponential",
     "high": 10000,
     "n_buckets": 50,
     "description": "Firefox: Time taken from the point of closing a tab (without animation) to the browser element being removed from the DOM. (ms)."
   },
   "FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS": {
-    "record_in_processes": ["main", "content"],
+    "record_in_processes": ["main"],
     "alert_emails": ["mconley@mozilla.com", "hkirschner@mozilla.com"],
     "bug_numbers": [1340842],
-    "expires_in_version": "65",
+    "expires_in_version": "68",
     "kind": "exponential",
     "high": 10000,
     "n_buckets": 50,
     "description": "Firefox: Time taken to run permitUnload on a browser during tab close to see whether or not we're allowed to close the tab (ms)."
   },
   "FX_REFRESH_DRIVER_CHROME_FRAME_DELAY_MS": {
     "record_in_processes": ["main", "content"],
     "alert_emails": ["perf-telemetry-alerts@mozilla.com", "gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
--- a/toolkit/content/tests/browser/browser_autoplay_policy_request_permission.js
+++ b/toolkit/content/tests/browser/browser_autoplay_policy_request_permission.js
@@ -154,17 +154,17 @@ async function testAutoplayUnknownPermis
     ok(promptShowing(), "Should now be showing permission prompt");
 
     // Click the appropriate doorhanger button.
     if (args.button == "allow") {
       info("Clicking allow button");
       PopupNotifications.panel.firstElementChild.button.click();
     } else if (args.button == "block") {
       info("Clicking block button");
-      PopupNotifications.panel.firstChild.secondaryButton.click();
+      PopupNotifications.panel.firstElementChild.secondaryButton.click();
     } else {
       ok(false, "Invalid button field");
     }
     // Check that the video started playing.
     await checkVideoDidPlay(browser, args);
 
     // Reset permission.
     SitePermissions.remove(browser.currentURI, "autoplay-media");
--- a/toolkit/content/widgets/tabbox.xml
+++ b/toolkit/content/widgets/tabbox.xml
@@ -273,17 +273,17 @@
       </method>
 
       <method name="advanceSelectedTab">
         <parameter name="aDir"/>
         <parameter name="aWrap"/>
         <body>
         <![CDATA[
           var startTab = this.selectedItem;
-          var next = startTab[aDir == -1 ? "previousSibling" : "nextSibling"];
+          var next = startTab[(aDir == -1 ? "previous" : "next") + "ElementSibling"];
           if (!next && aWrap) {
             next = aDir == -1 ? this.children[this.children.length - 1] :
                                 this.children[0];
           }
           if (next && next != startTab) {
             this._selectNewTab(next, aDir, aWrap);
           }
         ]]>
--- a/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js
@@ -28,17 +28,17 @@ function promisePopupNotificationShown(n
     function popupshown() {
       let notification = PopupNotifications.getNotification(name);
       if (!notification) { return; }
 
       ok(notification, `${name} notification shown`);
       ok(PopupNotifications.isPanelOpen, "notification panel open");
 
       PopupNotifications.panel.removeEventListener("popupshown", popupshown);
-      resolve(PopupNotifications.panel.firstChild);
+      resolve(PopupNotifications.panel.firstElementChild);
     }
 
     PopupNotifications.panel.addEventListener("popupshown", popupshown);
   });
 }
 
 async function checkInstallConfirmation(...names) {
   let notificationCount = 0;
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -1381,15 +1381,15 @@ function promiseNotification(id = "addon
     return Promise.resolve();
   }
 
   return new Promise(resolve => {
     function popupshown() {
       let notification = PopupNotifications.getNotification(id);
       if (notification) {
         PopupNotifications.panel.removeEventListener("popupshown", popupshown);
-        PopupNotifications.panel.firstChild.button.click();
+        PopupNotifications.panel.firstElementChild.button.click();
         resolve();
       }
     }
     PopupNotifications.panel.addEventListener("popupshown", popupshown);
   });
 }
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js
@@ -473,19 +473,19 @@ async function test_allUnverified() {
   await progressPromise;
   let installDialog = await dialogPromise;
 
   let notification = document.getElementById("addon-install-confirmation-notification");
   let message = notification.getAttribute("label");
   is(message, "Caution: This site would like to install an unverified add-on in " + gApp + ". Proceed at your own risk.");
 
   let container = document.getElementById("addon-install-confirmation-content");
-  is(container.childNodes.length, 1, "Should be one item listed");
-  is(container.childNodes[0].firstChild.getAttribute("value"), "XPI Test", "Should have the right add-on");
-  is(container.childNodes[0].childNodes.length, 1, "Shouldn't have the unverified marker");
+  is(container.children.length, 1, "Should be one item listed");
+  is(container.children[0].firstElementChild.getAttribute("value"), "XPI Test", "Should have the right add-on");
+  is(container.children[0].children.length, 1, "Shouldn't have the unverified marker");
 
   let notificationPromise = waitForNotification("addon-installed");
   acceptInstallDialog(installDialog);
   await notificationPromise;
 
   let addon = await AddonManager.getAddonByID("restartless-xpi@tests.mozilla.org");
   addon.uninstall();
 
--- a/toolkit/mozapps/extensions/test/xpinstall/head.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/head.js
@@ -260,22 +260,22 @@ var Harness = {
       panel.secondaryButton.click();
     } else {
       panel.button.click();
     }
   },
 
   handleEvent(event) {
     if (event.type === "popupshown") {
-      if (event.target.firstChild) {
+      if (event.target.firstElementChild) {
         let popupId = event.target.getAttribute("popupid");
         if (popupId === "addon-webext-permissions") {
-          this.popupReady(event.target.firstChild);
+          this.popupReady(event.target.firstElementChild);
         } else if (popupId === "addon-installed" || popupId === "addon-install-failed") {
-          event.target.firstChild.button.click();
+          event.target.firstElementChild.button.click();
         }
       }
     }
   },
 
   // Install blocked handling
 
   installDisabled(installInfo) {
--- a/xpcom/io/FileUtilsWin.h
+++ b/xpcom/io/FileUtilsWin.h
@@ -12,45 +12,35 @@
 #include "mozilla/Scoped.h"
 #include "nsString.h"
 
 namespace mozilla {
 
 inline bool
 EnsureLongPath(nsAString& aDosPath)
 {
-  uint32_t aDosPathOriginalLen = aDosPath.Length();
-  auto inputPath = PromiseFlatString(aDosPath);
-  // Try to get the long path, or else get the required length of the long path
-  DWORD longPathLen = GetLongPathNameW(inputPath.get(),
-                                       reinterpret_cast<wchar_t*>(aDosPath.BeginWriting()),
-                                       aDosPathOriginalLen);
-  if (longPathLen == 0) {
-    return false;
-  }
-  aDosPath.SetLength(longPathLen);
-  if (longPathLen <= aDosPathOriginalLen) {
-    // Our string happened to be long enough for the first call to succeed.
-    return true;
+  nsAutoString inputPath(aDosPath);
+  while (true) {
+    DWORD requiredLength = GetLongPathNameW(inputPath.get(),
+                                            reinterpret_cast<wchar_t*>(aDosPath.BeginWriting()),
+                                            aDosPath.Length());
+    if (!requiredLength) {
+      return false;
+    }
+    if (requiredLength < aDosPath.Length()) {
+      // When GetLongPathNameW deems the last argument too small,
+      // it returns a value, but when you pass that value back, it's
+      // satisfied and returns a number that's one smaller. If the above
+      // check was == instead of <, the loop would go on forever with
+      // GetLongPathNameW returning oscillating values!
+      aDosPath.Truncate(requiredLength);
+      return true;
+    }
+    aDosPath.SetLength(requiredLength);
   }
-  // Now we have a large enough buffer, get the actual string
-  longPathLen = GetLongPathNameW(inputPath.get(),
-                                 reinterpret_cast<wchar_t*>(aDosPath.BeginWriting()), aDosPath.Length());
-  if (longPathLen == 0) {
-    return false;
-  }
-  // This success check should always be less-than because longPathLen excludes
-  // the null terminator on success, but includes it in the first call that
-  // returned the required size.
-  if (longPathLen < aDosPath.Length()) {
-    aDosPath.SetLength(longPathLen);
-    return true;
-  }
-  // We shouldn't reach this, but if we do then it's a failure!
-  return false;
 }
 
 inline bool
 NtPathToDosPath(const nsAString& aNtPath, nsAString& aDosPath)
 {
   aDosPath.Truncate();
   if (aNtPath.IsEmpty()) {
     return true;
@@ -62,34 +52,42 @@ NtPathToDosPath(const nsAString& aNtPath
       ntPathLen >= symLinkPrefixLen &&
       Substring(aNtPath, 0, symLinkPrefixLen).Equals(symLinkPrefix)) {
     // Symbolic link for DOS device. Just strip off the prefix.
     aDosPath = aNtPath;
     aDosPath.Cut(0, 4);
     return true;
   }
   nsAutoString logicalDrives;
-  DWORD len = 0;
   while (true) {
-    len = GetLogicalDriveStringsW(
-      len, reinterpret_cast<wchar_t*>(logicalDrives.BeginWriting()));
-    if (!len) {
+    DWORD requiredLength = GetLogicalDriveStringsW(
+      logicalDrives.Length(), reinterpret_cast<wchar_t*>(logicalDrives.BeginWriting()));
+    if (!requiredLength) {
       return false;
-    } else if (len > logicalDrives.Length()) {
-      logicalDrives.SetLength(len);
-    } else {
+    }
+    if (requiredLength < logicalDrives.Length()) {
+      // When GetLogicalDriveStringsW deems the first argument too small,
+      // it returns a value, but when you pass that value back, it's
+      // satisfied and returns a number that's one smaller. If the above
+      // check was == instead of <, the loop would go on forever with
+      // GetLogicalDriveStringsW returning oscillating values!
+      logicalDrives.Truncate(requiredLength);
+      // logicalDrives now has the format "C:\\\0D:\\\0Z:\\\0". That is,
+      // the sequence drive letter, colon, backslash, U+0000 repeats.
       break;
     }
+    logicalDrives.SetLength(requiredLength);
   }
+
   const char16_t* cur = logicalDrives.BeginReading();
   const char16_t* end = logicalDrives.EndReading();
   nsString targetPath;
   targetPath.SetLength(MAX_PATH);
   wchar_t driveTemplate[] = L" :";
-  do {
+  while (cur < end) {
     // Unfortunately QueryDosDevice doesn't support the idiom for querying the
     // output buffer size, so it may require retries.
     driveTemplate[0] = *cur;
     DWORD targetPathLen = 0;
     SetLastError(ERROR_SUCCESS);
     while (true) {
       targetPathLen = QueryDosDeviceW(driveTemplate,
                                       reinterpret_cast<wchar_t*>(targetPath.BeginWriting()),
@@ -108,19 +106,26 @@ NtPathToDosPath(const nsAString& aNtPath
                              firstTargetPathLen) == 0 &&
                    *pathComponent == L'\\';
       if (found) {
         aDosPath = driveTemplate;
         aDosPath += pathComponent;
         return EnsureLongPath(aDosPath);
       }
     }
-    // Advance to the next NUL character in logicalDrives
-    while (*cur++);
-  } while (cur != end);
+    // Find the next U+0000 within the logical string
+    while (*cur) {
+      // This loop skips the drive letter, the colon
+      // and the backslash.
+      cur++;
+    }
+    // Skip over the U+0000 that ends a drive entry
+    // within the logical string
+    cur++;
+  }
   // Try to handle UNC paths. NB: This must happen after we've checked drive
   // mappings in case a UNC path is mapped to a drive!
   NS_NAMED_LITERAL_STRING(uncPrefix, "\\\\");
   NS_NAMED_LITERAL_STRING(deviceMupPrefix, "\\Device\\Mup\\");
   if (StringBeginsWith(aNtPath, deviceMupPrefix)) {
     aDosPath = uncPrefix;
     aDosPath += Substring(aNtPath, deviceMupPrefix.Length());
     return true;