Merge autoland to mozilla-central. a=merge
authorTiberius Oros <toros@mozilla.com>
Wed, 21 Mar 2018 11:39:51 +0200
changeset 409125 f4ddf30ecf57e0d4bc5dac5707c6c153ffb9c726
parent 409074 9fa586e87f34bef3bb15431d149ec7aeae0f7d24 (current diff)
parent 409124 d918859fa3fb3fac0a5bbff1a9a74f908566b7f6 (diff)
child 409180 e636edf00e6fbdc3206c9df4a1548ae38b3d13fa
push id33675
push usertoros@mozilla.com
push dateWed, 21 Mar 2018 09:40:24 +0000
treeherdermozilla-central@f4ddf30ecf57 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge autoland to mozilla-central. a=merge
devtools/server/child.js
devtools/server/content-process-debugger-server.js
devtools/server/content-server.jsm
devtools/server/css-logic.js
devtools/server/event-parsers.js
devtools/server/primitive.js
devtools/server/service-worker-child.js
devtools/server/tests/mochitest/test_connectToChild.html
devtools/server/tests/mochitest/test_websocket-server.html
devtools/server/websocket-server.js
devtools/server/worker.js
devtools/shim/DevToolsShim.jsm
devtools/shim/aboutdebugging-registration.js
devtools/shim/aboutdebugging.manifest
devtools/shim/aboutdevtools/aboutdevtools-registration.js
devtools/shim/aboutdevtools/aboutdevtools.css
devtools/shim/aboutdevtools/aboutdevtools.js
devtools/shim/aboutdevtools/aboutdevtools.manifest
devtools/shim/aboutdevtools/aboutdevtools.xhtml
devtools/shim/aboutdevtools/images/dev-edition-logo.svg
devtools/shim/aboutdevtools/images/external-link.svg
devtools/shim/aboutdevtools/images/feature-console.svg
devtools/shim/aboutdevtools/images/feature-debugger.svg
devtools/shim/aboutdevtools/images/feature-inspector.svg
devtools/shim/aboutdevtools/images/feature-memory.svg
devtools/shim/aboutdevtools/images/feature-network.svg
devtools/shim/aboutdevtools/images/feature-performance.svg
devtools/shim/aboutdevtools/images/feature-responsive.svg
devtools/shim/aboutdevtools/images/feature-storage.svg
devtools/shim/aboutdevtools/images/feature-visualediting.svg
devtools/shim/aboutdevtools/images/otter.svg
devtools/shim/aboutdevtools/moz.build
devtools/shim/aboutdevtools/subscribe.css
devtools/shim/aboutdevtools/subscribe.js
devtools/shim/aboutdevtools/test/.eslintrc.js
devtools/shim/aboutdevtools/test/browser.ini
devtools/shim/aboutdevtools/test/browser_aboutdevtools_closes_page.js
devtools/shim/aboutdevtools/test/browser_aboutdevtools_enables_devtools.js
devtools/shim/aboutdevtools/test/browser_aboutdevtools_focus_owner_tab.js
devtools/shim/aboutdevtools/test/browser_aboutdevtools_reuse_existing.js
devtools/shim/aboutdevtools/test/head.js
devtools/shim/aboutdevtoolstoolbox-registration.js
devtools/shim/aboutdevtoolstoolbox.manifest
devtools/shim/devtools-startup-prefs.js
devtools/shim/devtools-startup.js
devtools/shim/devtools-startup.manifest
devtools/shim/jar.mn
devtools/shim/locales/en-US/aboutdevtools.dtd
devtools/shim/locales/en-US/aboutdevtools.properties
devtools/shim/locales/en-US/key-shortcuts.properties
devtools/shim/locales/en-US/startup.properties
devtools/shim/locales/jar.mn
devtools/shim/locales/moz.build
devtools/shim/moz.build
devtools/shim/tests/browser/.eslintrc.js
devtools/shim/tests/browser/browser.ini
devtools/shim/tests/browser/browser_shim_disable_devtools.js
devtools/shim/tests/unit/.eslintrc.js
devtools/shim/tests/unit/test_devtools_shim.js
devtools/shim/tests/unit/xpcshell.ini
testing/mochitest/browser-test-overlay.xul
--- a/.eslintignore
+++ b/.eslintignore
@@ -151,17 +151,17 @@ devtools/server/tests/browser/stylesheet
 devtools/server/tests/unit/xpcshell_debugging_script.js
 devtools/client/shared/webpack/shims/test/test_clipboard.html
 devtools/shared/qrcode/tests/mochitest/test_decode.html
 devtools/shared/tests/mochitest/*.html
 devtools/shared/webconsole/test/test_*.html
 
 # Ignore devtools preferences files
 devtools/client/preferences/**
-devtools/shim/devtools-startup-prefs.js
+devtools/startup/devtools-startup-prefs.js
 
 # Ignore devtools third-party libs
 devtools/shared/jsbeautify/*
 devtools/shared/acorn/*
 devtools/shared/gcli/source/*
 devtools/shared/node-properties/*
 devtools/shared/pretty-fast/*
 devtools/shared/sourcemap/*
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -10,17 +10,17 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.jsm",
   LoginHelper: "resource://gre/modules/LoginHelper.jsm",
   LoginManagerContextMenu: "resource://gre/modules/LoginManagerContextMenu.jsm",
   WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
-  DevToolsShim: "chrome://devtools-shim/content/DevToolsShim.jsm",
+  DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.jsm",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
 });
 
 var gContextMenuContentData = null;
 
 function setContextMenuContentData(data) {
   gContextMenuContentData = data;
 }
--- a/browser/base/content/test/general/browser_bug537474.js
+++ b/browser/base/content/test/general/browser_bug537474.js
@@ -1,8 +1,8 @@
 add_task(async function() {
-  let browserLoadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+  let browserLoadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, "about:mozilla");
   window.browserDOMWindow.openURI(makeURI("about:mozilla"), null,
                                   Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW, null,
                                   Services.scriptSecurityManager.getSystemPrincipal());
   await browserLoadedPromise;
   is(gBrowser.currentURI.spec, "about:mozilla", "page loads in the current content window");
 });
--- a/browser/components/extensions/ext-devtools.js
+++ b/browser/components/extensions/ext-devtools.js
@@ -9,17 +9,17 @@
 /* import-globals-from ext-browser.js */
 
 /**
  * This module provides helpers used by the other specialized `ext-devtools-*.js` modules
  * and the implementation of the `devtools_page`.
  */
 
 ChromeUtils.defineModuleGetter(this, "DevToolsShim",
-                               "chrome://devtools-shim/content/DevToolsShim.jsm");
+                               "chrome://devtools-startup/content/DevToolsShim.jsm");
 
 ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 
 var {
   HiddenExtensionPage,
   watchExtensionProxyContextLoad,
 } = ExtensionParent;
 
--- a/browser/components/preferences/in-content/findInPage.js
+++ b/browser/components/preferences/in-content/findInPage.js
@@ -4,16 +4,19 @@
 
 /* import-globals-from extensionControlled.js */
 /* import-globals-from preferences.js */
 
 var gSearchResultsPane = {
   listSearchTooltips: new Set(),
   listSearchMenuitemIndicators: new Set(),
   searchInput: null,
+  // A map of DOM Elements to a string of keywords used in search
+  // XXX: We should invalidate this cache on `intl:app-locales-changed`
+  searchKeywords: new WeakMap(),
   inited: false,
 
   init() {
     if (this.inited) {
       return;
     }
     this.inited = true;
     this.searchInput = document.getElementById("searchInput");
@@ -216,107 +219,100 @@ var gSearchResultsPane = {
     this.removeAllSearchMenuitemIndicators();
 
     // Clear telemetry request if user types very frequently.
     if (this.telemetryTimer) {
       clearTimeout(this.telemetryTimer);
     }
 
     let srHeader = document.getElementById("header-searchResults");
-
+    let noResultsEl = document.getElementById("no-results-message");
+    srHeader.hidden = !this.query;
     if (this.query) {
       // Showing the Search Results Tag
       gotoPref("paneSearchResults");
 
       let resultsFound = false;
 
       // Building the range for highlighted areas
-      let rootPreferencesChildren = document
-        .querySelectorAll("#mainPrefPane > *:not([data-hidden-from-search])");
-
-      // Show all second level headers in search result
-      for (let element of document.querySelectorAll("caption.search-header")) {
-        element.hidden = false;
-      }
+      let rootPreferencesChildren = [...document
+        .querySelectorAll("#mainPrefPane > *:not([data-hidden-from-search])")];
 
       if (subQuery) {
         // Since the previous query is a subset of the current query,
         // there is no need to check elements that is hidden already.
-        rootPreferencesChildren =
-          Array.prototype.filter.call(rootPreferencesChildren, el => !el.hidden);
+        rootPreferencesChildren = rootPreferencesChildren.filter(el => !el.hidden);
       }
 
       // Mark all the children to check be visible to bind JS, Access Keys, etc,
       // but don't really show them by setting their visibility to hidden in CSS.
-      for (let i = 0; i < rootPreferencesChildren.length; i++) {
-        rootPreferencesChildren[i].hidden = false;
-        rootPreferencesChildren[i].classList.add("visually-hidden");
+      for (let child of rootPreferencesChildren) {
+        child.classList.add("visually-hidden");
+        child.hidden = false;
       }
 
       let ts = performance.now();
       let FRAME_THRESHOLD = 1000 / 60;
 
       // Showing or Hiding specific section depending on if words in query are found
-      for (let i = 0; i < rootPreferencesChildren.length; i++) {
+      for (let child of rootPreferencesChildren) {
         if (performance.now() - ts > FRAME_THRESHOLD) {
           // Creating tooltips for all the instances found
           for (let anchorNode of this.listSearchTooltips) {
             this.createSearchTooltip(anchorNode, this.query);
           }
-          // It hides Search Results header so turning it on
-          srHeader.hidden = false;
-          srHeader.classList.remove("visually-hidden");
           ts = await new Promise(resolve => window.requestAnimationFrame(resolve));
           if (query !== this.query) {
             return;
           }
         }
 
-        rootPreferencesChildren[i].classList.remove("visually-hidden");
-        if (!rootPreferencesChildren[i].classList.contains("header") &&
-            !rootPreferencesChildren[i].classList.contains("subcategory") &&
-            !rootPreferencesChildren[i].classList.contains("no-results-message") &&
-            this.searchWithinNode(rootPreferencesChildren[i], this.query)) {
-          rootPreferencesChildren[i].hidden = false;
+        if (!child.classList.contains("header") &&
+            !child.classList.contains("subcategory") &&
+            await this.searchWithinNode(child, this.query)) {
+          child.hidden = false;
+          child.classList.remove("visually-hidden");
+
+          // Show the preceding search-header if one exists.
+          let groupbox = child.closest("groupbox");
+          let groupHeader = groupbox && groupbox.querySelector(".search-header");
+          if (groupHeader) {
+            groupHeader.hidden = false;
+          }
+
           resultsFound = true;
         } else {
-          rootPreferencesChildren[i].hidden = true;
+          child.hidden = true;
         }
       }
-      // It hides Search Results header so turning it on
-      srHeader.hidden = false;
-      srHeader.classList.remove("visually-hidden");
 
-      if (!resultsFound) {
-        let noResultsEl = document.querySelector(".no-results-message");
-        noResultsEl.setAttribute("query", this.query);
-
-        // XXX: This is potentially racy in case where Fluent retranslates the
-        // message and ereases the query within.
-        // The feature is not yet supported, but we should fix for it before
-        // we enable it. See bug 1446389 for details.
-        let msgQueryElem = document.getElementById("sorry-message-query");
-        msgQueryElem.textContent = this.query;
-
-        noResultsEl.hidden = false;
-      } else {
+      noResultsEl.hidden = !!resultsFound;
+      noResultsEl.setAttribute("query", this.query);
+      // XXX: This is potentially racy in case where Fluent retranslates the
+      // message and ereases the query within.
+      // The feature is not yet supported, but we should fix for it before
+      // we enable it. See bug 1446389 for details.
+      let msgQueryElem = document.getElementById("sorry-message-query");
+      msgQueryElem.textContent = this.query;
+      if (resultsFound) {
         // Creating tooltips for all the instances found
         for (let anchorNode of this.listSearchTooltips) {
           this.createSearchTooltip(anchorNode, this.query);
         }
 
         // Implant search telemetry probe after user stops typing for a while
         if (this.query.length >= 2) {
           this.telemetryTimer = setTimeout(() => {
             Services.telemetry.keyedScalarAdd("preferences.search_query", this.query, 1);
           }, 1000);
         }
       }
     } else {
-      document.getElementById("sorry-message").textContent = "";
+      noResultsEl.hidden = true;
+      document.getElementById("sorry-message-query").textContent = "";
       // Going back to General when cleared
       gotoPref("paneGeneral");
 
       // Hide some special second level headers in normal view
       for (let element of document.querySelectorAll("caption.search-header")) {
         element.hidden = true;
       }
     }
@@ -329,17 +325,17 @@ var gSearchResultsPane = {
    * It is a recursive function
    *
    * @param Node nodeObject
    *    DOM Element
    * @param String searchPhrase
    * @returns boolean
    *    Returns true when found in at least one childNode, false otherwise
    */
-  searchWithinNode(nodeObject, searchPhrase) {
+  async searchWithinNode(nodeObject, searchPhrase) {
     let matchesFound = false;
     if (nodeObject.childElementCount == 0 ||
         nodeObject.tagName == "label" ||
         nodeObject.tagName == "description" ||
         nodeObject.tagName == "menulist") {
       let simpleTextNodes = this.textNodeDescendants(nodeObject);
       for (let node of simpleTextNodes) {
         let result = this.highlightMatches([node], [node.length], node.textContent.toLowerCase(), searchPhrase);
@@ -368,18 +364,30 @@ var gSearchResultsPane = {
       // Searching some elements, such as xul:button, have a 'label' attribute that contains the user-visible text.
       let labelResult = this.queryMatchesContent(nodeObject.getAttribute("label"), searchPhrase);
 
       // Searching some elements, such as xul:label, store their user-visible text in a "value" attribute.
       // Value will be skipped for menuitem since value in menuitem could represent index number to distinct each item.
       let valueResult = nodeObject.tagName !== "menuitem" ?
         this.queryMatchesContent(nodeObject.getAttribute("value"), searchPhrase) : false;
 
-      // Searching some elements, such as xul:button, buttons to open subdialogs.
-      let keywordsResult = this.queryMatchesContent(nodeObject.getAttribute("searchkeywords"), searchPhrase);
+      // Searching some elements, such as xul:button, buttons to open subdialogs
+      // using l10n ids.
+      let keywordsResult =
+        nodeObject.hasAttribute("search-l10n-ids") &&
+        await this.matchesSearchL10nIDs(nodeObject, searchPhrase);
+
+      if (!keywordsResult) {
+        // Searching some elements, such as xul:button, buttons to open subdialogs
+        // using searchkeywords attribute.
+        keywordsResult =
+          !keywordsResult &&
+          nodeObject.hasAttribute("searchkeywords") &&
+          this.queryMatchesContent(nodeObject.getAttribute("searchkeywords"), searchPhrase);
+      }
 
       // Creating tooltips for buttons
       if (keywordsResult && (nodeObject.tagName === "button" || nodeObject.tagName == "menulist")) {
         this.listSearchTooltips.add(nodeObject);
       }
 
       if (keywordsResult && nodeObject.tagName === "menuitem") {
         nodeObject.setAttribute("indicator", "true");
@@ -400,22 +408,22 @@ var gSearchResultsPane = {
       matchesFound = matchesFound || complexTextNodesResult || labelResult || valueResult || keywordsResult;
     }
 
     // Should not search unselected child nodes of a <xul:deck> element
     // except the "historyPane" <xul:deck> element.
     if (nodeObject.tagName == "deck" && nodeObject.id != "historyPane") {
       let index = nodeObject.selectedIndex;
       if (index != -1) {
-        let result = this.searchChildNodeIfVisible(nodeObject, index, searchPhrase);
+        let result = await this.searchChildNodeIfVisible(nodeObject, index, searchPhrase);
         matchesFound = matchesFound || result;
       }
     } else {
       for (let i = 0; i < nodeObject.childNodes.length; i++) {
-        let result = this.searchChildNodeIfVisible(nodeObject, i, searchPhrase);
+        let result = await this.searchChildNodeIfVisible(nodeObject, i, searchPhrase);
         matchesFound = matchesFound || result;
       }
     }
     return matchesFound;
   },
 
   /**
    * Search for a phrase within a child node if it is visible.
@@ -423,29 +431,79 @@ var gSearchResultsPane = {
    * @param Node nodeObject
    *    The parent DOM Element
    * @param Number index
    *    The index for the childNode
    * @param String searchPhrase
    * @returns boolean
    *    Returns true when found the specific childNode, false otherwise
    */
-  searchChildNodeIfVisible(nodeObject, index, searchPhrase) {
+  async searchChildNodeIfVisible(nodeObject, index, searchPhrase) {
     let result = false;
     if (!nodeObject.childNodes[index].hidden && nodeObject.getAttribute("data-hidden-from-search") !== "true") {
-      result = this.searchWithinNode(nodeObject.childNodes[index], searchPhrase);
+      result = await this.searchWithinNode(nodeObject.childNodes[index], searchPhrase);
       // Creating tooltips for menulist element
       if (result && nodeObject.tagName === "menulist") {
         this.listSearchTooltips.add(nodeObject);
       }
     }
     return result;
   },
 
   /**
+   * Search for a phrase in l10n messages associated with the element.
+   *
+   * @param Node nodeObject
+   *    The parent DOM Element
+   * @param String searchPhrase
+   * @returns boolean
+   *    true when the text content contains the query string else false
+   */
+  async matchesSearchL10nIDs(nodeObject, searchPhrase) {
+    if (!this.searchKeywords.has(nodeObject)) {
+      // The `search-l10n-ids` attribute is a comma-separated list of
+      // l10n ids. It may also uses a dot notation to specify an attribute
+      // of the message to be used.
+      //
+      // Example: "containers-add-button.label, user-context-personal"
+      //
+      // The result is an array of arrays of l10n ids and optionally attribute names.
+      //
+      // Example: [["containers-add-button", "label"], ["user-context-personal"]]
+      const refs = nodeObject.getAttribute("search-l10n-ids")
+        .split(",")
+        .map(s => s.trim().split(".")).filter(s => s[0].length > 0);
+
+      const messages = await document.l10n.formatMessages(refs.map(ref => [ref[0]]));
+
+      // Map the localized messages taking value or a selected attribute and
+      // building a string of concatenated translated strings out of it.
+      let keywords = messages.map((msg, i) => {
+        if (msg === null) {
+          console.warn(`Missing search l10n id "${refs[i][0]}"`);
+          return null;
+        }
+        if (refs[i][1]) {
+          let attr = msg.attrs.find(a => a.name === refs[i][1]);
+          if (attr) {
+            return attr.value;
+          }
+          return null;
+        }
+        return msg.value;
+      }).filter(keyword => keyword !== null).join(" ");
+
+      this.searchKeywords.set(nodeObject, keywords);
+      return this.queryMatchesContent(keywords, searchPhrase);
+    }
+
+    return this.queryMatchesContent(this.searchKeywords.get(nodeObject), searchPhrase);
+  },
+
+  /**
    * Inserting a div structure infront of the DOM element matched textContent.
    * Then calculation the offsets to position the tooltip in the correct place.
    *
    * @param Node anchorNode
    *    DOM Element
    * @param String query
    *    Word or words that are being searched for
    */
--- a/browser/components/preferences/in-content/searchResults.xul
+++ b/browser/components/preferences/in-content/searchResults.xul
@@ -1,20 +1,24 @@
 <!-- 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/. -->
 
 <hbox id="header-searchResults"
       class="subcategory"
       hidden="true"
+      data-hidden-from-search="true"
       data-category="paneSearchResults">
   <label class="header-name" flex="1" data-l10n-id="search-results-header" />
 </hbox>
 
-<groupbox class="no-results-message" data-category="paneSearchResults" hidden="true">
+<groupbox id="no-results-message"
+          data-hidden-from-search="true"
+          data-category="paneSearchResults"
+          hidden="true">
   <vbox class="no-results-container">
     <label id="sorry-message" data-l10n-id="search-results-sorry-message">
       <html:span id="sorry-message-query"/>
     </label>
     <label id="need-help" data-l10n-id="search-results-need-help">
       <a class="text-link" target="_blank"></a>
     </label>
   </vbox>
--- a/browser/components/preferences/in-content/tests/browser_search_within_preferences_1.js
+++ b/browser/components/preferences/in-content/tests/browser_search_within_preferences_1.js
@@ -3,29 +3,29 @@
  * This file contains tests for the Preferences search bar.
  */
 
 requestLongerTimeout(6);
 
 /**
  * Tests to see if search bar is being shown when pref is turned on
  */
-add_task(async function() {
+add_task(async function show_search_bar_when_pref_is_enabled() {
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", { leaveOpen: true });
   let searchInput = gBrowser.contentDocument.getElementById("searchInput");
   is_element_visible(searchInput, "Search box should be shown");
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 /**
  * Test for "Search Result" panel.
  * After it runs a search, it tests if the "Search Results" panel is the only selected category.
  * The search is then cleared, it then tests if the "General" panel is the only selected category.
  */
-add_task(async function() {
+add_task(async function show_search_results_pane_only_then_revert_to_general() {
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", { leaveOpen: true });
 
   // Performs search
   let searchInput = gBrowser.contentDocument.getElementById("searchInput");
 
   is(searchInput, gBrowser.contentDocument.activeElement.closest("#searchInput"),
     "Search input should be focused when visiting preferences");
 
@@ -61,17 +61,17 @@ add_task(async function() {
   }
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 /**
  * Test for "password" case. When we search "password", it should show the "passwordGroup"
  */
-add_task(async function() {
+add_task(async function search_for_password_show_passwordGroup() {
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", { leaveOpen: true });
 
   // Performs search
   let searchInput = gBrowser.contentDocument.getElementById("searchInput");
 
   is(searchInput, gBrowser.contentDocument.activeElement.closest("#searchInput"),
     "Search input should be focused when visiting preferences");
 
@@ -133,55 +133,58 @@ add_task(async function() {
   }
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 /**
  * Test for if nothing is found
  */
-add_task(async function() {
+add_task(async function search_with_nothing_found() {
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", { leaveOpen: true });
 
-  let noResultsEl = gBrowser.contentDocument.querySelector(".no-results-message");
+  let noResultsEl = gBrowser.contentDocument.querySelector("#no-results-message");
+  let sorryMsgQueryEl = gBrowser.contentDocument.getElementById("sorry-message-query");
 
   is_element_hidden(noResultsEl, "Should not be in search results yet");
 
   // Performs search
   let searchInput = gBrowser.contentDocument.getElementById("searchInput");
 
   is(searchInput, gBrowser.contentDocument.activeElement.closest("#searchInput"),
     "Search input should be focused when visiting preferences");
 
   let query = "coach";
   let searchCompletedPromise = BrowserTestUtils.waitForEvent(
     gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == query);
   EventUtils.sendString(query);
   await searchCompletedPromise;
 
   is_element_visible(noResultsEl, "Should be in search results");
+  is(sorryMsgQueryEl.textContent, query, "sorry-message-query should contain the query");
 
   // Takes search off
   searchCompletedPromise = BrowserTestUtils.waitForEvent(
     gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == "");
   let count = query.length;
   while (count--) {
     EventUtils.sendKey("BACK_SPACE");
   }
   await searchCompletedPromise;
 
   is_element_hidden(noResultsEl, "Should not be in search results");
+  is(sorryMsgQueryEl.textContent.length, 0, "sorry-message-query should be empty");
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 /**
  * Test for if we go back to general tab after search case
  */
-add_task(async function() {
+add_task(async function exiting_search_reverts_to_general_pane() {
   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
   let generalPane = gBrowser.contentDocument.getElementById("generalCategory");
 
   is_element_hidden(generalPane, "Should not be in general");
 
   // Performs search
   let searchInput = gBrowser.contentDocument.getElementById("searchInput");
 
@@ -208,17 +211,17 @@ add_task(async function() {
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 /**
  * Test for "Site Data" case, verifying elements with data-hidden-from-search = true
  * are hidden in search result.
  */
-add_task(async function() {
+add_task(async function verify_hidden_from_search_elements_dont_show_up() {
   await SpecialPowers.pushPrefEnv({ "set": [["browser.storageManager.enabled", false]] });
   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
   let generalPane = gBrowser.contentDocument.getElementById("generalCategory");
 
   is_element_hidden(generalPane, "Should not be in general");
 
   // Performs search
   let searchInput = gBrowser.contentDocument.getElementById("searchInput");
@@ -250,17 +253,17 @@ add_task(async function() {
   is_element_visible(generalPane, "Should be in generalPane");
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 /**
  * Test for if we go to another tab after searching
  */
-add_task(async function() {
+add_task(async function changing_tabs_after_searching() {
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", { leaveOpen: true });
   let searchInput = gBrowser.contentDocument.getElementById("searchInput");
 
   is(searchInput, gBrowser.contentDocument.activeElement.closest("#searchInput"),
     "Search input should be focused when visiting preferences");
 
   let query = "password";
   let searchCompletedPromise = BrowserTestUtils.waitForEvent(
--- a/browser/components/preferences/in-content/tests/browser_search_within_preferences_2.js
+++ b/browser/components/preferences/in-content/tests/browser_search_within_preferences_2.js
@@ -55,13 +55,74 @@ add_task(async function() {
   // Performs search.
   searchInput.focus();
   query = "Cancel Setup";
   searchCompletedPromise = BrowserTestUtils.waitForEvent(
       gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == query);
   EventUtils.sendString(query);
   await searchCompletedPromise;
 
-  let noResultsEl = gBrowser.contentDocument.querySelector(".no-results-message");
+  let noResultsEl = gBrowser.contentDocument.querySelector("#no-results-message");
   is_element_visible(noResultsEl, "Should be reporting no results");
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
+
+
+/**
+ * Test that we search using `search-l10n-ids`.
+ *
+ * The test uses element `browserContainersSettings` and
+ * l10n id `language-and-appearance-header` and expects the element
+ * to be matched on the first word from the l10n id value ("Language" in en-US).
+ */
+add_task(async function() {
+  let l10nId = "language-and-appearance-header";
+
+  await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+
+  // First, lets make sure that the element is not matched without
+  // `search-l10n-ids`.
+  {
+    let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+    let bcsElem = gBrowser.contentDocument.getElementById("browserContainersSettings");
+
+    is(searchInput, gBrowser.contentDocument.activeElement.closest("#searchInput"),
+      "Search input should be focused when visiting preferences");
+
+    ok(!bcsElem.getAttribute("search-l10n-ids").includes(l10nId),
+      "browserContainersSettings element should not contain the l10n id here.");
+
+    let query = "Language";
+    let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+        gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == query);
+    EventUtils.sendString(query);
+    await searchCompletedPromise;
+
+    is_element_hidden(bcsElem, "browserContainersSettings should not be in search results");
+  }
+
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+  // Now, let's add the l10n id to the element and perform the same search again.
+
+  await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+
+  {
+    let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+
+    is(searchInput, gBrowser.contentDocument.activeElement.closest("#searchInput"),
+      "Search input should be focused when visiting preferences");
+
+    let bcsElem = gBrowser.contentDocument.getElementById("browserContainersSettings");
+    bcsElem.setAttribute("search-l10n-ids", l10nId);
+
+    let query = "Language";
+    let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+        gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == query);
+    EventUtils.sendString(query);
+    await searchCompletedPromise;
+
+    is_element_visible(bcsElem, "browserContainersSettings should be in search results");
+  }
+
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -160,17 +160,17 @@ XPCOMUtils.defineLazyServiceGetters(this
   gSessionStartup: ["@mozilla.org/browser/sessionstartup;1", "nsISessionStartup"],
   gScreenManager: ["@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"],
   Telemetry: ["@mozilla.org/base/telemetry;1", "nsITelemetry"],
 });
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
-  DevToolsShim: "chrome://devtools-shim/content/DevToolsShim.jsm",
+  DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.jsm",
   GlobalState: "resource:///modules/sessionstore/GlobalState.jsm",
   PrivacyFilter: "resource:///modules/sessionstore/PrivacyFilter.jsm",
   PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
   RecentWindow: "resource:///modules/RecentWindow.jsm",
   RunState: "resource:///modules/sessionstore/RunState.jsm",
   SessionCookies: "resource:///modules/sessionstore/SessionCookies.jsm",
   SessionFile: "resource:///modules/sessionstore/SessionFile.jsm",
   SessionSaver: "resource:///modules/sessionstore/SessionSaver.jsm",
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -608,19 +608,19 @@
 #endif
 @RESPATH@/browser/features/*
 
 ; [Webide Files]
 @RESPATH@/browser/chrome/webide@JAREXT@
 @RESPATH@/browser/chrome/webide.manifest
 @RESPATH@/browser/@PREF_DIR@/webide-prefs.js
 
-; [DevTools Shim Files]
-@RESPATH@/browser/chrome/devtools-shim@JAREXT@
-@RESPATH@/browser/chrome/devtools-shim.manifest
+; [DevTools Startup Files]
+@RESPATH@/browser/chrome/devtools-startup@JAREXT@
+@RESPATH@/browser/chrome/devtools-startup.manifest
 @RESPATH@/browser/@PREF_DIR@/devtools-startup-prefs.js
 
 ; DevTools
 @RESPATH@/browser/chrome/devtools@JAREXT@
 @RESPATH@/browser/chrome/devtools.manifest
 @RESPATH@/browser/@PREF_DIR@/devtools.js
 @RESPATH@/browser/@PREF_DIR@/debugger.js
 
--- a/browser/locales/Makefile.in
+++ b/browser/locales/Makefile.in
@@ -78,17 +78,17 @@ endif
 	@$(MAKE) -C ../extensions/pocket/locale AB_CD=$* XPI_NAME=locale-$*
 ifndef RELEASE_OR_BETA
 	@$(MAKE) -C ../extensions/presentation/locale AB_CD=$* XPI_NAME=locale-$*
 endif
 ifneq '$(or $(MOZ_DEV_EDITION),$(NIGHTLY_BUILD))' ''
 	@$(MAKE) -C ../extensions/webcompat-reporter/locales AB_CD=$* XPI_NAME=locale-$*
 endif
 	@$(MAKE) -C ../../devtools/client/locales AB_CD=$* XPI_NAME=locale-$* XPI_ROOT_APPID='$(XPI_ROOT_APPID)'
-	@$(MAKE) -C ../../devtools/shim/locales AB_CD=$* XPI_NAME=locale-$* XPI_ROOT_APPID='$(XPI_ROOT_APPID)'
+	@$(MAKE) -C ../../devtools/startup/locales AB_CD=$* XPI_NAME=locale-$* XPI_ROOT_APPID='$(XPI_ROOT_APPID)'
 	@$(MAKE) -B searchplugins AB_CD=$* XPI_NAME=locale-$*
 	@$(MAKE) libs AB_CD=$* XPI_NAME=locale-$* PREF_DIR=$(PREF_DIR)
 	@$(MAKE) multilocale.txt-$* AB_CD=$* XPI_NAME=locale-$*
 	@$(MAKE) -C $(DEPTH)/$(MOZ_BRANDING_DIRECTORY)/locales AB_CD=$* XPI_NAME=locale-$*
 
 chrome-%: AB_CD=$*
 chrome-%: IS_LANGUAGE_REPACK=1
 chrome-%:
@@ -99,17 +99,17 @@ chrome-%:
 ifneq (,$(wildcard ../extensions/formautofill/locales))
 	@$(MAKE) -C ../extensions/formautofill/locales chrome AB_CD=$*
 endif
 	@$(MAKE) -C ../extensions/pocket/locale chrome AB_CD=$*
 ifndef RELEASE_OR_BETA
 	@$(MAKE) -C ../extensions/presentation/locale chrome AB_CD=$*
 endif
 	@$(MAKE) -C ../../devtools/client/locales chrome AB_CD=$*
-	@$(MAKE) -C ../../devtools/shim/locales chrome AB_CD=$*
+	@$(MAKE) -C ../../devtools/startup/locales chrome AB_CD=$*
 	@$(MAKE) chrome AB_CD=$*
 	@$(MAKE) -C $(DEPTH)/$(MOZ_BRANDING_DIRECTORY)/locales chrome AB_CD=$*
 ifdef NIGHTLY_BUILD
 	@$(MAKE) -C ../extensions/webcompat-reporter/locales chrome AB_CD=$*
 endif
 
 package-win32-installer: WIN32_INSTALLER_OUT=$(ABS_DIST)/$(PKG_INST_PATH)$(PKG_INST_BASENAME).exe
 package-win32-installer: $(SUBMAKEFILES)
--- a/browser/locales/filter.py
+++ b/browser/locales/filter.py
@@ -1,17 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 def test(mod, path, entity = None):
   import re
   # ignore anything but Firefox
   if mod not in ("netwerk", "dom", "toolkit", "security/manager",
-                 "devtools/client", "devtools/shared", "devtools/shim",
+                 "devtools/client", "devtools/shared", "devtools/startup",
                  "browser",
                  "browser/extensions/formautofill",
                  "browser/extensions/onboarding",
                  "browser/extensions/webcompat-reporter",
                  "extensions/spellcheck",
                  "other-licenses/branding/firefox",
                  "browser/branding/official",
                  "services/sync"):
--- a/browser/locales/l10n.ini
+++ b/browser/locales/l10n.ini
@@ -6,17 +6,17 @@
 depth = ../..
 all = browser/locales/all-locales
 
 [compare]
 dirs = browser
      other-licenses/branding/firefox
      browser/branding/official
      devtools/client
-     devtools/shim
+     devtools/startup
      browser/extensions/formautofill
      browser/extensions/onboarding
      browser/extensions/webcompat-reporter
 
 [includes]
 # non-central apps might want to use %(topsrcdir)s here, or other vars
 # RFE: that needs to be supported by compare-locales, too, though
 toolkit = toolkit/locales/l10n.ini
--- a/browser/locales/l10n.toml
+++ b/browser/locales/l10n.toml
@@ -140,18 +140,18 @@ locales = [
 
 [[includes]]
     path = "toolkit/locales/l10n.toml"
 
 [[includes]]
     path = "devtools/client/locales/l10n.toml"
 
 [[paths]]
-    reference = "devtools/shim/locales/en-US/**"
-    l10n = "{l}devtools/shim/**"
+    reference = "devtools/startup/locales/en-US/**"
+    l10n = "{l}devtools/startup/**"
 
 # Filters
 # The filters below are evaluated one after the other, in the given order.
 # Enter a combination of path as in the localization, key, and an action,
 # to change the default behavior of compare-locales and l10n merge.
 #
 # For browser/locales/en-US/chrome/browser/foo.properties,
 # path would be {l}browser/chrome/browser/foo.properties
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -834,17 +834,17 @@ button > hbox > label {
 .search-tooltip > span {
   -moz-user-select: none;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
 }
 
 .visually-hidden {
-  visibility: hidden;
+  visibility: collapse;
 }
 
 menulist {
   height: 30px;
   margin-top: 4px;
   margin-bottom: 4px;
 }
 
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -29,17 +29,17 @@ loader.lazyGetter(this, "DomPanel", () =
 loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
 loader.lazyRequireGetter(this, "CommandState", "devtools/shared/gcli/command-state", true);
 loader.lazyRequireGetter(this, "ResponsiveUIManager", "devtools/client/responsive.html/manager", true);
 loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
 
 const {MultiLocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new MultiLocalizationHelper(
   "devtools/client/locales/startup.properties",
-  "devtools/shim/locales/key-shortcuts.properties"
+  "devtools/startup/locales/key-shortcuts.properties"
 );
 
 var Tools = {};
 exports.Tools = Tools;
 
 // Definitions
 Tools.options = {
   id: "options",
--- a/devtools/client/framework/devtools.js
+++ b/devtools/client/framework/devtools.js
@@ -2,17 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {Cu} = require("chrome");
 const Services = require("Services");
 
-const {DevToolsShim} = require("chrome://devtools-shim/content/DevToolsShim.jsm");
+const {DevToolsShim} = require("chrome://devtools-startup/content/DevToolsShim.jsm");
 
 // Load gDevToolsBrowser toolbox lazily as they need gDevTools to be fully initialized
 loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
 loader.lazyRequireGetter(this, "TabTarget", "devtools/client/framework/target", true);
 loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
 loader.lazyRequireGetter(this, "ToolboxHostManager", "devtools/client/framework/toolbox-host-manager", true);
 loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
 loader.lazyRequireGetter(this, "HUDService", "devtools/client/webconsole/hudservice", true);
@@ -48,19 +48,18 @@ function DevTools() {
   this._onThemeChanged = this._onThemeChanged.bind(this);
   addThemeObserver(this._onThemeChanged);
 
   // This is important step in initialization codepath where we are going to
   // start registering all default tools and themes: create menuitems, keys, emit
   // related events.
   this.registerDefaults();
 
-  // Register this new DevTools instance to Firefox. DevToolsShim is part of Firefox,
-  // integrating with all Firefox codebase and making the glue between code from
-  // mozilla-central and DevTools add-on on github
+  // Register this DevTools instance on the DevToolsShim, which is used by non-devtools
+  // code to interact with DevTools.
   DevToolsShim.register(this);
 }
 
 DevTools.prototype = {
   // The windowtype of the main window, used in various tools. This may be set
   // to something different by other gecko apps.
   chromeWindowType: "navigator:browser",
 
@@ -650,27 +649,25 @@ DevTools.prototype = {
 
     await onNewNode;
     // Now that the node has been selected, wait until the inspector is
     // fully updated.
     await inspector.once("inspector-updated");
   },
 
   /**
-   * Either the DevTools Loader has been destroyed by the add-on contribution
-   * workflow, or firefox is shutting down.
+   * Either the DevTools Loader has been destroyed or firefox is shutting down.
 
    * @param {boolean} shuttingDown
    *        True if firefox is currently shutting down. We may prevent doing
    *        some cleanups to speed it up. Otherwise everything need to be
    *        cleaned up in order to be able to load devtools again.
    */
   destroy({ shuttingDown }) {
-    // Do not cleanup everything during firefox shutdown, but only when
-    // devtools are reloaded via the add-on contribution workflow.
+    // Do not cleanup everything during firefox shutdown.
     if (!shuttingDown) {
       for (let [, toolbox] of this._toolboxes) {
         toolbox.destroy();
       }
     }
 
     for (let [key, ] of this.getToolDefinitionMap()) {
       this.unregisterTool(key, true);
@@ -680,17 +677,17 @@ DevTools.prototype = {
 
     removeThemeObserver(this._onThemeChanged);
 
     // Do not unregister devtools from the DevToolsShim if the destroy is caused by an
     // application shutdown. For instance SessionStore needs to save the Scratchpad
     // manager state on shutdown.
     if (!shuttingDown) {
       // Notify the DevToolsShim that DevTools are no longer available, particularly if
-      // the destroy was caused by disabling/removing the DevTools add-on.
+      // the destroy was caused by disabling/removing DevTools.
       DevToolsShim.unregister();
     }
 
     // Cleaning down the toolboxes: i.e.
     //   for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
     // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
   },
 
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -402,17 +402,17 @@ TabTarget.prototype = {
     this._remote = defer();
 
     if (this.isLocalTab) {
       // Since a remote protocol connection will be made, let's start the
       // DebuggerServer here, once and for all tools.
       DebuggerServer.init();
 
       // When connecting to a local tab, we only need the root actor.
-      // Then we are going to call DebuggerServer.connectToChild and talk
+      // Then we are going to call DebuggerServer.connectToFrame and talk
       // directly with actors living in the child process.
       // We also need browser actors for actor registry which enabled addons
       // to register custom actors.
       // TODO: the comment and implementation are out of sync here. See Bug 1420134.
       DebuggerServer.registerAllActors();
 
       this._client = new DebuggerClient(DebuggerServer.connectPipe());
       // A local TabTarget will never perform chrome debugging.
--- a/devtools/client/netmonitor/src/components/RequestListContent.js
+++ b/devtools/client/netmonitor/src/components/RequestListContent.js
@@ -45,18 +45,18 @@ const MAX_SCROLL_HEIGHT = 2147483647;
  * Renders the actual contents of the request list.
  */
 class RequestListContent extends Component {
   static get propTypes() {
     return {
       connector: PropTypes.object.isRequired,
       columns: PropTypes.object.isRequired,
       networkDetailsOpen: PropTypes.bool.isRequired,
-      networkDetailsWidth: PropTypes.number.isRequired,
-      networkDetailsHeight: PropTypes.number.isRequired,
+      networkDetailsWidth: PropTypes.number,
+      networkDetailsHeight: PropTypes.number,
       cloneSelectedRequest: PropTypes.func.isRequired,
       displayedRequests: PropTypes.array.isRequired,
       firstRequestStartedMillis: PropTypes.number.isRequired,
       fromCache: PropTypes.bool,
       onCauseBadgeMouseDown: PropTypes.func.isRequired,
       onItemMouseDown: PropTypes.func.isRequired,
       onSecurityIconMouseDown: PropTypes.func.isRequired,
       onSelectDelta: PropTypes.func.isRequired,
--- a/devtools/client/netmonitor/webpack.config.js
+++ b/devtools/client/netmonitor/webpack.config.js
@@ -87,17 +87,17 @@ let webpackConfig = {
       "devtools/shared/event-emitter": "devtools-modules/src/utils/event-emitter",
       "devtools/shared/fronts/timeline": path.join(__dirname, "../../client/shared/webpack/shims/fronts-timeline-shim"),
       "devtools/shared/platform/clipboard": path.join(__dirname, "../../client/shared/webpack/shims/platform-clipboard-stub"),
       "devtools/client/netmonitor/src/utils/firefox/open-request-in-tab": path.join(__dirname, "src/utils/open-request-in-tab"),
 
       // Locales need to be explicitly mapped to the en-US subfolder
       "devtools/client/locales": path.join(__dirname, "../../client/locales/en-US"),
       "devtools/shared/locales": path.join(__dirname, "../../shared/locales/en-US"),
-      "devtools/shim/locales": path.join(__dirname, "../../shared/locales/en-US"),
+      "devtools/startup/locales": path.join(__dirname, "../../shared/locales/en-US"),
       "toolkit/locales": path.join(__dirname, "../../../toolkit/locales/en-US"),
 
       // Unless a path explicitly needs to be rewritten or shimmed, all devtools paths can
       // be mapped to ../../
       "devtools": path.join(__dirname, "../../"),
     },
   },
 };
--- a/devtools/client/responsive.html/browser/tunnel.js
+++ b/devtools/client/responsive.html/browser/tunnel.js
@@ -443,17 +443,17 @@ MessageManagerTunnel.prototype = {
     "Printing:",
     "PageMetadata:",
     // Messages sent to viewSourceUtils.js
     "ViewSource:",
   ],
 
   OUTER_TO_INNER_FRAME_SCRIPTS: [
     // DevTools server for OOP frames
-    "resource://devtools/server/child.js"
+    "resource://devtools/server/startup/frame.js"
   ],
 
   get outer() {
     return this.outerRef.get();
   },
 
   get outerParentMM() {
     if (!this.outer[FRAME_LOADER]) {
--- a/devtools/client/scratchpad/test/browser.ini
+++ b/devtools/client/scratchpad/test/browser.ini
@@ -5,46 +5,43 @@ support-files = head.js
 
 [browser_scratchpad_autocomplete.js]
 [browser_scratchpad_browser_last_window_closing.js]
 [browser_scratchpad_reset_undo.js]
 [browser_scratchpad_display_outputs_errors.js]
 [browser_scratchpad_eval_func.js]
 [browser_scratchpad_goto_line_ui.js]
 [browser_scratchpad_reload_and_run.js]
-uses-unsafe-cpows = true
 [browser_scratchpad_display_non_error_exceptions.js]
 [browser_scratchpad_modeline.js]
 [browser_scratchpad_chrome_context_pref.js]
 [browser_scratchpad_help_key.js]
 [browser_scratchpad_recent_files.js]
 [browser_scratchpad_confirm_close.js]
 disabled=bug 807234 becoming basically permanent
 [browser_scratchpad_sessions.js]
 [browser_scratchpad_tab.js]
 [browser_scratchpad_wrong_window_focus.js]
 [browser_scratchpad_unsaved.js]
 [browser_scratchpad_falsy.js]
 [browser_scratchpad_edit_ui_updates.js]
 [browser_scratchpad_revert_to_saved.js]
 [browser_scratchpad_run_error_goto_line.js]
 [browser_scratchpad_contexts.js]
-uses-unsafe-cpows = true
 [browser_scratchpad_execute_print.js]
 [browser_scratchpad_files.js]
 [browser_scratchpad_initialization.js]
 [browser_scratchpad_inspect.js]
 [browser_scratchpad_inspect_primitives.js]
 [browser_scratchpad_long_string.js]
 [browser_scratchpad_open.js]
 support-files = NS_ERROR_ILLEGAL_INPUT.txt
 [browser_scratchpad_open_error_console.js]
 [browser_scratchpad_throw_output.js]
 [browser_scratchpad_pprint-02.js]
 [browser_scratchpad_pprint.js]
 [browser_scratchpad_pprint_error_goto_line.js]
 [browser_scratchpad_restore.js]
 [browser_scratchpad_tab_switch.js]
-uses-unsafe-cpows = true
 [browser_scratchpad_ui.js]
 [browser_scratchpad_close_toolbox.js]
 [browser_scratchpad_remember_view_options.js]
 [browser_scratchpad_disable_view_menu_items.js]
--- a/devtools/client/scratchpad/test/browser_scratchpad_contexts.js
+++ b/devtools/client/scratchpad/test/browser_scratchpad_contexts.js
@@ -43,20 +43,20 @@ function runTests() {
 
       sp.editor.setText("window.foobarBug636725 = 'aloha';");
 
       let pageResult = await inContent(function() {
         return content.wrappedJSObject.foobarBug636725;
       });
       ok(!pageResult, "no content.foobarBug636725");
     },
-    then: function() {
-      is(gBrowser.contentWindowAsCPOW.wrappedJSObject.foobarBug636725, "aloha",
+    then: () => ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+      is(content.wrappedJSObject.foobarBug636725, "aloha",
          "content.foobarBug636725 has been set");
-    }
+    }),
   }, {
     method: "run",
     prepare: function() {
       sp.setBrowserContext();
 
       is(sp.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_BROWSER,
          "executionContext is chrome");
 
--- a/devtools/client/scratchpad/test/browser_scratchpad_reload_and_run.js
+++ b/devtools/client/scratchpad/test/browser_scratchpad_reload_and_run.js
@@ -5,32 +5,30 @@
 
 var DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
 var EDITOR_TEXT = [
   "var evt = new CustomEvent('foo', { bubbles: true });",
   "document.body.innerHTML = 'Modified text';",
   "window.dispatchEvent(evt);"
 ].join("\n");
 
-function test()
-{
-  requestLongerTimeout(2);
-  waitForExplicitFinish();
+add_task(async function test() {
   Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, true);
 
-  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
-  BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(function () {
-    openScratchpad(runTests);
-  });
+  let url = "data:text/html,Scratchpad test for bug 740948";
+  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url);
+  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
 
-  gBrowser.loadURI("data:text/html,Scratchpad test for bug 740948");
-}
+  await new Promise((resolve) => openScratchpad(resolve));
+  await runTests();
 
-function runTests()
-{
+  Services.prefs.clearUserPref(DEVTOOLS_CHROME_ENABLED);
+});
+
+async function runTests() {
   let sp = gScratchpadWindow.Scratchpad;
   ok(sp, "Scratchpad object exists in new window");
 
   // Test that Reload And Run command is enabled in the content
   // context and disabled in the browser context.
 
   let reloadAndRun = gScratchpadWindow.document.
     getElementById("sp-cmd-reloadAndRun");
@@ -47,25 +45,30 @@ function runTests()
   // a custom event 'foo'. We listen to that event and check the
   // body to make sure that the page has been reloaded and Scratchpad
   // code has been executed.
 
   sp.setContentContext();
   sp.setText(EDITOR_TEXT);
 
   let browser = gBrowser.selectedBrowser;
+  await ContentTask.spawn(browser, null, function() {
+    ok(content.document.body.innerHTML !== "Modified text",
+      "Before reloading, HTML is intact.");
+  });
 
-  let deferred = defer();
-  browser.contentWindowAsCPOW.addEventListener("DOMWindowCreated", function () {
-    browser.contentWindowAsCPOW.addEventListener("foo", function () {
-      is(browser.contentWindow.document.body.innerHTML, "Modified text",
-        "After reloading, HTML is different.");
+  let reloaded = BrowserTestUtils.browserLoaded(browser);
+  sp.reloadAndRun();
+  await reloaded;
 
-      Services.prefs.clearUserPref(DEVTOOLS_CHROME_ENABLED);
-      deferred.resolve();
-    }, {capture: true, once: true});
-  }, {capture: true, once: true});
-
-  ok(browser.contentWindowAsCPOW.document.body.innerHTML !== "Modified text",
-      "Before reloading, HTML is intact.");
-  sp.reloadAndRun().then(deferred.promise).then(finish);
+  await ContentTask.spawn(browser, null, async function() {
+    // If `evt` is not defined, the scratchpad code has not run yet,
+    // so we need to await the "foo" event.
+    if (!content.wrappedJSObject.evt) {
+      await new Promise((resolve) => {
+        content.addEventListener("foo", resolve, {once: true});
+      });
+    }
+    is(content.document.body.innerHTML, "Modified text",
+      "After reloading, HTML is different.");
+  });
 }
 
--- a/devtools/client/scratchpad/test/browser_scratchpad_tab_switch.js
+++ b/devtools/client/scratchpad/test/browser_scratchpad_tab_switch.js
@@ -19,18 +19,17 @@ function test()
       openScratchpad(runTests);
     });
     gBrowser.loadURI("data:text/html,test context switch in Scratchpad tab 2");
   });
 
   gBrowser.loadURI("data:text/html,test context switch in Scratchpad tab 1");
 }
 
-function runTests()
-{
+async function runTests() {
   sp = gScratchpadWindow.Scratchpad;
 
   let contentMenu = gScratchpadWindow.document.getElementById("sp-menu-content");
   let browserMenu = gScratchpadWindow.document.getElementById("sp-menu-browser");
   let notificationBox = sp.notificationBox;
 
   ok(contentMenu, "found #sp-menu-content");
   ok(browserMenu, "found #sp-menu-browser");
@@ -47,46 +46,49 @@ function runTests()
   isnot(browserMenu.getAttribute("checked"), "true",
      "chrome menuitem is not checked");
 
   is(notificationBox.currentNotification, null,
      "there is no notification currently shown for content context");
 
   sp.setText("window.foosbug653108 = 'aloha';");
 
-  ok(!gBrowser.contentWindowAsCPOW.wrappedJSObject.foosbug653108,
-     "no content.foosbug653108");
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    ok(!content.wrappedJSObject.foosbug653108, "no content.foosbug653108");
+  });
+
+  await sp.run();
 
-  sp.run().then(function () {
-    is(gBrowser.contentWindowAsCPOW.wrappedJSObject.foosbug653108, "aloha",
-       "content.foosbug653108 has been set");
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    is(content.wrappedJSObject.foosbug653108, "aloha",
+      "content.foosbug653108 has been set");
+  });
 
-    gBrowser.tabContainer.addEventListener("TabSelect", runTests2, true);
-    gBrowser.selectedTab = tab1;
-  });
+  gBrowser.tabContainer.addEventListener("TabSelect", runTests2, true);
+  gBrowser.selectedTab = tab1;
 }
 
-function runTests2() {
+async function runTests2() {
   gBrowser.tabContainer.removeEventListener("TabSelect", runTests2, true);
 
   ok(!window.foosbug653108, "no window.foosbug653108");
 
   sp.setText("window.foosbug653108");
-  sp.run().then(function ([, , result]) {
-    isnot(result, "aloha", "window.foosbug653108 is not aloha");
+  let [, , result] = await sp.run();
+  isnot(result, "aloha", "window.foosbug653108 is not aloha");
 
-    sp.setText("window.foosbug653108 = 'ahoyhoy';");
-    sp.run().then(function () {
-      is(gBrowser.contentWindowAsCPOW.wrappedJSObject.foosbug653108, "ahoyhoy",
-         "content.foosbug653108 has been set 2");
+  sp.setText("window.foosbug653108 = 'ahoyhoy';");
+  await sp.run();
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    is(content.wrappedJSObject.foosbug653108, "ahoyhoy",
+      "content.foosbug653108 has been set 2");
+  });
 
-      BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(runTests3);
-      gBrowser.loadURI("data:text/html,test context switch in Scratchpad location 2");
-    });
-  });
+  BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(runTests3);
+  gBrowser.loadURI("data:text/html,test context switch in Scratchpad location 2");
 }
 
 function runTests3() {
   // Check that the sandbox is not cached.
 
   sp.setText("typeof foosbug653108;");
   sp.run().then(function ([, , result]) {
     is(result, "undefined", "global variable does not exist");
--- a/devtools/client/sourceeditor/test/browser.ini
+++ b/devtools/client/sourceeditor/test/browser.ini
@@ -36,17 +36,14 @@ support-files =
 [browser_editor_history.js]
 [browser_editor_markers.js]
 [browser_editor_movelines.js]
 [browser_editor_prefs.js]
 [browser_editor_script_injection.js]
 [browser_editor_addons.js]
 [browser_codemirror.js]
 [browser_css_autocompletion.js]
-uses-unsafe-cpows = true
 [browser_css_getInfo.js]
-uses-unsafe-cpows = true
 [browser_css_statemachine.js]
-uses-unsafe-cpows = true
 [browser_detectindent.js]
 [browser_vimemacs.js]
 skip-if = os == 'linux'&&debug # bug 981707
 
--- a/devtools/client/sourceeditor/test/browser_css_autocompletion.js
+++ b/devtools/client/sourceeditor/test/browser_css_autocompletion.js
@@ -65,83 +65,76 @@ const TEST_URI = "data:text/html;charset
    /* eslint-enable max-len */
    "  <button class='category-name visible'></button>",
    "  <div class='devtools-toolbarbutton' label='true'>",
    "   <hbox class='toolbarbutton-menubutton-button'></hbox></div>",
    " </body>",
    " </html>"
   ].join("\n"));
 
-let doc = null;
+let browser;
 let index = 0;
 let completer = null;
-let progress;
-let progressDiv;
 let inspector;
 
-function test() {
-  waitForExplicitFinish();
-  addTab(TEST_URI).then(function() {
-    doc = gBrowser.contentDocumentAsCPOW;
-    runTests();
-  });
+add_task(async function test() {
+  let tab = await addTab(TEST_URI);
+  browser = tab.linkedBrowser;
+  await runTests();
+  browser = null;
+  gBrowser.removeCurrentTab();
+});
+
+async function runTests() {
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  await target.makeRemote();
+  inspector = InspectorFront(target.client, target.form);
+  let walker = await inspector.getWalker();
+  completer = new CSSCompleter({walker: walker,
+                                cssProperties: getClientCssProperties()});
+  await checkStateAndMoveOn();
+  await completer.walker.release();
+  inspector.destroy();
+  inspector = null;
+  completer = null;
 }
 
-function runTests() {
-  progress = doc.getElementById("progress");
-  progressDiv = doc.querySelector("#progress > div");
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-  target.makeRemote().then(() => {
-    inspector = InspectorFront(target.client, target.form);
-    inspector.getWalker().then(walker => {
-      completer = new CSSCompleter({walker: walker,
-                                    cssProperties: getClientCssProperties()});
-      checkStateAndMoveOn();
-    });
-  });
-}
-
-function checkStateAndMoveOn() {
+async function checkStateAndMoveOn() {
   if (index == tests.length) {
-    finishUp();
     return;
   }
 
   let [lineCh, expectedSuggestions] = tests[index];
   let [line, ch] = lineCh;
 
-  progress.dataset.progress = ++index;
-  progressDiv.style.width = 100 * index / tests.length + "%";
+  ++index;
+  await ContentTask.spawn(browser, [index, tests.length], function([idx, len]) {
+    let progress = content.document.getElementById("progress");
+    let progressDiv = content.document.querySelector("#progress > div");
+    progress.dataset.progress = idx;
+    progressDiv.style.width = 100 * idx / len + "%";
+  });
 
-  completer.complete(limit(source, lineCh), {line, ch})
-           .then(actualSuggestions => checkState(expectedSuggestions, actualSuggestions))
-           .then(checkStateAndMoveOn);
+  let actualSuggestions = await completer.complete(limit(source, lineCh), {line, ch});
+  await checkState(expectedSuggestions, actualSuggestions);
+  await checkStateAndMoveOn();
 }
 
-function checkState(expected, actual) {
+async function checkState(expected, actual) {
   if (expected.length != actual.length) {
     ok(false, "Number of suggestions did not match up for state " + index +
               ". Expected: " + expected.length + ", Actual: " + actual.length);
-    progress.classList.add("failed");
+    await ContentTask.spawn(browser, null, function() {
+      let progress = content.document.getElementById("progress");
+      progress.classList.add("failed");
+    });
     return;
   }
 
   for (let i = 0; i < actual.length; i++) {
     if (expected[i] != actual[i].label) {
       ok(false, "Suggestion " + i + " of state " + index + " did not match up" +
                  ". Expected: " + expected[i] + ". Actual: " + actual[i].label);
       return;
     }
   }
   ok(true, "Test " + index + " passed. ");
 }
-
-function finishUp() {
-  completer.walker.release().then(() => {
-    inspector.destroy();
-    inspector = null;
-    completer = null;
-    gBrowser.removeCurrentTab();
-    finish();
-  });
-  progress = null;
-  progressDiv = null;
-}
--- a/devtools/client/sourceeditor/test/browser_css_getInfo.js
+++ b/devtools/client/sourceeditor/test/browser_css_getInfo.js
@@ -118,28 +118,20 @@ const TEST_URI = "data:text/html;charset
    "  <h2>State machine tests for CSS autocompleter.</h2><br>",
    "  <div id='progress' data-progress='0'>",
    "   <div></div>",
    "  </div>",
    " </body>",
    " </html>"
   ].join("\n"));
 
-let doc = null;
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
-  BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
-    doc = gBrowser.contentDocumentAsCPOW;
-    runTests();
-  });
-  gBrowser.loadURI(TEST_URI);
-}
+add_task(async function test() {
+  let tab = await addTab(TEST_URI);
+  let browser = tab.linkedBrowser;
 
-function runTests() {
   let completer = new CSSCompleter({
     cssProperties: getClientCssProperties()
   });
   let matches = (arr, toCheck) => !arr.some((x, i) => x != toCheck[i]);
   let checkState = (expected, actual) => {
     if (expected[0] == "null" && actual == null) {
       return true;
     } else if (expected[0] == actual.state && expected[0] == "selector" &&
@@ -153,29 +145,34 @@ function runTests() {
                matches(expected[1], actual.selectors) &&
                expected[2] == actual.propertyName &&
                expected[3] == actual.value) {
       return true;
     }
     return false;
   };
 
-  let progress = doc.getElementById("progress");
-  let progressDiv = doc.querySelector("#progress > div");
   let i = 0;
   for (let expected of tests) {
+    ++i;
     let caret = expected.splice(0, 1)[0];
-    progress.dataset.progress = ++i;
-    progressDiv.style.width = 100 * i / tests.length + "%";
+    await ContentTask.spawn(browser, [i, tests.length], function([idx, len]) {
+      let progress = content.document.getElementById("progress");
+      let progressDiv = content.document.querySelector("#progress > div");
+      progress.dataset.progress = idx;
+      progressDiv.style.width = 100 * idx / len + "%";
+    });
     let actual = completer.getInfoAt(source, caret);
     if (checkState(expected, actual)) {
       ok(true, "Test " + i + " passed. ");
     } else {
       ok(false, "Test " + i + " failed. Expected state : [" + expected + "] " +
          "but found [" + actual.state + ", " +
          (actual.selector || actual.selectors) + ", " +
          actual.propertyName + ", " + actual.value + "].");
-      progress.classList.add("failed");
+      await ContentTask.spawn(browser, null, function() {
+        let progress = content.document.getElementById("progress");
+        progress.classList.add("failed");
+      });
     }
   }
   gBrowser.removeCurrentTab();
-  finish();
-}
+});
--- a/devtools/client/sourceeditor/test/browser_css_statemachine.js
+++ b/devtools/client/sourceeditor/test/browser_css_statemachine.js
@@ -49,27 +49,20 @@ const TEST_URI = "data:text/html;charset
    "  <h2>State machine tests for CSS autocompleter.</h2><br>",
    "  <div id='progress' data-progress='0'>",
    "   <div></div>",
    "  </div>",
    " </body>",
    " </html>"
   ].join("\n"));
 
-var doc = null;
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, TEST_URI);
-  BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
-    doc = gBrowser.contentDocumentAsCPOW;
-    runTests();
-  });
-}
+add_task(async function test() {
+  let tab = await addTab(TEST_URI);
+  let browser = tab.linkedBrowser;
 
-function runTests() {
   let completer = new CSSCompleter({
     cssProperties: getClientCssProperties()
   });
   let checkState = state => {
     if (state[0] == "null" && (!completer.state || completer.state == "null")) {
       return true;
     } else if (state[0] == completer.state && state[0] == "selector" &&
                state[1] == completer.selectorState &&
@@ -83,29 +76,34 @@ function runTests() {
     } else if (state[0] == completer.state &&
                state[2] == completer.completing &&
                state[0] != "selector" && state[0] != "value") {
       return true;
     }
     return false;
   };
 
-  let progress = doc.getElementById("progress");
-  let progressDiv = doc.querySelector("#progress > div");
   let i = 0;
   for (let testcase of tests) {
-    progress.dataset.progress = ++i;
-    progressDiv.style.width = 100 * i / tests.length + "%";
+    ++i;
+    await ContentTask.spawn(browser, [i, tests.length], function([idx, len]) {
+      let progress = content.document.getElementById("progress");
+      let progressDiv = content.document.querySelector("#progress > div");
+      progress.dataset.progress = idx;
+      progressDiv.style.width = 100 * idx / len + "%";
+    });
     completer.resolveState(limit(source, testcase[0]),
                            {line: testcase[0][0], ch: testcase[0][1]});
     if (checkState(testcase[1])) {
       ok(true, "Test " + i + " passed. ");
     } else {
       ok(false, "Test " + i + " failed. Expected state : [" + testcase[1] + "] " +
          "but found [" + completer.state + ", " + completer.selectorState +
          ", " + completer.completing + ", " +
          (completer.propertyName || completer.selector) + "].");
-      progress.classList.add("failed");
+      await ContentTask.spawn(browser, null, function() {
+        let progress = content.document.getElementById("progress");
+        progress.classList.add("failed");
+      });
     }
   }
   gBrowser.removeCurrentTab();
-  finish();
-}
+});
--- a/devtools/client/webconsole/webpack.config.js
+++ b/devtools/client/webconsole/webpack.config.js
@@ -101,17 +101,17 @@ webpackConfig.resolve = {
     "devtools/shared/client/debugger-client": path.join(__dirname, "new-console-output/test/fixtures/DebuggerClient"),
     "devtools/shared/platform/clipboard": path.join(__dirname, "../../client/shared/webpack/shims/platform-clipboard-stub"),
     "devtools/shared/platform/stack": path.join(__dirname, "../../client/shared/webpack/shims/platform-stack-stub"),
 
     // Locales need to be explicitly mapped to the en-US subfolder
     "toolkit/locales": path.join(__dirname, "../../../toolkit/locales/en-US"),
     "devtools/client/locales": path.join(__dirname, "../../client/locales/en-US"),
     "devtools/shared/locales": path.join(__dirname, "../../shared/locales/en-US"),
-    "devtools/shim/locales": path.join(__dirname, "../../shared/locales/en-US"),
+    "devtools/startup/locales": path.join(__dirname, "../../shared/locales/en-US"),
 
     // Unless a path explicitly needs to be rewritten or shimmed, all devtools paths can
     // be mapped to ../../
     "devtools": path.join(__dirname, "../../"),
   }
 };
 
 const mappings = [
--- a/devtools/docs/preferences.md
+++ b/devtools/docs/preferences.md
@@ -67,22 +67,22 @@ value does not match the preference type
 These APIs are very similar for each preference type.
 
 ## Create a new preference
 
 To create a new preference, it should be assigned a default value. Default preferences are
 defined in preferences files such as:
 - devtools/client/preferences/devtools.js
 - devtools/client/preferences/debugger.js
-- devtools/shim/devtools-startup-prefs.js
+- devtools/startup/devtools-startup-prefs.js
 
 Most new preferences should go in devtools/client/preferences/devtools.js. Debugger
 specific preferences should go in devtools/client/preferences/debugger.js. Finally if a
 preference needs to be available very early during the Firefox startup sequence, it should
-go in devtools/shim/devtools-startup-prefs.js.
+go in devtools/startup/devtools-startup-prefs.js.
 
 ### Projects using Launchpad
 
 At the time of writing this doc, projects using Launchpad have to duplicate the default
 definition of a preference.
 * debugger.html: update [src/utils/prefs.js](https://github.com/devtools-html/debugger.html/blob/master/src/utils/prefs.js)
 * netmonitor: update [index.js](http://searchfox.org/mozilla-central/source/devtools/client/netmonitor/index.js)
 * webconsole: update [local-dev/index.js](http://searchfox.org/mozilla-central/source/devtools/client/webconsole/local-dev/index.js)
--- a/devtools/moz.build
+++ b/devtools/moz.build
@@ -1,37 +1,30 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-if CONFIG['MOZ_DEVTOOLS'] and CONFIG['MOZ_DEVTOOLS'] not in ('all', 'server', 'addon'):
+if CONFIG['MOZ_DEVTOOLS'] and CONFIG['MOZ_DEVTOOLS'] not in ('all', 'server'):
     error('Unsupported MOZ_DEVTOOLS value: %s' % (CONFIG['MOZ_DEVTOOLS']))
 
 if CONFIG['MOZ_DEVTOOLS'] == 'all':
     DIRS += [
         'client',
     ]
 
-# `addon` is a special build mode to strip everything except binary components
-# and shim modules that are going to stay in Firefox once DevTools ship as an
-# add-on.
 # `platform` contains all native components
 DIRS += [
-    'shim',
+    'server',
+    'shared',
+    'startup',
     'platform',
 ]
 
-if CONFIG['MOZ_DEVTOOLS'] != 'addon':
-    DIRS += [
-        'server',
-        'shared',
-    ]
-
 # /browser uses DIST_SUBDIR.  We opt-in to this treatment when building
 # DevTools for the browser to keep the root omni.ja slim for use by external XUL
 # apps.  Mulet also uses this since it includes /browser.
 if CONFIG['MOZ_BUILD_APP'] == 'browser':
     DIST_SUBDIR = 'browser'
     export('DIST_SUBDIR')
 
 with Files('**'):
--- a/devtools/server/actors/canvas.js
+++ b/devtools/server/actors/canvas.js
@@ -4,17 +4,17 @@
 "use strict";
 
 /* global XPCNativeWrapper */
 
 const defer = require("devtools/shared/defer");
 const protocol = require("devtools/shared/protocol");
 const {CallWatcherActor} = require("devtools/server/actors/call-watcher");
 const {CallWatcherFront} = require("devtools/shared/fronts/call-watcher");
-const {WebGLPrimitiveCounter} = require("devtools/server/primitive");
+const {WebGLPrimitiveCounter} = require("devtools/server/actors/canvas/primitive");
 const {
   frameSnapshotSpec,
   canvasSpec,
   CANVAS_CONTEXTS,
   ANIMATION_GENERATORS,
   LOOP_GENERATORS
 } = require("devtools/shared/specs/canvas");
 const {CanvasFront} = require("devtools/shared/fronts/canvas");
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/canvas/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'primitive.js',
+)
rename from devtools/server/primitive.js
rename to devtools/server/actors/canvas/primitive.js
--- a/devtools/server/actors/highlighters/utils/markup.js
+++ b/devtools/server/actors/highlighters/utils/markup.js
@@ -8,17 +8,17 @@ const { Ci, Cu, Cr } = require("chrome")
 const { getCurrentZoom, getWindowDimensions, getViewportDimensions,
   getRootBindingParent, loadSheet } = require("devtools/shared/layout/utils");
 const EventEmitter = require("devtools/shared/event-emitter");
 const InspectorUtils = require("InspectorUtils");
 
 const lazyContainer = {};
 
 loader.lazyRequireGetter(lazyContainer, "CssLogic",
-  "devtools/server/css-logic", true);
+  "devtools/server/actors/inspector/css-logic", true);
 exports.getComputedStyle = (node) =>
   lazyContainer.CssLogic.getComputedStyle(node);
 
 exports.getBindingElementAndPseudo = (node) =>
   lazyContainer.CssLogic.getBindingElementAndPseudo(node);
 
 exports.hasPseudoClassLock = (...args) =>
   InspectorUtils.hasPseudoClassLock(...args);
rename from devtools/server/css-logic.js
rename to devtools/server/actors/inspector/css-logic.js
rename from devtools/server/event-parsers.js
rename to devtools/server/actors/inspector/event-parsers.js
--- a/devtools/server/actors/inspector/moz.build
+++ b/devtools/server/actors/inspector/moz.build
@@ -1,16 +1,18 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
+  'css-logic.js',
   'document-walker.js',
+  'event-parsers.js',
   'inspector.js',
   'node.js',
   'utils.js',
   'walker.js',
 )
 
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Developer Tools: Inspector')
--- a/devtools/server/actors/inspector/node.js
+++ b/devtools/server/actors/inspector/node.js
@@ -20,18 +20,18 @@ loader.lazyRequireGetter(this, "findCssS
 loader.lazyRequireGetter(this, "isNativeAnonymous", "devtools/shared/layout/utils", true);
 loader.lazyRequireGetter(this, "isXBLAnonymous", "devtools/shared/layout/utils", true);
 loader.lazyRequireGetter(this, "isShadowAnonymous", "devtools/shared/layout/utils", true);
 loader.lazyRequireGetter(this, "isAnonymous", "devtools/shared/layout/utils", true);
 
 loader.lazyRequireGetter(this, "InspectorActorUtils", "devtools/server/actors/inspector/utils");
 loader.lazyRequireGetter(this, "LongStringActor", "devtools/server/actors/string", true);
 loader.lazyRequireGetter(this, "getFontPreviewData", "devtools/server/actors/styles", true);
-loader.lazyRequireGetter(this, "CssLogic", "devtools/server/css-logic", true);
-loader.lazyRequireGetter(this, "EventParsers", "devtools/server/event-parsers", true);
+loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
+loader.lazyRequireGetter(this, "EventParsers", "devtools/server/actors/inspector/event-parsers", true);
 
 const EventEmitter = require("devtools/shared/event-emitter");
 
 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
 const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
 const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
 
 /**
--- a/devtools/server/actors/layout.js
+++ b/devtools/server/actors/layout.js
@@ -7,17 +7,17 @@
 const { Cu } = require("chrome");
 const { Actor, ActorClassWithSpec } = require("devtools/shared/protocol");
 const { flexboxSpec, gridSpec, layoutSpec } = require("devtools/shared/specs/layout");
 const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");
 const { getStringifiableFragments } =
   require("devtools/server/actors/utils/css-grid-utils");
 
 loader.lazyRequireGetter(this, "nodeConstants", "devtools/shared/dom-node-constants");
-loader.lazyRequireGetter(this, "CssLogic", "devtools/server/css-logic", true);
+loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
 
 /**
  * Set of actors the expose the CSS layout information to the devtools protocol clients.
  *
  * The |Layout| actor is the main entry point. It is used to get various CSS
  * layout-related information from the document.
  *
  * The |Flexbox| actor provides the container node information to inspect the flexbox
--- a/devtools/server/actors/moz.build
+++ b/devtools/server/actors/moz.build
@@ -1,20 +1,22 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
+    'canvas',
     'emulation',
     'highlighters',
     'inspector',
     'utils',
     'webconsole',
+    'worker',
 ]
 
 DevToolsModules(
     'accessibility-parent.js',
     'accessibility.js',
     'actor-registry.js',
     'addon.js',
     'addons.js',
--- a/devtools/server/actors/root.js
+++ b/devtools/server/actors/root.js
@@ -519,17 +519,17 @@ RootActor.prototype = {
     };
   },
 
   onProcessListChanged: function() {
     this.conn.send({ from: this.actorID, type: "processListChanged" });
     this._parameters.processList.onListChanged = null;
   },
 
-  onGetProcess: function(request) {
+  async onGetProcess(request) {
     if (!DebuggerServer.allowChromeProcess) {
       return { error: "forbidden",
                message: "You are not allowed to debug chrome." };
     }
     if (("id" in request) && typeof (request.id) != "number") {
       return { error: "wrongParameter",
                message: "getProcess requires a valid `id` attribute." };
     }
@@ -554,20 +554,19 @@ RootActor.prototype = {
     }
     let form = this._processActors.get(id);
     if (form) {
       return { form };
     }
     let onDestroy = () => {
       this._processActors.delete(id);
     };
-    return DebuggerServer.connectToContent(this.conn, mm, onDestroy).then(formResult => {
-      this._processActors.set(id, formResult);
-      return { form: formResult };
-    });
+    form = await DebuggerServer.connectToContentProcess(this.conn, mm, onDestroy);
+    this._processActors.set(id, form);
+    return { form };
   },
 
   /* This is not in the spec, but it's used by tests. */
   onEcho: function(request) {
     /*
      * Request packets are frozen. Copy request, so that
      * DebuggerServerConnection.onPacket can attach a 'from' property.
      */
--- a/devtools/server/actors/styles.js
+++ b/devtools/server/actors/styles.js
@@ -9,17 +9,17 @@ const Services = require("Services");
 const protocol = require("devtools/shared/protocol");
 const {LongStringActor} = require("devtools/server/actors/string");
 const InspectorUtils = require("InspectorUtils");
 
 // This will also add the "stylesheet" actor type for protocol.js to recognize
 
 const {pageStyleSpec, styleRuleSpec, ELEMENT_STYLE} = require("devtools/shared/specs/styles");
 
-loader.lazyRequireGetter(this, "CssLogic", "devtools/server/css-logic", true);
+loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
 loader.lazyRequireGetter(this, "SharedCssLogic", "devtools/shared/inspector/css-logic");
 loader.lazyRequireGetter(this, "getDefinedGeometryProperties",
   "devtools/server/actors/highlighters/geometry-editor", true);
 loader.lazyRequireGetter(this, "isCssPropertyKnown",
   "devtools/server/actors/css-properties", true);
 loader.lazyRequireGetter(this, "parseNamedDeclarations",
   "devtools/shared/css/parsing-utils", true);
 loader.lazyRequireGetter(this, "UPDATE_PRESERVING_RULES",
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -716,17 +716,17 @@ BrowserTabActor.prototype = {
         // Reject the update promise if the tab was destroyed while requesting an update
         this._deferredUpdate.reject({
           error: "tabDestroyed",
           message: "Tab destroyed while performing a BrowserTabActor update"
         });
       }
       this.exit();
     };
-    let connect = DebuggerServer.connectToChild(this._conn, this._browser, onDestroy);
+    let connect = DebuggerServer.connectToFrame(this._conn, this._browser, onDestroy);
     let form = await connect;
 
     this._form = form;
     if (this.options.favicons) {
       this._form.favicon = await this.getFaviconData();
     }
 
     return this;
--- a/devtools/server/actors/webextension-parent.js
+++ b/devtools/server/actors/webextension-parent.js
@@ -156,17 +156,17 @@ ProxyChildActor.prototype = {
     }
 
     // Called when the debug browser element has been destroyed
     // (no actor is using it anymore to connect the child extension process).
     const onDestroy = this.destroy.bind(this);
 
     this._browser = await ExtensionParent.DebugUtils.getExtensionProcessBrowser(this);
 
-    this._form = await DebuggerServer.connectToChild(this._conn, this._browser, onDestroy,
+    this._form = await DebuggerServer.connectToFrame(this._conn, this._browser, onDestroy,
                                                      {addonId: this.addonId});
 
     this._childActorID = this._form.actor;
 
     // Exit the proxy child actor if the child actor has been destroyed.
     this._mm.addMessageListener("debug:webext_child_exit", this._onChildExit);
 
     return this._form;
--- a/devtools/server/actors/webextension.js
+++ b/devtools/server/actors/webextension.js
@@ -42,17 +42,17 @@ const FALLBACK_DOC_MESSAGE = "Your addon
  * add-on).
  * - When the WebExtensions OOP mode has been introduced, this actor has been refactored
  * and moved from the main process to the new child extension process.
  *
  * @param {DebuggerServerConnection} conn
  *        The connection to the client.
  * @param {nsIMessageSender} chromeGlobal.
  *        The chromeGlobal where this actor has been injected by the
- *        DebuggerServer.connectToChild method.
+ *        DebuggerServer.connectToFrame method.
  * @param {string} prefix
  *        the custom RDP prefix to use.
  * @param {string} addonId
  *        the addonId of the target WebExtension.
  */
 function WebExtensionChildActor(conn, chromeGlobal, prefix, addonId) {
   ChromeActor.call(this, conn);
 
--- a/devtools/server/actors/worker.js
+++ b/devtools/server/actors/worker.js
@@ -237,17 +237,17 @@ let ServiceWorkerActor = protocol.ActorC
   },
 
   destroy() {
     protocol.Actor.prototype.destroy.call(this);
     this._worker = null;
   },
 });
 
-// Lazily load the service-worker-child.js process script only once.
+// Lazily load the service-worker-process.js process script only once.
 let _serviceWorkerProcessScriptLoaded = false;
 
 let ServiceWorkerRegistrationActor =
 protocol.ActorClassWithSpec(serviceWorkerRegistrationSpec, {
   /**
    * Create the ServiceWorkerRegistrationActor
    * @param DebuggerServerConnection conn
    *   The server connection.
@@ -346,17 +346,17 @@ protocol.ActorClassWithSpec(serviceWorke
         this.emit("push-subscription-modified");
         break;
     }
   },
 
   start() {
     if (!_serviceWorkerProcessScriptLoaded) {
       Services.ppmm.loadProcessScript(
-        "resource://devtools/server/service-worker-child.js", true);
+        "resource://devtools/server/actors/worker/service-worker-process.js", true);
       _serviceWorkerProcessScriptLoaded = true;
     }
 
     // XXX: Send the permissions down to the content process before starting
     // the service worker within the content process. As we don't know what
     // content process we're starting the service worker in (as we're using a
     // broadcast channel to talk to it), we just broadcast the permissions to
     // everyone as well.
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/worker/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'service-worker-process.js',
+)
rename from devtools/server/service-worker-child.js
rename to devtools/server/actors/worker/service-worker-process.js
--- a/devtools/server/service-worker-child.js
+++ b/devtools/server/actors/worker/service-worker-process.js
@@ -1,16 +1,21 @@
 /* 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/. */
 
 /* global addMessageListener */
 
 "use strict";
 
+/*
+ * Process script used to control service workers via a DevTools actor.
+ * Loaded into content processes by the service worker actors.
+ */
+
 let swm = Cc["@mozilla.org/serviceworkers/manager;1"]
   .getService(Ci.nsIServiceWorkerManager);
 
 addMessageListener("serviceWorkerRegistration:start", message => {
   let { data } = message;
   let array = swm.getAllRegistrations();
 
   // Find the service worker registration with the desired scope.
--- a/devtools/server/main.js
+++ b/devtools/server/main.js
@@ -47,18 +47,18 @@ if (isWorker) {
   const VERBOSE_PREF = "devtools.debugger.log.verbose";
 
   flags.wantLogging = Services.prefs.getBoolPref(LOG_PREF);
   flags.wantVerbose =
     Services.prefs.getPrefType(VERBOSE_PREF) !== Services.prefs.PREF_INVALID &&
     Services.prefs.getBoolPref(VERBOSE_PREF);
 }
 
-const CONTENT_PROCESS_DBG_SERVER_SCRIPT =
-  "resource://devtools/server/content-process-debugger-server.js";
+const CONTENT_PROCESS_SERVER_STARTUP_SCRIPT =
+  "resource://devtools/server/startup/content-process.js";
 
 function loadSubScript(url) {
   try {
     Services.scriptloader.loadSubScript(url, this);
   } catch (e) {
     let errorStr = "Error loading: " + url + ":\n" +
                    (e.fileName ? "at " + e.fileName + " : " + e.lineNumber + "\n" : "") +
                    e + " - " + e.stack + "\n";
@@ -122,24 +122,24 @@ function ModuleAPI() {
       for (let factory of activeGlobalActors) {
         DebuggerServer.removeGlobalActor(factory);
       }
       activeGlobalActors = null;
     }
   };
 }
 
-/** *
+/**
  * Public API
  */
 var DebuggerServer = {
   _listeners: [],
   _initialized: false,
-  // Flag to check if the content process debugger server script was already loaded.
-  _contentProcessScriptLoaded: false,
+  // Flag to check if the content process server startup script was already loaded.
+  _contentProcessServerStartupScriptLoaded: false,
   // Map of global actor names to actor constructors provided by extensions.
   globalActorFactories: {},
   // Map of tab actor names to actor constructors provided by extensions.
   tabActorFactories: {},
 
   LONG_STRING_LENGTH: 10000,
   LONG_STRING_INITIAL_LENGTH: 1000,
   LONG_STRING_READ_LENGTH: 65 * 1024,
@@ -694,17 +694,21 @@ var DebuggerServer = {
 
     let transport = isWorker ?
                     new WorkerDebuggerTransport(scopeOrManager, prefix) :
                     new ChildDebuggerTransport(scopeOrManager, prefix);
 
     return this._onConnection(transport, prefix, true);
   },
 
-  connectToContent(connection, mm, onDestroy) {
+  /**
+   * Start a DevTools server in a content process (representing the entire process, not
+   * just a single frame) and add it as a child server for an active connection.
+   */
+  connectToContentProcess(connection, mm, onDestroy) {
     return new Promise(resolve => {
       let prefix = connection.allocID("content-process");
       let actor, childTransport;
 
       mm.addMessageListener("debug:content-process-actor", function listener(msg) {
         // Arbitrarily choose the first content process to reply
         // XXX: This code needs to be updated if we use more than one content process
         mm.removeMessageListener("debug:content-process-actor", listener);
@@ -714,31 +718,31 @@ var DebuggerServer = {
         childTransport.hooks = {
           onPacket: connection.send.bind(connection),
           onClosed() {}
         };
         childTransport.ready();
 
         connection.setForwarding(prefix, childTransport);
 
-        dumpn("establishing forwarding for process with prefix " + prefix);
+        dumpn(`Start forwarding for process with prefix ${prefix}`);
 
         actor = msg.json.actor;
 
         resolve(actor);
       });
 
-      // Load the content process debugger server script only once.
-      if (!this._contentProcessScriptLoaded) {
+      // Load the content process server startup script only once.
+      if (!this._contentProcessServerStartupScriptLoaded) {
         // Load the process script that will receive the debug:init-content-server message
-        Services.ppmm.loadProcessScript(CONTENT_PROCESS_DBG_SERVER_SCRIPT, true);
-        this._contentProcessScriptLoaded = true;
+        Services.ppmm.loadProcessScript(CONTENT_PROCESS_SERVER_STARTUP_SCRIPT, true);
+        this._contentProcessServerStartupScriptLoaded = true;
       }
 
-      // Send a message to the content process debugger server script to forward it the
+      // Send a message to the content process server startup script to forward it the
       // prefix.
       mm.sendAsyncMessage("debug:init-content-server", {
         prefix: prefix
       });
 
       function onClose() {
         Services.obs.removeObserver(onMessageManagerClose, "message-manager-close");
         EventEmitter.off(connection, "closed", onClose);
@@ -770,21 +774,25 @@ var DebuggerServer = {
       });
       Services.obs.addObserver(onMessageManagerClose,
                                "message-manager-close");
 
       EventEmitter.on(connection, "closed", onClose);
     });
   },
 
+  /**
+   * Start a DevTools server in a worker and add it as a child server for an active
+   * connection.
+   */
   connectToWorker(connection, dbg, id, options) {
     return new Promise((resolve, reject) => {
       // Step 1: Ensure the worker debugger is initialized.
       if (!dbg.isInitialized) {
-        dbg.initialize("resource://devtools/server/worker.js");
+        dbg.initialize("resource://devtools/server/startup/worker.js");
 
         // Create a listener for rpc requests from the worker debugger. Only do
         // this once, when the worker debugger is first initialized, rather than
         // for each connection.
         let listener = {
           onClose: () => {
             dbg.removeListener(listener);
           },
@@ -968,36 +976,37 @@ var DebuggerServer = {
   },
 
   /**
    * Live list of all currenctly attached child's message managers.
    */
   _childMessageManagers: new Set(),
 
   /**
-   * Connect to a child process.
+   * Start a DevTools server in a remote frame's process and add it as a child server for
+   * an active connection.
    *
    * @param object connection
    *        The debugger server connection to use.
    * @param nsIDOMElement frame
-   *        The browser element that holds the child process.
+   *        The frame element with remote content to connect to.
    * @param function [onDestroy]
    *        Optional function to invoke when the child process closes
    *        or the connection shuts down. (Need to forget about the
    *        related TabActor)
    * @return object
    *         A promise object that is resolved once the connection is
    *         established.
    */
-  connectToChild(connection, frame, onDestroy, {addonId} = {}) {
+  connectToFrame(connection, frame, onDestroy, {addonId} = {}) {
     return new Promise(resolve => {
       // Get messageManager from XUL browser (which might be a specialized tunnel for RDM)
       // or else fallback to asking the frameLoader itself.
       let mm = frame.messageManager || frame.frameLoader.messageManager;
-      mm.loadFrameScript("resource://devtools/server/child.js", false);
+      mm.loadFrameScript("resource://devtools/server/startup/frame.js", false);
 
       let trackMessageManager = () => {
         frame.addEventListener("DevTools:BrowserSwap", onBrowserSwap);
         mm.addMessageListener("debug:setup-in-parent", onSetupInParent);
         if (!actor) {
           mm.addMessageListener("debug:actor", onActorCreated);
         }
         DebuggerServer._childMessageManagers.add(mm);
@@ -1016,17 +1025,17 @@ var DebuggerServer = {
       let prefix = connection.allocID("child");
       // Compute the same prefix that's used by DebuggerServerConnection
       let connPrefix = prefix + "/";
 
       // provides hook to actor modules that need to exchange messages
       // between e10s parent and child processes
       let parentModules = [];
       let onSetupInParent = function(msg) {
-        // We may have multiple connectToChild instance running for the same tab
+        // We may have multiple connectToFrame instance running for the same tab
         // and need to filter the messages.
         if (msg.json.prefix != connPrefix) {
           return false;
         }
 
         let { module, setupParent } = msg.json;
         let m;
 
@@ -1062,17 +1071,17 @@ var DebuggerServer = {
         childTransport.hooks = {
           onPacket: connection.send.bind(connection),
           onClosed() {}
         };
         childTransport.ready();
 
         connection.setForwarding(prefix, childTransport);
 
-        dumpn("establishing forwarding for app with prefix " + prefix);
+        dumpn(`Start forwarding for frame with prefix ${prefix}`);
 
         actor = msg.json.actor;
         resolve(actor);
       }).bind(this);
 
       // Listen for browser frame swap
       let onBrowserSwap = ({ detail: newFrame }) => {
         // Remove listeners from old frame and mm
@@ -1124,24 +1133,24 @@ var DebuggerServer = {
           try {
             // Bug 1169643: Ignore any exception as the child process
             // may already be destroyed by now.
             mm.sendAsyncMessage("debug:disconnect", { prefix });
           } catch (e) {
             // Nothing to do
           }
         } else {
-          // Otherwise, the app has been closed before the actor
+          // Otherwise, the frame has been closed before the actor
           // had a chance to be created, so we are not able to create
           // the actor.
           resolve(null);
         }
         if (actor) {
           // The ContentActor within the child process doesn't necessary
-          // have time to uninitialize itself when the app is closed/killed.
+          // have time to uninitialize itself when the frame is closed/killed.
           // So ensure telling the client that the related actor is detached.
           connection.send({ from: actor.actor, type: "tabDetached" });
           actor = null;
         }
 
         if (onDestroy) {
           onDestroy(mm);
         }
@@ -1340,22 +1349,22 @@ var DebuggerServer = {
         for (let connID of Object.getOwnPropertyNames(this._connections)) {
           this._connections[connID].rootActor.removeActorByName(name);
         }
       }
     }
   },
 
   /**
-   * Called when DevTools are unloaded to remove the contend process server script for the
-   * list of scripts loaded for each new content process. Will also remove message
+   * Called when DevTools are unloaded to remove the contend process server startup script
+   * for the list of scripts loaded for each new content process. Will also remove message
    * listeners from already loaded scripts.
    */
   removeContentServerScript() {
-    Services.ppmm.removeDelayedProcessScript(CONTENT_PROCESS_DBG_SERVER_SCRIPT);
+    Services.ppmm.removeDelayedProcessScript(CONTENT_PROCESS_SERVER_STARTUP_SCRIPT);
     try {
       Services.ppmm.broadcastAsyncMessage("debug:close-content-server");
     } catch (e) {
       // Nothing to do
     }
   },
 
   /**
--- a/devtools/server/moz.build
+++ b/devtools/server/moz.build
@@ -4,29 +4,22 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 include('../templates.mozbuild')
 
 DIRS += [
     'actors',
     'performance',
+    'socket',
+    'startup',
 ]
 
 BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 MOCHITEST_CHROME_MANIFESTS += ['tests/mochitest/chrome.ini']
 XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
 
 DevToolsModules(
-    'child.js',
-    'content-process-debugger-server.js',
-    'content-server.jsm',
-    'css-logic.js',
-    'event-parsers.js',
     'main.js',
-    'primitive.js',
-    'service-worker-child.js',
-    'websocket-server.js',
-    'worker.js'
 )
 
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Developer Tools')
new file mode 100644
--- /dev/null
+++ b/devtools/server/socket/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini']
+
+DevToolsModules(
+    'websocket-server.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/server/socket/tests/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+  // Extend from the common devtools mochitest eslintrc config.
+  "extends": "../../../.eslintrc.xpcshell.js"
+};
new file mode 100644
--- /dev/null
+++ b/devtools/server/socket/tests/chrome.ini
@@ -0,0 +1,3 @@
+[DEFAULT]
+tags = devtools
+[test_websocket-server.html]
rename from devtools/server/tests/mochitest/test_websocket-server.html
rename to devtools/server/socket/tests/test_websocket-server.html
--- a/devtools/server/tests/mochitest/test_websocket-server.html
+++ b/devtools/server/socket/tests/test_websocket-server.html
@@ -9,17 +9,17 @@
 </head>
 <body>
 <script>
 "use strict";
 
 window.onload = function() {
   const CC = Components.Constructor;
   const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
-  const WebSocketServer = require("devtools/server/websocket-server");
+  const WebSocketServer = require("devtools/server/socket/websocket-server");
 
   const ServerSocket = CC("@mozilla.org/network/server-socket;1",
     "nsIServerSocket", "init");
 
   add_task(async function() {
     // Create a TCP server on auto-assigned port
     let server = new ServerSocket(-1, true, -1);
     ok(server, `Launched WebSocket server on port ${server.port}`);
rename from devtools/server/websocket-server.js
rename to devtools/server/socket/websocket-server.js
rename from devtools/server/content-process-debugger-server.js
rename to devtools/server/startup/content-process.js
--- a/devtools/server/content-process-debugger-server.js
+++ b/devtools/server/startup/content-process.js
@@ -1,22 +1,30 @@
 /* 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/. */
 
 /* global addMessageListener, removeMessageListener */
 
 "use strict";
 
+/*
+ * Process script that listens for requests to start a `DebuggerServer` for an entire
+ * content process.  Loaded into content processes by the main process during
+ * `DebuggerServer.connectToContentProcess`.
+ *
+ * The actual server startup itself is in a JSM so that code can be cached.
+ */
+
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
 
 function onInit(message) {
   // Only reply if we are in a real content process
   if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-    let {init} = ChromeUtils.import("resource://devtools/server/content-server.jsm", {});
+    let {init} = ChromeUtils.import("resource://devtools/server/startup/content-process.jsm", {});
     init(message);
   }
 }
 
 function onClose() {
   removeMessageListener("debug:init-content-server", onInit);
   removeMessageListener("debug:close-content-server", onClose);
 }
rename from devtools/server/content-server.jsm
rename to devtools/server/startup/content-process.jsm
--- a/devtools/server/content-server.jsm
+++ b/devtools/server/startup/content-process.jsm
@@ -1,14 +1,22 @@
 /* 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";
 
+/*
+ * Module that listens for requests to start a `DebuggerServer` for an entire content
+ * process.  Loaded into content processes by the main process during
+ * `DebuggerServer.connectToContentProcess` via the process script `content-process.js`.
+ *
+ * The actual server startup itself is in this JSM so that code can be cached.
+ */
+
 /* exported init */
 this.EXPORTED_SYMBOLS = ["init"];
 
 let gLoader;
 
 function setupServer(mm) {
   // Prevent spawning multiple server per process, even if the caller call us
   // multiple times
rename from devtools/server/child.js
rename to devtools/server/startup/frame.js
--- a/devtools/server/child.js
+++ b/devtools/server/startup/frame.js
@@ -1,16 +1,22 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 /* global addEventListener, addMessageListener, removeMessageListener, sendAsyncMessage */
 
+/*
+ * Frame script that listens for requests to start a `DebuggerServer` for a frame in a
+ * content process.  Loaded into content process frames by the main process during
+ * `DebuggerServer.connectToFrame`.
+ */
+
 try {
   var chromeGlobal = this;
 
   // Encapsulate in its own scope to allows loading this frame script more than once.
   (function() {
     const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
 
     const DevToolsUtils = require("devtools/shared/DevToolsUtils");
@@ -123,10 +129,10 @@ try {
     addEventListener("unload", () => {
       for (let conn of connections.values()) {
         conn.close();
       }
       connections.clear();
     });
   })();
 } catch (e) {
-  dump(`Exception in app child process: ${e}\n`);
+  dump(`Exception in DevTools frame startup: ${e}\n`);
 }
new file mode 100644
--- /dev/null
+++ b/devtools/server/startup/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'content-process.js',
+    'content-process.jsm',
+    'frame.js',
+    'worker.js',
+)
rename from devtools/server/worker.js
rename to devtools/server/startup/worker.js
--- a/devtools/server/worker.js
+++ b/devtools/server/startup/worker.js
@@ -2,16 +2,22 @@
  * 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";
 
 /* eslint-env mozilla/chrome-worker */
 /* global worker, loadSubScript, global */
 
+/*
+ * Worker debugger script that listens for requests to start a `DebuggerServer` for a
+ * worker in a process.  Loaded into a specific worker during
+ * `DebuggerServer.connectToWorker` which is called from the same process as the worker.
+ */
+
 // This function is used to do remote procedure calls from the worker to the
 // main thread. It is exposed as a built-in global to every module by the
 // worker loader. To make sure the worker loader can access it, it needs to be
 // defined before loading the worker loader script below.
 this.rpc = function(method, ...params) {
   let id = nextId++;
 
   postMessage(JSON.stringify({
--- a/devtools/server/tests/mochitest/chrome.ini
+++ b/devtools/server/tests/mochitest/chrome.ini
@@ -26,17 +26,17 @@ support-files =
   nonchrome_unsafeDereference.html
   small-image.gif
   setup-in-child.js
   setup-in-parent.js
   webconsole-helpers.js
   webextension-helpers.js
 [test_animation_actor-lifetime.html]
 [test_connection-manager.html]
-[test_connectToChild.html]
+[test_connectToFrame.html]
 [test_css-logic.html]
 [test_css-logic-media-queries.html]
 [test_css-logic-specificity.html]
 [test_css-properties.html]
 [test_Debugger.Source.prototype.introductionScript.html]
 [test_Debugger.Source.prototype.introductionType.html]
 [test_Debugger.Source.prototype.element.html]
 [test_Debugger.Script.prototype.global.html]
@@ -103,9 +103,8 @@ support-files =
 [test_styles-modify.html]
 [test_styles-svg.html]
 [test_unsafeDereference.html]
 [test_webconsole-node-grip.html]
 [test_webextension-addon-debugging-connect.html]
 skip-if = !e10s # test is designed to work on e10s only
 [test_webextension-addon-debugging-reload.html]
 skip-if = !e10s # test is designed to work on e10s only
-[test_websocket-server.html]
rename from devtools/server/tests/mochitest/test_connectToChild.html
rename to devtools/server/tests/mochitest/test_connectToFrame.html
--- a/devtools/server/tests/mochitest/test_connectToChild.html
+++ b/devtools/server/tests/mochitest/test_connectToFrame.html
@@ -1,12 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <!--
-Bug 966991 - Test DebuggerServer.connectToChild
+Bug 966991 - Test DebuggerServer.connectToFrame
 -->
 <head>
   <meta charset="utf-8">
   <title>Mozilla Bug</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
 </head>
 <body>
@@ -73,17 +73,17 @@ function runTests() {
     DebuggerServer.registerAllActors();
   }
 
   function firstClient() {
     // Fake a first connection to an iframe
     let transport = DebuggerServer.connectPipe();
     let conn = transport._serverConnection;
     let client = new DebuggerClient(transport);
-    DebuggerServer.connectToChild(conn, iframe).then(actor => {
+    DebuggerServer.connectToFrame(conn, iframe).then(actor => {
       ok(actor.testActor, "Got the test actor");
 
       // Ensure sending at least one request to our actor,
       // otherwise it won't be instanciated, nor be destroyed...
       client.request({
         to: actor.testActor,
         type: "hello",
       }, function(response) {
@@ -102,17 +102,17 @@ function runTests() {
     });
   }
 
   function secondClient(firstActor) {
     // Then fake a second one, that should spawn a new set of tab actors
     let transport = DebuggerServer.connectPipe();
     let conn = transport._serverConnection;
     let client = new DebuggerClient(transport);
-    DebuggerServer.connectToChild(conn, iframe).then(actor => {
+    DebuggerServer.connectToFrame(conn, iframe).then(actor => {
       ok(actor.testActor, "Got a test actor for the second connection");
       isnot(actor.testActor, firstActor,
             "We get different actor instances between two connections");
 
       client.close(cleanup);
     });
   }
 
--- a/devtools/server/tests/mochitest/test_css-logic-media-queries.html
+++ b/devtools/server/tests/mochitest/test_css-logic-media-queries.html
@@ -25,17 +25,17 @@ Test that css-logic handles media-querie
 <body>
   <div></div>
   <script type="application/javascript">
   "use strict";
 
   window.onload = function() {
     const {require} = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
     const Services = require("Services");
-    const {CssLogic} = require("devtools/server/css-logic");
+    const {CssLogic} = require("devtools/server/actors/inspector/css-logic");
 
     SimpleTest.waitForExplicitFinish();
 
     let div = document.querySelector("div");
     let cssLogic = new CssLogic(InspectorUtils.isInheritedProperty);
     cssLogic.highlight(div);
     cssLogic.processMatchedSelectors();
 
--- a/devtools/server/tests/mochitest/test_css-logic-specificity.html
+++ b/devtools/server/tests/mochitest/test_css-logic-specificity.html
@@ -9,17 +9,17 @@ Test that css-logic calculates CSS speci
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
 </head>
 <body style="background:blue;">
   <script type="application/javascript">
   "use strict";
 
   window.onload = function() {
     const {require} = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
-    const {CssLogic, CssSelector} = require("devtools/server/css-logic");
+    const {CssLogic, CssSelector} = require("devtools/server/actors/inspector/css-logic");
     const InspectorUtils = SpecialPowers.InspectorUtils;
 
     const TEST_DATA = [
       {text: "*", expected: 0},
       {text: "LI", expected: 1},
       {text: "UL LI", expected: 2},
       {text: "UL OL + LI", expected: 3},
       {text: "H1 + [REL=\"up\"]", expected: 1025},
--- a/devtools/server/tests/mochitest/test_css-logic.html
+++ b/devtools/server/tests/mochitest/test_css-logic.html
@@ -8,17 +8,17 @@ https://bugzilla.mozilla.org/show_bug.cg
   <title>Test for Bug </title>
 
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript" src="inspector-helpers.js"></script>
   <script type="application/javascript">
 "use strict";
 
-const {CssLogic} = require("devtools/server/css-logic");
+const {CssLogic} = require("devtools/server/actors/inspector/css-logic");
 
 window.onload = function() {
   SimpleTest.waitForExplicitFinish();
   runNextTest();
 };
 
 addTest(function getComputedStyle() {
   let node = document.querySelector("#computed-style");
--- a/devtools/server/tests/mochitest/test_setupInParentChild.html
+++ b/devtools/server/tests/mochitest/test_setupInParentChild.html
@@ -75,17 +75,17 @@ function runTests() {
     is(args[0], true, "Got `mm` argument, a message manager");
     ok(args[1].match(/server\d+.conn\d+.child\d+/), "Got `prefix` argument");
 
     cleanup();
   };
   Services.obs.addObserver(onParent, "test:setupParent");
 
   // Instanciate e10s machinery and call setupInChild
-  DebuggerServer.connectToChild(conn, iframe).then(actor => {
+  DebuggerServer.connectToFrame(conn, iframe).then(actor => {
     DebuggerServer.setupInChild({
       module: "chrome://mochitests/content/chrome/devtools/server/tests/mochitest/setup-in-child.js",
       setupChild: "setupChild",
       args: [1, "two", {three: true}]
     });
   });
 
   function cleanup() {
--- a/devtools/shared/Loader.jsm
+++ b/devtools/shared/Loader.jsm
@@ -42,17 +42,17 @@ BuiltinProvider.prototype = {
       // Allow access to xpcshell test items from the loader.
       "xpcshell-test": "resource://test",
 
       // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
       // Allow access to locale data using paths closer to what is
       // used in the source tree.
       "devtools/client/locales": "chrome://devtools/locale",
       "devtools/shared/locales": "chrome://devtools-shared/locale",
-      "devtools/shim/locales": "chrome://devtools-shim/locale",
+      "devtools/startup/locales": "chrome://devtools-startup/locale",
       "toolkit/locales": "chrome://global/locale",
     };
     // When creating a Loader invisible to the Debugger, we have to ensure
     // using only modules and not depend on any JSM. As everything that is
     // not loaded with Loader isn't going to respect `invisibleToDebugger`.
     // But we have to keep using Promise.jsm for other loader to prevent
     // breaking unhandled promise rejection in tests.
     if (this.invisibleToDebugger) {
--- a/devtools/shared/indexed-db.js
+++ b/devtools/shared/indexed-db.js
@@ -26,23 +26,23 @@ const principal =
  *         specific principal.
  */
 exports.createDevToolsIndexedDB = function(indexedDB) {
   return Object.freeze({
     /**
      * Only the standard version of indexedDB.open is supported.
      */
     open(name, version) {
-      let options = { storage: "persistent" };
+      let options = {};
       if (typeof version === "number") {
         options.version = version;
       }
       return indexedDB.openForPrincipal(principal, name, options);
     },
     /**
      * Only the standard version of indexedDB.deleteDatabase is supported.
      */
     deleteDatabase(name) {
-      return indexedDB.deleteForPrincipal(principal, name, { storage: "persistent" });
+      return indexedDB.deleteForPrincipal(principal, name);
     },
     cmp: indexedDB.cmp.bind(indexedDB),
   });
 };
--- a/devtools/shared/l10n.js
+++ b/devtools/shared/l10n.js
@@ -25,17 +25,17 @@ const propertiesMap = {};
 // directory.  Then we use the correct context to load the property
 // file.  In the webpack case this results in just the locale property
 // files being included in the pack; and in the devtools case this is
 // a wordy no-op.
 const reqShared = require.context("raw!devtools/shared/locales/",
                                   true, /^.*\.properties$/);
 const reqClient = require.context("raw!devtools/client/locales/",
                                   true, /^.*\.properties$/);
-const reqShim = require.context("raw!devtools/shim/locales/",
+const reqStartup = require.context("raw!devtools/startup/locales/",
                                   true, /^.*\.properties$/);
 const reqGlobal = require.context("raw!toolkit/locales/",
                                   true, /^.*\.properties$/);
 
 // Map used to memoize Number formatters.
 const numberFormatters = new Map();
 const getNumberFormatter = function(decimals) {
   let formatter = numberFormatters.get(decimals);
@@ -69,18 +69,18 @@ function getProperties(url) {
     let index = url.lastIndexOf("/");
     // Turn "mumble/locales/resource.properties" => "./resource.properties".
     let baseName = "." + url.substr(index);
     let reqFn;
     if (/^toolkit/.test(url)) {
       reqFn = reqGlobal;
     } else if (/^devtools\/shared/.test(url)) {
       reqFn = reqShared;
-    } else if (/^devtools\/shim/.test(url)) {
-      reqFn = reqShim;
+    } else if (/^devtools\/startup/.test(url)) {
+      reqFn = reqStartup;
     } else {
       reqFn = reqClient;
     }
     propertiesMap[url] = parsePropertiesFile(reqFn(baseName));
   }
 
   return propertiesMap[url];
 }
--- a/devtools/shared/security/socket.js
+++ b/devtools/shared/security/socket.js
@@ -12,17 +12,17 @@ var { Ci, Cc, CC, Cr } = require("chrome
 Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
 
 var Services = require("Services");
 var promise = require("promise");
 var defer = require("devtools/shared/defer");
 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
 var { dumpn, dumpv } = DevToolsUtils;
 loader.lazyRequireGetter(this, "WebSocketServer",
-  "devtools/server/websocket-server");
+  "devtools/server/socket/websocket-server");
 loader.lazyRequireGetter(this, "DebuggerTransport",
   "devtools/shared/transport/transport", true);
 loader.lazyRequireGetter(this, "WebSocketDebuggerTransport",
   "devtools/shared/transport/websocket-transport");
 loader.lazyRequireGetter(this, "DebuggerServer",
   "devtools/server/main", true);
 loader.lazyRequireGetter(this, "discovery",
   "devtools/shared/discovery/discovery");
rename from devtools/shim/DevToolsShim.jsm
rename to devtools/startup/DevToolsShim.jsm
--- a/devtools/shim/DevToolsShim.jsm
+++ b/devtools/startup/DevToolsShim.jsm
@@ -23,45 +23,28 @@ this.EXPORTED_SYMBOLS = [
 function removeItem(array, callback) {
   let index = array.findIndex(callback);
   if (index >= 0) {
     array.splice(index, 1);
   }
 }
 
 /**
- * The DevToolsShim is a part of the DevTools go faster project, which moves the Firefox
- * DevTools outside of mozilla-central to an add-on. It aims to bridge the gap for
- * existing mozilla-central code that still needs to interact with DevTools (such as
- * web-extensions).
+ * DevToolsShim is a singleton that provides a set of helpers to interact with DevTools,
+ * that work whether Devtools are enabled or not.
  *
- * DevToolsShim is a singleton that provides a set of helpers to interact with DevTools,
- * that work whether the DevTools addon is installed or not. It can be used to start
- * listening to events. As soon as a DevTools addon is installed the DevToolsShim will
- * forward all the requests received until then to the real DevTools instance.
- *
- * DevToolsShim.isInstalled() can also be used to know if DevTools are currently
- * installed.
+ * It can be used to start listening to devtools events before DevTools are ready. As soon
+ * as DevTools are enabled, the DevToolsShim will forward all the requests received until
+ * then to the real DevTools instance.
  */
 this.DevToolsShim = {
   _gDevTools: null,
   listeners: [],
 
   /**
-   * Check if DevTools are currently installed (but not necessarily initialized).
-   *
-   * @return {Boolean} true if DevTools are installed.
-   */
-  isInstalled: function() {
-    return Services.io.getProtocolHandler("resource")
-             .QueryInterface(Ci.nsIResProtocolHandler)
-             .hasSubstitution("devtools");
-  },
-
-  /**
    * Returns true if DevTools are enabled for the current profile. If devtools are not
    * enabled, initializing DevTools will open the onboarding page. Some entry points
    * should no-op in this case.
    */
   isEnabled: function() {
     let enabled = Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF);
     return enabled && !this.isDisabledByPolicy();
   },
@@ -180,17 +163,17 @@ this.DevToolsShim = {
    *
    * @param {XULTab} tab
    *        The browser tab on which inspect node was used.
    * @param {Array} selectors
    *        An array of CSS selectors to find the target node. Several selectors can be
    *        needed if the element is nested in frames and not directly in the root
    *        document.
    * @return {Promise} a promise that resolves when the node is selected in the inspector
-   *         markup view or that resolves immediately if DevTools are not installed.
+   *         markup view or that resolves immediately if DevTools are not enabled.
    */
   inspectNode: function(tab, selectors) {
     if (!this.isEnabled()) {
       if (!this.isDisabledByPolicy()) {
         DevtoolsStartup.openInstallPage("ContextMenu");
       }
       return Promise.resolve();
     }
rename from devtools/shim/aboutdebugging-registration.js
rename to devtools/startup/aboutdebugging-registration.js
rename from devtools/shim/aboutdebugging.manifest
rename to devtools/startup/aboutdebugging.manifest
rename from devtools/shim/aboutdevtools/aboutdevtools-registration.js
rename to devtools/startup/aboutdevtools/aboutdevtools-registration.js
--- a/devtools/shim/aboutdevtools/aboutdevtools-registration.js
+++ b/devtools/startup/aboutdevtools/aboutdevtools-registration.js
@@ -9,17 +9,17 @@
 const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", {});
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
 
 const { nsIAboutModule } = Ci;
 
 function AboutDevtools() {}
 
 AboutDevtools.prototype = {
-  uri: Services.io.newURI("chrome://devtools-shim/content/aboutdevtools/aboutdevtools.xhtml"),
+  uri: Services.io.newURI("chrome://devtools-startup/content/aboutdevtools/aboutdevtools.xhtml"),
   classDescription: "about:devtools",
   classID: Components.ID("3a16d383-92bd-4c24-ac10-0e2bd66883ab"),
   contractID: "@mozilla.org/network/protocol/about;1?what=devtools",
 
   QueryInterface: XPCOMUtils.generateQI([nsIAboutModule]),
 
   newChannel: function(uri, loadInfo) {
     let chan = Services.io.newChannelFromURIWithLoadInfo(
rename from devtools/shim/aboutdevtools/aboutdevtools.css
rename to devtools/startup/aboutdevtools/aboutdevtools.css
rename from devtools/shim/aboutdevtools/aboutdevtools.js
rename to devtools/startup/aboutdevtools/aboutdevtools.js
--- a/devtools/shim/aboutdevtools/aboutdevtools.js
+++ b/devtools/startup/aboutdevtools/aboutdevtools.js
@@ -23,20 +23,20 @@ const MESSAGES = {
 };
 
 // Google analytics parameters that should be added to all outgoing links.
 const GA_PARAMETERS = [
   ["utm_source", "devtools"],
   ["utm_medium", "onboarding"],
 ];
 
-const ABOUTDEVTOOLS_STRINGS = "chrome://devtools-shim/locale/aboutdevtools.properties";
+const ABOUTDEVTOOLS_STRINGS = "chrome://devtools-startup/locale/aboutdevtools.properties";
 const aboutDevtoolsBundle = Services.strings.createBundle(ABOUTDEVTOOLS_STRINGS);
 
-const KEY_SHORTCUTS_STRINGS = "chrome://devtools-shim/locale/key-shortcuts.properties";
+const KEY_SHORTCUTS_STRINGS = "chrome://devtools-startup/locale/key-shortcuts.properties";
 const keyShortcutsBundle = Services.strings.createBundle(KEY_SHORTCUTS_STRINGS);
 
 // URL constructor doesn't support about: scheme,
 // we have to use http in order to have working searchParams.
 let url = new URL(window.location.href.replace("about:", "http://"));
 let reason = url.searchParams.get("reason");
 let keyid = url.searchParams.get("keyid");
 let tabid = parseInt(url.searchParams.get("tabid"), 10);
@@ -81,57 +81,57 @@ function updatePage() {
  * Each feature should contain:
  * - icon: the name of the image to use
  * - title: the key of the localized title (from aboutdevtools.properties)
  * - desc: the key of the localized description (from aboutdevtools.properties)
  * - link: the MDN documentation link
  */
 const features = [
   {
-    icon: "chrome://devtools-shim/content/aboutdevtools/images/feature-inspector.svg",
+    icon: "chrome://devtools-startup/content/aboutdevtools/images/feature-inspector.svg",
     title: "features.inspector.title",
     desc: "features.inspector.desc",
     link: "https://developer.mozilla.org/docs/Tools/Page_Inspector",
   }, {
-    icon: "chrome://devtools-shim/content/aboutdevtools/images/feature-console.svg",
+    icon: "chrome://devtools-startup/content/aboutdevtools/images/feature-console.svg",
     title: "features.console.title",
     desc: "features.console.desc",
     link: "https://developer.mozilla.org/docs/Tools/Web_Console",
   }, {
-    icon: "chrome://devtools-shim/content/aboutdevtools/images/feature-debugger.svg",
+    icon: "chrome://devtools-startup/content/aboutdevtools/images/feature-debugger.svg",
     title: "features.debugger.title",
     desc: "features.debugger.desc",
     link: "https://developer.mozilla.org/docs/Tools/Debugger",
   }, {
-    icon: "chrome://devtools-shim/content/aboutdevtools/images/feature-network.svg",
+    icon: "chrome://devtools-startup/content/aboutdevtools/images/feature-network.svg",
     title: "features.network.title",
     desc: "features.network.desc",
     link: "https://developer.mozilla.org/docs/Tools/Network_Monitor",
   }, {
-    icon: "chrome://devtools-shim/content/aboutdevtools/images/feature-storage.svg",
+    icon: "chrome://devtools-startup/content/aboutdevtools/images/feature-storage.svg",
     title: "features.storage.title",
     desc: "features.storage.desc",
     link: "https://developer.mozilla.org/docs/Tools/Storage_Inspector",
   }, {
-    icon: "chrome://devtools-shim/content/aboutdevtools/images/feature-responsive.svg",
+    icon: "chrome://devtools-startup/content/aboutdevtools/images/feature-responsive.svg",
     title: "features.responsive.title",
     desc: "features.responsive.desc",
     link: "https://developer.mozilla.org/docs/Tools/Responsive_Design_Mode",
   }, {
-    icon: "chrome://devtools-shim/content/aboutdevtools/images/feature-visualediting.svg",
+    icon: "chrome://devtools-startup/content/aboutdevtools/images/feature-visualediting.svg",
     title: "features.visualediting.title",
     desc: "features.visualediting.desc",
     link: "https://developer.mozilla.org/docs/Tools/Style_Editor",
   }, {
-    icon: "chrome://devtools-shim/content/aboutdevtools/images/feature-performance.svg",
+    icon: "chrome://devtools-startup/content/aboutdevtools/images/feature-performance.svg",
     title: "features.performance.title",
     desc: "features.performance.desc",
     link: "https://developer.mozilla.org/docs/Tools/Performance",
   }, {
-    icon: "chrome://devtools-shim/content/aboutdevtools/images/feature-memory.svg",
+    icon: "chrome://devtools-startup/content/aboutdevtools/images/feature-memory.svg",
     title: "features.memory.title",
     desc: "features.memory.desc",
     link: "https://developer.mozilla.org/docs/Tools/Memory",
   },
 ];
 
 /**
  * Helper to create a DOM element to represent a DevTools feature.
rename from devtools/shim/aboutdevtools/aboutdevtools.manifest
rename to devtools/startup/aboutdevtools/aboutdevtools.manifest
rename from devtools/shim/aboutdevtools/aboutdevtools.xhtml
rename to devtools/startup/aboutdevtools/aboutdevtools.xhtml
--- a/devtools/shim/aboutdevtools/aboutdevtools.xhtml
+++ b/devtools/startup/aboutdevtools/aboutdevtools.xhtml
@@ -1,27 +1,27 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 <!DOCTYPE html [
 <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD;
 <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD;
-<!ENTITY % aboutdevtoolsDTD SYSTEM "chrome://devtools-shim/locale/aboutdevtools.dtd"> %aboutdevtoolsDTD;
+<!ENTITY % aboutdevtoolsDTD SYSTEM "chrome://devtools-startup/locale/aboutdevtools.dtd"> %aboutdevtoolsDTD;
 ]>
 
 <html xmlns="http://www.w3.org/1999/xhtml" dir="&locale.dir;">
 <head>
   <title>&aboutDevtools.headTitle;</title>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>a
   <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" type="text/css"/>
-  <link rel="stylesheet" href="chrome://devtools-shim/content/aboutdevtools/aboutdevtools.css"  type="text/css"/>
-  <link rel="stylesheet" href="chrome://devtools-shim/content/aboutdevtools/subscribe.css"  type="text/css"/>
-  <script type="application/javascript" src="chrome://devtools-shim/content/aboutdevtools/aboutdevtools.js"></script>
-  <script type="application/javascript" src="chrome://devtools-shim/content/aboutdevtools/subscribe.js"></script>
+  <link rel="stylesheet" href="chrome://devtools-startup/content/aboutdevtools/aboutdevtools.css"  type="text/css"/>
+  <link rel="stylesheet" href="chrome://devtools-startup/content/aboutdevtools/subscribe.css"  type="text/css"/>
+  <script type="application/javascript" src="chrome://devtools-startup/content/aboutdevtools/aboutdevtools.js"></script>
+  <script type="application/javascript" src="chrome://devtools-startup/content/aboutdevtools/subscribe.js"></script>
 </head>
 <body>
   <div id="install-page" class="wrapper" hidden="true">
     <div class="box">
       <div class="left-pane" />
       <div class="right-pane">
         <h1 class="title" id="common-title" hidden="true">&aboutDevtools.enable.title;</h1>
         <h1 class="title" id="inspect-title" hidden="true">&aboutDevtools.enable.inspectElementTitle;</h1>
@@ -84,17 +84,17 @@
 
     <div class="features">
       <ul class="features-list">
       </ul>
     </div>
 
     <footer>
       <img class="dev-edition-logo"
-           src="chrome://devtools-shim/content/aboutdevtools/images/dev-edition-logo.svg"
+           src="chrome://devtools-startup/content/aboutdevtools/images/dev-edition-logo.svg"
            alt="Firefox Developer Edition logo"/>
       <div class="footer-message">
         <h1 class="footer-message-title">&aboutDevtools.footer.title;</h1>
         <p>&aboutDevtools.footer.message;</p>
         <a class="external footer-link"
            href="https://www.mozilla.org/firefox/developer/"
            target="_blank">&aboutDevtools.footer.learnMoreLink;</a>
       </div>
rename from devtools/shim/aboutdevtools/images/dev-edition-logo.svg
rename to devtools/startup/aboutdevtools/images/dev-edition-logo.svg
rename from devtools/shim/aboutdevtools/images/external-link.svg
rename to devtools/startup/aboutdevtools/images/external-link.svg
rename from devtools/shim/aboutdevtools/images/feature-console.svg
rename to devtools/startup/aboutdevtools/images/feature-console.svg
rename from devtools/shim/aboutdevtools/images/feature-debugger.svg
rename to devtools/startup/aboutdevtools/images/feature-debugger.svg
rename from devtools/shim/aboutdevtools/images/feature-inspector.svg
rename to devtools/startup/aboutdevtools/images/feature-inspector.svg
rename from devtools/shim/aboutdevtools/images/feature-memory.svg
rename to devtools/startup/aboutdevtools/images/feature-memory.svg
rename from devtools/shim/aboutdevtools/images/feature-network.svg
rename to devtools/startup/aboutdevtools/images/feature-network.svg
rename from devtools/shim/aboutdevtools/images/feature-performance.svg
rename to devtools/startup/aboutdevtools/images/feature-performance.svg
rename from devtools/shim/aboutdevtools/images/feature-responsive.svg
rename to devtools/startup/aboutdevtools/images/feature-responsive.svg
rename from devtools/shim/aboutdevtools/images/feature-storage.svg
rename to devtools/startup/aboutdevtools/images/feature-storage.svg
rename from devtools/shim/aboutdevtools/images/feature-visualediting.svg
rename to devtools/startup/aboutdevtools/images/feature-visualediting.svg
rename from devtools/shim/aboutdevtools/images/otter.svg
rename to devtools/startup/aboutdevtools/images/otter.svg
rename from devtools/shim/aboutdevtools/moz.build
rename to devtools/startup/aboutdevtools/moz.build
rename from devtools/shim/aboutdevtools/subscribe.css
rename to devtools/startup/aboutdevtools/subscribe.css
rename from devtools/shim/aboutdevtools/subscribe.js
rename to devtools/startup/aboutdevtools/subscribe.js
--- a/devtools/shim/aboutdevtools/subscribe.js
+++ b/devtools/startup/aboutdevtools/subscribe.js
@@ -10,17 +10,17 @@
  */
 
 window.addEventListener("load", function() {
   const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
 
   // Timeout for the subscribe XHR.
   const REQUEST_TIMEOUT = 5000;
 
-  const ABOUTDEVTOOLS_STRINGS = "chrome://devtools-shim/locale/aboutdevtools.properties";
+  const ABOUTDEVTOOLS_STRINGS = "chrome://devtools-startup/locale/aboutdevtools.properties";
   const aboutDevtoolsBundle = Services.strings.createBundle(ABOUTDEVTOOLS_STRINGS);
 
   let emailInput = document.getElementById("email");
   let newsletterErrors = document.getElementById("newsletter-errors");
   let newsletterForm = document.getElementById("newsletter-form");
   let newsletterPrivacySection = document.getElementById("newsletter-privacy");
   let newsletterThanks = document.getElementById("newsletter-thanks");
 
rename from devtools/shim/aboutdevtools/test/.eslintrc.js
rename to devtools/startup/aboutdevtools/test/.eslintrc.js
rename from devtools/shim/aboutdevtools/test/browser.ini
rename to devtools/startup/aboutdevtools/test/browser.ini
rename from devtools/shim/aboutdevtools/test/browser_aboutdevtools_closes_page.js
rename to devtools/startup/aboutdevtools/test/browser_aboutdevtools_closes_page.js
rename from devtools/shim/aboutdevtools/test/browser_aboutdevtools_enables_devtools.js
rename to devtools/startup/aboutdevtools/test/browser_aboutdevtools_enables_devtools.js
rename from devtools/shim/aboutdevtools/test/browser_aboutdevtools_focus_owner_tab.js
rename to devtools/startup/aboutdevtools/test/browser_aboutdevtools_focus_owner_tab.js
rename from devtools/shim/aboutdevtools/test/browser_aboutdevtools_reuse_existing.js
rename to devtools/startup/aboutdevtools/test/browser_aboutdevtools_reuse_existing.js
rename from devtools/shim/aboutdevtools/test/head.js
rename to devtools/startup/aboutdevtools/test/head.js
rename from devtools/shim/aboutdevtoolstoolbox-registration.js
rename to devtools/startup/aboutdevtoolstoolbox-registration.js
rename from devtools/shim/aboutdevtoolstoolbox.manifest
rename to devtools/startup/aboutdevtoolstoolbox.manifest
rename from devtools/shim/devtools-startup-prefs.js
rename to devtools/startup/devtools-startup-prefs.js
--- a/devtools/shim/devtools-startup-prefs.js
+++ b/devtools/startup/devtools-startup-prefs.js
@@ -1,17 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-// This Devtools preferences file will be loaded as a usual Firefox preferences file.
-// Most DevTools prefs are included with the addon and loaded dynamically during the addon
-// startup. For preferences that are required before the addon is loaded or that we can't
-// process in JS, they can be defined in this file.
-// Note that this preference file follows Firefox release cycle.
+// The preferences defined here should be used by the components in devtools-startup.
+// devtools-startup is always shipped and those preferences will always be available.
 
 // Enable the JSON View tool (an inspector for application/json documents).
 pref("devtools.jsonview.enabled", true);
 
 // Default theme ("dark" or "light")
 #ifdef MOZ_DEV_EDITION
 pref("devtools.theme", "dark", sticky);
 #else
rename from devtools/shim/devtools-startup.js
rename to devtools/startup/devtools-startup.js
--- a/devtools/shim/devtools-startup.js
+++ b/devtools/startup/devtools-startup.js
@@ -40,22 +40,22 @@ ChromeUtils.defineModuleGetter(this, "Se
 ChromeUtils.defineModuleGetter(this, "AppConstants",
                                "resource://gre/modules/AppConstants.jsm");
 ChromeUtils.defineModuleGetter(this, "CustomizableUI",
                                "resource:///modules/CustomizableUI.jsm");
 ChromeUtils.defineModuleGetter(this, "CustomizableWidgets",
                                "resource:///modules/CustomizableWidgets.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "StartupBundle", function() {
-  const url = "chrome://devtools-shim/locale/startup.properties";
+  const url = "chrome://devtools-startup/locale/startup.properties";
   return Services.strings.createBundle(url);
 });
 
 XPCOMUtils.defineLazyGetter(this, "KeyShortcutsBundle", function() {
-  const url = "chrome://devtools-shim/locale/key-shortcuts.properties";
+  const url = "chrome://devtools-startup/locale/key-shortcuts.properties";
   return Services.strings.createBundle(url);
 });
 
 XPCOMUtils.defineLazyGetter(this, "KeyShortcuts", function() {
   const isMac = AppConstants.platform == "macosx";
 
   // Common modifier shared by most key shortcuts
   const modifiers = isMac ? "accel,alt" : "accel,shift";
rename from devtools/shim/devtools-startup.manifest
rename to devtools/startup/devtools-startup.manifest
rename from devtools/shim/jar.mn
rename to devtools/startup/jar.mn
--- a/devtools/shim/jar.mn
+++ b/devtools/startup/jar.mn
@@ -1,14 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-devtools-shim.jar:
-%   content devtools-shim %content/
+devtools-startup.jar:
+%   content devtools-startup %content/
     content/aboutdevtools/aboutdevtools.xhtml  (aboutdevtools/aboutdevtools.xhtml)
     content/aboutdevtools/aboutdevtools.css (aboutdevtools/aboutdevtools.css)
     content/aboutdevtools/aboutdevtools.js (aboutdevtools/aboutdevtools.js)
     content/aboutdevtools/subscribe.css (aboutdevtools/subscribe.css)
     content/aboutdevtools/subscribe.js (aboutdevtools/subscribe.js)
 
     content/aboutdevtools/images/otter.svg (aboutdevtools/images/otter.svg)
 
rename from devtools/shim/locales/en-US/aboutdevtools.dtd
rename to devtools/startup/locales/en-US/aboutdevtools.dtd
rename from devtools/shim/locales/en-US/aboutdevtools.properties
rename to devtools/startup/locales/en-US/aboutdevtools.properties
rename from devtools/shim/locales/en-US/key-shortcuts.properties
rename to devtools/startup/locales/en-US/key-shortcuts.properties
rename from devtools/shim/locales/en-US/startup.properties
rename to devtools/startup/locales/en-US/startup.properties
rename from devtools/shim/locales/jar.mn
rename to devtools/startup/locales/jar.mn
--- a/devtools/shim/locales/jar.mn
+++ b/devtools/startup/locales/jar.mn
@@ -1,8 +1,8 @@
 #filter substitution
 # 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/.
 
 @AB_CD@.jar:
-%   locale devtools-shim @AB_CD@ %locale/@AB_CD@/devtools/shim/
-    locale/@AB_CD@/devtools/shim/ (%*)
+%   locale devtools-startup @AB_CD@ %locale/@AB_CD@/devtools/startup/
+    locale/@AB_CD@/devtools/startup/ (%*)
rename from devtools/shim/locales/moz.build
rename to devtools/startup/locales/moz.build
rename from devtools/shim/moz.build
rename to devtools/startup/moz.build
--- a/devtools/shim/moz.build
+++ b/devtools/startup/moz.build
@@ -5,18 +5,18 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 JAR_MANIFESTS += ['jar.mn']
 
 JS_PREFERENCE_PP_FILES += [
     'devtools-startup-prefs.js',
 ]
 
-# Register the about:debugging page only for 'addon' and 'all' builds.
-if CONFIG['MOZ_DEVTOOLS'] != 'server':
+# Register the startup components only for 'all' builds.
+if CONFIG['MOZ_DEVTOOLS'] == 'all':
     EXTRA_COMPONENTS += [
         'aboutdebugging-registration.js',
         'aboutdebugging.manifest',
         'aboutdevtoolstoolbox-registration.js',
         'aboutdevtoolstoolbox.manifest',
         'devtools-startup.js',
         'devtools-startup.manifest',
     ]
rename from devtools/shim/tests/browser/.eslintrc.js
rename to devtools/startup/tests/browser/.eslintrc.js
rename from devtools/shim/tests/browser/browser.ini
rename to devtools/startup/tests/browser/browser.ini
rename from devtools/shim/tests/browser/browser_shim_disable_devtools.js
rename to devtools/startup/tests/browser/browser_shim_disable_devtools.js
rename from devtools/shim/tests/unit/.eslintrc.js
rename to devtools/startup/tests/unit/.eslintrc.js
--- a/devtools/shim/tests/unit/.eslintrc.js
+++ b/devtools/startup/tests/unit/.eslintrc.js
@@ -1,21 +1,5 @@
-// This file was copied from the .eslintrc.xpcshell.js
-// This new xpcshell test folder should stay in mozilla-central while devtools move to a
-// GitHub repository, hence the duplication.
+"use strict";
+
 module.exports = {
-  "extends": [
-    "plugin:mozilla/xpcshell-test"
-  ],
-  "rules": {
-    // Allow non-camelcase so that run_test doesn't produce a warning.
-    "camelcase": "off",
-    // Allow using undefined variables so that tests can refer to functions
-    // and variables defined in head.js files, without having to maintain a
-    // list of globals in each .eslintrc file.
-    // Note that bug 1168340 will eventually help auto-registering globals
-    // from head.js files.
-    "no-undef": "off",
-    "block-scoped-var": "off",
-    // Tests can always import anything.
-    "mozilla/reject-some-requires": "off",
-  }
+  "extends": "../../../.eslintrc.xpcshell.js",
 };
rename from devtools/shim/tests/unit/test_devtools_shim.js
rename to devtools/startup/tests/unit/test_devtools_shim.js
--- a/devtools/shim/tests/unit/test_devtools_shim.js
+++ b/devtools/startup/tests/unit/test_devtools_shim.js
@@ -1,16 +1,16 @@
 /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const { DevToolsShim } =
-    ChromeUtils.import("chrome://devtools-shim/content/DevToolsShim.jsm", {});
+    ChromeUtils.import("chrome://devtools-startup/content/DevToolsShim.jsm", {});
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
 
 // Test the DevToolsShim
 
 /**
  * Create a mocked version of DevTools that records all calls made to methods expected
  * to be called by DevToolsShim.
  */
@@ -57,17 +57,17 @@ function checkCalls(mock, method, length
         `Devtools.${method} was called with the expected argument (index ${i})`);
   }
 }
 
 function test_register_unregister() {
   ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
 
   DevToolsShim.register(createMockDevTools());
-  ok(DevToolsShim.isInitialized(), "DevTools are installed");
+  ok(DevToolsShim.isInitialized(), "DevTools are initialized");
 
   DevToolsShim.unregister();
   ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
 }
 
 function test_on_is_forwarded_to_devtools() {
   ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
 
rename from devtools/shim/tests/unit/xpcshell.ini
rename to devtools/startup/tests/unit/xpcshell.ini
--- a/dom/base/ChromeUtils.cpp
+++ b/dom/base/ChromeUtils.cpp
@@ -8,16 +8,17 @@
 
 #include "jsfriendapi.h"
 #include "WrapperFactory.h"
 
 #include "mozilla/Base64.h"
 #include "mozilla/BasePrincipal.h"
 #include "mozilla/CycleCollectedJSRuntime.h"
 #include "mozilla/TimeStamp.h"
+#include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/IdleDeadline.h"
 #include "mozilla/dom/UnionTypes.h"
 #include "mozilla/dom/WindowBinding.h" // For IdleRequestCallback/Options
 #include "nsThreadUtils.h"
 #include "mozJSComponentLoader.h"
 #include "GeckoProfiler.h"
 
 namespace mozilla {
@@ -648,16 +649,29 @@ ChromeUtils::ClearRecentJSDevError(Globa
 {
   auto runtime = CycleCollectedJSRuntime::Get();
   MOZ_ASSERT(runtime);
 
   runtime->ClearRecentDevError();
 }
 #endif // NIGHTLY_BUILD
 
+#ifndef RELEASE_OR_BETA
+/* static */ void
+ChromeUtils::RequestPerformanceMetrics(GlobalObject&)
+{
+  MOZ_ASSERT(XRE_IsParentProcess());
+  nsTArray<ContentParent*> children;
+  ContentParent::GetAll(children);
+  for (uint32_t i = 0; i < children.Length(); i++) {
+    mozilla::Unused << children[i]->SendRequestPerformanceMetrics();
+  }
+}
+#endif
+
 constexpr auto kSkipSelfHosted = JS::SavedFrameSelfHosted::Exclude;
 
 /* static */ void
 ChromeUtils::GetCallerLocation(const GlobalObject& aGlobal, nsIPrincipal* aPrincipal,
                                JS::MutableHandle<JSObject*> aRetval)
 {
   JSContext* cx = aGlobal.Context();
 
--- a/dom/base/ChromeUtils.h
+++ b/dom/base/ChromeUtils.h
@@ -150,16 +150,20 @@ public:
                            ErrorResult& aRv);
 
   static void GetRecentJSDevError(GlobalObject& aGlobal,
                                   JS::MutableHandleValue aRetval,
                                   ErrorResult& aRv);
 
   static void ClearRecentJSDevError(GlobalObject& aGlobal);
 
+#ifndef RELEASE_OR_BETA
+  static void RequestPerformanceMetrics(GlobalObject& aGlobal);
+#endif
+
   static void Import(const GlobalObject& aGlobal,
                      const nsAString& aResourceURI,
                      const Optional<JS::Handle<JSObject*>>& aTargetObj,
                      JS::MutableHandle<JSObject*> aRetval,
                      ErrorResult& aRv);
 
   static void DefineModuleGetter(const GlobalObject& global,
                                  JS::Handle<JSObject*> target,
--- a/dom/base/DocGroup.cpp
+++ b/dom/base/DocGroup.cpp
@@ -1,19 +1,25 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/DocGroup.h"
+#include "mozilla/dom/DOMTypes.h"
 #include "mozilla/dom/TabGroup.h"
 #include "mozilla/Telemetry.h"
 #include "nsIDocShell.h"
 #include "nsDOMMutationObserver.h"
+#if defined(XP_WIN)
+#include <processthreadsapi.h>  // for GetCurrentProcessId()
+#else
+#include <unistd.h> // for getpid()
+#endif // defined(XP_WIN)
 
 namespace mozilla {
 namespace dom {
 
 AutoTArray<RefPtr<DocGroup>, 2>* DocGroup::sPendingDocGroups = nullptr;
 
 /* static */ nsresult
 DocGroup::GetKey(nsIPrincipal* aPrincipal, nsACString& aKey)
@@ -56,16 +62,78 @@ DocGroup::~DocGroup()
   if (!NS_IsMainThread()) {
     nsIEventTarget* target = EventTargetFor(TaskCategory::Other);
     NS_ProxyRelease("DocGroup::mReactionsStack", target, mReactionsStack.forget());
   }
 
   mTabGroup->mDocGroups.RemoveEntry(mKey);
 }
 
+#ifndef RELEASE_OR_BETA
+PerformanceInfo
+DocGroup::ReportPerformanceInfo()
+{
+  AssertIsOnMainThread();
+#if defined(XP_WIN)
+  uint32_t pid = GetCurrentProcessId();
+#else
+  uint32_t pid = getpid();
+#endif
+  uint64_t wid = 0;
+  uint64_t pwid = 0;
+  uint16_t count = 0;
+  uint64_t duration = 0;
+  nsCString host = NS_LITERAL_CSTRING("None");
+
+  for (const auto& document : *this) {
+    // grabbing the host name of the first document
+    nsCOMPtr<nsIDocument> doc = do_QueryInterface(document);
+    MOZ_ASSERT(doc);
+    nsCOMPtr<nsIURI> docURI = doc->GetDocumentURI();
+    if (!docURI) {
+      continue;
+    }
+    docURI->GetHost(host);
+    wid = doc->OuterWindowID();
+
+    // getting the top window id - if not possible
+    // pwid gets the same value than wid
+    pwid = wid;
+    nsPIDOMWindowInner* win = doc->GetInnerWindow();
+    if (win) {
+      nsPIDOMWindowOuter* outer = win->GetOuterWindow();
+      if (outer) {
+        nsCOMPtr<nsPIDOMWindowOuter> top = outer->GetTop();
+        if (top) {
+          pwid = top->WindowID();
+        }
+      }
+    }
+  }
+
+  duration = mPerformanceCounter->GetExecutionDuration();
+  FallibleTArray<CategoryDispatch> items;
+
+  // now that we have the host and window ids, let's look at the perf counters
+  for (uint32_t index = 0; index < (uint32_t)TaskCategory::Count; index++) {
+    TaskCategory category = static_cast<TaskCategory>(index);
+    count = mPerformanceCounter->GetDispatchCount(DispatchCategory(category));
+    CategoryDispatch item = CategoryDispatch(index, count);
+    if (!items.AppendElement(item, fallible)) {
+      NS_ERROR("Could not complete the operation");
+      return PerformanceInfo(host, pid, wid, pwid, duration, false, items);
+    }
+  }
+
+  // setting back all counters to zero
+  mPerformanceCounter->ResetPerformanceCounters();
+  return PerformanceInfo(host, pid, wid, pwid, duration, false, items);
+}
+#endif
+
 nsresult
 DocGroup::Dispatch(TaskCategory aCategory,
                    already_AddRefed<nsIRunnable>&& aRunnable)
 {
 #ifndef RELEASE_OR_BETA
   mPerformanceCounter->IncrementDispatchCounter(DispatchCategory(aCategory));
 #endif
   return mTabGroup->DispatchWithDocGroup(aCategory, Move(aRunnable), this);
--- a/dom/base/DocGroup.h
+++ b/dom/base/DocGroup.h
@@ -18,16 +18,18 @@
 #include "mozilla/dom/HTMLSlotElement.h"
 #include "mozilla/PerformanceCounter.h"
 
 
 namespace mozilla {
 class AbstractThread;
 namespace dom {
 
+class PerformanceInfo;
+
 // Two browsing contexts are considered "related" if they are reachable from one
 // another through window.opener, window.parent, or window.frames. This is the
 // spec concept of a "unit of related browsing contexts"
 //
 // Two browsing contexts are considered "similar-origin" if they can be made to
 // have the same origin by setting document.domain. This is the spec concept of
 // a "unit of similar-origin related browsing contexts"
 //
@@ -56,16 +58,19 @@ public:
   {
     return aKey == mKey;
   }
 #ifndef RELEASE_OR_BETA
   PerformanceCounter* GetPerformanceCounter()
   {
     return mPerformanceCounter;
   }
+
+  PerformanceInfo
+  ReportPerformanceInfo();
 #endif
   TabGroup* GetTabGroup()
   {
     return mTabGroup;
   }
   mozilla::dom::CustomElementReactionsStack* CustomElementReactionsStack()
   {
     MOZ_ASSERT(NS_IsMainThread());
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -21,16 +21,17 @@ XPIDL_SOURCES += [
     'nsIDOMDOMRequest.idl',
     'nsIDOMParser.idl',
     'nsIDOMSerializer.idl',
     'nsIDroppedLinkHandler.idl',
     'nsIFrameLoader.idl',
     'nsIImageLoadingContent.idl',
     'nsIMessageManager.idl',
     'nsIObjectLoadingContent.idl',
+    'nsIPerformanceMetrics.idl',
     'nsIRemoteWindowContext.idl',
     'nsIScriptChannel.idl',
     'nsISelection.idl',
     'nsISelectionController.idl',
     'nsISelectionDisplay.idl',
     'nsISelectionListener.idl',
     'nsISelectionPrivate.idl',
     'nsISlowScriptDebug.idl',
@@ -100,16 +101,17 @@ EXPORTS += [
     'nsITimeoutHandler.h',
     'nsJSEnvironment.h',
     'nsJSUtils.h',
     'nsLineBreaker.h',
     'nsMappedAttributeElement.h',
     'nsNameSpaceManager.h',
     'nsNodeInfoManager.h',
     'nsNodeUtils.h',
+    'nsPerformanceMetrics.h',
     'nsPIDOMWindow.h',
     'nsPIDOMWindowInlines.h',
     'nsPIWindowRoot.h',
     'nsPropertyTable.h',
     'nsRange.h',
     'nsSandboxFlags.h',
     'nsStructuredCloneContainer.h',
     'nsStubAnimationObserver.h',
@@ -308,16 +310,17 @@ UNIFIED_SOURCES += [
     'nsMappedAttributeElement.cpp',
     'nsMappedAttributes.cpp',
     'nsMimeTypeArray.cpp',
     'nsNameSpaceManager.cpp',
     'nsNoDataProtocolContentPolicy.cpp',
     'nsNodeInfoManager.cpp',
     'nsNodeUtils.cpp',
     'nsOpenURIInFrameParams.cpp',
+    'nsPerformanceMetrics.cpp',
     'nsPlainTextSerializer.cpp',
     'nsPropertyTable.cpp',
     'nsQueryContentEventResult.cpp',
     'nsRange.cpp',
     'nsScreen.cpp',
     'nsScriptNameSpaceManager.cpp',
     'nsStructuredCloneContainer.cpp',
     'nsStubAnimationObserver.cpp',
new file mode 100644
--- /dev/null
+++ b/dom/base/nsIPerformanceMetrics.idl
@@ -0,0 +1,49 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-*/
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsIArray.idl"
+
+/*
+ * nsIPerformanceMetricsData is used to store performance data collected
+ * in all content processes by nsThread and nsWorkerThread.
+ *
+ * Each (host, category, pid, wid, pwid) is unique to a given DocGroup or
+ * Worker, and we collect the number of dispatches and execution duration.
+ *
+ * This XPCOM interface reflects the data collected in Performance counters.
+ * see xpcom/threads/PerformanceCounter.h
+ */
+[scriptable, builtinclass, uuid(1f9a58c9-be37-4463-8996-c7f5b9a5bef8)]
+interface nsIPerformanceMetricsDispatchCategory : nsISupports
+{
+  // DispatchCategory value
+  readonly attribute unsigned long category;
+  // Number of dispatch.
+  readonly attribute unsigned long count;
+};
+
+
+[scriptable, builtinclass, uuid(02b0cdc6-4be2-4154-a8a9-e8d462073200)]
+interface nsIPerformanceMetricsData : nsISupports
+{
+  // Host of the document, if any
+  readonly attribute AUTF8String host;
+  // process id
+  readonly attribute unsigned long pid;
+  // window id
+  readonly attribute unsigned long long wid;
+  // "parent" window id
+  readonly attribute unsigned long long pwid;
+  // Execution time in microseconds
+  readonly attribute unsigned long long duration;
+  // True if the data is collected in a worker
+  readonly attribute bool worker;
+  // Dispatch Category counters
+  readonly attribute nsIArray items;
+};
+
+
new file mode 100644
--- /dev/null
+++ b/dom/base/nsPerformanceMetrics.cpp
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- *//* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <nsIMutableArray.h>
+#include <nsArrayUtils.h>
+#include <nsPerformanceMetrics.h>
+
+/* ------------------------------------------------------
+ *
+ * class PerformanceMetricsDispatchCategory
+ *
+ */
+
+PerformanceMetricsDispatchCategory::PerformanceMetricsDispatchCategory(uint32_t aCategory, uint32_t aCount)
+  : mCategory(aCategory), mCount(aCount)
+{
+}
+
+NS_IMPL_ISUPPORTS(PerformanceMetricsDispatchCategory,
+                  nsIPerformanceMetricsDispatchCategory);
+
+
+NS_IMETHODIMP
+PerformanceMetricsDispatchCategory::GetCategory(uint32_t* aCategory)
+{
+  *aCategory = mCategory;
+  return NS_OK;
+};
+
+NS_IMETHODIMP
+PerformanceMetricsDispatchCategory::GetCount(uint32_t* aCount)
+{
+  *aCount = mCount;
+  return NS_OK;
+};
+
+/* ------------------------------------------------------
+ *
+ * class PerformanceMetricsData
+ *
+ */
+
+PerformanceMetricsData::PerformanceMetricsData(uint32_t aPid, uint64_t aWid,
+                                               uint64_t aPwid, const nsCString& aHost,
+                                               uint64_t aDuration, bool aWorker,
+                                               nsIArray* aItems)
+  : mPid(aPid), mWid(aWid), mPwid(aPwid), mHost(aHost)
+  , mDuration(aDuration), mWorker(aWorker)
+{
+    uint32_t len;
+    nsresult rv = aItems->GetLength(&len);
+    if (NS_FAILED(rv)) {
+      NS_ASSERTION(rv == NS_OK, "Failed to ge the length");
+    }
+    for (uint32_t i = 0; i < len; i++) {
+        nsCOMPtr<nsIPerformanceMetricsDispatchCategory> item = do_QueryElementAt(aItems, i);
+        mItems.AppendElement(item);
+    }
+};
+
+NS_IMPL_ISUPPORTS(PerformanceMetricsData, nsIPerformanceMetricsData);
+
+NS_IMETHODIMP
+PerformanceMetricsData::GetHost(nsACString& aHost)
+{
+  aHost = mHost;
+  return NS_OK;
+};
+
+NS_IMETHODIMP
+PerformanceMetricsData::GetWorker(bool* aWorker)
+{
+  *aWorker = mWorker;
+  return NS_OK;
+};
+
+NS_IMETHODIMP
+PerformanceMetricsData::GetPid(uint32_t* aPid)
+{
+  *aPid = mPid;
+  return NS_OK;
+};
+
+NS_IMETHODIMP
+PerformanceMetricsData::GetWid(uint64_t* aWid)
+{
+  *aWid = mWid;
+  return NS_OK;
+};
+
+NS_IMETHODIMP
+PerformanceMetricsData::GetDuration(uint64_t* aDuration)
+{
+  *aDuration = mDuration;
+  return NS_OK;
+};
+
+NS_IMETHODIMP
+PerformanceMetricsData::GetPwid(uint64_t* aPwid)
+{
+  *aPwid = mPwid;
+  return NS_OK;
+};
+
+NS_IMETHODIMP
+PerformanceMetricsData::GetItems(nsIArray** aItems)
+{
+  NS_ENSURE_ARG_POINTER(aItems);
+  *aItems = nullptr;
+
+  nsresult rv = NS_OK;
+  nsCOMPtr<nsIMutableArray> items =
+    do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  uint32_t len = mItems.Length();
+  for (uint32_t i = 0; i < len; i++) {
+    items->AppendElement(mItems[i]);
+  }
+
+  items.forget(aItems);
+  return NS_OK;
+}
new file mode 100644
--- /dev/null
+++ b/dom/base/nsPerformanceMetrics.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsPerformanceMetrics_h___
+#define nsPerformanceMetrics_h___
+
+#include "nsIPerformanceMetrics.h"
+
+
+class PerformanceMetricsDispatchCategory final : public nsIPerformanceMetricsDispatchCategory
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIPERFORMANCEMETRICSDISPATCHCATEGORY
+  PerformanceMetricsDispatchCategory(uint32_t aCategory, uint32_t aCount);
+private:
+  ~PerformanceMetricsDispatchCategory() = default;
+
+  uint32_t mCategory;
+  uint32_t mCount;
+};
+
+
+class PerformanceMetricsData final : public nsIPerformanceMetricsData
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIPERFORMANCEMETRICSDATA
+  PerformanceMetricsData(uint32_t aPid, uint64_t aWid, uint64_t aPwid, const nsCString& aHost,
+                         uint64_t aDuration, bool aWorker, nsIArray* aItems);
+private:
+  ~PerformanceMetricsData() = default;
+
+  uint32_t mPid;
+  uint64_t mWid;
+  uint64_t mPwid;
+  nsCString mHost;
+  uint64_t mDuration;
+  bool mWorker;
+  nsCOMArray<nsIPerformanceMetricsDispatchCategory> mItems;
+};
+
+#endif  // end nsPerformanceMetrics_h__
--- a/dom/bindings/CallbackObject.cpp
+++ b/dom/bindings/CallbackObject.cpp
@@ -10,16 +10,17 @@
 #include "jsfriendapi.h"
 #include "nsIScriptGlobalObject.h"
 #include "nsIXPConnect.h"
 #include "nsIScriptContext.h"
 #include "nsPIDOMWindow.h"
 #include "nsJSUtils.h"
 #include "xpcprivate.h"
 #include "WorkerPrivate.h"
+#include "nsContentUtils.h"
 #include "nsGlobalWindow.h"
 #include "WorkerScope.h"
 #include "jsapi.h"
 #include "nsJSPrincipals.h"
 
 namespace mozilla {
 namespace dom {
 
--- a/dom/chrome-webidl/ChromeUtils.webidl
+++ b/dom/chrome-webidl/ChromeUtils.webidl
@@ -118,16 +118,20 @@ namespace ChromeUtils {
   readonly attribute any recentJSDevError;
 
   /**
    * Reset `recentJSDevError` to `undefined` for the current JSRuntime.
    */
   void clearRecentJSDevError();
 #endif // NIGHTLY_BUILD
 
+#ifndef RELEASE_OR_BETA
+  void requestPerformanceMetrics();
+#endif
+
   /**
    * IF YOU ADD NEW METHODS HERE, MAKE SURE THEY ARE THREAD-SAFE.
    */
 };
 
 /**
  * Additional ChromeUtils methods that are _not_ thread-safe, and hence not
  * exposed in workers.
--- a/dom/file/nsHostObjectURI.h
+++ b/dom/file/nsHostObjectURI.h
@@ -26,27 +26,28 @@
  * "mediasource".
  */
 class nsHostObjectURI final
   : public mozilla::net::nsSimpleURI
   , public nsIURIWithPrincipal
   , public nsIURIWithBlobImpl
   , public nsSupportsWeakReference
 {
-public:
+private:
   nsHostObjectURI(nsIPrincipal* aPrincipal,
                   mozilla::dom::BlobImpl* aBlobImpl)
     : mozilla::net::nsSimpleURI()
     , mPrincipal(aPrincipal)
     , mBlobImpl(aBlobImpl)
   {}
 
   // For use only from deserialization
   nsHostObjectURI() : mozilla::net::nsSimpleURI() {}
 
+public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_NSIURIWITHBLOBIMPL
   NS_DECL_NSIURIWITHPRINCIPAL
   NS_DECL_NSISERIALIZABLE
   NS_DECL_NSICLASSINFO
   NS_DECL_NSIIPCSERIALIZABLEURI
 
   // Override CloneInternal() and EqualsInternal()
--- a/dom/indexedDB/IDBFactory.cpp
+++ b/dom/indexedDB/IDBFactory.cpp
@@ -9,16 +9,17 @@
 #include "BackgroundChildImpl.h"
 #include "IDBRequest.h"
 #include "IndexedDatabaseManager.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/SystemGroup.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/IDBFactoryBinding.h"
+#include "mozilla/dom/quota/QuotaManager.h"
 #include "mozilla/dom/TabChild.h"
 #include "mozilla/ipc/BackgroundChild.h"
 #include "mozilla/ipc/BackgroundUtils.h"
 #include "mozilla/ipc/PBackground.h"
 #include "mozilla/ipc/PBackgroundChild.h"
 #include "mozilla/Telemetry.h"
 #include "mozIThirdPartyUtil.h"
 #include "nsAboutProtocolUtils.h"
@@ -681,18 +682,24 @@ IDBFactory::OpenInternal(JSContext* aCx,
   if (mBackgroundActorFailed) {
     IDB_REPORT_INTERNAL_ERR();
     aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
     return nullptr;
   }
 
   PersistenceType persistenceType;
 
-  if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
-    // Chrome privilege always gets persistent storage.
+  bool isInternal = principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo;
+  if (!isInternal && principalInfo.type() == PrincipalInfo::TContentPrincipalInfo) {
+    nsCString origin = principalInfo.get_ContentPrincipalInfo().originNoSuffix();
+    isInternal = QuotaManager::IsOriginInternal(origin);
+  }
+
+  if (isInternal) {
+    // Chrome privilege and internal origins always get persistent storage.
     persistenceType = PERSISTENCE_TYPE_PERSISTENT;
   } else {
     persistenceType = PersistenceTypeFromStorage(aStorageType);
   }
 
   DatabaseMetadata& metadata = commonParams.metadata();
   metadata.name() = aName;
   metadata.persistenceType() = persistenceType;
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -38,16 +38,18 @@
 #include "mozilla/dom/MemoryReportRequest.h"
 #include "mozilla/dom/PLoginReputationChild.h"
 #include "mozilla/dom/ProcessGlobal.h"
 #include "mozilla/dom/PushNotifier.h"
 #include "mozilla/dom/ServiceWorkerManager.h"
 #include "mozilla/dom/TabGroup.h"
 #include "mozilla/dom/nsIContentChild.h"
 #include "mozilla/dom/URLClassifierChild.h"
+#include "mozilla/dom/WorkerDebugger.h"
+#include "mozilla/dom/WorkerDebuggerManager.h"
 #include "mozilla/gfx/gfxVars.h"
 #include "mozilla/gfx/Logging.h"
 #include "mozilla/psm/PSMContentListener.h"
 #include "mozilla/hal_sandbox/PHalChild.h"
 #include "mozilla/ipc/BackgroundChild.h"
 #include "mozilla/ipc/FileDescriptorSetChild.h"
 #include "mozilla/ipc/FileDescriptorUtils.h"
 #include "mozilla/ipc/GeckoChildProcessHost.h"
@@ -73,16 +75,19 @@
 #include "nsBaseDragService.h"
 #include "mozilla/media/MediaChild.h"
 #include "mozilla/BasePrincipal.h"
 #include "mozilla/WebBrowserPersistDocumentChild.h"
 #include "mozilla/HangDetails.h"
 #include "imgLoader.h"
 #include "GMPServiceChild.h"
 #include "NullPrincipal.h"
+#include "nsIPerformanceMetrics.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIWorkerDebuggerManager.h"
 
 #if !defined(XP_WIN)
 #include "mozilla/Omnijar.h"
 #endif
 
 #ifdef MOZ_GECKO_PROFILER
 #include "ChildProfilerController.h"
 #endif
@@ -1367,16 +1372,48 @@ ContentChild::GetResultForRenderingInitF
 
   // If we are talking to the GPU process, then we should recover from this on
   // the next ContentChild::RecvReinitRendering call.
   gfxCriticalNote << "Could not initialize rendering with GPU process";
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
+ContentChild::RecvRequestPerformanceMetrics()
+{
+#ifndef RELEASE_OR_BETA
+  // iterate on all WorkerDebugger
+  RefPtr<WorkerDebuggerManager> wdm = WorkerDebuggerManager::GetOrCreate();
+  if (NS_WARN_IF(!wdm)) {
+    return IPC_OK();
+  }
+
+  for (uint32_t index = 0; index < wdm->GetDebuggersLength(); index++) {
+    WorkerDebugger* debugger = wdm->GetDebuggerAt(index);
+    MOZ_ASSERT(debugger);
+    SendAddPerformanceMetrics(debugger->ReportPerformanceInfo());
+  }
+
+  // iterate on all DocGroup
+  nsTArray<RefPtr<TabChild>> tabs = TabChild::GetAll();
+  for (const auto& tabChild : tabs) {
+    TabGroup* tabGroup = tabChild->TabGroup();
+    for (auto iter = tabGroup->Iter(); !iter.Done(); iter.Next()) {
+        RefPtr<DocGroup> docGroup = iter.Get()->mDocGroup;
+        SendAddPerformanceMetrics(docGroup->ReportPerformanceInfo());
+    }
+  }
+  return IPC_OK();
+#endif
+#ifdef RELEASE_OR_BETA
+  return IPC_OK();
+#endif
+}
+
+mozilla::ipc::IPCResult
 ContentChild::RecvInitRendering(Endpoint<PCompositorManagerChild>&& aCompositor,
                                 Endpoint<PImageBridgeChild>&& aImageBridge,
                                 Endpoint<PVRManagerChild>&& aVRBridge,
                                 Endpoint<PVideoDecoderManagerChild>&& aVideoManager,
                                 nsTArray<uint32_t>&& namespaces)
 {
   MOZ_ASSERT(namespaces.Length() == 3);
 
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -185,16 +185,19 @@ public:
   RecvInitRendering(
     Endpoint<PCompositorManagerChild>&& aCompositor,
     Endpoint<PImageBridgeChild>&& aImageBridge,
     Endpoint<PVRManagerChild>&& aVRBridge,
     Endpoint<PVideoDecoderManagerChild>&& aVideoManager,
     nsTArray<uint32_t>&& namespaces) override;
 
   mozilla::ipc::IPCResult
+  RecvRequestPerformanceMetrics() override;
+
+  mozilla::ipc::IPCResult
   RecvReinitRendering(
     Endpoint<PCompositorManagerChild>&& aCompositor,
     Endpoint<PImageBridgeChild>&& aImageBridge,
     Endpoint<PVRManagerChild>&& aVRBridge,
     Endpoint<PVideoDecoderManagerChild>&& aVideoManager,
     nsTArray<uint32_t>&& namespaces) override;
 
   virtual mozilla::ipc::IPCResult RecvAudioDefaultDeviceChange() override;
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -187,16 +187,17 @@
 #include "nsPluginHost.h"
 #include "nsPluginTags.h"
 #include "nsIBlocklistService.h"
 #include "mozilla/StyleSheet.h"
 #include "mozilla/StyleSheetInlines.h"
 #include "nsHostObjectProtocolHandler.h"
 #include "nsICaptivePortalService.h"
 #include "nsIObjectLoadingContent.h"
+#include "nsPerformanceMetrics.h"
 
 #include "nsIBidiKeyboard.h"
 
 #include "nsLayoutStylesheetCache.h"
 
 #include "mozilla/Sprintf.h"
 
 #ifdef MOZ_WEBRTC
@@ -3324,16 +3325,54 @@ ContentParent::RecvFinishMemoryReport(co
 {
   if (mMemoryReportRequest) {
     mMemoryReportRequest->Finish(aGeneration);
     mMemoryReportRequest = nullptr;
   }
   return IPC_OK();
 }
 
+mozilla::ipc::IPCResult
+ContentParent::RecvAddPerformanceMetrics(const PerformanceInfo& aMetrics)
+{
+#ifndef RELEASE_OR_BETA
+  // converting the data we get from a child as a notification
+  if (aMetrics.items().IsEmpty()) {
+      return IPC_OK();
+  }
+
+  nsCOMPtr<nsIMutableArray> xpItems = do_CreateInstance(NS_ARRAY_CONTRACTID);
+  if (NS_WARN_IF(!xpItems)) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  for (uint32_t i = 0; i<aMetrics.items().Length(); i++) {
+       const CategoryDispatch& entry = aMetrics.items()[i];
+       nsCOMPtr<nsIPerformanceMetricsDispatchCategory> item =
+           new PerformanceMetricsDispatchCategory(entry.category(),
+                                                  entry.count());
+       xpItems->AppendElement(item);
+  }
+
+  nsCOMPtr<nsIPerformanceMetricsData> data =
+      new PerformanceMetricsData(aMetrics.pid(), aMetrics.wid(), aMetrics.pwid(),
+                                 aMetrics.host(), aMetrics.duration(),
+                                 aMetrics.worker(), xpItems);
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (!obs) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+  obs->NotifyObservers(data, "performance-metrics", nullptr);
+  return IPC_OK();
+#endif
+#ifdef RELEASE_OR_BETA
+  return IPC_OK();
+#endif
+}
+
 PCycleCollectWithLogsParent*
 ContentParent::AllocPCycleCollectWithLogsParent(const bool& aDumpAllTraces,
                                                 const FileDescriptor& aGCLog,
                                                 const FileDescriptor& aCCLog)
 {
   MOZ_CRASH("Don't call this; use ContentParent::CycleCollectWithLogs");
 }
 
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -843,16 +843,17 @@ private:
    * Get or create the corresponding content parent array to |aContentProcessType|.
    */
   static nsTArray<ContentParent*>& GetOrCreatePool(const nsAString& aContentProcessType);
 
   virtual mozilla::ipc::IPCResult RecvInitBackground(Endpoint<mozilla::ipc::PBackgroundParent>&& aEndpoint) override;
 
   mozilla::ipc::IPCResult RecvAddMemoryReport(const MemoryReport& aReport) override;
   mozilla::ipc::IPCResult RecvFinishMemoryReport(const uint32_t& aGeneration) override;
+  mozilla::ipc::IPCResult RecvAddPerformanceMetrics(const PerformanceInfo& aMetrics) override;
 
   virtual bool
   DeallocPJavaScriptParent(mozilla::jsipc::PJavaScriptParent*) override;
 
   virtual bool
   DeallocPRemoteSpellcheckEngineParent(PRemoteSpellcheckEngineParent*) override;
 
   virtual PBrowserParent* AllocPBrowserParent(const TabId& aTabId,
--- a/dom/ipc/DOMTypes.ipdlh
+++ b/dom/ipc/DOMTypes.ipdlh
@@ -119,10 +119,48 @@ struct CreatedWindowInfo
   nsCString urlToLoad;
   TextureFactoryIdentifier textureFactoryIdentifier;
   uint64_t layersId;
   CompositorOptions compositorOptions;
   uint32_t maxTouchPoints;
   DimensionInfo dimensions;
 };
 
+
+/**
+ * PerformanceInfo is used to pass performance info stored
+ * in WorkerPrivate & DocGroup instances
+ *
+ * Each (host, pid, wid, pwid) is unique to a given DocGroup or
+ * Worker, and we collect the number of dispatches per Dispatch
+ * category and total execution duration.
+ *
+ * This IPDL struct reflects the data collected in Performance counters.
+ * see xpcom/threads/PerformanceCounter.h
+ */
+struct CategoryDispatch
+{
+  // DispatchCategory value
+  uint16_t category;
+  // Number of dispatch
+  uint16_t count;
+};
+
+struct PerformanceInfo
+{
+  // Host of the document, if any
+  nsCString host;
+  // process id
+  uint16_t pid;
+  // window id
+  uint64_t wid;
+  // "parent" window id
+  uint64_t pwid;
+  // Execution time in microseconds
+  uint64_t duration;
+  // True if the data is collected in a worker
+  bool worker;
+  // Counters per category. For workers, a single entry
+  CategoryDispatch[] items;
+};
+
 } // namespace dom
 } // namespace mozilla
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -395,16 +395,17 @@ child:
      * abnormally exit if this fails; the details are OS-specific.
      */
     async SetProcessSandbox(MaybeFileDesc aBroker);
 
     async RequestMemoryReport(uint32_t generation,
                               bool anonymize,
                               bool minimizeMemoryUsage,
                               MaybeFileDesc DMDFile);
+    async RequestPerformanceMetrics();
 
     /**
      * Communication between the PuppetBidiKeyboard and the actual
      * BidiKeyboard hosted by the parent
      */
     async BidiKeyboardNotify(bool isLangRTL, bool haveBidiKeyboards);
 
     /**
@@ -1128,16 +1129,18 @@ parent:
                              IHandlerControlHolder aHandlerControl);
 
     async AddMemoryReport(MemoryReport aReport);
     async FinishMemoryReport(uint32_t aGeneration);
 
     async MaybeReloadPlugins();
 
     async BHRThreadHang(HangDetails aHangDetails);
+
+    async AddPerformanceMetrics(PerformanceInfo aMetrics);
 both:
      async AsyncMessage(nsString aMessage, CpowEntry[] aCpows,
                         Principal aPrincipal, ClonedMessageData aData);
 
     /**
      * Notify `push-subscription-modified` observers in the parent and child.
      */
     async NotifyPushSubscriptionModifiedObservers(nsCString scope,
--- a/dom/jsurl/nsJSProtocolHandler.cpp
+++ b/dom/jsurl/nsJSProtocolHandler.cpp
@@ -1186,38 +1186,41 @@ nsJSProtocolHandler::GetProtocolFlags(ui
 }
 
 NS_IMETHODIMP
 nsJSProtocolHandler::NewURI(const nsACString &aSpec,
                             const char *aCharset,
                             nsIURI *aBaseURI,
                             nsIURI **result)
 {
-    nsresult rv;
+    nsresult rv = NS_OK;
 
     // javascript: URLs (currently) have no additional structure beyond that
     // provided by standard URLs, so there is no "outer" object given to
     // CreateInstance.
 
-    nsCOMPtr<nsIURI> url = new nsJSURI(aBaseURI);
-    NS_MutateURI mutator(url);
+    NS_MutateURI mutator(new nsJSURI::Mutator());
+    nsCOMPtr<nsIURI> base(aBaseURI);
+    mutator.Apply(NS_MutatorMethod(&nsIJSURIMutator::SetBase, base));
     if (!aCharset || !nsCRT::strcasecmp("UTF-8", aCharset)) {
-      mutator.SetSpec(aSpec);
+        mutator.SetSpec(aSpec);
     } else {
-      nsAutoCString utf8Spec;
-      rv = EnsureUTF8Spec(PromiseFlatCString(aSpec), aCharset, utf8Spec);
-      if (NS_SUCCEEDED(rv)) {
+        nsAutoCString utf8Spec;
+        rv = EnsureUTF8Spec(PromiseFlatCString(aSpec), aCharset, utf8Spec);
+        if (NS_FAILED(rv)) {
+            return rv;
+        }
         if (utf8Spec.IsEmpty()) {
-          mutator.SetSpec(aSpec);
+            mutator.SetSpec(aSpec);
         } else {
-          mutator.SetSpec(utf8Spec);
+            mutator.SetSpec(utf8Spec);
         }
-      }
     }
 
+    nsCOMPtr<nsIURI> url;
     rv = mutator.Finalize(url);
     if (NS_FAILED(rv)) {
         return rv;
     }
 
     url.forget(result);
     return rv;
 }
@@ -1386,17 +1389,18 @@ nsJSURI::StartClone(mozilla::net::nsSimp
     SetRefOnClone(url, refHandlingMode, newRef);
     return url;
 }
 
 // Queries this list of interfaces. If none match, it queries mURI.
 NS_IMPL_NSIURIMUTATOR_ISUPPORTS(nsJSURI::Mutator,
                                 nsIURISetters,
                                 nsIURIMutator,
-                                nsISerializable)
+                                nsISerializable,
+                                nsIJSURIMutator)
 
 NS_IMETHODIMP
 nsJSURI::Mutate(nsIURIMutator** aMutator)
 {
     RefPtr<nsJSURI::Mutator> mutator = new nsJSURI::Mutator();
     nsresult rv = mutator->InitFromURI(this);
     if (NS_FAILED(rv)) {
         return rv;
--- a/dom/jsurl/nsJSProtocolHandler.h
+++ b/dom/jsurl/nsJSProtocolHandler.h
@@ -9,16 +9,17 @@
 #include "mozilla/Attributes.h"
 #include "nsIProtocolHandler.h"
 #include "nsITextToSubURI.h"
 #include "nsIURI.h"
 #include "nsIMutable.h"
 #include "nsISerializable.h"
 #include "nsIClassInfo.h"
 #include "nsSimpleURI.h"
+#include "nsINestedURI.h"
 
 #define NS_JSPROTOCOLHANDLER_CID                     \
 { /* bfc310d2-38a0-11d3-8cd3-0060b0fc14a3 */         \
     0xbfc310d2,                                      \
     0x38a0,                                          \
     0x11d3,                                          \
     {0x8c, 0xd3, 0x00, 0x60, 0xb0, 0xfc, 0x14, 0xa3} \
 }
@@ -63,28 +64,23 @@ protected:
     virtual ~nsJSProtocolHandler();
 
     nsresult EnsureUTF8Spec(const nsCString& aSpec, const char *aCharset,
                             nsACString &aUTF8Spec);
 
     nsCOMPtr<nsITextToSubURI>  mTextToSubURI;
 };
 
-
 class nsJSURI final
     : public mozilla::net::nsSimpleURI
 {
 public:
     using mozilla::net::nsSimpleURI::Read;
     using mozilla::net::nsSimpleURI::Write;
 
-    nsJSURI() {}
-
-    explicit nsJSURI(nsIURI* aBaseURI) : mBaseURI(aBaseURI) {}
-
     nsIURI* GetBaseURI() const
     {
         return mBaseURI;
     }
 
     NS_DECL_ISUPPORTS_INHERITED
 
     // nsIURI overrides
@@ -100,32 +96,36 @@ public:
     NS_DECL_NSIIPCSERIALIZABLEURI
 
     // Override the nsIClassInfo method GetClassIDNoAlloc to make sure our
     // nsISerializable impl works right.
     NS_IMETHOD GetClassIDNoAlloc(nsCID *aClassIDNoAlloc) override;
     //NS_IMETHOD QueryInterface( const nsIID& aIID, void** aInstancePtr ) override;
 
 protected:
+    nsJSURI() {}
+    explicit nsJSURI(nsIURI* aBaseURI) : mBaseURI(aBaseURI) {}
+
     virtual ~nsJSURI() {}
 
     virtual nsresult EqualsInternal(nsIURI* other,
                                     RefHandlingEnum refHandlingMode,
                                     bool* result) override;
     bool Deserialize(const mozilla::ipc::URIParams&);
     nsresult ReadPrivate(nsIObjectInputStream *aStream);
 
 private:
     nsCOMPtr<nsIURI> mBaseURI;
 
 public:
     class Mutator final
         : public nsIURIMutator
         , public BaseURIMutator<nsJSURI>
         , public nsISerializable
+        , public nsIJSURIMutator
     {
         NS_DECL_ISUPPORTS
         NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI)
         NS_DEFINE_NSIMUTATOR_COMMON
 
         NS_IMETHOD
         Write(nsIObjectOutputStream *aOutputStream) override
         {
@@ -133,16 +133,23 @@ public:
         }
 
         MOZ_MUST_USE NS_IMETHOD
         Read(nsIObjectInputStream* aStream) override
         {
             return InitFromInputStream(aStream);
         }
 
+        MOZ_MUST_USE NS_IMETHOD
+        SetBase(nsIURI* aBaseURI) override
+        {
+            mURI = new nsJSURI(aBaseURI);
+            return NS_OK;
+        }
+
         explicit Mutator() { }
     private:
         virtual ~Mutator() { }
 
         friend class nsJSURI;
     };
 
     friend BaseURIMutator<nsJSURI>;
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -75,16 +75,17 @@
 #include "MediaEngineDefault.h"
 #if defined(MOZ_WEBRTC)
 #include "MediaEngineWebRTC.h"
 #include "browser_logging/WebRtcLog.h"
 #endif
 
 #if defined (XP_WIN)
 #include "mozilla/WindowsVersion.h"
+#include <objbase.h>
 #include <winsock2.h>
 #include <iphlpapi.h>
 #include <tchar.h>
 #endif
 
 // GetCurrentTime is defined in winbase.h as zero argument macro forwarding to
 // GetTickCount() and conflicts with MediaStream::GetCurrentTime.
 #ifdef GetCurrentTime
@@ -1991,33 +1992,62 @@ NS_IMPL_ISUPPORTS(MediaManager, nsIMedia
 MediaManager::IsInMediaThread()
 {
   return sSingleton?
       (sSingleton->mMediaThread->thread_id() == PlatformThread::CurrentId()) :
       false;
 }
 #endif
 
+#ifdef XP_WIN
+class MTAThread : public base::Thread {
+public:
+  explicit MTAThread(const char* aName)
+    : base::Thread(aName)
+    , mResult(E_FAIL)
+  {
+  }
+
+protected:
+  virtual void Init() override {
+    mResult = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
+  }
+
+  virtual void CleanUp() override {
+    if (SUCCEEDED(mResult)) {
+      CoUninitialize();
+    }
+  }
+
+private:
+  HRESULT mResult;
+};
+#endif
+
 // NOTE: never Dispatch(....,NS_DISPATCH_SYNC) to the MediaManager
 // thread from the MainThread, as we NS_DISPATCH_SYNC to MainThread
 // from MediaManager thread.
 
 // Guaranteed never to return nullptr.
 /* static */  MediaManager*
 MediaManager::Get() {
   if (!sSingleton) {
     MOZ_ASSERT(NS_IsMainThread());
 
     static int timesCreated = 0;
     timesCreated++;
     MOZ_RELEASE_ASSERT(timesCreated == 1);
 
     sSingleton = new MediaManager();
 
+#ifdef XP_WIN
+    sSingleton->mMediaThread = new MTAThread("MediaManager");
+#else
     sSingleton->mMediaThread = new base::Thread("MediaManager");
+#endif
     base::Thread::Options options;
 #if defined(_WIN32)
     options.message_loop_type = MessageLoop::TYPE_MOZILLA_NONMAINUITHREAD;
 #else
     options.message_loop_type = MessageLoop::TYPE_MOZILLA_NONMAINTHREAD;
 #endif
     if (!sSingleton->mMediaThread->StartWithOptions(options)) {
       MOZ_CRASH();
--- a/dom/media/webrtc/MediaEngineWebRTC.h
+++ b/dom/media/webrtc/MediaEngineWebRTC.h
@@ -156,17 +156,19 @@ public:
       mDeviceIndexes = new nsTArray<int>;
       mDeviceNames = new nsTArray<nsCString>;
       mDefaultDevice = -1;
     }
   }
 
   static void CleanupGlobalData()
   {
-    cubeb_device_collection_destroy(CubebUtils::GetCubebContext(), &mDevices);
+    if (mDevices.device) {
+      cubeb_device_collection_destroy(CubebUtils::GetCubebContext(), &mDevices);
+    }
     delete mDeviceIndexes;
     mDeviceIndexes = nullptr;
     delete mDeviceNames;
     mDeviceNames = nullptr;
   }
 
   int GetNumOfRecordingDevices(int& aDevices)
   {
--- a/dom/push/test/xpcshell/test_crypto_encrypt.js
+++ b/dom/push/test/xpcshell/test_crypto_encrypt.js
@@ -96,27 +96,28 @@ add_task(async function aes128gcm_simple
                                            {encoding},
                                            ciphertext);
   deepEqual(message, plaintext);
 });
 
 // Variable record size tests
 add_task(async function aes128gcm_rs() {
   let [recvPublicKey, recvPrivateKey] = await PushCrypto.generateKeys();
-  let payload = "x".repeat(1024 * 10);
 
   for (let rs of [-1, 0, 1, 17]) {
+    let payload = "x".repeat(1024);
     info(`testing expected failure with rs=${rs}`);
     let message = new TextEncoder("utf-8").encode(payload);
     let authSecret = crypto.getRandomValues(new Uint8Array(16));
     await Assert.rejects(PushCrypto.encrypt(message, recvPublicKey, authSecret, {rs}),
                          /recordsize is too small/);
   }
   for (let rs of [18, 50, 1024, 4096, 16384]) {
     info(`testing expected success with rs=${rs}`);
+    let payload = "x".repeat(rs * 3);
     let message = new TextEncoder("utf-8").encode(payload);
     let authSecret = crypto.getRandomValues(new Uint8Array(16));
     let {ciphertext, encoding} = await PushCrypto.encrypt(message, recvPublicKey, authSecret, {rs});
     Assert.equal(encoding, "aes128gcm");
     // and decrypt it.
     let plaintext = await PushCrypto.decrypt(recvPrivateKey, recvPublicKey,
                                              authSecret,
                                              {encoding},
@@ -125,18 +126,17 @@ add_task(async function aes128gcm_rs() {
   }
 });
 
 // And try and hit some edge-cases.
 add_task(async function aes128gcm_edgecases() {
   let [recvPublicKey, recvPrivateKey] = await PushCrypto.generateKeys();
 
   for (let size of [0, 4096-16, 4096-16-1, 4096-16+1,
-                    4095, 4096, 4097,
-                    1024*100]) {
+                    4095, 4096, 4097, 10240]) {
     info(`testing encryption of ${size} byte payload`);
     let message = new TextEncoder("utf-8").encode("x".repeat(size));
     let authSecret = crypto.getRandomValues(new Uint8Array(16));
     let {ciphertext, encoding} = await PushCrypto.encrypt(message, recvPublicKey, authSecret);
     Assert.equal(encoding, "aes128gcm");
     // and decrypt it.
     let plaintext = await PushCrypto.decrypt(recvPrivateKey, recvPublicKey,
                                              authSecret,
--- a/dom/tests/browser/browser.ini
+++ b/dom/tests/browser/browser.ini
@@ -67,8 +67,9 @@ support-files =
 support-files =
   test_new_window_from_content_child.html
 [browser_xhr_sandbox.js]
 [browser_noopener.js]
 support-files =
   test_noopener_source.html
   test_noopener_target.html
 [browser_noopener_null_uri.js]
+[browser_test_performance_metrics.js]
new file mode 100644
--- /dev/null
+++ b/dom/tests/browser/browser_test_performance_metrics.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const TEST_URL = "http://example.com/browser/dom/tests/browser/dummy.html";
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+
+add_task(async function test() {
+  if (!AppConstants.RELEASE_OR_BETA) {
+    SpecialPowers.setBoolPref('dom.performance.enable_scheduler_timing', true);
+    waitForExplicitFinish();
+
+    await BrowserTestUtils.withNewTab({ gBrowser, url: "http://example.com" },
+      async function(browser) {
+
+      // grab events..
+      var events = [];
+      function getInfoFromService(subject, topic, value) {
+        subject = subject.QueryInterface(Ci.nsIPerformanceMetricsData);
+        if (subject.host == "example.com") {
+          events.push(subject);
+        }
+      }
+      Services.obs.addObserver(getInfoFromService, "performance-metrics");
+
+      // trigger an IPDL call
+      ChromeUtils.requestPerformanceMetrics();
+
+      // wait until we get the events back
+      await BrowserTestUtils.waitForCondition(() => {
+        return events.length > 0;
+      }, "wait for events to come in", 100, 20);
+
+      // let's check the last example.com tab event we got
+      let last = events[0];
+      Assert.equal(last.host, "example.com", "host should be example.com");
+      Assert.ok(last.duration > 0, "Duration should be positive");
+
+      // let's look at the XPCOM data we got back
+      let items = last.items.QueryInterface(Ci.nsIMutableArray);
+      let enumerator = items.enumerate();
+      let total = 0;
+      while (enumerator.hasMoreElements()) {
+        let item = enumerator.getNext();
+        item = item.QueryInterface(Ci.nsIPerformanceMetricsDispatchCategory);
+        total += item.count;
+      }
+      Assert.ok(total > 0);
+    });
+    SpecialPowers.clearUserPref('dom.performance.enable_scheduler_timing');
+  }
+});
--- a/dom/workers/WorkerDebugger.cpp
+++ b/dom/workers/WorkerDebugger.cpp
@@ -12,16 +12,21 @@
 #include "nsQueryObject.h"
 #include "nsThreadUtils.h"
 #include "ScriptLoader.h"
 #include "WorkerCommon.h"
 #include "WorkerError.h"
 #include "WorkerPrivate.h"
 #include "WorkerRunnable.h"
 #include "WorkerScope.h"
+#if defined(XP_WIN)
+#include <processthreadsapi.h>  // for GetCurrentProcessId()
+#else
+#include <unistd.h> // for getpid()
+#endif // defined(XP_WIN)
 
 namespace mozilla {
 namespace dom {
 
 namespace {
 
 class DebuggerMessageEventRunnable : public WorkerDebuggerRunnable
 {
@@ -466,11 +471,49 @@ WorkerDebugger::ReportErrorToDebuggerOnM
   }
 
   WorkerErrorReport report;
   report.mMessage = aMessage;
   report.mFilename = aFilename;
   WorkerErrorReport::LogErrorToConsole(report, 0);
 }
 
+#ifndef RELEASE_OR_BETA
+PerformanceInfo
+WorkerDebugger::ReportPerformanceInfo()
+{
+  AssertIsOnMainThread();
+#if defined(XP_WIN)
+  uint32_t pid = GetCurrentProcessId();
+#else
+  uint32_t pid = getpid();
+#endif
+  uint64_t wid = mWorkerPrivate->WindowID();
+  uint64_t pwid = wid;
+  nsPIDOMWindowInner* win = mWorkerPrivate->GetWindow();
+  if (win) {
+    nsPIDOMWindowOuter* outer = win->GetOuterWindow();
+    if (outer) {
+      nsCOMPtr<nsPIDOMWindowOuter> top = outer->GetTop();
+      if (top) {
+        pwid = top->WindowID();
+      }
+    }
+  }
+  RefPtr<PerformanceCounter> perf = mWorkerPrivate->GetPerformanceCounter();
+  uint16_t count =  perf->GetTotalDispatchCount();
+  uint64_t duration = perf->GetExecutionDuration();
+  RefPtr<nsIURI> uri = mWorkerPrivate->GetResolvedScriptURI();
+  CategoryDispatch item = CategoryDispatch(DispatchCategory::Worker.GetValue(), count);
+  FallibleTArray<CategoryDispatch> items;
+  if (!items.AppendElement(item, fallible)) {
+    NS_ERROR("Could not complete the operation");
+    return PerformanceInfo(uri->GetSpecOrDefault(), pid, wid, pwid, duration,
+                           true, items);
+  }
+  perf->ResetPerformanceCounters();
+  return PerformanceInfo(uri->GetSpecOrDefault(), pid, wid, pwid, duration,
+                         true, items);
+}
+#endif
 
 } // dom namespace
 } // mozilla namespace
--- a/dom/workers/WorkerDebugger.h
+++ b/dom/workers/WorkerDebugger.h
@@ -2,16 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_workers_WorkerDebugger_h
 #define mozilla_dom_workers_WorkerDebugger_h
 
+#include "mozilla/dom/DOMTypes.h"
 #include "mozilla/dom/WorkerCommon.h"
 #include "nsIWorkerDebugger.h"
 
 namespace mozilla {
 namespace dom {
 
 class WorkerPrivate;
 
@@ -38,16 +39,25 @@ public:
 
   void
   PostMessageToDebugger(const nsAString& aMessage);
 
   void
   ReportErrorToDebugger(const nsAString& aFilename, uint32_t aLineno,
                         const nsAString& aMessage);
 
+#ifndef RELEASE_OR_BETA
+  /*
+   * Sends back a PerformanceInfo struct from the counters
+   * in mWorkerPrivate. Counters are reset to zero after this call.
+   */
+  PerformanceInfo
+  ReportPerformanceInfo();
+#endif
+
 private:
   virtual
   ~WorkerDebugger();
 
   void
   PostMessageToDebuggerOnMainThread(const nsAString& aMessage);
 
   void
--- a/dom/workers/WorkerDebuggerManager.cpp
+++ b/dom/workers/WorkerDebuggerManager.cpp
@@ -356,10 +356,22 @@ WorkerDebuggerManager::UnregisterDebugge
   for (size_t index = 0; index < listeners.Length(); ++index) {
     listeners[index]->OnUnregister(debugger);
   }
 
   debugger->Close();
   aWorkerPrivate->SetIsDebuggerRegistered(false);
 }
 
+uint32_t
+WorkerDebuggerManager::GetDebuggersLength() const
+{
+  return mDebuggers.Length();
+}
+
+WorkerDebugger*
+WorkerDebuggerManager::GetDebuggerAt(uint32_t aIndex) const
+{
+  return mDebuggers.SafeElementAt(aIndex, nullptr);
+}
+
 } // dom namespace
 } // mozilla namespace
--- a/dom/workers/WorkerDebuggerManager.h
+++ b/dom/workers/WorkerDebuggerManager.h
@@ -69,16 +69,22 @@ public:
                              bool aNotifyListeners);
 
   void
   UnregisterDebugger(WorkerPrivate* aWorkerPrivate);
 
   void
   UnregisterDebuggerMainThread(WorkerPrivate* aWorkerPrivate);
 
+  uint32_t
+  GetDebuggersLength() const;
+
+  WorkerDebugger*
+  GetDebuggerAt(uint32_t aIndex) const;
+
 private:
   virtual ~WorkerDebuggerManager();
 };
 
 inline nsresult
 RegisterWorkerDebugger(WorkerPrivate* aWorkerPrivate)
 {
   WorkerDebuggerManager* manager;
--- a/image/decoders/icon/nsIconModule.cpp
+++ b/image/decoders/icon/nsIconModule.cpp
@@ -14,27 +14,24 @@
 // objects that just require generic constructors
 //*****************************************************************************
 // Protocol CIDs
 
 #define NS_ICONPROTOCOL_CID { 0xd0f9db12, 0x249c, 0x11d5, \
                               { 0x99, 0x5, 0x0, 0x10, 0x83, 0x1, 0xe, 0x9b } }
 
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsIconProtocolHandler)
-NS_GENERIC_FACTORY_CONSTRUCTOR(nsMozIconURI)
 typedef nsMozIconURI::Mutator nsMozIconURIMutator;
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsMozIconURIMutator)
 
 NS_DEFINE_NAMED_CID(NS_ICONPROTOCOL_CID);
-NS_DEFINE_NAMED_CID(NS_MOZICONURI_CID);
 NS_DEFINE_NAMED_CID(NS_MOZICONURIMUTATOR_CID);
 
 static const mozilla::Module::CIDEntry kIconCIDs[] = {
   { &kNS_ICONPROTOCOL_CID, false, nullptr, nsIconProtocolHandlerConstructor },
-  { &kNS_MOZICONURI_CID, false, nullptr, nsMozIconURIConstructor },
   { &kNS_MOZICONURIMUTATOR_CID, false, nullptr, nsMozIconURIMutatorConstructor },
   { nullptr }
 };
 
 static const mozilla::Module::ContractIDEntry kIconContracts[] = {
   { NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "moz-icon", &kNS_ICONPROTOCOL_CID },
   { nullptr }
 };
--- a/image/decoders/icon/nsIconURI.h
+++ b/image/decoders/icon/nsIconURI.h
@@ -25,20 +25,18 @@ class nsMozIconURI final
 {
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIURI
   NS_DECL_NSIMOZICONURI
   NS_DECL_NSIIPCSERIALIZABLEURI
   NS_DECL_NSINESTEDURI
 
-  // nsMozIconURI
+protected:
   nsMozIconURI();
-
-protected:
   virtual ~nsMozIconURI();
   nsCOMPtr<nsIURL> mIconURL; // a URL that we want the icon for
   uint32_t mSize; // the # of pixels in a row that we want for this image.
                   // Typically 16, 32, 128, etc.
   nsCString mContentType; // optional field explicitly specifying the content
                           // type
   nsCString mFileName; // for if we don't have an actual file path, we're just
                        // given a filename with an extension
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -38,60 +38,24 @@ include $(topsrcdir)/config/AB_rCD.mk
 chrome-%:: AB_CD=$*
 chrome-%::
 	$(MAKE) \
 	  res/values$(AB_rCD)/strings.xml \
 	  res/raw$(AB_rCD)/suggestedsites.json \
 	  res/raw$(AB_rCD)/browsersearch.json \
 	  AB_CD=$*
 
-gradle_dir := $(topobjdir)/gradle/build/mobile/android
-
-define gradle_command
-$(1): $(2)
-	@$$(TOUCH) $$@
-	$$(topsrcdir)/mach android assemble-app
-endef
-
-# .gradle.deps: $(generated_resources) $(generated_files) FORCE
-$(eval $(call gradle_command,.gradle.deps,$(generated_resources) $(generated_files) FORCE))
-
-GeneratedJNIWrappers.cpp GeneratedJNIWrappers.h GeneratedJNINatives.h : .gradle.deps
+GeneratedJNIWrappers.cpp GeneratedJNIWrappers.h GeneratedJNINatives.h : android_apks
 	$(REPORT_BUILD)
 
-FennecJNIWrappers.cpp FennecJNIWrappers.h FennecJNINatives.h: .gradle.deps
+FennecJNIWrappers.cpp FennecJNIWrappers.h FennecJNINatives.h: android_apks
 	$(REPORT_BUILD)
 
 include $(topsrcdir)/config/rules.mk
 
-gecko.ap_: .gradle.deps ;
-R.txt: .gradle.deps ;
-
-# This tom-foolery provides a target that forces a rebuild of
-# gecko.ap_.  This is used during packaging to ensure that resources
-# are fresh.  The alternative would be complicated.
-
-gecko-nodeps.ap_: .gradle.nodeps
-	cp $(GRADLE_ANDROID_APP_APK) $@
-
-gecko-nodeps/R.txt: .gradle.nodeps ;
-
-# The first of these rules is used during regular builds.  The second
-# writes an ap_ file that is only used during packaging.  It doesn't
-# write the normal ap_, or R.java, since we don't want the packaging
-# step to write anything that would make a further no-op build do
-# work.  See also toolkit/mozapps/installer/packager.mk.
-
-# It's not quite "no dependencies": nodeps means that it doesn't
-# depend on the generated resources that incorporate l10n, principally
-# strings.xml.
-
-# .gradle.nodeps: AndroidManifest.xml generated/preprocessed/org/mozilla/gecko/AppConstants.java ... FORCE
-$(eval $(call gradle_command,.gradle.nodeps,AndroidManifest.xml $(generated_files) FORCE))
-
 # Override the Java settings with some specific android settings
 include $(topsrcdir)/config/android-common.mk
 
 update-generated-wrappers:
 	@cp $(CURDIR)/GeneratedJNIWrappers.cpp \
 	    $(CURDIR)/GeneratedJNIWrappers.h \
 	    $(CURDIR)/GeneratedJNINatives.h $(topsrcdir)/widget/android
 	@echo Updated generated JNI code
@@ -114,19 +78,19 @@ update-fennec-wrappers:
 	$(MAKE) -C ../../../faster
 	$(MAKE) -C ../installer stage-package
 	$(MKDIR) -p $(@D)
 	rsync --update $(DIST)/fennec/$(notdir $(OMNIJAR_NAME)) $@
 	$(RM) $(DIST)/fennec/$(notdir $(OMNIJAR_NAME))
 
 ifndef MOZILLA_OFFICIAL
 # Targets built very early during a Gradle build.  In automation,
-# these are built before Gradle is invoked by .gradle.deps and
-# gradle-targets is not made at all.  This is required to avoid
-# building gradle-targets with AB_CD=multi during multi-l10n builds.
+# these are built before Gradle is invoked, and gradle-targets is not
+# made at all.  This is required to avoid building gradle-targets with
+# AB_CD=multi during multi-l10n builds.
 gradle-targets: $(generated_resources) $(generated_files)
 
 # Local developers update omni.ja during their builds.  There's a
 # chicken-and-egg problem here.
 gradle-omnijar: $(abspath $(DIST)/fennec/$(OMNIJAR_NAME))
 else
 # In automation, omni.ja is built only during packaging.
 gradle-omnijar:
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -188,8 +188,32 @@ for f in ['res/values/strings.xml',
     strings = LOCALIZED_GENERATED_FILES[f]
     strings.script = '/python/mozbuild/mozbuild/action/generate_strings_xml.py'
     strings.inputs = [
         'strings.xml.in',
         # The `locales/en-US/` will be rewritten to the locale-specific path.
         'locales/en-US/android_strings.dtd',
         'locales/en-US/sync_strings.dtd',
     ]
+
+# The recursive make backend treats the first output specially: it's passed as
+# an open FileAvoidWrite to the invoked script.  That doesn't work well with
+# the Gradle task that generates all of the outputs, so we add a dummy first
+# output.
+t = ('android_apks',
+     CONFIG['GRADLE_ANDROID_APP_APK'],
+     CONFIG['GRADLE_ANDROID_APP_ANDROIDTEST_APK'])
+
+GENERATED_FILES += [t]
+GENERATED_FILES[t].force = True
+GENERATED_FILES[t].script = '/mobile/android/gradle.py:assemble_app'
+GENERATED_FILES[t].inputs += [
+    '!AndroidManifest.xml',
+    '!generated/preprocessed/org/mozilla/gecko/AdjustConstants.java',
+    '!generated/preprocessed/org/mozilla/gecko/AppConstants.java',
+    '!generated/preprocessed/org/mozilla/gecko/MmaConstants.java',
+    # These all depend on AB_CD, which isn't captured in this definition.  Due
+    # to subtle RecursiveMake details, everything works out.  In the future we
+    # can try to express the APKs themselves as LOCALIZED_GENERATED_FILES.
+    '!res/raw/browsersearch.json',
+    '!res/raw/suggestedsites.json',
+    '!res/values/strings.xml',
+]
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ProgressDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ProgressDelegateTest.kt
@@ -46,16 +46,46 @@ class ProgressDelegateTest : BaseSession
             @AssertCalled(count = 1, order = intArrayOf(3))
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("Load should succeed", success, equalTo(true))
             }
         })
     }
 
+    fun loadExpectNetError(testUri: String) {
+        sessionRule.session.loadUri(testUri);
+        sessionRule.waitForPageStop()
+
+        sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressDelegate, Callbacks.NavigationDelegate {
+            @AssertCalled(count = 2)
+            override fun onLoadRequest(session: GeckoSession, uri: String, where: Int): Boolean {
+                if (sessionRule.currentCall.counter == 1) {
+                    assertThat("URI should be " + testUri, uri, equalTo(testUri));
+                } else {
+                    assertThat("URI should be about:neterror", uri, startsWith("about:neterror"));
+                }
+                return false
+            }
+
+            @AssertCalled(count = 1)
+            override fun onPageStop(session: GeckoSession, success: Boolean) {
+                assertThat("Load should fail", success, equalTo(false))
+            }
+        })
+    }
+
+    @Test fun loadUnknownHost() {
+        loadExpectNetError("http://does.not.exist.mozilla.org/")
+    }
+
+    @Test fun loadBadPort() {
+        loadExpectNetError("http://localhost:1/")
+    }
+
     @Test fun multipleLoads() {
         sessionRule.session.loadUri(INVALID_URI)
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStops(2)
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
             @AssertCalled(count = 2, order = intArrayOf(1, 3))
             override fun onPageStart(session: GeckoSession, url: String) {
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
@@ -708,16 +708,17 @@ public class GeckoSessionTestRule extend
                 }
             }
         };
 
         final Class<?>[] classes = CALLBACK_CLASSES.toArray(new Class<?>[CALLBACK_CLASSES.size()]);
         mCallbackProxy = Proxy.newProxyInstance(GeckoSession.class.getClassLoader(),
                                                 classes, recorder);
 
+        GeckoSession.preload(InstrumentationRegistry.getTargetContext(), new String[] { "-purgecaches" }, null, false);
         mMainSession = new GeckoSession(settings);
         prepareSession(mMainSession);
 
         if (mDisplaySize != null) {
             mDisplayTexture = new SurfaceTexture(0);
             mDisplaySurface = new Surface(mDisplayTexture);
             mDisplay = mMainSession.acquireDisplay();
             mDisplay.surfaceChanged(mDisplaySurface, mDisplaySize.x, mDisplaySize.y);
new file mode 100644
--- /dev/null
+++ b/mobile/android/gradle.py
@@ -0,0 +1,39 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import print_function
+
+import buildconfig
+import subprocess
+
+from mozbuild.util import (
+    ensureParentDir,
+    lock_file,
+)
+import mozpack.path as mozpath
+
+
+def android(verb, *args):
+    # Building the same Gradle root project with multiple concurrent processes
+    # is not well supported, so we use a simple lock file to serialize build
+    # steps.
+    lock_path = '{}/gradle/mach_android.lockfile'.format(buildconfig.topobjdir)
+    ensureParentDir(lock_path)
+    lock_instance = lock_file(lock_path)
+    try:
+        cmd = [
+            mozpath.join(buildconfig.topsrcdir, 'mach'),
+            'android',
+            verb,
+        ]
+        cmd.extend(args)
+        subprocess.check_call(cmd)
+
+        return 0
+    finally:
+        del lock_instance
+
+
+def assemble_app(dummy_output_file, *inputs):
+    return android('assemble-app')
--- a/mobile/android/locales/Makefile.in
+++ b/mobile/android/locales/Makefile.in
@@ -67,16 +67,17 @@ langpack: langpack-$(AB_CD)
 
 # This is a generic target that will make a langpack and repack tarball
 # builds. It is called from the tinderbox scripts. Alter it with caution.
 
 installers-%: IS_LANGUAGE_REPACK=1
 installers-%:
 	$(MAKE) clobber-stage
 	$(MAKE) libs-$*
+	$(MAKE) -C $(DEPTH)/mobile/android/base android_apks
 	$(MAKE) package-langpack-$*
 	$(MAKE) repackage-zip-$*
 	@echo 'repackaging done'
 
 # When we unpack fennec on MacOS X the platform.ini and application.ini are in slightly
 # different locations that on all other platforms
 ifeq (Darwin, $(OS_ARCH))
 GECKO_PLATFORM_INI_PATH='$(STAGEDIST)/platform.ini'
--- a/mobile/android/modules/geckoview/GeckoViewProgress.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewProgress.jsm
@@ -217,42 +217,46 @@ class GeckoViewProgress extends GeckoVie
     let settings = this.settings;
     debug("onSettingsUpdate: " + JSON.stringify(settings));
 
     IdentityHandler.setUseTrackingProtection(!!settings.useTrackingProtection);
     IdentityHandler.setUsePrivateMode(!!settings.usePrivateMode);
   }
 
   onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
-    debug("onStateChange()");
+    debug(`onStateChange() isTopLevel=${aWebProgress.isTopLevel}, stateFlags=${aStateFlags}, state=${aStatus}`);
 
     if (!aWebProgress.isTopLevel) {
       return;
     }
 
+    const uriSpec = aRequest.QueryInterface(Ci.nsIChannel).URI.displaySpec;
+    debug(`onStateChange() URI=${uriSpec}`);
+
     if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
-      let uri = aRequest.QueryInterface(Ci.nsIChannel).URI;
-      let message = {
+      const message = {
         type: "GeckoView:PageStart",
-        uri: uri.displaySpec,
+        uri: uriSpec,
       };
 
       this.eventDispatcher.sendRequest(message);
     } else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
                !aWebProgress.isLoadingDocument) {
       let message = {
         type: "GeckoView:PageStop",
         success: !aStatus
       };
 
       this.eventDispatcher.sendRequest(message);
     }
   }
 
   onSecurityChange(aWebProgress, aRequest, aState) {
+    debug("onSecurityChange()");
+
     // Don't need to do anything if the data we use to update the UI hasn't changed
     if (this._state === aState && !this._hostChanged) {
       return;
     }
 
     this._state = aState;
     this._hostChanged = false;
 
@@ -262,11 +266,20 @@ class GeckoViewProgress extends GeckoVie
       type: "GeckoView:SecurityChanged",
       identity: identity
     };
 
     this.eventDispatcher.sendRequest(message);
   }
 
   onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
+    debug(`onLocationChange() location=${aLocationURI.displaySpec}, flags=${aFlags}`);
+
     this._hostChanged = true;
+    if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+      // We apparently don't get a STATE_STOP in onStateChange(), so emit PageStop here
+      this.eventDispatcher.sendRequest({
+        type: "GeckoView:PageStop",
+        success: false
+      });
+    }
   }
 }
--- a/modules/libjar/nsIJARURI.idl
+++ b/modules/libjar/nsIJARURI.idl
@@ -31,8 +31,17 @@ interface nsIJARURI : nsIURL {
     readonly attribute AUTF8String JAREntry;
 
     /**
      * Create a clone of the JAR URI with a new root URI (the URI for the
      * actual JAR file).
      */
     nsIJARURI cloneWithJARFile(in nsIURI jarFile);
 };
+
+[builtinclass, uuid(d66df117-eda7-4324-b4e4-1f670ff6718e)]
+interface nsIJARURIMutator : nsISupports
+{
+    /**
+     * Will initalize a URI using the passed spec, baseURI and charset.
+     */
+    void setSpecBaseCharset(in AUTF8String aSpec, in nsIURI aBase, in string aCharset);
+};
--- a/modules/libjar/nsJARProtocolHandler.cpp
+++ b/modules/libjar/nsJARProtocolHandler.cpp
@@ -113,31 +113,21 @@ nsJARProtocolHandler::GetProtocolFlags(u
 }
 
 NS_IMETHODIMP
 nsJARProtocolHandler::NewURI(const nsACString &aSpec,
                              const char *aCharset,
                              nsIURI *aBaseURI,
                              nsIURI **result)
 {
-    nsresult rv = NS_OK;
-
-    RefPtr<nsJARURI> jarURI = new nsJARURI();
-    if (!jarURI)
-        return NS_ERROR_OUT_OF_MEMORY;
-
-    rv = jarURI->Init(aCharset);
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    rv = jarURI->SetSpecWithBase(aSpec, aBaseURI);
-    if (NS_FAILED(rv))
-        return rv;
-
-    NS_ADDREF(*result = jarURI);
-    return rv;
+    nsCOMPtr<nsIURI> base(aBaseURI);
+    return NS_MutateURI(new nsJARURI::Mutator())
+             .Apply(NS_MutatorMethod(&nsIJARURIMutator::SetSpecBaseCharset,
+                                     nsCString(aSpec), base, aCharset))
+             .Finalize(result);
 }
 
 NS_IMETHODIMP
 nsJARProtocolHandler::NewChannel2(nsIURI* uri,
                                   nsILoadInfo* aLoadInfo,
                                   nsIChannel** result)
 {
     nsJARChannel *chan = new nsJARChannel();
--- a/modules/libjar/nsJARURI.cpp
+++ b/modules/libjar/nsJARURI.cpp
@@ -263,17 +263,18 @@ nsJARURI::SetSpecInternal(const nsACStri
     return SetSpecWithBase(aSpec, nullptr);
 }
 
 // Queries this list of interfaces. If none match, it queries mURI.
 NS_IMPL_NSIURIMUTATOR_ISUPPORTS(nsJARURI::Mutator,
                                 nsIURISetters,
                                 nsIURIMutator,
                                 nsIURLMutator,
-                                nsISerializable)
+                                nsISerializable,
+                                nsIJARURIMutator)
 
 NS_IMETHODIMP
 nsJARURI::Mutator::SetFileName(const nsACString& aFileName, nsIURIMutator** aMutator)
 {
     if (!mURI) {
         return NS_ERROR_NULL_POINTER;
     }
     if (aMutator) {
--- a/modules/libjar/nsJARURI.h
+++ b/modules/libjar/nsJARURI.h
@@ -54,29 +54,28 @@ public:
     NS_DECL_NSISERIALIZABLE
     NS_DECL_NSICLASSINFO
     NS_DECL_NSINESTEDURI
     NS_DECL_NSIIPCSERIALIZABLEURI
 
     NS_DECLARE_STATIC_IID_ACCESSOR(NS_THIS_JARURI_IMPL_CID)
 
     // nsJARURI
-    nsJARURI();
-
-    nsresult Init(const char *charsetHint);
     nsresult FormatSpec(const nsACString &entryPath, nsACString &result,
                         bool aIncludeScheme = true);
     nsresult CreateEntryURL(const nsACString& entryFilename,
                             const char* charset,
                             nsIURL** url);
-    nsresult SetSpecWithBase(const nsACString& aSpec, nsIURI* aBaseURL);
 
 protected:
+    nsJARURI();
     virtual ~nsJARURI();
     nsresult SetJAREntry(const nsACString &entryPath);
+    nsresult Init(const char *charsetHint);
+    nsresult SetSpecWithBase(const nsACString& aSpec, nsIURI* aBaseURL);
 
     // enum used in a few places to specify how .ref attribute should be handled
     enum RefHandlingEnum {
         eIgnoreRef,
         eHonorRef,
         eReplaceRef
     };
 
@@ -121,16 +120,17 @@ private:
     nsresult SetFileExtensionInternal(const nsACString& fileExtension);
 
 public:
     class Mutator final
         : public nsIURIMutator
         , public BaseURIMutator<nsJARURI>
         , public nsIURLMutator
         , public nsISerializable
+        , public nsIJARURIMutator
     {
         NS_DECL_ISUPPORTS
         NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI)
         NS_DEFINE_NSIMUTATOR_COMMON
         NS_DECL_NSIURLMUTATOR
 
         NS_IMETHOD
         Write(nsIObjectOutputStream *aOutputStream) override
@@ -139,16 +139,40 @@ public:
         }
 
         MOZ_MUST_USE NS_IMETHOD
         Read(nsIObjectInputStream* aStream) override
         {
             return InitFromInputStream(aStream);
         }
 
+        NS_IMETHOD
+        SetSpecBaseCharset(const nsACString& aSpec,
+                           nsIURI* aBaseURI,
+                           const char* aCharset) override
+        {
+            RefPtr<nsJARURI> uri;
+            if (mURI) {
+                mURI.swap(uri);
+            } else {
+                uri = new nsJARURI();
+            }
+
+            nsresult rv = uri->Init(aCharset);
+            NS_ENSURE_SUCCESS(rv, rv);
+
+            rv = uri->SetSpecWithBase(aSpec, aBaseURI);
+            if (NS_FAILED(rv)) {
+                return rv;
+            }
+
+            mURI.swap(uri);
+            return NS_OK;
+        }
+
         explicit Mutator() { }
     private:
         virtual ~Mutator() { }
 
         friend class nsJARURI;
     };
 
     friend BaseURIMutator<nsJARURI>;
--- a/netwerk/base/nsIFileURL.idl
+++ b/netwerk/base/nsIFileURL.idl
@@ -25,13 +25,18 @@ interface nsIFileURL : nsIURL
      */
     readonly attribute nsIFile file;
 };
 
 [scriptable, builtinclass, uuid(a588b6f2-d2b9-4024-84c7-be3368546b57)]
 interface nsIFileURLMutator : nsISupports
 {
     /*
+     *  - Marks the inner URI implementation as one that supports nsIFileURL.
+     */
+    [must_use, noscript] void markFileURL();
+
+    /*
      *  - Setter clones the nsIFile object (allowing the caller to safely modify
      *    the nsIFile object after setting it on this interface).
      */
     [must_use, noscript] void setFile(in nsIFile aFile);
 };
--- a/netwerk/base/nsINestedURI.idl
+++ b/netwerk/base/nsINestedURI.idl
@@ -39,8 +39,37 @@ interface nsINestedURI : nsISupports
    * getter succeeds.  This is equivalent to repeatedly calling innerURI while
    * the returned URI QIs to nsINestedURI.
    *
    * Modifying the returned URI must not in any way modify the nested URI; this
    * means the returned URI must be either immutable or a clone.   
    */
   readonly attribute nsIURI innermostURI;
 };
+
+[scriptable, builtinclass, uuid(ca3d6c03-4eee-4271-a97a-d16c0a0b2c5c)]
+interface nsINestedURIMutator : nsISupports
+{
+  /*
+   *  - Creates a new URI with the given innerURI.
+   */
+  [must_use, noscript] void init(in nsIURI innerURI);
+};
+
+[scriptable, builtinclass, uuid(c6357a3b-c2bb-4b4b-9278-513377398a38)]
+interface nsINestedAboutURIMutator : nsISupports
+{
+  /*
+   *  - Creates a new URI with the given innerURI and base.
+   */
+  [must_use, noscript] void initWithBase(in nsIURI innerURI, in nsIURI baseURI);
+};
+
+[scriptable, builtinclass, uuid(3bd44535-08ea-478f-99b9-85fa1084e820)]
+interface nsIJSURIMutator : nsISupports
+{
+  /*
+   *  - Inits the URI by setting the base URI
+   *  - It is the caller's responsibility to also call SetSpec afterwards,
+   *    otherwise we will return an incomplete URI (with only a base)
+   */
+  [must_use, noscript] void setBase(in nsIURI aBaseURI);
+};
--- a/netwerk/base/nsIURIMutator.idl
+++ b/netwerk/base/nsIURIMutator.idl
@@ -27,58 +27,63 @@ class URIParams;
 
 template <class T>
 class BaseURIMutator
 {
 // This is the base class that can be extended by implementors of nsIURIMutator
 // in order to avoid code duplication
 // Class type T should be the type of the class that implements nsIURI
 protected:
+  virtual T* Create()
+  {
+    return new T();
+  }
+
   MOZ_MUST_USE nsresult InitFromURI(T* aURI)
   {
     nsCOMPtr<nsIURI> clone;
     nsresult rv = aURI->Clone(getter_AddRefs(clone));
     if (NS_FAILED(rv)) {
       return rv;
     }
     mURI = static_cast<T*>(clone.get());
     return NS_OK;
   }
 
   MOZ_MUST_USE nsresult InitFromInputStream(nsIObjectInputStream* aStream)
   {
-    RefPtr<T> uri = new T();
+    RefPtr<T> uri = Create();
     nsresult rv = uri->ReadPrivate(aStream);
     if (NS_FAILED(rv)) {
       return rv;
     }
     mURI = uri.forget();
     return NS_OK;
   }
 
   MOZ_MUST_USE nsresult InitFromIPCParams(const mozilla::ipc::URIParams& aParams)
   {
-    RefPtr<T> uri = new T();
+    RefPtr<T> uri = Create();
     bool ret = uri->Deserialize(aParams);
     if (!ret) {
       return NS_ERROR_FAILURE;
     }
     mURI = uri.forget();
     return NS_OK;
   }
 
   MOZ_MUST_USE nsresult InitFromSpec(const nsACString& aSpec)
   {
     nsresult rv = NS_OK;
     RefPtr<T> uri;
     if (mURI) {
       // This only works because all other Init methods create a new object
       mURI.swap(uri);
     } else {
-      uri = new T();
+      uri = Create();
     }
 
     rv = uri->SetSpecInternal(aSpec);
     if (NS_FAILED(rv)) {
       return rv;
     }
     mURI = uri.forget();
     return NS_OK;
--- a/netwerk/base/nsSimpleNestedURI.cpp
+++ b/netwerk/base/nsSimpleNestedURI.cpp
@@ -192,17 +192,18 @@ nsSimpleNestedURI::GetClassIDNoAlloc(nsC
     *aClassIDNoAlloc = kSimpleNestedURICID;
     return NS_OK;
 }
 
 // Queries this list of interfaces. If none match, it queries mURI.
 NS_IMPL_NSIURIMUTATOR_ISUPPORTS(nsSimpleNestedURI::Mutator,
                                 nsIURISetters,
                                 nsIURIMutator,
-                                nsISerializable)
+                                nsISerializable,
+                                nsINestedURIMutator)
 
 NS_IMETHODIMP
 nsSimpleNestedURI::Mutate(nsIURIMutator** aMutator)
 {
     RefPtr<nsSimpleNestedURI::Mutator> mutator = new nsSimpleNestedURI::Mutator();
     nsresult rv = mutator->InitFromURI(this);
     if (NS_FAILED(rv)) {
         return rv;
--- a/netwerk/base/nsSimpleNestedURI.h
+++ b/netwerk/base/nsSimpleNestedURI.h
@@ -25,29 +25,21 @@
 
 namespace mozilla {
 namespace net {
 
 class nsSimpleNestedURI : public nsSimpleURI,
                           public nsINestedURI
 {
 protected:
-    ~nsSimpleNestedURI() {}
-
-public:
-    // To be used by deserialization only.  Leaves this object in an
-    // uninitialized state that will throw on most accesses.
-    nsSimpleNestedURI()
-    {
-    }
-
-    // Constructor that should generally be used when constructing an object of
-    // this class with |operator new|.
+    nsSimpleNestedURI() = default;
     explicit nsSimpleNestedURI(nsIURI* innerURI);
 
+    ~nsSimpleNestedURI() = default;
+public:
     NS_DECL_ISUPPORTS_INHERITED
     NS_DECL_NSINESTEDURI
 
     // Overrides for various methods nsSimpleURI implements follow.
 
     // nsSimpleURI overrides
     virtual nsresult EqualsInternal(nsIURI* other,
                                     RefHandlingEnum refHandlingMode,
@@ -73,16 +65,17 @@ protected:
     bool Deserialize(const mozilla::ipc::URIParams&);
     nsresult ReadPrivate(nsIObjectInputStream *stream);
 
 public:
     class Mutator final
         : public nsIURIMutator
         , public BaseURIMutator<nsSimpleNestedURI>
         , public nsISerializable
+        , public nsINestedURIMutator
     {
         NS_DECL_ISUPPORTS
         NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI)
 
         explicit Mutator() { }
     private:
         virtual ~Mutator() { }
 
@@ -116,16 +109,23 @@ public:
         SetSpec(const nsACString& aSpec, nsIURIMutator** aMutator) override
         {
             if (aMutator) {
                 NS_ADDREF(*aMutator = this);
             }
             return InitFromSpec(aSpec);
         }
 
+        MOZ_MUST_USE NS_IMETHOD
+        Init(nsIURI* innerURI) override
+        {
+            mURI = new nsSimpleNestedURI(innerURI);
+            return NS_OK;
+        }
+
         void ResetMutable()
         {
             if (mURI) {
                 mURI->mMutable = true;
             }
         }
 
         friend class nsSimpleNestedURI;
--- a/netwerk/base/nsSimpleURI.h
+++ b/netwerk/base/nsSimpleURI.h
@@ -31,32 +31,31 @@ class nsSimpleURI
     : public nsIURI
     , public nsISerializable
     , public nsIClassInfo
     , public nsIMutable
     , public nsISizeOf
     , public nsIIPCSerializableURI
 {
 protected:
+    nsSimpleURI();
     virtual ~nsSimpleURI();
 
 public:
     NS_DECL_ISUPPORTS
     NS_DECL_NSIURI
     NS_DECL_NSISERIALIZABLE
     NS_DECL_NSICLASSINFO
     NS_DECL_NSIMUTABLE
     NS_DECL_NSIIPCSERIALIZABLEURI
 
     static already_AddRefed<nsSimpleURI> From(nsIURI* aURI);
 
     // nsSimpleURI methods:
 
-    nsSimpleURI();
-
     bool Equals(nsSimpleURI* aOther)
     {
       return EqualsInternal(aOther, eHonorRef);
     }
 
     // nsISizeOf
     // Among the sub-classes that inherit (directly or indirectly) from
     // nsSimpleURI, measurement of the following members may be added later if
--- a/netwerk/base/nsStandardURL.h
+++ b/netwerk/base/nsStandardURL.h
@@ -50,16 +50,17 @@ class nsStandardURL : public nsIFileURL
                     , public nsIIPCSerializableURI
                     , public nsISensitiveInfoHiddenURI
 #ifdef DEBUG_DUMP_URLS_AT_SHUTDOWN
                     , public LinkedListElement<nsStandardURL>
 #endif
 {
 protected:
     virtual ~nsStandardURL();
+    explicit nsStandardURL(bool aSupportsFileURL = false, bool aTrackURL = true);
 
 public:
     NS_DECL_ISUPPORTS
     NS_DECL_NSIURI
     NS_DECL_NSIURL
     NS_DECL_NSIFILEURL
     NS_DECL_NSISTANDARDURL
     NS_DECL_NSISERIALIZABLE
@@ -67,18 +68,16 @@ public:
     NS_DECL_NSIMUTABLE
     NS_DECL_NSIIPCSERIALIZABLEURI
     NS_DECL_NSISENSITIVEINFOHIDDENURI
 
     // nsISizeOf
     virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
     virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
 
-    explicit nsStandardURL(bool aSupportsFileURL = false, bool aTrackURL = true);
-
     static void InitGlobalObjects();
     static void ShutdownGlobalObjects();
 
 public: /* internal -- HPUX compiler can't handle this being private */
     //
     // location and length of an url segment relative to mSpec
     //
     struct URLSegment
@@ -397,17 +396,17 @@ public:
                 nsCOMPtr<nsIURIMutator> mutator = this;
                 mutator.forget(aMutator);
             }
             RefPtr<T> uri;
             if (BaseURIMutator<T>::mURI) {
                 // We don't need a new URI object if we already have one
                 BaseURIMutator<T>::mURI.swap(uri);
             } else {
-                uri = new T();
+                uri = Create();
             }
             nsresult rv = uri->Init(aURLType, aDefaultPort, aSpec, aCharset, aBaseURI);
             if (NS_FAILED(rv)) {
                 return rv;
             }
             BaseURIMutator<T>::mURI = uri.forget();
             return NS_OK;
         }
@@ -459,16 +458,28 @@ public:
             }
             if (aMutator) {
                 nsCOMPtr<nsIURIMutator> mutator = this;
                 mutator.forget(aMutator);
             }
             return BaseURIMutator<T>::mURI->SetFileExtensionInternal(aFileExtension);
         }
 
+        T* Create() override
+        {
+            return new T(mMarkedFileURL);
+        }
+
+        MOZ_MUST_USE NS_IMETHOD
+        MarkFileURL() override
+        {
+            mMarkedFileURL = true;
+            return NS_OK;
+        }
+
         MOZ_MUST_USE NS_IMETHOD
         SetFile(nsIFile* aFile) override
         {
             RefPtr<T> uri;
             if (BaseURIMutator<T>::mURI) {
                 // We don't need a new URI object if we already have one
                 BaseURIMutator<T>::mURI.swap(uri);
             } else {
@@ -482,16 +493,18 @@ public:
             BaseURIMutator<T>::mURI.swap(uri);
             return NS_OK;
         }
 
         explicit TemplatedMutator() { }
     private:
         virtual ~TemplatedMutator() { }
 
+        bool mMarkedFileURL = false;
+
         friend T;
     };
 
     class Mutator final
         : public TemplatedMutator<nsStandardURL>
     {
         NS_DECL_ISUPPORTS
     public:
--- a/netwerk/build/nsNetCID.h
+++ b/netwerk/build/nsNetCID.h
@@ -66,18 +66,16 @@
 { /* e1c61582-2a84-11d3-8cce-0060b0fc14a3 */         \
     0xe1c61582,                                      \
     0x2a84,                                          \
     0x11d3,                                          \
     {0x8c, 0xce, 0x00, 0x60, 0xb0, 0xfc, 0x14, 0xa3} \
 }
 
 // component implementing nsIURI, nsISerializable, and nsIClassInfo.
-#define NS_SIMPLEURI_CONTRACTID \
-    "@mozilla.org/network/simple-uri;1"
 #define NS_SIMPLEURI_CID                              \
 { /* e0da1d70-2f7b-11d3-8cd0-0060b0fc14a3 */          \
      0xe0da1d70,                                      \
      0x2f7b,                                          \
      0x11d3,                                          \
      {0x8c, 0xd0, 0x00, 0x60, 0xb0, 0xfc, 0x14, 0xa3} \
 }
 
@@ -124,18 +122,16 @@
      0xb0054ef3,                                         \
      0xb096,                                             \
      0x483d,                                             \
      { 0x82, 0x42, 0x4e, 0xe3, 0x6b, 0x7b, 0x21, 0x15 }  \
 }
 
 // component implementing nsIStandardURL, nsIURI, nsIURL, nsISerializable,
 // and nsIClassInfo.
-#define NS_STANDARDURL_CONTRACTID \
-    "@mozilla.org/network/standard-url;1"
 #define NS_STANDARDURL_CID                           \
 { /* de9472d0-8034-11d3-9399-00104ba0fd40 */         \
     0xde9472d0,                                      \
     0x8034,                                          \
     0x11d3,                                          \
     {0x93, 0x99, 0x00, 0x10, 0x4b, 0xa0, 0xfd, 0x40} \
 }
 
--- a/netwerk/protocol/about/nsAboutProtocolHandler.cpp
+++ b/netwerk/protocol/about/nsAboutProtocolHandler.cpp
@@ -146,18 +146,20 @@ nsAboutProtocolHandler::NewURI(const nsA
         NS_ENSURE_SUCCESS(rv, rv);
 
         spec.InsertLiteral("moz-safe-about:", 0);
 
         nsCOMPtr<nsIURI> inner;
         rv = NS_NewURI(getter_AddRefs(inner), spec);
         NS_ENSURE_SUCCESS(rv, rv);
 
-        RefPtr<nsSimpleNestedURI> outer = new nsNestedAboutURI(inner, aBaseURI);
-        rv = NS_MutateURI(outer)
+        nsCOMPtr<nsIURI> base(aBaseURI);
+        rv = NS_MutateURI(new nsNestedAboutURI::Mutator())
+               .Apply(NS_MutatorMethod(&nsINestedAboutURIMutator::InitWithBase,
+                                       inner, base))
                .SetSpec(aSpec)
                .Finalize(url);
         NS_ENSURE_SUCCESS(rv, rv);
     }
 
     // We don't want to allow mutation, since it would allow safe and
     // unsafe URIs to change into each other...
     NS_TryToSetImmutable(url);
@@ -446,17 +448,18 @@ nsNestedAboutURI::StartClone(nsSimpleURI
 
     return url;
 }
 
 // Queries this list of interfaces. If none match, it queries mURI.
 NS_IMPL_NSIURIMUTATOR_ISUPPORTS(nsNestedAboutURI::Mutator,
                                 nsIURISetters,
                                 nsIURIMutator,
-                                nsISerializable)
+                                nsISerializable,
+                                nsINestedAboutURIMutator)
 
 NS_IMETHODIMP
 nsNestedAboutURI::Mutate(nsIURIMutator** aMutator)
 {
     RefPtr<nsNestedAboutURI::Mutator> mutator = new nsNestedAboutURI::Mutator();
     nsresult rv = mutator->InitFromURI(this);
     if (NS_FAILED(rv)) {
         return rv;
--- a/netwerk/protocol/about/nsAboutProtocolHandler.h
+++ b/netwerk/protocol/about/nsAboutProtocolHandler.h
@@ -51,27 +51,25 @@ private:
     ~nsSafeAboutProtocolHandler() {}
 };
 
 
 // Class to allow us to propagate the base URI to about:blank correctly
 class nsNestedAboutURI final
     : public nsSimpleNestedURI
 {
-public:
+private:
     nsNestedAboutURI(nsIURI* aInnerURI, nsIURI* aBaseURI)
         : nsSimpleNestedURI(aInnerURI)
         , mBaseURI(aBaseURI)
     {}
-
-    // For use only from deserialization
     nsNestedAboutURI() : nsSimpleNestedURI() {}
-
     virtual ~nsNestedAboutURI() {}
 
+public:
     // Override QI so we can QI to our CID as needed
     NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
 
     // Override StartClone(), the nsISerializable methods, and
     // GetClassIDNoAlloc; this last is needed to make our nsISerializable impl
     // work right.
     virtual nsSimpleURI* StartClone(RefHandlingEnum aRefHandlingMode,
                                     const nsACString& newRef) override;
@@ -89,16 +87,17 @@ protected:
     nsCOMPtr<nsIURI> mBaseURI;
     nsresult ReadPrivate(nsIObjectInputStream *stream);
 
 public:
     class Mutator final
         : public nsIURIMutator
         , public BaseURIMutator<nsNestedAboutURI>
         , public nsISerializable
+        , public nsINestedAboutURIMutator
     {
         NS_DECL_ISUPPORTS
         NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI)
 
         explicit Mutator() { }
     private:
         virtual ~Mutator() { }
 
@@ -132,16 +131,23 @@ public:
         SetSpec(const nsACString& aSpec, nsIURIMutator** aMutator) override
         {
             if (aMutator) {
                 NS_ADDREF(*aMutator = this);
             }
             return InitFromSpec(aSpec);
         }
 
+        MOZ_MUST_USE NS_IMETHOD
+        InitWithBase(nsIURI* aInnerURI, nsIURI* aBaseURI) override
+        {
+            mURI = new nsNestedAboutURI(aInnerURI, aBaseURI);
+            return NS_OK;
+        }
+
         void ResetMutable()
         {
             if (mURI) {
                 mURI->mMutable = true;
             }
         }
 
         friend class nsNestedAboutURI;
--- a/netwerk/protocol/file/nsFileProtocolHandler.cpp
+++ b/netwerk/protocol/file/nsFileProtocolHandler.cpp
@@ -162,28 +162,27 @@ nsFileProtocolHandler::GetProtocolFlags(
 }
 
 NS_IMETHODIMP
 nsFileProtocolHandler::NewURI(const nsACString &spec,
                               const char *charset,
                               nsIURI *aBaseURI,
                               nsIURI **result)
 {
-    nsCOMPtr<nsIURI> url = new nsStandardURL(true);
-
     nsAutoCString buf(spec);
 #if defined(XP_WIN)
     buf.Truncate();
     if (!net_NormalizeFileURL(spec, buf)) {
         buf = spec;
     }
 #endif
 
     nsCOMPtr<nsIURI> base(aBaseURI);
-    return NS_MutateURI(url)
+    return NS_MutateURI(new nsStandardURL::Mutator())
+      .Apply(NS_MutatorMethod(&nsIFileURLMutator::MarkFileURL))
       .Apply(NS_MutatorMethod(&nsIStandardURLMutator::Init,
                               nsIStandardURL::URLTYPE_NO_AUTHORITY,
                               -1, buf, charset, base, nullptr))
       .Finalize(result);
 }
 
 NS_IMETHODIMP
 nsFileProtocolHandler::NewChannel2(nsIURI* uri,
@@ -252,22 +251,17 @@ nsFileProtocolHandler::NewFileURI(nsIFil
 }
 
 NS_IMETHODIMP
 nsFileProtocolHandler::NewFileURIMutator(nsIFile *aFile, nsIURIMutator **aResult)
 {
     NS_ENSURE_ARG_POINTER(aFile);
     nsresult rv;
 
-    nsCOMPtr<nsIURI> url = new nsStandardURL(true);
-    nsCOMPtr<nsIURIMutator> mutator;
-    rv = url->Mutate(getter_AddRefs(mutator));
-    if (NS_FAILED(rv)) {
-        return rv;
-    }
+    nsCOMPtr<nsIURIMutator> mutator = new nsStandardURL::Mutator();
     nsCOMPtr<nsIFileURLMutator> fileMutator = do_QueryInterface(mutator, &rv);
     if (NS_FAILED(rv)) {
         return rv;
     }
 
     // NOTE: the origin charset is assigned the value of the platform
     // charset by the SetFile method.
     rv = fileMutator->SetFile(aFile);
--- a/netwerk/protocol/res/SubstitutingProtocolHandler.h
+++ b/netwerk/protocol/res/SubstitutingProtocolHandler.h
@@ -117,42 +117,51 @@ private:
   // we just try to enforce it on new protocols going forward.
   bool mEnforceFileOrJar;
 };
 
 // SubstitutingURL : overrides nsStandardURL::GetFile to provide nsIFile resolution
 class SubstitutingURL : public nsStandardURL
 {
 public:
-  explicit SubstitutingURL() : nsStandardURL(true) {}
-  explicit SubstitutingURL(bool aSupportsFileURL) : nsStandardURL(true) { MOZ_ASSERT(aSupportsFileURL); }
   virtual nsStandardURL* StartClone() override;
   virtual MOZ_MUST_USE nsresult EnsureFile() override;
   NS_IMETHOD GetClassIDNoAlloc(nsCID *aCID) override;
 
+private:
+  explicit SubstitutingURL() : nsStandardURL(true) {}
+  explicit SubstitutingURL(bool aSupportsFileURL) : nsStandardURL(true) { MOZ_ASSERT(aSupportsFileURL); }
+
+public:
   class Mutator
     : public TemplatedMutator<SubstitutingURL>
   {
     NS_DECL_ISUPPORTS
   public:
     explicit Mutator() = default;
   private:
     virtual ~Mutator() = default;
+
+    SubstitutingURL* Create() override
+    {
+      return new SubstitutingURL();
+    }
   };
 
   NS_IMETHOD Mutate(nsIURIMutator** aMutator) override
   {
     RefPtr<SubstitutingURL::Mutator> mutator = new SubstitutingURL::Mutator();
     nsresult rv = mutator->InitFromURI(this);
     if (NS_FAILED(rv)) {
       return rv;
     }
     mutator.forget(aMutator);
     return NS_OK;
   }
 
   friend BaseURIMutator<SubstitutingURL>;
+  friend TemplatedMutator<SubstitutingURL>;
 };
 
 } // namespace net
 } // namespace mozilla
 
 #endif /* SubstitutingProtocolHandler_h___ */
--- a/netwerk/protocol/viewsource/nsViewSourceHandler.cpp
+++ b/netwerk/protocol/viewsource/nsViewSourceHandler.cpp
@@ -91,22 +91,19 @@ nsViewSourceHandler::NewURI(const nsACSt
     rv = innerURI->GetAsciiSpec(asciiSpec);
     if (NS_FAILED(rv))
         return rv;
 
     // put back our scheme and construct a simple-uri wrapper
 
     asciiSpec.Insert(VIEW_SOURCE ":", 0);
 
-    // We can't swap() from an RefPtr<nsSimpleNestedURI> to an nsIURI**,
-    // sadly.
-    RefPtr<nsSimpleNestedURI> ourURI = new nsSimpleNestedURI(innerURI);
-
     nsCOMPtr<nsIURI> uri;
-    rv = NS_MutateURI(ourURI)
+    rv = NS_MutateURI(new nsSimpleNestedURI::Mutator())
+           .Apply(NS_MutatorMethod(&nsINestedURIMutator::Init, innerURI))
            .SetSpec(asciiSpec)
            .Finalize(uri);
     if (NS_FAILED(rv)) {
         return rv;
     }
 
     // Make the URI immutable so it's impossible to get it out of sync
     // with its inner URI.
--- a/netwerk/test/gtest/TestStandardURL.cpp
+++ b/netwerk/test/gtest/TestStandardURL.cpp
@@ -3,58 +3,60 @@
 
 #include "nsCOMPtr.h"
 #include "nsNetCID.h"
 #include "nsIURL.h"
 #include "nsString.h"
 #include "nsPrintfCString.h"
 #include "nsComponentManagerUtils.h"
 #include "nsIURIMutator.h"
-#define protected public // hack to access setter methods
-#include "../../base/nsStandardURL.h"
-#undef protected
-using mozilla::net::nsStandardURL;
+#include "mozilla/Unused.h"
 
 // In nsStandardURL.cpp
 extern nsresult Test_NormalizeIPv4(const nsACString& host, nsCString& result);
 
 
 TEST(TestStandardURL, Simple) {
-    RefPtr<nsStandardURL> url = new nsStandardURL();
+    nsCOMPtr<nsIURI> url;
+    ASSERT_EQ(NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+                .SetSpec(NS_LITERAL_CSTRING("http://example.com"))
+                .Finalize(url),
+              NS_OK);
     ASSERT_TRUE(url);
-    ASSERT_EQ(url->SetSpecInternal(NS_LITERAL_CSTRING("http://example.com")), NS_OK);
+
+    ASSERT_EQ(NS_MutateURI(url).SetSpec(NS_LITERAL_CSTRING("http://example.com")).Finalize(url), NS_OK);
 
     nsAutoCString out;
 
     ASSERT_EQ(url->GetSpec(out), NS_OK);
     ASSERT_TRUE(out == NS_LITERAL_CSTRING("http://example.com/"));
 
     ASSERT_EQ(url->Resolve(NS_LITERAL_CSTRING("foo.html?q=45"), out), NS_OK);
     ASSERT_TRUE(out == NS_LITERAL_CSTRING("http://example.com/foo.html?q=45"));
 
-    ASSERT_EQ(url->SetScheme(NS_LITERAL_CSTRING("foo")), NS_OK);
+    ASSERT_EQ(NS_MutateURI(url).SetScheme(NS_LITERAL_CSTRING("foo")).Finalize(url), NS_OK);
 
     ASSERT_EQ(url->GetScheme(out), NS_OK);
     ASSERT_TRUE(out == NS_LITERAL_CSTRING("foo"));
 
     ASSERT_EQ(url->GetHost(out), NS_OK);
     ASSERT_TRUE(out == NS_LITERAL_CSTRING("example.com"));
-    ASSERT_EQ(url->SetHost(NS_LITERAL_CSTRING("www.yahoo.com")), NS_OK);
+    ASSERT_EQ(NS_MutateURI(url).SetHost(NS_LITERAL_CSTRING("www.yahoo.com")).Finalize(url), NS_OK);
     ASSERT_EQ(url->GetHost(out), NS_OK);
     ASSERT_TRUE(out == NS_LITERAL_CSTRING("www.yahoo.com"));
 
-    ASSERT_EQ(url->SetPathQueryRef(NS_LITERAL_CSTRING("/some-path/one-the-net/about.html?with-a-query#for-you")), NS_OK);
+    ASSERT_EQ(NS_MutateURI(url).SetPathQueryRef(NS_LITERAL_CSTRING("/some-path/one-the-net/about.html?with-a-query#for-you")).Finalize(url), NS_OK);
     ASSERT_EQ(url->GetPathQueryRef(out), NS_OK);
     ASSERT_TRUE(out == NS_LITERAL_CSTRING("/some-path/one-the-net/about.html?with-a-query#for-you"));
 
-    ASSERT_EQ(url->SetQuery(NS_LITERAL_CSTRING("a=b&d=c&what-ever-you-want-to-be-called=45")), NS_OK);
+    ASSERT_EQ(NS_MutateURI(url).SetQuery(NS_LITERAL_CSTRING("a=b&d=c&what-ever-you-want-to-be-called=45")).Finalize(url), NS_OK);
     ASSERT_EQ(url->GetQuery(out), NS_OK);
     ASSERT_TRUE(out == NS_LITERAL_CSTRING("a=b&d=c&what-ever-you-want-to-be-called=45"));
 
-    ASSERT_EQ(url->SetRef(NS_LITERAL_CSTRING("#some-book-mark")), NS_OK);
+    ASSERT_EQ(NS_MutateURI(url).SetRef(NS_LITERAL_CSTRING("#some-book-mark")).Finalize(url), NS_OK);
     ASSERT_EQ(url->GetRef(out), NS_OK);
     ASSERT_TRUE(out == NS_LITERAL_CSTRING("some-book-mark"));
 }
 
 TEST(TestStandardURL, NormalizeGood)
 {
     nsCString result;
     const char* manual[] = {"0.0.0.0", "0.0.0.0",
@@ -177,33 +179,36 @@ TEST(TestStandardURL, From_test_standard
         nsCString encHost(nonIPv4s[i]);
         ASSERT_EQ(NS_ERROR_FAILURE, Test_NormalizeIPv4(encHost, result));
     }
 }
 
 #define COUNT 10000
 
 MOZ_GTEST_BENCH(TestStandardURL, DISABLED_Perf, [] {
-    RefPtr<nsStandardURL> url = new nsStandardURL();
-    ASSERT_TRUE(url);
+    nsCOMPtr<nsIURI> url;
+    ASSERT_EQ(NS_OK,
+      NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+        .SetSpec(NS_LITERAL_CSTRING("http://example.com"))
+        .Finalize(url));
+
     nsAutoCString out;
-
     for (int i = COUNT; i; --i) {
-        ASSERT_EQ(url->SetSpecInternal(NS_LITERAL_CSTRING("http://example.com")), NS_OK);
+        ASSERT_EQ(NS_MutateURI(url).SetSpec(NS_LITERAL_CSTRING("http://example.com")).Finalize(url), NS_OK);
         ASSERT_EQ(url->GetSpec(out), NS_OK);
         url->Resolve(NS_LITERAL_CSTRING("foo.html?q=45"), out);
-        url->SetScheme(NS_LITERAL_CSTRING("foo"));
+        mozilla::Unused << NS_MutateURI(url).SetScheme(NS_LITERAL_CSTRING("foo")).Finalize(url);
         url->GetScheme(out);
-        url->SetHost(NS_LITERAL_CSTRING("www.yahoo.com"));
+        mozilla::Unused << NS_MutateURI(url).SetHost(NS_LITERAL_CSTRING("www.yahoo.com")).Finalize(url);
         url->GetHost(out);
-        url->SetPathQueryRef(NS_LITERAL_CSTRING("/some-path/one-the-net/about.html?with-a-query#for-you"));
+        mozilla::Unused << NS_MutateURI(url).SetPathQueryRef(NS_LITERAL_CSTRING("/some-path/one-the-net/about.html?with-a-query#for-you")).Finalize(url);
         url->GetPathQueryRef(out);
-        url->SetQuery(NS_LITERAL_CSTRING("a=b&d=c&what-ever-you-want-to-be-called=45"));
+        mozilla::Unused << NS_MutateURI(url).SetQuery(NS_LITERAL_CSTRING("a=b&d=c&what-ever-you-want-to-be-called=45")).Finalize(url);
         url->GetQuery(out);
-        url->SetRef(NS_LITERAL_CSTRING("#some-book-mark"));
+        mozilla::Unused << NS_MutateURI(url).SetRef(NS_LITERAL_CSTRING("#some-book-mark")).Finalize(url);
         url->GetRef(out);
     }
 });
 
 // Note the five calls in the loop, so divide by 100k
 MOZ_GTEST_BENCH(TestStandardURL, DISABLED_NormalizePerf, [] {
     nsAutoCString result;
     for (int i = 0; i < 20000; i++) {
--- a/netwerk/test/httpserver/httpd.js
+++ b/netwerk/test/httpserver/httpd.js
@@ -583,36 +583,44 @@ nsHttpServer.prototype =
     }
   },
 
   //
   // see nsIHttpServer.stop
   //
   stop: function(callback)
   {
-    if (!callback)
-      throw Cr.NS_ERROR_NULL_POINTER;
     if (!this._socket)
       throw Cr.NS_ERROR_UNEXPECTED;
 
+    // If no argument was provided to stop, return a promise.
+    let returnValue = undefined;
+    if (!callback) {
+      returnValue = new Promise(resolve => {
+        callback = resolve;
+      });
+    }
+
     this._stopCallback = typeof callback === "function"
                        ? callback
                        : function() { callback.onStopped(); };
 
     dumpn(">>> stopping listening on port " + this._socket.port);
     this._socket.close();
     this._socket = null;
 
     // We can't have this identity any more, and the port on which we're running
     // this server now could be meaningless the next time around.
     this._identity._teardown();
 
     this._doQuit = false;
 
     // socket-close notification and pending request completion happen async
+
+    return returnValue;
   },
 
   //
   // see nsIHttpServer.registerFile
   //
   registerFile: function(path, file)
   {
     if (file && (!file.exists() || file.isDirectory()))
--- a/netwerk/test/httpserver/test/test_start_stop.js
+++ b/netwerk/test/httpserver/test/test_start_stop.js
@@ -37,36 +37,16 @@ function run_test()
     srv.start(PORT);
     do_throw("starting a started server");
   }
   catch (e)
   {
     isException(e, Cr.NS_ERROR_ALREADY_INITIALIZED);
   }
 
-  try
-  {
-    srv.stop();
-    do_throw("missing argument to stop");
-  }
-  catch (e)
-  {
-    isException(e, Cr.NS_ERROR_NULL_POINTER);
-  }
-
-  try
-  {
-    srv.stop(null);
-    do_throw("null argument to stop");
-  }
-  catch (e)
-  {
-    isException(e, Cr.NS_ERROR_NULL_POINTER);
-  }
-
   do_test_pending();
   srv.stop(function()
   {
     try
     {
       do_test_pending();
       run_test_2();
     }
@@ -120,32 +100,59 @@ function run_test_2()
 }
 
 function run_test_3()
 {
   dumpn("*** run_test_3");
 
   do_test_finished();
 
+  srv.start(PORT);
+
+  do_test_pending();
+  try
+  {
+    srv.stop().then(function()
+    {
+      try {
+        do_test_pending();
+        run_test_4();
+      } finally {
+        do_test_finished();
+      }
+    });
+  }
+  catch (e)
+  {
+    do_throw("error stopping with an object: " + e);
+  }
+}
+
+function run_test_4()
+{
+  dumpn("*** run_test_4");
+
+  do_test_finished();
+
   srv.registerPathHandler("/handle", handle);
   srv.start(PORT);
 
   // Don't rely on the exact (but implementation-constant) sequence of events
-  // as it currently exists by making either run_test_4 or serverStopped handle
+  // as it currently exists by making either run_test_5 or serverStopped handle
   // the final shutdown.
   do_test_pending();
 
-  runHttpTests([new Test(PREPATH + "/handle")], run_test_4);
+  runHttpTests([new Test(PREPATH + "/handle")], run_test_5);
 }
 
 var testsComplete = false;
 
-function run_test_4()
+function run_test_5()
 {
-  dumpn("*** run_test_4");
+  dumpn("*** run_test_5");
 
   testsComplete = true;
   if (stopped)
     do_test_finished();
 }
 
 
 const INTERVAL = 500;
--- a/python/mozbuild/mozbuild/backend/recursivemake.py
+++ b/python/mozbuild/mozbuild/backend/recursivemake.py
@@ -573,31 +573,38 @@ class RecursiveMakeBackend(CommonBackend
                     # Android localized resources have special Makefile
                     # handling.
                     backend_file.write('%s:: %s\n' % (tier, first_output))
             for output in outputs:
                 if output != first_output:
                     backend_file.write('%s: %s ;\n' % (output, first_output))
                 backend_file.write('GARBAGE += %s\n' % output)
             backend_file.write('EXTRA_MDDEPEND_FILES += %s\n' % dep_file)
+
+            force = ''
+            if obj.force:
+                force = ' FORCE'
+            elif obj.localized:
+                force = ' $(if $(IS_LANGUAGE_REPACK),FORCE)'
+
             if obj.script:
-                backend_file.write("""{output}: {script}{inputs}{backend}{repack_force}
+                backend_file.write("""{output}: {script}{inputs}{backend}{force}
 \t$(REPORT_BUILD)
 \t$(call py_action,file_generate,{locale}{script} {method} {output} $(MDDEPDIR)/{dep_file}{inputs}{flags})
 
 """.format(output=first_output,
            dep_file=dep_file,
            inputs=' ' + ' '.join(inputs) if inputs else '',
            flags=' ' + ' '.join(shell_quote(f) for f in obj.flags) if obj.flags else '',
            backend=' backend.mk' if obj.flags else '',
            # Locale repacks repack multiple locales from a single configured objdir,
            # so standard mtime dependencies won't work properly when the build is re-run
            # with a different locale as input. IS_LANGUAGE_REPACK will reliably be set
            # in this situation, so simply force the generation to run in that case.
-           repack_force=' $(if $(IS_LANGUAGE_REPACK),FORCE)' if obj.localized else '',
+           force=force,
            locale='--locale=$(AB_CD) ' if obj.localized else '',
            script=obj.script,
            method=obj.method))
 
         elif isinstance(obj, JARManifest):
             self._no_skip['libs'].add(backend_file.relobjdir)
             backend_file.write('JAR_MANIFEST := %s\n' % obj.path.full_path)
 
--- a/python/mozbuild/mozbuild/backend/tup.py
+++ b/python/mozbuild/mozbuild/backend/tup.py
@@ -428,17 +428,18 @@ class TupOnly(CommonBackend, PartialBack
                         gen_backend_file = self._get_backend_file(f.context.relobjdir)
                         gen_backend_file.symlink_rule(f.full_path, output=output,
                                                       output_group=self._installed_files)
 
     def _process_final_target_pp_files(self, obj, backend_file):
         for i, (path, files) in enumerate(obj.files.walk()):
             for f in files:
                 self._preprocess(backend_file, f.full_path,
-                                 destdir=mozpath.join(self.environment.topobjdir, obj.install_target, path))
+                                 destdir=mozpath.join(self.environment.topobjdir, obj.install_target, path),
+                                 target=f.target_basename)
 
     def _process_computed_flags(self, obj, backend_file):
         for var, flags in obj.get_flags():
             backend_file.local_flags[var] = flags
 
     def _process_unified_sources(self, obj):
         backend_file = self._get_backend_file_for(obj)
         files = [f[0] for f in obj.unified_source_mapping]
@@ -490,26 +491,28 @@ class TupOnly(CommonBackend, PartialBack
 
         for manifest, entries in manager.interface_manifests.items():
             for xpt in entries:
                 self._manifest_entries[manifest].add('interfaces %s' % xpt)
 
         for m in manager.chrome_manifests:
             self._manifest_entries[m].add('manifest components/interfaces.manifest')
 
-    def _preprocess(self, backend_file, input_file, destdir=None):
+    def _preprocess(self, backend_file, input_file, destdir=None, target=None):
+        if target is None:
+            target = mozpath.basename(input_file)
         # .css files use '%' as the preprocessor marker, which must be scaped as
         # '%%' in the Tupfile.
-        marker = '%%' if input_file.endswith('.css') else '#'
+        marker = '%%' if target.endswith('.css') else '#'
 
         cmd = self._py_action('preprocessor')
         cmd.extend([shell_quote(d) for d in backend_file.defines])
         cmd.extend(['$(ACDEFINES)', '%f', '-o', '%o', '--marker=%s' % marker])
 
-        base_input = mozpath.basename(input_file)
+        base_input = mozpath.basename(target)
         if base_input.endswith('.in'):
             base_input = mozpath.splitext(base_input)[0]
         output = mozpath.join(destdir, base_input) if destdir else base_input
 
         backend_file.rule(
             inputs=[input_file],
             display='Preprocess %o',
             cmd=cmd,
--- a/python/mozbuild/mozbuild/frontend/context.py
+++ b/python/mozbuild/mozbuild/frontend/context.py
@@ -926,16 +926,17 @@ BugzillaComponent = TypedNamedTuple('Bug
                         [('product', unicode), ('component', unicode)])
 SchedulingComponents = ContextDerivedTypedRecord(
         ('inclusive', TypedList(unicode, StrictOrderingOnAppendList)),
         ('exclusive', TypedList(unicode, StrictOrderingOnAppendList)))
 
 GeneratedFilesList = StrictOrderingOnAppendListWithFlagsFactory({
     'script': unicode,
     'inputs': list,
+    'force': bool,
     'flags': list, })
 
 
 class Files(SubContext):
     """Metadata attached to files.
 
     It is common to want to annotate files with metadata, such as which
     Bugzilla component tracks issues with certain files. This sub-context is
@@ -1271,18 +1272,18 @@ VARIABLES = {
         size.
         """),
 
     'GENERATED_FILES': (GeneratedFilesList, list,
         """Generic generated files.
 
         This variable contains a list of files for the build system to
         generate at export time. The generation method may be declared
-        with optional ``script``, ``inputs`` and ``flags`` attributes on
-        individual entries.
+        with optional ``script``, ``inputs``, ``flags``, and ``force``
+        attributes on individual entries.
         If the optional ``script`` attribute is not present on an entry, it
         is assumed that rules for generating the file are present in
         the associated Makefile.in.
 
         Example::
 
            GENERATED_FILES += ['bar.c', 'baz.c', 'foo.c']
            bar = GENERATED_FILES['bar.c']
@@ -1310,16 +1311,21 @@ VARIABLES = {
           bar = GENERATED_FILES['bar.c']
           bar.script = 'generate.py:make_bar'
 
         The chosen script entry point may optionally return a set of strings,
         indicating extra files the output depends on.
 
         When the ``flags`` attribute is present, the given list of flags is
         passed as extra arguments following the inputs.
+
+        When the ``force`` attribute is present, the file is generated every
+        build, regardless of whether it is stale.  This is special to the
+        RecursiveMake backend and intended for special situations only (e.g.,
+        localization).  Please consult a build peer before using ``force``.
         """),
 
     'DEFINES': (InitializedDefines, dict,
         """Dictionary of compiler defines to declare.
 
         These are passed in to the compiler as ``-Dkey='value'`` for string
         values, ``-Dkey=value`` for numeric values, or ``-Dkey`` if the
         value is True. Note that for string values, the outer-level of
--- a/python/mozbuild/mozbuild/frontend/data.py
+++ b/python/mozbuild/mozbuild/frontend/data.py
@@ -1117,26 +1117,29 @@ class GeneratedFile(ContextDerived):
     __slots__ = (
         'script',
         'method',
         'outputs',
         'inputs',
         'flags',
         'required_for_compile',
         'localized',
+        'force',
     )
 
-    def __init__(self, context, script, method, outputs, inputs, flags=(), localized=False):
+    def __init__(self, context, script, method, outputs, inputs,
+                 flags=(), localized=False, force=False):
         ContextDerived.__init__(self, context)
         self.script = script
         self.method = method
         self.outputs = outputs if isinstance(outputs, tuple) else (outputs,)
         self.inputs = inputs
         self.flags = flags
         self.localized = localized
+        self.force = force
 
         suffixes = (
             '.c',
             '.cpp',
             '.h',
             '.inc',
             '.py',
             '.rs',
--- a/python/mozbuild/mozbuild/frontend/emitter.py
+++ b/python/mozbuild/mozbuild/frontend/emitter.py
@@ -1386,17 +1386,17 @@ class TreeMetadataEmitter(LoggingMixin):
                     if (isinstance(p, SourcePath) and
                             not os.path.exists(p.full_path)):
                         raise SandboxValidationError(
                             'Input for generating %s does not exist: %s'
                             % (f, p.full_path), context)
                     inputs.append(p)
 
                 yield GeneratedFile(context, script, method, outputs, inputs,
-                                    flags.flags, localized=localized)
+                                    flags.flags, localized=localized, force=flags.force)
 
     def _process_test_manifests(self, context):
         for prefix, info in TEST_MANIFESTS.items():
             for path, manifest in context.get('%s_MANIFESTS' % prefix, []):
                 for obj in self._process_test_manifest(context, info, path, manifest):
                     yield obj
 
         for flavor in REFTEST_FLAVORS:
copy from python/mozbuild/mozbuild/test/backend/data/generated-files/foo-data
copy to python/mozbuild/mozbuild/test/backend/data/generated-files-force/foo-data
copy from python/mozbuild/mozbuild/test/backend/data/generated-files/generate-bar.py
copy to python/mozbuild/mozbuild/test/backend/data/generated-files-force/generate-bar.py
copy from python/mozbuild/mozbuild/test/backend/data/generated-files/generate-foo.py
copy to python/mozbuild/mozbuild/test/backend/data/generated-files-force/generate-foo.py
copy from python/mozbuild/mozbuild/test/backend/data/generated-files/moz.build
copy to python/mozbuild/mozbuild/test/backend/data/generated-files-force/moz.build
--- a/python/mozbuild/mozbuild/test/backend/data/generated-files/moz.build
+++ b/python/mozbuild/mozbuild/test/backend/data/generated-files-force/moz.build
@@ -1,12 +1,14 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # Any copyright is dedicated to the Public Domain.
 # http://creativecommons.org/publicdomain/zero/1.0/
 
 GENERATED_FILES += [ 'bar.c', 'foo.c', 'quux.c' ]
 
 bar = GENERATED_FILES['bar.c']
 bar.script = 'generate-bar.py:baz'
+bar.force = True
 
 foo = GENERATED_FILES['foo.c']
 foo.script = 'generate-foo.py'
 foo.inputs = ['foo-data']
+foo.force = False
copy from python/mozbuild/mozbuild/test/backend/data/localized-generated-files/en-US/localized-input
copy to python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/en-US/localized-input
copy from python/mozbuild/mozbuild/test/backend/data/localized-generated-files/foo-data
copy to python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/foo-data
copy from python/mozbuild/mozbuild/test/backend/data/localized-generated-files/generate-foo.py
copy to python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/generate-foo.py
copy from python/mozbuild/mozbuild/test/backend/data/localized-generated-files/moz.build
copy to python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/moz.build
--- a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/moz.build
+++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/moz.build
@@ -6,10 +6,17 @@ LOCALIZED_GENERATED_FILES += [ 'foo.xyz'
 
 foo = LOCALIZED_GENERATED_FILES['foo.xyz']
 foo.script = 'generate-foo.py'
 foo.inputs = [
     'en-US/localized-input',
     'non-localized-input',
 ]
 
-# Also check that using it in LOCALIZED_FILES does the right thing.
-LOCALIZED_FILES += [ '!foo.xyz' ]
+LOCALIZED_GENERATED_FILES += [ 'abc.xyz' ]
+
+abc = LOCALIZED_GENERATED_FILES['abc.xyz']
+abc.script = 'generate-foo.py'
+abc.inputs = [
+    'en-US/localized-input',
+    'non-localized-input',
+]
+abc.force = True
copy from python/mozbuild/mozbuild/test/backend/data/localized-generated-files/non-localized-input
copy to python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/non-localized-input
--- a/python/mozbuild/mozbuild/test/backend/test_recursivemake.py
+++ b/python/mozbuild/mozbuild/test/backend/test_recursivemake.py
@@ -413,16 +413,46 @@ class TestRecursiveMakeBackend(BackendTe
             'export:: quux.c',
             'GARBAGE += quux.c',
             'EXTRA_MDDEPEND_FILES += quux.c.pp',
         ]
 
         self.maxDiff = None
         self.assertEqual(lines, expected)
 
+    def test_generated_files_force(self):
+        """Ensure GENERATED_FILES with .force is handled properly."""
+        env = self._consume('generated-files-force', RecursiveMakeBackend)
+
+        backend_path = mozpath.join(env.topobjdir, 'backend.mk')
+        lines = [l.strip() for l in open(backend_path, 'rt').readlines()[2:]]
+
+        expected = [
+            'export:: bar.c',
+            'GARBAGE += bar.c',
+            'EXTRA_MDDEPEND_FILES += bar.c.pp',
+            'bar.c: %s/generate-bar.py FORCE' % env.topsrcdir,
+            '$(REPORT_BUILD)',
+            '$(call py_action,file_generate,%s/generate-bar.py baz bar.c $(MDDEPDIR)/bar.c.pp)' % env.topsrcdir,
+            '',
+            'export:: foo.c',
+            'GARBAGE += foo.c',
+            'EXTRA_MDDEPEND_FILES += foo.c.pp',
+            'foo.c: %s/generate-foo.py $(srcdir)/foo-data' % (env.topsrcdir),
+            '$(REPORT_BUILD)',
+            '$(call py_action,file_generate,%s/generate-foo.py main foo.c $(MDDEPDIR)/foo.c.pp $(srcdir)/foo-data)' % (env.topsrcdir),
+            '',
+            'export:: quux.c',
+            'GARBAGE += quux.c',
+            'EXTRA_MDDEPEND_FILES += quux.c.pp',
+        ]
+
+        self.maxDiff = None
+        self.assertEqual(lines, expected)
+
     def test_localized_generated_files(self):
         """Ensure LOCALIZED_GENERATED_FILES is handled properly."""
         env = self._consume('localized-generated-files', RecursiveMakeBackend)
 
         backend_path = mozpath.join(env.topobjdir, 'backend.mk')
         lines = [l.strip() for l in open(backend_path, 'rt').readlines()[2:]]
 
         expected = [
@@ -437,16 +467,43 @@ class TestRecursiveMakeBackend(BackendTe
             'LOCALIZED_FILES_0_DEST = $(FINAL_TARGET)/',
             'LOCALIZED_FILES_0_TARGET := libs',
             'INSTALL_TARGETS += LOCALIZED_FILES_0',
         ]
 
         self.maxDiff = None
         self.assertEqual(lines, expected)
 
+    def test_localized_generated_files_force(self):
+        """Ensure LOCALIZED_GENERATED_FILES with .force is handled properly."""
+        env = self._consume('localized-generated-files-force', RecursiveMakeBackend)
+
+        backend_path = mozpath.join(env.topobjdir, 'backend.mk')
+        lines = [l.strip() for l in open(backend_path, 'rt').readlines()[2:]]
+
+        expected = [
+            'libs:: foo.xyz',
+            'GARBAGE += foo.xyz',
+            'EXTRA_MDDEPEND_FILES += foo.xyz.pp',
+            'foo.xyz: %s/generate-foo.py $(call MERGE_FILE,localized-input) $(srcdir)/non-localized-input $(if $(IS_LANGUAGE_REPACK),FORCE)' % env.topsrcdir,
+            '$(REPORT_BUILD)',
+            '$(call py_action,file_generate,--locale=$(AB_CD) %s/generate-foo.py main foo.xyz $(MDDEPDIR)/foo.xyz.pp $(call MERGE_FILE,localized-input) $(srcdir)/non-localized-input)' % env.topsrcdir,
+            '',
+            'libs:: abc.xyz',
+            'GARBAGE += abc.xyz',
+            'EXTRA_MDDEPEND_FILES += abc.xyz.pp',
+            'abc.xyz: %s/generate-foo.py $(call MERGE_FILE,localized-input) $(srcdir)/non-localized-input FORCE' % env.topsrcdir,
+            '$(REPORT_BUILD)',
+            '$(call py_action,file_generate,--locale=$(AB_CD) %s/generate-foo.py main abc.xyz $(MDDEPDIR)/abc.xyz.pp $(call MERGE_FILE,localized-input) $(srcdir)/non-localized-input)' % env.topsrcdir,
+            '',
+        ]
+
+        self.maxDiff = None
+        self.assertEqual(lines, expected)
+
     def test_localized_generated_files_AB_CD(self):
         """Ensure LOCALIZED_GENERATED_FILES is handled properly
         when {AB_CD} and {AB_rCD} are used."""
         env = self._consume('localized-generated-files-AB_CD', RecursiveMakeBackend)
 
         backend_path = mozpath.join(env.topobjdir, 'backend.mk')
         lines = [l.strip() for l in open(backend_path, 'rt').readlines()[2:]]
 
copy from python/mozbuild/mozbuild/test/frontend/data/generated-files/moz.build
copy to python/mozbuild/mozbuild/test/frontend/data/generated-files-force/moz.build
--- a/python/mozbuild/mozbuild/test/frontend/data/generated-files/moz.build
+++ b/python/mozbuild/mozbuild/test/frontend/data/generated-files-force/moz.build
@@ -1,5 +1,7 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # Any copyright is dedicated to the Public Domain.
 # http://creativecommons.org/publicdomain/zero/1.0/
 
 GENERATED_FILES += [ 'bar.c', 'foo.c', ('xpidllex.py', 'xpidlyacc.py'), ]
+GENERATED_FILES['bar.c'].force = True
+GENERATED_FILES['foo.c'].force = False
copy from python/mozbuild/mozbuild/test/frontend/data/localized-generated-files/moz.build
copy to python/mozbuild/mozbuild/test/frontend/data/localized-generated-files-force/moz.build
--- a/python/mozbuild/mozbuild/test/frontend/data/localized-generated-files/moz.build
+++ b/python/mozbuild/mozbuild/test/frontend/data/localized-generated-files-force/moz.build
@@ -1,5 +1,6 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # Any copyright is dedicated to the Public Domain.
 # http://creativecommons.org/publicdomain/zero/1.0/
 
 LOCALIZED_GENERATED_FILES += [ 'abc.ini', ('bar', 'baz') ]
+LOCALIZED_GENERATED_FILES['abc.ini'].force = True
--- a/python/mozbuild/mozbuild/test/frontend/test_emitter.py
+++ b/python/mozbuild/mozbuild/test/frontend/test_emitter.py
@@ -447,34 +447,43 @@ class TestEmitterBasic(unittest.TestCase
 
         maxDiff = self.maxDiff
         self.maxDiff = None
         self.assertEqual(passthru.variables,
                          {'AS': 'yasm',
                           'AS_DASH_C_FLAG': ''})
         self.maxDiff = maxDiff
 
-
     def test_generated_files(self):
         reader = self.reader('generated-files')
         objs = self.read_topsrcdir(reader)
 
         self.assertEqual(len(objs), 3)
         for o in objs:
             self.assertIsInstance(o, GeneratedFile)
             self.assertFalse(o.localized)
+            self.assertFalse(o.force)
 
         expected = ['bar.c', 'foo.c', ('xpidllex.py', 'xpidlyacc.py'), ]
         for o, f in zip(objs, expected):
             expected_filename = f if isinstance(f, tuple) else (f,)
             self.assertEqual(o.outputs, expected_filename)
             self.assertEqual(o.script, None)
             self.assertEqual(o.method, None)
             self.assertEqual(o.inputs, [])
 
+    def test_generated_files_force(self):
+        reader = self.reader('generated-files-force')
+        objs = self.read_topsrcdir(reader)
+
+        self.assertEqual(len(objs), 3)
+        for o in objs:
+            self.assertIsInstance(o, GeneratedFile)
+            self.assertEqual(o.force, 'bar.c' in o.outputs)
+
     def test_localized_generated_files(self):
         reader = self.reader('localized-generated-files')
         objs = self.read_topsrcdir(reader)
 
         self.assertEqual(len(objs), 2)
         for o in objs:
             self.assertIsInstance(o, GeneratedFile)
             self.assertTrue(o.localized)
@@ -482,16 +491,26 @@ class TestEmitterBasic(unittest.TestCase
         expected = ['abc.ini', ('bar', 'baz'), ]
         for o, f in zip(objs, expected):
             expected_filename = f if isinstance(f, tuple) else (f,)
             self.assertEqual(o.outputs, expected_filename)
             self.assertEqual(o.script, None)
             self.assertEqual(o.method, None)
             self.assertEqual(o.inputs, [])
 
+    def test_localized_generated_files_force(self):
+        reader = self.reader('localized-generated-files-force')
+        objs = self.read_topsrcdir(reader)
+
+        self.assertEqual(len(objs), 2)
+        for o in objs:
+            self.assertIsInstance(o, GeneratedFile)
+            self.assertTrue(o.localized)
+            self.assertEqual(o.force, 'abc.ini' in o.outputs)
+
     def test_localized_files_from_generated(self):
         """Test that using LOCALIZED_GENERATED_FILES and then putting the output in
         LOCALIZED_FILES as an objdir path works.
         """
         reader = self.reader('localized-files-from-generated')
         objs = self.read_topsrcdir(reader)
 
         self.assertEqual(len(objs), 2)
--- a/services/sync/tests/tps/test_history_collision.js
+++ b/services/sync/tests/tps/test_history_collision.js
@@ -99,16 +99,17 @@ var history_after_delete = [
  */
 
 Phase("phase1", [
   [History.add, history1],
   [Sync]
 ]);
 
 Phase("phase2", [
+  [Sync],
   [History.add, history1],
   [Sync, SYNC_WIPE_REMOTE]
 ]);
 
 Phase("phase3", [
   [Sync],
   [History.verify, history1],
   [History.delete, history_to_delete],
--- a/services/sync/tps/extensions/tps/resource/modules/history.jsm
+++ b/services/sync/tps/extensions/tps/resource/modules/history.jsm
@@ -18,19 +18,20 @@ var DumpHistory = async function TPS_His
   let query = PlacesUtils.history.getNewQuery();
   let options = PlacesUtils.history.getNewQueryOptions();
   let root = PlacesUtils.history.executeQuery(query, options).root;
   root.containerOpen = true;
   Logger.logInfo("\n\ndumping history\n", true);
   for (var i = 0; i < root.childCount; i++) {
     let node = root.getChild(i);
     let uri = node.uri;
+    let guid = await PlacesSyncUtils.history.fetchGuidForURL(uri).catch(() => "?".repeat(12));
     let curvisits = await PlacesSyncUtils.history.fetchVisitsForURL(uri);
     for (var visit of curvisits) {
-      Logger.logInfo("URI: " + uri + ", type=" + visit.type + ", date=" + visit.date, true);
+      Logger.logInfo(`GUID: ${guid}, URI: ${uri}, type=${visit.type}, date=${visit.date}`, true);
     }
   }
   root.containerOpen = false;
   Logger.logInfo("\nend history dump\n", true);
 };
 
 /**
  * HistoryEntry object
--- a/services/sync/tps/extensions/tps/resource/modules/tabs.jsm
+++ b/services/sync/tps/extensions/tps/resource/modules/tabs.jsm
@@ -6,16 +6,17 @@
    Components.utils.import() and acts as a singleton.
    Only the following listed symbols will exposed on import, and only when
    and where imported. */
 
 const EXPORTED_SYMBOLS = ["BrowserTabs"];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://services-sync/main.js");
+ChromeUtils.import("resource:///modules/sessionstore/TabStateFlusher.jsm");
 
 // Unfortunately, due to where TPS is run, we can't directly reuse the logic from
 // BrowserTestUtils.jsm. Moreover, we can't resolve the URI it loads the content
 // frame script from ("chrome://mochikit/content/tests/BrowserTestUtils/content-utils.js"),
 // hence the hackiness here and in BrowserTabs.Add.
 Cc["@mozilla.org/globalmessagemanager;1"]
 .getService(Ci.nsIMessageListenerManager)
 .loadFrameScript("data:application/javascript;charset=utf-8," + encodeURIComponent(`
@@ -25,34 +26,39 @@ Cc["@mozilla.org/globalmessagemanager;1"
     sendAsyncMessage("tps:loadEvent", {subframe: subframe, url: event.target.documentURI});
   }, true)`), true);
 
 var BrowserTabs = {
   /**
    * Add
    *
    * Opens a new tab in the current browser window for the
-   * given uri.  Throws on error.
+   * given uri. Rejects on error.
    *
    * @param uri The uri to load in the new tab
-   * @return nothing
+   * @return Promise
    */
-  Add(uri, fn) {
-
-    // Open the uri in a new tab in the current browser window, and calls
-    // the callback fn from the tab's onload handler.
+  async Add(uri) {
     let mainWindow = Services.wm.getMostRecentWindow("navigator:browser");
     let browser = mainWindow.getBrowser();
-    let mm = browser.ownerGlobal.messageManager;
-    mm.addMessageListener("tps:loadEvent", function onLoad(msg) {
-      mm.removeMessageListener("tps:loadEvent", onLoad);
-      fn();
+    let newtab = browser.addTab(uri);
+
+    // Wait for the tab to load.
+    await new Promise(resolve => {
+      let mm = browser.ownerGlobal.messageManager;
+      mm.addMessageListener("tps:loadEvent", function onLoad(msg) {
+        mm.removeMessageListener("tps:loadEvent", onLoad);
+        resolve();
+      });
     });
-    let newtab = browser.addTab(uri);
+
     browser.selectedTab = newtab;
+    // We might sync before SessionStore is done recording information, so try
+    // and force it to record everything. This is overkill, but effective.
+    await TabStateFlusher.flushWindow(mainWindow);
   },
 
   /**
    * Find
    *
    * Finds the specified uri and title in Weave's list of remote tabs
    * for the specified profile.
    *
--- a/services/sync/tps/extensions/tps/resource/tps.jsm
+++ b/services/sync/tps/extensions/tps/resource/tps.jsm
@@ -318,19 +318,17 @@ var TPS = {
   },
 
   async HandleTabs(tabs, action) {
     for (let tab of tabs) {
       Logger.logInfo("executing action " + action.toUpperCase() +
                      " on tab " + JSON.stringify(tab));
       switch (action) {
         case ACTION_ADD:
-          await new Promise(resolve => {
-            BrowserTabs.Add(tab.uri, resolve);
-          });
+          await BrowserTabs.Add(tab.uri);
           break;
         case ACTION_VERIFY:
           Logger.AssertTrue(typeof(tab.profile) != "undefined",
             "profile must be defined when verifying tabs");
           Logger.AssertTrue(
             BrowserTabs.Find(tab.uri, tab.title, tab.profile), "error locating tab");
           break;
         case ACTION_VERIFY_NOT:
--- a/servo/components/canvas/webgl_thread.rs
+++ b/servo/components/canvas/webgl_thread.rs
@@ -1061,31 +1061,18 @@ impl WebGLImpl {
         };
 
         chan.send(result).unwrap();
     }
 
     fn get_tex_parameter(gl: &gl::Gl,
                        target: u32,
                        pname: u32,
-                       chan: WebGLSender<WebGLResult<WebGLParameter>> ) {
-        let result = match pname {
-            gl::TEXTURE_MAG_FILTER |
-            gl::TEXTURE_MIN_FILTER |
-            gl::TEXTURE_WRAP_S |
-            gl::TEXTURE_WRAP_T => {
-                let parameter = gl.get_tex_parameter_iv(target, pname);
-                if parameter == 0 {
-                    Ok(WebGLParameter::Invalid)
-                } else {
-                    Ok(WebGLParameter::Int(parameter))
-                }
-            }
-            _ => Err(WebGLError::InvalidEnum)
-        };
+                       chan: WebGLSender<i32> ) {
+        let result = gl.get_tex_parameter_iv(target, pname);
         chan.send(result).unwrap();
     }
 
     fn finish(gl: &gl::Gl, chan: WebGLSender<()>) {
         gl.finish();
         chan.send(()).unwrap();
     }
 
@@ -1112,35 +1099,26 @@ impl WebGLImpl {
         };
 
         chan.send(result).unwrap();
     }
 
     fn vertex_attrib_offset(gl: &gl::Gl,
                             index: u32,
                             pname: u32,
-                            chan: WebGLSender<WebGLResult<isize>>) {
-        let result = match pname {
-                gl::VERTEX_ATTRIB_ARRAY_POINTER => Ok(gl.get_vertex_attrib_pointer_v(index, pname)),
-                _ => Err(WebGLError::InvalidEnum),
-        };
-
+                            chan: WebGLSender<isize>) {
+        let result = gl.get_vertex_attrib_pointer_v(index, pname);
         chan.send(result).unwrap();
     }
 
     fn buffer_parameter(gl: &gl::Gl,
                         target: u32,
                         param_id: u32,
-                        chan: WebGLSender<WebGLResult<WebGLParameter>>) {
-        let result = match param_id {
-            gl::BUFFER_SIZE |
-            gl::BUFFER_USAGE =>
-                Ok(WebGLParameter::Int(gl.get_buffer_parameter_iv(target, param_id))),
-            _ => Err(WebGLError::InvalidEnum),
-        };
+                        chan: WebGLSender<i32>) {
+        let result = gl.get_buffer_parameter_iv(target, param_id);
 
         chan.send(result).unwrap();
     }
 
     fn program_parameter(gl: &gl::Gl,
                          program_id: WebGLProgramId,
                          param_id: u32,
                          chan: WebGLSender<WebGLResult<WebGLParameter>>) {
@@ -1173,31 +1151,18 @@ impl WebGLImpl {
         };
 
         chan.send(result).unwrap();
     }
 
     fn shader_precision_format(gl: &gl::Gl,
                                shader_type: u32,
                                precision_type: u32,
-                               chan: WebGLSender<WebGLResult<(i32, i32, i32)>>) {
-        let result = match precision_type {
-            gl::LOW_FLOAT |
-            gl::MEDIUM_FLOAT |
-            gl::HIGH_FLOAT |
-            gl::LOW_INT |
-            gl::MEDIUM_INT |
-            gl::HIGH_INT => {
-                Ok(gl.get_shader_precision_format(shader_type, precision_type))
-            },
-            _=> {
-                Err(WebGLError::InvalidEnum)
-            }
-        };
-
+                               chan: WebGLSender<(i32, i32, i32)>) {
+        let result = gl.get_shader_precision_format(shader_type, precision_type);
         chan.send(result).unwrap();
     }
 
     fn get_extensions(gl: &gl::Gl, chan: WebGLSender<String>) {
         chan.send(gl.get_string(gl::EXTENSIONS)).unwrap();
     }
 
     fn uniform_location(gl: &gl::Gl,
--- a/servo/components/canvas_traits/webgl.rs
+++ b/servo/components/canvas_traits/webgl.rs
@@ -199,29 +199,29 @@ pub enum WebGLCommand {
     BindRenderbuffer(u32, Option<WebGLRenderbufferId>),
     BindTexture(u32, Option<WebGLTextureId>),
     DisableVertexAttribArray(u32),
     DrawArrays(u32, i32, i32),
     DrawElements(u32, i32, u32, i64),
     EnableVertexAttribArray(u32),
     FramebufferRenderbuffer(u32, u32, u32, Option<WebGLRenderbufferId>),
     FramebufferTexture2D(u32, u32, u32, Option<WebGLTextureId>, i32),
-    GetBufferParameter(u32, u32, WebGLSender<WebGLResult<WebGLParameter>>),
+    GetBufferParameter(u32, u32, WebGLSender<i32>),
     GetExtensions(WebGLSender<String>),
     GetParameter(u32, WebGLSender<WebGLResult<WebGLParameter>>),
-    GetTexParameter(u32, u32, WebGLSender<WebGLResult<WebGLParameter>>),
+    GetTexParameter(u32, u32, WebGLSender<i32>),
     GetProgramParameter(WebGLProgramId, u32, WebGLSender<WebGLResult<WebGLParameter>>),
     GetShaderParameter(WebGLShaderId, u32, WebGLSender<WebGLResult<WebGLParameter>>),
-    GetShaderPrecisionFormat(u32, u32, WebGLSender<WebGLResult<(i32, i32, i32)>>),
+    GetShaderPrecisionFormat(u32, u32, WebGLSender<(i32, i32, i32)>),
     GetActiveAttrib(WebGLProgramId, u32, WebGLSender<WebGLResult<(i32, u32, String)>>),
     GetActiveUniform(WebGLProgramId, u32, WebGLSender<WebGLResult<(i32, u32, String)>>),
     GetAttribLocation(WebGLProgramId, String, WebGLSender<Option<i32>>),
     GetUniformLocation(WebGLProgramId, String, WebGLSender<Option<i32>>),
     GetVertexAttrib(u32, u32, WebGLSender<WebGLResult<WebGLParameter>>),
-    GetVertexAttribOffset(u32, u32, WebGLSender<WebGLResult<isize>>),
+    GetVertexAttribOffset(u32, u32, WebGLSender<isize>),
     GetShaderInfoLog(WebGLShaderId, WebGLSender<String>),
     GetProgramInfoLog(WebGLProgramId, WebGLSender<String>),
     PolygonOffset(f32, f32),
     RenderbufferStorage(u32, u32, i32, i32),
     ReadPixels(i32, i32, i32, i32, u32, u32, WebGLSender<Vec<u8>>),
     SampleCoverage(f32, bool),
     Scissor(i32, i32, i32, i32),
     StencilFunc(u32, i32, u32),
--- a/servo/components/script/dom/webglrenderingcontext.rs
+++ b/servo/components/script/dom/webglrenderingcontext.rs
@@ -1241,27 +1241,31 @@ impl WebGLRenderingContextMethods for We
         let (sender, receiver) = webgl_channel().unwrap();
         self.send_command(WebGLCommand::DrawingBufferHeight(sender));
         receiver.recv().unwrap()
     }
 
     #[allow(unsafe_code)]
     // https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14.5
     unsafe fn GetBufferParameter(&self, _cx: *mut JSContext, target: u32, parameter: u32) -> JSVal {
+        let parameter_matches = match parameter {
+            constants::BUFFER_SIZE |
+            constants::BUFFER_USAGE => true,
+            _ => false,
+        };
+
+        if !parameter_matches {
+            self.webgl_error(InvalidEnum);
+            return NullValue();
+        }
+
         let (sender, receiver) = webgl_channel().unwrap();
         self.send_command(WebGLCommand::GetBufferParameter(target, parameter, sender));
 
-        match handle_potential_webgl_error!(self, receiver.recv().unwrap(), WebGLParameter::Invalid) {
-            WebGLParameter::Int(val) => Int32Value(val),
-            WebGLParameter::Bool(_) => panic!("Buffer parameter should not be bool"),
-            WebGLParameter::Float(_) => panic!("Buffer parameter should not be float"),
-            WebGLParameter::FloatArray(_) => panic!("Buffer parameter should not be float array"),
-            WebGLParameter::String(_) => panic!("Buffer parameter should not be string"),
-            WebGLParameter::Invalid => NullValue(),
-        }
+        Int32Value(receiver.recv().unwrap())
     }
 
     #[allow(unsafe_code)]
     // https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14.3
     unsafe fn GetParameter(&self, cx: *mut JSContext, parameter: u32) -> JSVal {
         // Handle the GL_*_BINDING without going all the way
         // to the GL, since we would just need to map back from GL's
         // returned ID to the WebGL* object we're tracking.
@@ -1337,41 +1341,49 @@ impl WebGLRenderingContextMethods for We
             }
             WebGLParameter::Invalid => NullValue(),
         }
     }
 
     #[allow(unsafe_code)]
     // https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14.8
     unsafe fn GetTexParameter(&self, _cx: *mut JSContext, target: u32, pname: u32) -> JSVal {
-        let texture = match target {
+        let target_matches = match target {
             constants::TEXTURE_2D |
-            constants::TEXTURE_CUBE_MAP => self.bound_texture(target),
+            constants::TEXTURE_CUBE_MAP => true,
+            _ => false,
+        };
+
+        let pname_matches = match pname {
+            constants::TEXTURE_MAG_FILTER |
+            constants::TEXTURE_MIN_FILTER |
+            constants::TEXTURE_WRAP_S |
+            constants::TEXTURE_WRAP_T => true,
+            _ => false,
+        };
+
+        if !target_matches || !pname_matches {
+            self.webgl_error(InvalidEnum);
+            return NullValue();
+        }
+
+        if self.bound_texture(target).is_none() {
+            self.webgl_error(InvalidOperation);
+            return NullValue();
+        }
+
+        let (sender, receiver) = webgl_channel().unwrap();
+        self.send_command(WebGLCommand::GetTexParameter(target, pname, sender));
+
+        match receiver.recv().unwrap() {
+            value if value != 0 => Int32Value(value),
             _ => {
                 self.webgl_error(InvalidEnum);
-                return NullValue();
+                NullValue()
             }
-        };
-        if texture.is_some() {
-            let (sender, receiver) = webgl_channel().unwrap();
-            self.send_command(WebGLCommand::GetTexParameter(target, pname, sender));
-            match handle_potential_webgl_error!(self, receiver.recv().unwrap(), WebGLParameter::Invalid) {
-                WebGLParameter::Int(val) => Int32Value(val),
-                WebGLParameter::Bool(_) => panic!("Texture parameter should not be bool"),
-                WebGLParameter::Float(_) => panic!("Texture parameter should not be float"),
-                WebGLParameter::FloatArray(_) => panic!("Texture parameter should not be float array"),
-                WebGLParameter::String(_) => panic!("Texture parameter should not be string"),
-                WebGLParameter::Invalid => {
-                    self.webgl_error(InvalidEnum);
-                    NullValue()
-                }
-            }
-        } else {
-            self.webgl_error(InvalidOperation);
-            NullValue()
         }
     }
 
     // https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14.3
     fn GetError(&self) -> u32 {
         let error_code = if let Some(error) = self.last_error.get() {
             match error {
                 WebGLError::InvalidEnum => constants::INVALID_ENUM,
@@ -2340,34 +2352,41 @@ impl WebGLRenderingContextMethods for We
                 WebGLParameter::Invalid => NullValue(),
             }
         } else {
             NullValue()
         }
     }
 
     // https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14.9
-    fn GetShaderPrecisionFormat(&self,
-                                shader_type: u32,
-                                precision_type: u32)
-                                -> Option<DomRoot<WebGLShaderPrecisionFormat>> {
+    fn GetShaderPrecisionFormat(
+        &self,
+        shader_type: u32,
+        precision_type: u32
+    ) -> Option<DomRoot<WebGLShaderPrecisionFormat>> {
+        match precision_type {
+            constants::LOW_FLOAT |
+            constants::MEDIUM_FLOAT |
+            constants::HIGH_FLOAT |
+            constants::LOW_INT |
+            constants::MEDIUM_INT |
+            constants::HIGH_INT => (),
+            _ => {
+                self.webgl_error(InvalidEnum);
+                return None;
+            },
+        }
+
         let (sender, receiver) = webgl_channel().unwrap();
         self.send_command(WebGLCommand::GetShaderPrecisionFormat(shader_type,
                                                                  precision_type,
                                                                  sender));
 
-        match receiver.recv().unwrap() {
-            Ok((range_min, range_max, precision)) => {
-                Some(WebGLShaderPrecisionFormat::new(self.global().as_window(), range_min, range_max, precision))
-            },
-            Err(error) => {
-                self.webgl_error(error);
-                None
-            }
-        }
+        let (range_min, range_max, precision) = receiver.recv().unwrap();
+        Some(WebGLShaderPrecisionFormat::new(self.global().as_window(), range_min, range_max, precision))
     }
 
     // https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14.10
     fn GetUniformLocation(&self,
                           program: Option<&WebGLProgram>,
                           name: DOMString) -> Option<DomRoot<WebGLUniformLocation>> {
         program.and_then(|p| {
             handle_potential_webgl_error!(self, p.get_uniform_location(name), None)
@@ -2408,20 +2427,24 @@ impl WebGLRenderingContextMethods for We
                 result.get()
             }
             WebGLParameter::Invalid => NullValue(),
         }
     }
 
     // https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14.10
     fn GetVertexAttribOffset(&self, index: u32, pname: u32) -> i64 {
+        if pname != constants::VERTEX_ATTRIB_ARRAY_POINTER {
+            self.webgl_error(InvalidEnum);
+            return 0;
+        }
         let (sender, receiver) = webgl_channel().unwrap();
         self.send_command(WebGLCommand::GetVertexAttribOffset(index, pname, sender));
 
-        handle_potential_webgl_error!(self, receiver.recv().unwrap(), 0) as i64
+        receiver.recv().unwrap() as i64
     }
 
     // https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14.3
     fn Hint(&self, target: u32, mode: u32) {
         if target != constants::GENERATE_MIPMAP_HINT && !self.extension_manager.is_hint_target_enabled(target) {
             return self.webgl_error(InvalidEnum);
         }
 
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_cli_arguments.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_cli_arguments.py
@@ -9,17 +9,17 @@ import platform
 try:
     import winreg
 except ImportError:
     try:
         import _winreg as winreg
     except ImportError:
         pass
 
-from marionette_harness import MarionetteTestCase
+from marionette_harness import MarionetteTestCase, skip
 
 
 class TestCommandLineArguments(MarionetteTestCase):
 
     def setUp(self):
         super(TestCommandLineArguments, self).setUp()
 
         self.orig_arguments = copy.copy(self.marionette.instance.app_args)
@@ -67,16 +67,17 @@ class TestCommandLineArguments(Marionett
             self.assertFalse(safe_mode, "Safe Mode has been enabled")
 
         winreg.CloseKey(reg_firefox)
         winreg.DeleteKey(reg_mozilla, "Firefox")
         winreg.CloseKey(reg_mozilla)
         winreg.DeleteKey(reg_policies, "Mozilla")
         winreg.CloseKey(reg_policies)
 
+    @skip("Bug 1430717 - Causes '1000s of no output' failures")
     def test_startup_timeout(self):
         startup_timeout = self.marionette.startup_timeout
 
         # Use a timeout which always cause an IOError
         self.marionette.startup_timeout = .1
         msg = "Process killed after {}s".format(self.marionette.startup_timeout)
 
         try:
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/RemoteMochitestStartup.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+function RemoteMochitestStartup() {}
+
+RemoteMochitestStartup.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+  classID: Components.ID("{791cb384-ba12-464e-82cd-d0e269da1b8f}"),
+
+  observe: function (subject, topic, data) {
+    switch (topic) {
+      case "profile-after-change":
+        Services.obs.addObserver(this, "chrome-document-global-created");
+        break;
+      case "chrome-document-global-created":
+        subject.addEventListener("DOMContentLoaded", function () {
+            let win = this;
+            if (win.location.href !== "chrome://browser/content/browser.xul") {
+                return;
+            }
+            Services.scriptloader.loadSubScript("chrome://mochikit/content/chrome-harness.js", win);
+            Services.scriptloader.loadSubScript("chrome://mochikit/content/mochitest-e10s-utils.js", win);
+            Services.scriptloader.loadSubScript("chrome://mochikit/content/browser-test.js", win);
+        }, {once: true});
+        break;
+    }
+  }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RemoteMochitestStartup]);
--- a/testing/mochitest/bootstrap.js
+++ b/testing/mochitest/bootstrap.js
@@ -43,18 +43,19 @@ function loadMochitest(e) {
   win.loadURI(url);
   if (flavor == "mochitest") {
     return;
   }
 
   WindowListener.setupWindow(win);
   Services.wm.addListener(WindowListener);
 
-  let overlay = "chrome://mochikit/content/browser-test-overlay.xul";
-  win.document.loadOverlay(overlay, null);
+  Services.scriptloader.loadSubScript("chrome://mochikit/content/chrome-harness.js", win);
+  Services.scriptloader.loadSubScript("chrome://mochikit/content/mochitest-e10s-utils.js", win);
+  Services.scriptloader.loadSubScript("chrome://mochikit/content/browser-test.js", win);
 }
 
 function startup(data, reason) {
   let win = Services.wm.getMostRecentWindow("navigator:browser");
   // wait for event fired from start_desktop.js containing the
   // suite and url to load
   win.addEventListener('mochitest-load', loadMochitest);
 }
deleted file mode 100644
--- a/testing/mochitest/browser-test-overlay.xul
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0"?>
-<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
-<!-- 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/. -->
-
-<overlay id="browserTestOverlay"
-         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-  <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"/>
-  <script type="application/javascript" src="chrome://mochikit/content/mochitest-e10s-utils.js"/>
-  <script type="application/javascript" src="chrome://mochikit/content/browser-test.js"/>
-</overlay>
--- a/testing/mochitest/jar.mn
+++ b/testing/mochitest/jar.mn
@@ -1,13 +1,12 @@
 mochikit.jar:
 % content mochikit %content/
   content/browser-harness.xul (browser-harness.xul)
   content/browser-test.js (browser-test.js)
-  content/browser-test-overlay.xul (browser-test-overlay.xul)
   content/chrome-harness.js (chrome-harness.js)
   content/mochitest-e10s-utils.js (mochitest-e10s-utils.js)
   content/shutdown-leaks-collector.js (shutdown-leaks-collector.js)
   content/ShutdownLeaksCollector.jsm (ShutdownLeaksCollector.jsm)
   content/harness.xul (harness.xul)
   content/redirect.html (redirect.html)
   content/server.js (server.js)
   content/chunkifyTests.js (chunkifyTests.js)
--- a/testing/mochitest/moz.build
+++ b/testing/mochitest/moz.build
@@ -39,17 +39,16 @@ TEST_HARNESS_FILES.testing.mochitest += 
     '/build/sanitizers/lsan_suppressions.txt',
     '/build/sanitizers/ubsan_suppressions.txt',
     '/build/valgrind/cross-architecture.sup',
     '/build/valgrind/i386-pc-linux-gnu.sup',
     '/build/valgrind/x86_64-pc-linux-gnu.sup',
     '/netwerk/test/httpserver/httpd.js',
     'bisection.py',
     'browser-harness.xul',
-    'browser-test-overlay.xul',
     'browser-test.js',
     'chrome-harness.js',
     'chunkifyTests.js',
     'gen_template.pl',
     'harness.xul',
     'leaks.py',
     'mach_test_package_commands.py',
     'manifest.webapp',
@@ -61,16 +60,21 @@ TEST_HARNESS_FILES.testing.mochitest += 
     'rungeckoview.py',
     'runrobocop.py',
     'runtests.py',
     'runtestsremote.py',
     'server.js',
     'start_desktop.js',
 ]
 
+if CONFIG['OS_TARGET'] == 'Android':
+    TEST_HARNESS_FILES.testing.mochitest += [
+        'RemoteMochitestStartup.js',
+    ]
+
 TEST_HARNESS_FILES.testing.mochitest.embed += [
     'embed/Xm5i5kbIXzc',
     'embed/Xm5i5kbIXzc^headers^',
 ]
 
 TEST_HARNESS_FILES.testing.mochitest.pywebsocket += [
     'pywebsocket/standalone.py',
 ]
--- a/testing/mochitest/runtestsremote.py
+++ b/testing/mochitest/runtestsremote.py
@@ -10,16 +10,17 @@ sys.path.insert(
     0, os.path.abspath(
         os.path.realpath(
             os.path.dirname(__file__))))
 
 from automation import Automation
 from remoteautomation import RemoteAutomation, fennecLogcatFilters
 from runtests import MochitestDesktop, MessageLogger
 from mochitest_options import MochitestArgumentParser
+from shutil import copyfile
 
 import mozdevice
 import mozinfo
 
 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
 
 
 class MochiRemote(MochitestDesktop):
@@ -198,23 +199,32 @@ class MochiRemote(MochitestDesktop):
         options.profilePath = self.remoteProfile
         return manifest
 
     def addChromeToProfile(self, options):
         manifest = MochitestDesktop.addChromeToProfile(self, options)
 
         # Support Firefox (browser), SeaMonkey (navigator), and Webapp Runtime (webapp).
         if options.flavor == 'chrome':
-            # append overlay to chrome.manifest
-            chrome = ("overlay chrome://browser/content/browser.xul "
-                      "chrome://mochikit/content/browser-test-overlay.xul")
+            # append component to chrome.manifest to start tests
+            chrome = """
+component {791cb384-ba12-464e-82cd-d0e269da1b8f} RemoteMochitestStartup.js
+contract @mozilla.org/mochitest/startup;1 {791cb384-ba12-464e-82cd-d0e269da1b8f}
+category profile-after-change RemoteMochitestStartup @mozilla.org/mochitest/startup;1 process=main
+"""
+
             path = os.path.join(options.profilePath, 'extensions', 'staged',
                                 'mochikit@mozilla.org', 'chrome.manifest')
             with open(path, "a") as f:
                 f.write(chrome)
+
+            copyfile(os.path.join(SCRIPT_DIR, 'RemoteMochitestStartup.js'),
+                     os.path.join(options.profilePath, 'extensions', 'staged',
+                                  'mochikit@mozilla.org', "RemoteMochitestStartup.js"))
+
         return manifest
 
     def buildURLOptions(self, options, env):
         self.localLog = options.logFile
         options.logFile = self.remoteLog
         options.fileLevel = 'INFO'
         options.profilePath = self.localProfile
         env["MOZ_HIDE_RESULTS_TABLE"] = "1"
--- a/testing/mozbase/docs/mozrunner.rst
+++ b/testing/mozbase/docs/mozrunner.rst
@@ -108,35 +108,35 @@ gecko process.
 
 
 Using a device runner
 ---------------------
 
 The previous examples used a GeckoRuntimeRunner. If you want to control a
 gecko process on a remote device, you need to use a DeviceRunner. The api is
 nearly identical except you don't pass in a binary, instead you create a device
-object. For example, for B2G (Firefox OS) emulators you might do:
+object. For example to run Firefox for Android on the emulator, you might do:
 
 .. code-block:: python
 
-    from mozrunner import B2GEmulatorRunner
+    from mozrunner import FennecEmulatorRunner
 
-    b2g_home = 'path/to/B2G'
-    runner = B2GEmulatorRunner(arch='arm', b2g_home=b2g_home)
+    avd_home = 'path/to/avd'
+    runner = FennecEmulatorRunner(app='org.mozilla.fennec', avd_home=avd_home)
     runner.start()
     runner.wait()
 
 Device runners have a `device` object. Remember that the gecko process runs on
 the device. In the case of the emulator, it is possible to start the
 device independently of the gecko process.
 
 .. code-block:: python
 
-    runner.device.start() # launches the emulator (which also launches gecko)
-    runner.start()        # stops the gecko process, installs the profile, restarts the gecko process
+    runner.device.start() # launches the emulator
+    runner.start()        # stops the gecko process (if started), installs the profile, (re)starts the gecko process
 
 
 Runner API Documentation
 ------------------------
 
 Application Runners
 ~~~~~~~~~~~~~~~~~~~
 .. automodule:: mozrunner.runners
@@ -165,13 +165,13 @@ Device API Documentation
 Generally using the device classes directly shouldn't be required, but in some
 cases it may be desirable.
 
 Device
 ~~~~~~
 .. autoclass:: mozrunner.devices.Device
    :members:
 
-Emulator
-~~~~~~~~
-.. autoclass:: mozrunner.devices.Emulator
+EmulatorAVD
+~~~~~~~~~~~
+.. autoclass:: mozrunner.devices.EmulatorAVD
    :show-inheritance:
    :members:
--- a/testing/mozbase/mozrunner/mozrunner/application.py
+++ b/testing/mozbase/mozrunner/mozrunner/application.py
@@ -1,33 +1,31 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
 from abc import ABCMeta, abstractmethod
 from distutils.spawn import find_executable
-import glob
 import os
 import posixpath
 
-from mozdevice import DeviceManagerADB, DMError, DroidADB
+from mozdevice import DeviceManagerADB, DroidADB
 from mozprofile import (
     Profile,
     FirefoxProfile,
     ThunderbirdProfile
 )
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 
 def get_app_context(appname):
     context_map = {'default': DefaultContext,
-                   'b2g': B2GContext,
                    'firefox': FirefoxContext,
                    'thunderbird': ThunderbirdContext,
                    'fennec': FennecContext}
     if appname not in context_map:
         raise KeyError("Application '%s' not supported!" % appname)
     return context_map[appname]
 
 
@@ -122,130 +120,14 @@ class FennecContext(RemoteContext):
         if not self._remote_profiles_ini:
             self._remote_profiles_ini = posixpath.join(
                 self.dm.getAppRoot(self.remote_process),
                 'files', 'mozilla', 'profiles.ini'
             )
         return self._remote_profiles_ini
 
 
-class B2GContext(RemoteContext):
-    _remote_settings_db = None
-
-    def __init__(self, b2g_home=None, adb_path=None):
-        self.homedir = b2g_home or os.environ.get('B2G_HOME')
-
-        if self.homedir is not None and not os.path.isdir(self.homedir):
-            raise OSError('Homedir \'%s\' does not exist!' % self.homedir)
-
-        self._adb = adb_path
-        self._update_tools = None
-        self._fastboot = None
-
-        self.remote_binary = '/system/bin/b2g.sh'
-        self.remote_bundles_dir = '/system/b2g/distribution/bundles'
-        self.remote_busybox = '/system/bin/busybox'
-        self.remote_process = '/system/b2g/b2g'
-        self.remote_profiles_ini = '/data/b2g/mozilla/profiles.ini'
-        self.remote_settings_json = '/system/b2g/defaults/settings.json'
-        self.remote_idb_dir = '/data/local/storage/permanent/chrome/idb'
-        self.remote_test_root = '/data/local/tests'
-        self.remote_webapps_dir = '/data/local/webapps'
-
-        self.remote_backup_files = [
-            self.remote_settings_json,
-            self.remote_webapps_dir,
-        ]
-
-    @property
-    def fastboot(self):
-        if self._fastboot is None:
-            self._fastboot = self.which('fastboot')
-        return self._fastboot
-
-    @property
-    def update_tools(self):
-        if self._update_tools is None and self.homedir is not None:
-            self._update_tools = os.path.join(self.homedir, 'tools', 'update-tools')
-        return self._update_tools
-
-    @property
-    def bindir(self):
-        if self._bindir is None and self.homedir is not None:
-            # TODO get this via build configuration
-            path = os.path.join(self.homedir, 'out', 'host', '*', 'bin')
-            paths = glob.glob(path)
-            if paths:
-                self._bindir = paths[0]
-        return self._bindir
-
-    @property
-    def remote_settings_db(self):
-        if not self._remote_settings_db:
-            for filename in self.dm.listFiles(self.remote_idb_dir):
-                if filename.endswith('ssegtnti.sqlite'):
-                    self._remote_settings_db = posixpath.join(self.remote_idb_dir, filename)
-                    break
-            else:
-                raise DMError("Could not find settings db in '%s'!" % self.remote_idb_dir)
-        return self._remote_settings_db
-
-    def stop_application(self):
-        self.dm.shellCheckOutput(['stop', 'b2g'])
-
-    def setup_profile(self, profile):
-        # For some reason user.js in the profile doesn't get picked up.
-        # Manually copy it over to prefs.js. See bug 1009730 for more details.
-        self.dm.moveTree(posixpath.join(self.remote_profile, 'user.js'),
-                         posixpath.join(self.remote_profile, 'prefs.js'))
-
-        if self.dm.fileExists(posixpath.join(self.remote_profile, 'settings.json')):
-            # On devices, settings.json is only read from the profile if
-            # the system location doesn't exist.
-            if self.dm.fileExists(self.remote_settings_json):
-                self.dm.removeFile(self.remote_settings_json)
-
-            # Delete existing settings db and create a new empty one to force new
-            # settings to be loaded.
-            self.dm.removeFile(self.remote_settings_db)
-            self.dm.shellCheckOutput(['touch', self.remote_settings_db])
-
-        # On devices, the webapps are located in /data/local/webapps instead of the profile.
-        # In some cases we may need to replace the existing webapps, in others we may just
-        # need to leave them in the profile. If the system app is present in the profile
-        # webapps, it's a good indication that they should replace the existing ones wholesale.
-        profile_webapps = posixpath.join(self.remote_profile, 'webapps')
-        if self.dm.dirExists(posixpath.join(profile_webapps, 'system.gaiamobile.org')):
-            self.dm.removeDir(self.remote_webapps_dir)
-            self.dm.moveTree(profile_webapps, self.remote_webapps_dir)
-
-        # On devices extensions are installed in the system dir
-        extension_dir = os.path.join(profile.profile, 'extensions', 'staged')
-        if os.path.isdir(extension_dir):
-            # Copy the extensions to the B2G bundles dir.
-            for filename in os.listdir(extension_dir):
-                path = posixpath.join(self.remote_bundles_dir, filename)
-                if self.dm.fileExists(path):
-                    self.dm.removeFile(path)
-            self.dm.pushDir(extension_dir, self.remote_bundles_dir)
-
-    def cleanup_profile(self):
-        # Delete any bundled extensions
-        extension_dir = posixpath.join(self.remote_profile, 'extensions', 'staged')
-        if self.dm.dirExists(extension_dir):
-            for filename in self.dm.listFiles(extension_dir):
-                try:
-                    self.dm.removeDir(posixpath.join(self.remote_bundles_dir, filename))
-                except DMError:
-                    pass
-
-        if self.dm.fileExists(posixpath.join(self.remote_profile, 'settings.json')):
-            # Force settings.db to be restored to defaults
-            self.dm.removeFile(self.remote_settings_db)
-            self.dm.shellCheckOutput(['touch', self.remote_settings_db])
-
-
 class FirefoxContext(object):
     profile_class = FirefoxProfile
 
 
 class ThunderbirdContext(object):
     profile_class = ThunderbirdProfile
--- a/testing/mozbase/mozrunner/mozrunner/base/device.py
+++ b/testing/mozbase/mozrunner/mozrunner/base/device.py
@@ -15,17 +15,17 @@ import mozfile
 
 from .runner import BaseRunner
 from ..devices import BaseEmulator
 
 
 class DeviceRunner(BaseRunner):
     """
     The base runner class used for running gecko on
-    remote devices (or emulators), such as B2G.
+    remote devices (or emulators).
     """
     env = {'MOZ_CRASHREPORTER': '1',
            'MOZ_CRASHREPORTER_NO_REPORT': '1',
            'MOZ_CRASHREPORTER_SHUTDOWN': '1',
            'MOZ_HIDE_RESULTS_TABLE': '1',
            'MOZ_LOG': 'signaling:3,mtransport:4,DataChannel:4,jsep:4,MediaPipelineFactory:4',
            'R_LOG_LEVEL': '6',
            'R_LOG_DESTINATION': 'stderr',
--- a/testing/mozbase/mozrunner/mozrunner/devices/__init__.py
+++ b/testing/mozbase/mozrunner/mozrunner/devices/__init__.py
@@ -1,15 +1,15 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
-from .emulator import BaseEmulator, Emulator, EmulatorAVD
+from .emulator import BaseEmulator, EmulatorAVD
 from .base import Device
 
 from mozrunner.devices import emulator_battery
 from mozrunner.devices import emulator_geo
 from mozrunner.devices import emulator_screen
 
-__all__ = ['BaseEmulator', 'Emulator', 'EmulatorAVD', 'Device',
+__all__ = ['BaseEmulator', 'EmulatorAVD', 'Device',
            'emulator_battery', 'emulator_geo', 'emulator_screen']
--- a/testing/mozbase/mozrunner/mozrunner/devices/emulator.py
+++ b/testing/mozbase/mozrunner/mozrunner/devices/emulator.py
@@ -190,85 +190,16 @@ class BaseEmulator(Device):
         return self._get_telnet_response(command)
 
     def __del__(self):
         if self.telnet:
             self.telnet.write('exit\n')
             self.telnet.read_all()
 
 
-class Emulator(BaseEmulator):
-
-    def __init__(self, app_ctx, arch, resolution=None, sdcard=None, userdata=None,
-                 no_window=None, binary=None, **kwargs):
-        super(Emulator, self).__init__(app_ctx, arch=arch, binary=binary, **kwargs)
-
-        # emulator args
-        self.resolution = resolution or '320x480'
-        self._sdcard_size = sdcard
-        self._sdcard = None
-        self.userdata = tempfile.NamedTemporaryFile(prefix='userdata-qemu', dir=self.tmpdir)
-        self.initdata = userdata if userdata else os.path.join(self.arch.sysdir, 'userdata.img')
-        self.no_window = no_window
-
-    @property
-    def sdcard(self):
-        if self._sdcard_size and not self._sdcard:
-            self._sdcard = SDCard(self, self._sdcard_size).path
-        else:
-            return self._sdcard
-
-    @property
-    def args(self):
-        """
-        Arguments to pass into the emulator binary.
-        """
-        qemu_args = super(Emulator, self).args
-        qemu_args.extend([
-            '-kernel', self.arch.kernel,
-            '-sysdir', self.arch.sysdir,
-            '-data', self.userdata.name,
-            '-initdata', self.initdata,
-            '-wipe-data'])
-        if self.no_window:
-            qemu_args.append('-no-window')
-        if self.sdcard:
-            qemu_args.extend(['-sdcard', self.sdcard])
-        qemu_args.extend(['-memory', '512',
-                          '-partition-size', '512',
-                          '-verbose',
-                          '-skin', self.resolution,
-                          '-gpu', 'on',
-                          '-qemu'] + self.arch.extra_args)
-        return qemu_args
-
-    def connect(self):
-        """
-        Connects to a running device. If no serial was specified in the
-        constructor, defaults to the first entry in `adb devices`.
-        """
-        if self.connected:
-            return
-
-        super(Emulator, self).connect()
-        self.geo.set_default_location()
-        self.screen.initialize()
-
-        # setup DNS fix for networking
-        self.app_ctx.dm.shellCheckOutput(['setprop', 'net.dns1', '10.0.2.3'])
-
-    def cleanup(self):
-        """
-        Cleans up and kills the emulator, if it was started by mozrunner.
-        """
-        super(Emulator, self).cleanup()
-        # Remove temporary files
-        self.userdata.close()
-
-
 class EmulatorAVD(BaseEmulator):
 
     def __init__(self, app_ctx, binary, avd, port=5554, **kwargs):
         super(EmulatorAVD, self).__init__(app_ctx, binary=binary, avd=avd, **kwargs)
         self.port = port
 
     @property
     def args(self):
--- a/testing/mozbase/mozrunner/mozrunner/runners.py
+++ b/testing/mozbase/mozrunner/mozrunner/runners.py
@@ -1,23 +1,23 @@
 # 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/.
 
 
 """
 This module contains a set of shortcut methods that create runners for commonly
-used Mozilla applications, such as Firefox or B2G emulator.
+used Mozilla applications, such as Firefox, Firefox for Android or Thunderbird.
 """
 
 from __future__ import absolute_import
 
 from .application import get_app_context
-from .base import DeviceRunner, GeckoRuntimeRunner, FennecRunner
-from .devices import Emulator, EmulatorAVD, Device
+from .base import GeckoRuntimeRunner, FennecRunner
+from .devices import EmulatorAVD
 
 
 def Runner(*args, **kwargs):
     """
     Create a generic GeckoRuntime runner.
 
     :param binary: Path to binary.
     :param cmdargs: Arguments to pass into binary.
@@ -69,37 +69,16 @@ def ThunderbirdRunner(*args, **kwargs):
     :param show_crash_reporter: allow the crash reporter window to pop up.
         Defaults to False.
     :returns: A GeckoRuntimeRunner for Thunderbird.
     """
     kwargs['app_ctx'] = get_app_context('thunderbird')()
     return GeckoRuntimeRunner(*args, **kwargs)
 
 
-def B2GDesktopRunner(*args, **kwargs):
-    """
-    Create a B2G desktop runner.
-
-    :param binary: Path to b2g desktop binary.
-    :param cmdargs: Arguments to pass into binary.
-    :param profile: Profile object to use.
-    :param env: Environment variables to pass into the gecko process.
-    :param clean_profile: If True, restores profile back to original state.
-    :param process_class: Class used to launch the binary.
-    :param process_args: Arguments to pass into process_class.
-    :param symbols_path: Path to symbol files used for crash analysis.
-    :param show_crash_reporter: allow the crash reporter window to pop up.
-        Defaults to False.
-    :returns: A GeckoRuntimeRunner for b2g desktop.
-    """
-    # There is no difference between a generic and b2g desktop runner,
-    # but expose a separate entry point for clarity.
-    return Runner(*args, **kwargs)
-
-
 def FennecEmulatorRunner(avd='mozemulator-4.3',
                          adb_path=None,
                          avd_home=None,
                          logdir=None,
                          serial=None,
                          binary=None,
                          app='org.mozilla.fennec',
                          **kwargs):
@@ -128,87 +107,14 @@ def FennecEmulatorRunner(avd='mozemulato
                    'binary': binary,
                    'serial': serial,
                    'logdir': logdir}
     return FennecRunner(device_class=EmulatorAVD,
                         device_args=device_args,
                         **kwargs)
 
 
-def B2GEmulatorRunner(arch='arm',
-                      b2g_home=None,
-                      adb_path=None,
-                      logdir=None,
-                      binary=None,
-                      no_window=None,
-                      resolution=None,
-                      sdcard=None,
-                      userdata=None,
-                      **kwargs):
-    """
-    Create a B2G emulator runner.
-
-    :param arch: The architecture of the emulator, either 'arm' or 'x86'. Defaults to 'arm'.
-    :param b2g_home: Path to root B2G repository.
-    :param logdir: Path to save logfiles such as logcat and qemu output.
-    :param no_window: Run emulator without a window.
-    :param resolution: Screen resolution to set emulator to, e.g '800x1000'.
-    :param sdcard: Path to local emulated sdcard storage.
-    :param userdata: Path to custom userdata image.
-    :param profile: Profile object to use.
-    :param env: Environment variables to pass into the b2g.sh process.
-    :param clean_profile: If True, restores profile back to original state.
-    :param process_class: Class used to launch the b2g.sh process.
-    :param process_args: Arguments to pass into the b2g.sh process.
-    :param symbols_path: Path to symbol files used for crash analysis.
-    :returns: A DeviceRunner for B2G emulators.
-    """
-    kwargs['app_ctx'] = get_app_context('b2g')(b2g_home, adb_path=adb_path)
-    device_args = {'app_ctx': kwargs['app_ctx'],
-                   'arch': arch,
-                   'binary': binary,
-                   'resolution': resolution,
-                   'sdcard': sdcard,
-                   'userdata': userdata,
-                   'no_window': no_window,
-                   'logdir': logdir}
-    return DeviceRunner(device_class=Emulator,
-                        device_args=device_args,
-                        **kwargs)
-
-
-def B2GDeviceRunner(b2g_home=None,
-                    adb_path=None,
-                    logdir=None,
-                    serial=None,
-                    **kwargs):
-    """
-    Create a B2G device runner.
-
-    :param b2g_home: Path to root B2G repository.
-    :param logdir: Path to save logfiles such as logcat.
-    :param serial: Serial of device to connect to as seen in `adb devices`.
-    :param profile: Profile object to use.
-    :param env: Environment variables to pass into the b2g.sh process.
-    :param clean_profile: If True, restores profile back to original state.
-    :param process_class: Class used to launch the b2g.sh process.
-    :param process_args: Arguments to pass into the b2g.sh process.
-    :param symbols_path: Path to symbol files used for crash analysis.
-    :returns: A DeviceRunner for B2G devices.
-    """
-    kwargs['app_ctx'] = get_app_context('b2g')(b2g_home, adb_path=adb_path)
-    device_args = {'app_ctx': kwargs['app_ctx'],
-                   'logdir': logdir,
-                   'serial': serial}
-    return DeviceRunner(device_class=Device,
-                        device_args=device_args,
-                        **kwargs)
-
-
 runners = {
     'default': Runner,
-    'b2g_desktop': B2GDesktopRunner,
-    'b2g_emulator': B2GEmulatorRunner,
-    'b2g_device': B2GDeviceRunner,
     'firefox': FirefoxRunner,
     'thunderbird': ThunderbirdRunner,
     'fennec': FennecEmulatorRunner
 }
--- a/testing/mozharness/configs/multi_locale/standalone_mozilla-central.py
+++ b/testing/mozharness/configs/multi_locale/standalone_mozilla-central.py
@@ -5,29 +5,27 @@ BUILD_DIR = "mozilla-central"
 # e.g. "releases/mozilla-aurora"
 REPO_PATH = "mozilla-central"
 # This is where the l10n repos are (everything after https://hg.mozilla.org/)
 # for mozilla-central, that's "l10n-central".
 # For mozilla-aurora, that's "releases/l10n/mozilla-aurora"
 L10N_REPO_PATH = "l10n-central"
 # Currently this is assumed to be a subdirectory of your build dir
 OBJDIR = "objdir-droid"
-# Set this to mobile/xul for XUL Fennec
-ANDROID_DIR = "mobile/android"
 # Absolute path to your mozconfig.
 # By default it looks at "./mozconfig"
 MOZCONFIG = os.path.join(os.getcwd(), "mozconfig")
 
 config = {
     "work_dir": ".",
     "log_name": "multilocale",
     "objdir": OBJDIR,
     "locales_file": "%s/mobile/locales/l10n-changesets.json" % BUILD_DIR,
     "locales_platform": "android-multilocale",
-    "locales_dir": "%s/locales" % ANDROID_DIR,
+    "locales_dir": "mobile/android/locales",
     "ignore_locales": ["en-US", "multi"],
     "repos": [{
         "repo": "https://hg.mozilla.org/%s" % REPO_PATH,
         "branch": "default",
         "dest": BUILD_DIR,
     }],
     "vcs_share_base": "/builds/hg-shared",
     "l10n_repos": [],
@@ -38,12 +36,13 @@ config = {
     "mozconfig": MOZCONFIG,
     "default_actions": [
         "pull-locale-source",
         "build",
         "package-en-US",
         "backup-objdir",
         "restore-objdir",
         "add-locales",
+        "android-assemble-app",
         "package-multi",
         "summary",
     ],
 }
--- a/testing/mozharness/mozharness/mozilla/building/buildbase.py
+++ b/testing/mozharness/mozharness/mozilla/building/buildbase.py
@@ -1338,16 +1338,17 @@ or run without that action (ie: --no-{ac
             sys.executable,
             multil10n_path,
             '--config-file',
             'multi_locale/%s_%s.json' % (branch, multi_config_pf),
             '--config-file',
             'multi_locale/android-mozharness-build.json',
             '--pull-locale-source',
             '--add-locales',
+            '--android-assemble-app',
             '--package-multi',
             '--summary',
         ]
 
         self.run_command(cmd, env=self.query_build_env(), cwd=base_work_dir,
                          halt_on_failure=True)
 
         package_cmd = [
--- a/testing/mozharness/mozharness/mozilla/l10n/multi_locale_build.py
+++ b/testing/mozharness/mozharness/mozilla/l10n/multi_locale_build.py
@@ -78,24 +78,29 @@ class MultiLocaleBuild(LocalesMixin, Mer
          "default": "l10n",
          "help": "Specify the l10n dir name"
          }
     ]]
 
     def __init__(self, require_config_file=True):
         LocalesMixin.__init__(self)
         MercurialScript.__init__(self, config_options=self.config_options,
-                                 all_actions=['clobber', 'pull-build-source',
+                                 all_actions=['clobber',
+                                              'pull-build-source',
                                               'pull-locale-source',
-                                              'build', 'package-en-US',
+                                              'build',
+                                              'package-en-US',
                                               'upload-en-US',
                                               'backup-objdir',
                                               'restore-objdir',
-                                              'add-locales', 'package-multi',
-                                              'upload-multi', 'summary'],
+                                              'add-locales',
+                                              'android-assemble-app',
+                                              'package-multi',
+                                              'upload-multi',
+                                              'summary'],
                                  require_config_file=require_config_file)
 
     def query_l10n_env(self):
         return self.query_env()
 
     def clobber(self):
         c = self.config
         if c['work_dir'] != '.':
@@ -130,16 +135,26 @@ class MultiLocaleBuild(LocalesMixin, Mer
 
         mach = os.path.join(dirs['abs_mozilla_dir'], 'mach')
         env = self.query_env()
         if self._process_command(command=[sys.executable, mach, 'build'],
                                  cwd=dirs['abs_mozilla_dir'],
                                  env=env, error_list=MakefileErrorList):
             self.fatal("Erroring out after the build failed.")
 
+    def android_assemble_app(self):
+        dirs = self.query_abs_dirs()
+
+        command = 'make -C mobile/android/base android_apks'
+        env = self.query_env()
+        if self._process_command(command=command,
+                                 cwd=dirs['abs_objdir'],
+                                 env=env, error_list=MakefileErrorList):
+            self.fatal("Erroring out after assembling Android APKs failed.")
+
     def add_locales(self):
         c = self.config
         dirs = self.query_abs_dirs()
         locales = self.query_locales()
 
         for locale in locales:
             command = 'make chrome-%s L10NBASEDIR=%s' % (locale, dirs['abs_l10n_dir'])
             status = self._process_command(command=command,
--- a/toolkit/components/extensions/ext-runtime.js
+++ b/toolkit/components/extensions/ext-runtime.js
@@ -7,17 +7,17 @@ ChromeUtils.defineModuleGetter(this, "Ad
                                "resource://gre/modules/AddonManager.jsm");
 ChromeUtils.defineModuleGetter(this, "AddonManagerPrivate",
                                "resource://gre/modules/AddonManager.jsm");
 ChromeUtils.defineModuleGetter(this, "ExtensionParent",
                                "resource://gre/modules/ExtensionParent.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
                                "resource://gre/modules/Services.jsm");
 ChromeUtils.defineModuleGetter(this, "DevToolsShim",
-                               "chrome://devtools-shim/content/DevToolsShim.jsm");
+                               "chrome://devtools-startup/content/DevToolsShim.jsm");
 
 this.runtime = class extends ExtensionAPI {
   getAPI(context) {
     let {extension} = context;
     return {
       runtime: {
         onStartup: new EventManager(context, "runtime.onStartup", fire => {
           if (context.incognito) {
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html
@@ -54,188 +54,16 @@ function getAuthHandler(result, blocking
   return ExtensionTestUtils.loadExtension({
     manifest: {
       permissions,
     },
     background: `(${background})(${JSON.stringify(result)})`,
   });
 }
 
-add_task(async function test_webRequest_auth() {
-  // Make use of head_webrequest to ensure event sequence.
-  let events = {
-    "onBeforeRequest":     [{urls: ["*://mochi.test/*"]}, ["blocking"]],
-    "onBeforeSendHeaders": [{urls: ["*://mochi.test/*"]}, ["blocking", "requestHeaders"]],
-    "onSendHeaders":       [{urls: ["*://mochi.test/*"]}, ["requestHeaders"]],
-    "onBeforeRedirect":    [{urls: ["*://mochi.test/*"]}],
-    "onHeadersReceived":   [{urls: ["*://mochi.test/*"]}, ["blocking", "responseHeaders"]],
-    "onAuthRequired":      [{urls: ["*://mochi.test/*"]}, ["blocking", "responseHeaders"]],
-    "onResponseStarted":   [{urls: ["*://mochi.test/*"]}],
-    "onCompleted":         [{urls: ["*://mochi.test/*"]}, ["responseHeaders"]],
-    "onErrorOccurred":     [{urls: ["*://mochi.test/*"]}],
-  };
-
-  let extension = makeExtension(events);
-  await extension.startup();
-  let authInfo = {
-    username: "testuser",
-    password: "testpass",
-  };
-  let expect = {
-    "authenticate.sjs": {
-      type: "xmlhttprequest",
-      // we expect these additional events after onAuthRequired
-      optional_events: ["onBeforeRequest", "onHeadersReceived"],
-      authInfo,
-    },
-  };
-  // expecting origin == undefined
-  extension.sendMessage("set-expected", {expect, origin: location.href});
-  await extension.awaitMessage("continue");
-
-  await testXHR(`${baseUrl}?realm=webRequest_auth&user=${authInfo.username}&pass=${authInfo.password}`);
-
-  await extension.awaitMessage("done");
-  await extension.unload();
-});
-
-// This test is the same as above, however we shouldn't receive onAuthRequired
-// since those credentials are now cached (thus optional_events is not set).
-add_task(async function test_webRequest_cached_credentials() {
-  // Make use of head_webrequest to ensure event sequence.
-  let events = {
-    "onBeforeRequest":     [{urls: ["*://mochi.test/*"]}, ["blocking"]],
-    "onBeforeSendHeaders": [{urls: ["*://mochi.test/*"]}, ["blocking", "requestHeaders"]],
-    "onSendHeaders":       [{urls: ["*://mochi.test/*"]}, ["requestHeaders"]],
-    "onBeforeRedirect":    [{urls: ["*://mochi.test/*"]}],
-    "onHeadersReceived":   [{urls: ["*://mochi.test/*"]}, ["blocking", "responseHeaders"]],
-    "onAuthRequired":      [{urls: ["*://mochi.test/*"]}, ["blocking", "responseHeaders"]],
-    "onResponseStarted":   [{urls: ["*://mochi.test/*"]}],
-    "onCompleted":         [{urls: ["*://mochi.test/*"]}, ["responseHeaders"]],
-    "onErrorOccurred":     [{urls: ["*://mochi.test/*"]}],
-  };
-
-  let extension = makeExtension(events);
-  await extension.startup();
-  let authInfo = {
-    username: "testuser",
-    password: "testpass",
-  };
-  let expect = {
-    "authenticate.sjs": {
-      type: "xmlhttprequest",
-      events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onResponseStarted", "onCompleted"],
-    },
-  };
-  // expecting origin == undefined
-  extension.sendMessage("set-expected", {expect, origin: location.href});
-  await extension.awaitMessage("continue");
-
-  await testXHR(`${baseUrl}?realm=webRequest_auth&user=${authInfo.username}&pass=${authInfo.password}`);
-
-  await extension.awaitMessage("done");
-  await extension.unload();
-});
-
-add_task(async function test_webRequest_cached_credentials2() {
-  let authCredentials = {
-    username: "testuser",
-    password: "testpass",
-  };
-  let ex1 = getAuthHandler();
-  await ex1.startup();
-
-  await testXHR(`${baseUrl}?realm=webRequest_auth&user=${authCredentials.username}&pass=${authCredentials.password}`);
-
-  await ex1.awaitMessage("onCompleted");
-  await ex1.unload();
-});
-
-add_task(async function test_webRequest_window() {
-  let authCredentials = {
-    username: "testuser",
-    password: "testpass",
-  };
-  let ex1 = getAuthHandler();
-  await ex1.startup();
-
-  let win = window.open(`${baseUrl}?realm=test_webRequest_window&user=${authCredentials.username}&pass=${authCredentials.password}`);
-
-  await ex1.awaitMessage("onCompleted");
-  await ex1.unload();
-  win.close();
-});
-
-add_task(async function test_webRequest_auth_cancelled() {
-  let authCredentials = {
-    username: "testuser_canceled",
-    password: "testpass_canceled",
-  };
-  let ex1 = getAuthHandler({authCredentials});
-  await ex1.startup();
-  let ex2 = getAuthHandler({cancel: true});
-  await ex2.startup();
-
-  await Assert.rejects(testXHR(`${baseUrl}?realm=test_webRequest_auth_cancelled&user=${authCredentials.username}&pass=${authCredentials.password}`), "caught rejected xhr");
-
-  await Promise.all([
-    ex1.awaitMessage("onAuthRequired"),
-    ex2.awaitMessage("onAuthRequired"),
-    ex1.awaitMessage("onErrorOccurred"),
-    ex2.awaitMessage("onErrorOccurred"),
-  ]);
-  await ex1.unload();
-  await ex2.unload();
-});
-
-add_task(async function test_webRequest_auth_nonblocking() {
-  // The first listener handles the auth request, the second listener
-  // is a non-blocking listener and cannot respond but will get the call.
-  let authCredentials = {
-    username: "foobar",
-    password: "testpass",
-  };
-  let handlingExt = getAuthHandler({authCredentials});
-  await handlingExt.startup();
-  let extension = getAuthHandler({}, false);
-  await extension.startup();
-
-  await testXHR(`${baseUrl}?realm=webRequest_auth_nonblocking&user=${authCredentials.username}&pass=${authCredentials.password}`);
-
-  await Promise.all([
-    extension.awaitMessage("onAuthRequired"),
-    extension.awaitMessage("onCompleted"),
-    handlingExt.awaitMessage("onAuthRequired"),
-    handlingExt.awaitMessage("onCompleted"),
-  ]);
-  await extension.unload();
-  await handlingExt.unload();
-});
-
-
-add_task(async function test_webRequest_auth_blocking_noreturn() {
-  // The first listener is blocking but doesn't return anything.  The second
-  // listener cancels the request.
-  let ext = getAuthHandler();
-  await ext.startup();
-  let canceler = getAuthHandler({cancel: true});
-  await canceler.startup();
-
-  await Assert.rejects(testXHR(`${baseUrl}?realm=auth_blocking_noreturn&user=auth_blocking_noreturn&pass=auth_blocking_noreturn`), "caught rejected xhr");
-
-  await Promise.all([
-    ext.awaitMessage("onAuthRequired"),
-    ext.awaitMessage("onErrorOccurred"),
-    canceler.awaitMessage("onAuthRequired"),
-    canceler.awaitMessage("onErrorOccurred"),
-  ]);
-  await ext.unload();
-  await canceler.unload();
-});
-
 add_task(async function test_webRequest_auth_nonblocking_forwardAuthProvider() {
   // The chrome script sets up a default auth handler on the channel, the
   // extension does not return anything in the authRequred call.  We should
   // get the call in the extension first, then in the chrome code where we
   // cancel the request to avoid dealing with the prompt dialog here.  The test
   // is to ensure that WebRequest calls the previous notificationCallbacks
   // if the authorization is not handled by the onAuthRequired handler.
 
@@ -307,24 +135,24 @@ add_task(async function test_webRequest_
       if (!(channel instanceof Ci.nsIHttpChannel && channel.URI.host === "mochi.test")) {
         return;
       }
       Services.obs.removeObserver(observer, "http-on-modify-request");
       channel.notificationCallbacks = {
         QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor,
                                                Ci.nsIAuthPrompt2]),
         getInterface: XPCOMUtils.generateQI([Ci.nsIAuthPrompt2]),
-        promptAuth(channel, level, authInfo) {
+        promptAuth(request, level, authInfo) {
           throw Cr.NS_ERROR_NO_INTERFACE;
         },
-        asyncPromptAuth(channel, callback, context, level, authInfo) {
+        asyncPromptAuth(request, callback, context, level, authInfo) {
           // We just cancel here, we're only ensuring that non-webrequest
           // notificationcallbacks get called if webrequest doesn't handle it.
           Promise.resolve().then(() => {
-            channel.cancel(Cr.NS_BINDING_ABORTED);
+            request.cancel(Cr.NS_BINDING_ABORTED);
             sendAsyncMessage("callback-complete");
           });
         },
       };
     };
     Services.obs.addObserver(observer, "http-on-modify-request");
     sendAsyncMessage("chrome-ready");
   });
@@ -339,88 +167,15 @@ add_task(async function test_webRequest_
   await callbackComplete;
   await handlingExt.awaitMessage("onAuthRequired");
   // We expect onErrorOccurred because the "default" authprompt above cancelled
   // the auth request to avoid a dialog.
   await handlingExt.awaitMessage("onErrorOccurred");
   await handlingExt.unload();
   chromeScript.destroy();
 });
-
-add_task(async function test_webRequest_duelingAuth() {
-  let exNone = getAuthHandler();
-  await exNone.startup();
-  let authCredentials = {
-    username: "testuser_da1",
-    password: "testpass_da1",
-  };
-  let ex1 = getAuthHandler({authCredentials});
-  await ex1.startup();
-  let exEmpty = getAuthHandler({});
-  await exEmpty.startup();
-  let ex2 = getAuthHandler({authCredentials: {
-    username: "testuser_da2",
-    password: "testpass_da2",
-  }});
-  await ex2.startup();
-
-  // XHR should succeed since the first credentials win, and they are correct.
-  await testXHR(`${baseUrl}?realm=test_webRequest_duelingAuth&user=${authCredentials.username}&pass=${authCredentials.password}`);
-
-  await Promise.all([
-    exNone.awaitMessage("onAuthRequired"),
-    exNone.awaitMessage("onCompleted"),
-    exEmpty.awaitMessage("onAuthRequired"),
-    exEmpty.awaitMessage("onCompleted"),
-    ex1.awaitMessage("onAuthRequired"),
-    ex1.awaitMessage("onCompleted"),
-    ex2.awaitMessage("onAuthRequired"),
-    ex2.awaitMessage("onCompleted"),
-  ]);
-  await Promise.all([
-    exNone.unload(),
-    exEmpty.unload(),
-    ex1.unload(),
-    ex2.unload(),
-  ]);
-});
-
-add_task(async function test_webRequest_auth_proxy() {
-  function background() {
-    let proxyOk = false;
-    browser.webRequest.onAuthRequired.addListener((details) => {
-      browser.test.succeed(`handlingExt onAuthRequired called with ${details.requestId} ${details.url}`);
-      if (details.isProxy) {
-        browser.test.succeed("providing proxy authorization");
-        proxyOk = true;
-        return {authCredentials: {username: "puser", password: "ppass"}};
-      }
-      browser.test.assertTrue(proxyOk, "providing www authorization after proxy auth");
-      browser.test.sendMessage("done");
-      return {authCredentials: {username: "auser", password: "apass"}};
-    }, {urls: ["*://mochi.test/*"]}, ["blocking"]);
-  }
-
-  let handlingExt = ExtensionTestUtils.loadExtension({
-    manifest: {
-      permissions: [
-        "webRequest",
-        "webRequestBlocking",
-        "*://mochi.test/*",
-      ],
-    },
-    background,
-  });
-
-  await handlingExt.startup();
-
-  await testXHR(`${baseUrl}?realm=auth_proxy&user=auser&pass=apass&proxy_user=puser&proxy_pass=ppass`);
-
-  await handlingExt.awaitMessage("done");
-  await handlingExt.unload();
-});
 </script>
 </head>
 <body>
 <div id="test">Authorization Test</div>
 
 </body>
 </html>
copy from toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html
copy to toolkit/components/extensions/test/xpcshell/test_ext_webRequest_auth.js
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_auth.js
@@ -1,426 +1,384 @@
-<!DOCTYPE HTML>
-
-<html>
-<head>
-<meta charset="utf-8">
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
-  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
-  <script type="text/javascript" src="head_webrequest.js"></script>
-  <script type="text/javascript" src="head.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-<script>
 "use strict";
 
-// This file defines content scripts.
-/* eslint-env mozilla/frame-script */
+Cu.importGlobalProperties(["URL"]);
+
+const HOSTS = new Set([
+  "example.com",
+]);
+
+const server = createHttpServer({hosts: HOSTS});
 
-let baseUrl = "http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/authenticate.sjs";
-function testXHR(url) {
-  return new Promise((resolve, reject) => {
-    let xhr = new XMLHttpRequest();
-    xhr.open("GET", url);
-    xhr.onload = resolve;
-    xhr.onabort = reject;
-    xhr.onerror = reject;
-    xhr.send();
-  });
-}
+const BASE_URL = "http://example.com";
+
+// Save seen realms for cache checking.
+let realms = new Set([]);
+
+server.registerPathHandler("/authenticate.sjs", (request, response) => {
+  let url = new URL(`${BASE_URL}${request.path}?${request.queryString}`);
+  let realm = url.searchParams.get("realm") || "mochitest";
+  let proxy_realm = url.searchParams.get("proxy_realm");
 
-function getAuthHandler(result, blocking = true) {
-  function background(result) {
-    browser.webRequest.onAuthRequired.addListener((details) => {
-      browser.test.succeed(`authHandler.onAuthRequired called with ${details.requestId} ${details.url} result ${JSON.stringify(result)}`);
-      browser.test.sendMessage("onAuthRequired");
-      return result;
-    }, {urls: ["*://mochi.test/*"]}, ["blocking"]);
-    browser.webRequest.onCompleted.addListener((details) => {
-      browser.test.succeed(`authHandler.onCompleted called with ${details.requestId} ${details.url}`);
-      browser.test.sendMessage("onCompleted");
-    }, {urls: ["*://mochi.test/*"]});
-    browser.webRequest.onErrorOccurred.addListener((details) => {
-      browser.test.succeed(`authHandler.onErrorOccurred called with ${details.requestId} ${details.url}`);
-      browser.test.sendMessage("onErrorOccurred");
-    }, {urls: ["*://mochi.test/*"]});
+  function checkAuthorization(authorization) {
+    let expected_user = url.searchParams.get("user");
+    if (!expected_user) {
+      return true;
+    }
+    let expected_pass = url.searchParams.get("pass");
+    let actual_user, actual_pass;
+    let authHeader = request.getHeader("Authorization");
+    let match = /Basic (.+)/.exec(authHeader);
+    if (match.length != 2) {
+      throw new Error("Couldn't parse auth header: " + authHeader);
+    }
+    let userpass = atob(match[1]); // no atob() :-(
+    match = /(.*):(.*)/.exec(userpass);
+    if (match.length != 3) {
+      throw new Error("Couldn't decode auth header: " + userpass);
+    }
+    actual_user = match[1];
+    actual_pass = match[2];
+    return expected_user === actual_user && expected_pass === actual_pass;
   }
 
-  let permissions = [
-    "webRequest",
-    "*://mochi.test/*",
-  ];
-  if (blocking) {
-    permissions.push("webRequestBlocking");
+  response.setHeader("Content-Type", "text/plain; charset=UTF-8", false);
+  if (proxy_realm && !request.hasHeader("Proxy-Authorization")) {
+    // We're not testing anything that requires checking the proxy auth user/password.
+    response.setStatusLine("1.0", 407, "Proxy authentication required");
+    response.setHeader("Proxy-Authenticate", `basic realm="${proxy_realm}"`, true);
+    response.write("proxy auth required");
+  } else if (!(realms.has(realm) && request.hasHeader("Authorization") && checkAuthorization())) {
+    realms.add(realm);
+    response.setStatusLine(request.httpVersion, 401, "Authentication required");
+    response.setHeader("WWW-Authenticate", `basic realm="${realm}"`, true);
+    response.write("auth required");
+  } else {
+    response.setStatusLine(request.httpVersion, 200, "OK");
+    response.write("ok, got authorization");
   }
+});
+
+function getExtension(bgConfig) {
+  function background(config) {
+    let path = config.path;
+    browser.webRequest.onBeforeRequest.addListener((details) => {
+      browser.test.log(`onBeforeRequest called with ${details.requestId} ${details.url}`);
+      browser.test.sendMessage("onBeforeRequest");
+      return config.onBeforeRequest.hasOwnProperty("result") && config.onBeforeRequest.result;
+    }, {urls: [path]}, config.onBeforeRequest.hasOwnProperty("extra") ? config.onBeforeRequest.extra : []);
+    browser.webRequest.onAuthRequired.addListener((details) => {
+      browser.test.log(`onAuthRequired called with ${details.requestId} ${details.url}`);
+      browser.test.assertEq(config.realm, details.realm, "providing www authorization");
+      browser.test.sendMessage("onAuthRequired");
+      return config.onAuthRequired.hasOwnProperty("result") && config.onAuthRequired.result;
+    }, {urls: [path]}, config.onAuthRequired.hasOwnProperty("extra") ? config.onAuthRequired.extra : []);
+    browser.webRequest.onCompleted.addListener((details) => {
+      browser.test.log(`onCompleted called with ${details.requestId} ${details.url}`);
+      browser.test.sendMessage("onCompleted");
+    }, {urls: [path]});
+    browser.webRequest.onErrorOccurred.addListener((details) => {
+      browser.test.log(`onErrorOccurred called with ${JSON.stringify(details)}`);
+      browser.test.sendMessage("onErrorOccurred");
+    }, {urls: [path]});
+  }
+
   return ExtensionTestUtils.loadExtension({
     manifest: {
-      permissions,
+      permissions: [
+        "webRequest",
+        "webRequestBlocking",
+        bgConfig.path,
+      ],
     },
-    background: `(${background})(${JSON.stringify(result)})`,
+    background: `(${background})(${JSON.stringify(bgConfig)})`,
   });
 }
 
 add_task(async function test_webRequest_auth() {
-  // Make use of head_webrequest to ensure event sequence.
-  let events = {
-    "onBeforeRequest":     [{urls: ["*://mochi.test/*"]}, ["blocking"]],
-    "onBeforeSendHeaders": [{urls: ["*://mochi.test/*"]}, ["blocking", "requestHeaders"]],
-    "onSendHeaders":       [{urls: ["*://mochi.test/*"]}, ["requestHeaders"]],
-    "onBeforeRedirect":    [{urls: ["*://mochi.test/*"]}],
-    "onHeadersReceived":   [{urls: ["*://mochi.test/*"]}, ["blocking", "responseHeaders"]],
-    "onAuthRequired":      [{urls: ["*://mochi.test/*"]}, ["blocking", "responseHeaders"]],
-    "onResponseStarted":   [{urls: ["*://mochi.test/*"]}],
-    "onCompleted":         [{urls: ["*://mochi.test/*"]}, ["responseHeaders"]],
-    "onErrorOccurred":     [{urls: ["*://mochi.test/*"]}],
+  let config = {
+    path: `${BASE_URL}/*`,
+    realm: `webRequest_auth${Math.random()}`,
+    onBeforeRequest: {
+      extra: ["blocking"],
+    },
+    onAuthRequired: {
+      extra: ["blocking"],
+      result: {
+        authCredentials: {
+          username: "testuser",
+          password: "testpass",
+        },
+      },
+    },
   };
 
-  let extension = makeExtension(events);
+  let extension = getExtension(config);
   await extension.startup();
-  let authInfo = {
-    username: "testuser",
-    password: "testpass",
-  };
-  let expect = {
-    "authenticate.sjs": {
-      type: "xmlhttprequest",
-      // we expect these additional events after onAuthRequired
-      optional_events: ["onBeforeRequest", "onHeadersReceived"],
-      authInfo,
-    },
-  };
-  // expecting origin == undefined
-  extension.sendMessage("set-expected", {expect, origin: location.href});
-  await extension.awaitMessage("continue");
 
-  await testXHR(`${baseUrl}?realm=webRequest_auth&user=${authInfo.username}&pass=${authInfo.password}`);
+  let requestUrl = `${BASE_URL}/authenticate.sjs?realm=${config.realm}`;
+  let contentPage = await ExtensionTestUtils.loadContentPage(requestUrl);
+  await Promise.all([
+    extension.awaitMessage("onBeforeRequest"),
+    extension.awaitMessage("onAuthRequired").then(() => {
+      return Promise.all([
+        extension.awaitMessage("onBeforeRequest"),
+        extension.awaitMessage("onCompleted"),
+      ]);
+    }),
+  ]);
+  await contentPage.close();
 
-  await extension.awaitMessage("done");
+  // Second time around to test cached credentials
+  contentPage = await ExtensionTestUtils.loadContentPage(requestUrl);
+  await Promise.all([
+    extension.awaitMessage("onBeforeRequest"),
+    extension.awaitMessage("onCompleted"),
+  ]);
+
+  await contentPage.close();
   await extension.unload();
 });
 
-// This test is the same as above, however we shouldn't receive onAuthRequired
-// since those credentials are now cached (thus optional_events is not set).
-add_task(async function test_webRequest_cached_credentials() {
-  // Make use of head_webrequest to ensure event sequence.
-  let events = {
-    "onBeforeRequest":     [{urls: ["*://mochi.test/*"]}, ["blocking"]],
-    "onBeforeSendHeaders": [{urls: ["*://mochi.test/*"]}, ["blocking", "requestHeaders"]],
-    "onSendHeaders":       [{urls: ["*://mochi.test/*"]}, ["requestHeaders"]],
-    "onBeforeRedirect":    [{urls: ["*://mochi.test/*"]}],
-    "onHeadersReceived":   [{urls: ["*://mochi.test/*"]}, ["blocking", "responseHeaders"]],
-    "onAuthRequired":      [{urls: ["*://mochi.test/*"]}, ["blocking", "responseHeaders"]],
-    "onResponseStarted":   [{urls: ["*://mochi.test/*"]}],
-    "onCompleted":         [{urls: ["*://mochi.test/*"]}, ["responseHeaders"]],
-    "onErrorOccurred":     [{urls: ["*://mochi.test/*"]}],
-  };
-
-  let extension = makeExtension(events);
-  await extension.startup();
-  let authInfo = {
-    username: "testuser",
-    password: "testpass",
-  };
-  let expect = {
-    "authenticate.sjs": {
-      type: "xmlhttprequest",
-      events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onResponseStarted", "onCompleted"],
+add_task(async function test_webRequest_auth_cancelled() {
+  // Test that any auth listener can cancel.
+  let config = {
+    path: `${BASE_URL}/*`,
+    realm: `webRequest_auth${Math.random()}`,
+    onBeforeRequest: {
+      extra: ["blocking"],
+    },
+    onAuthRequired: {
+      extra: ["blocking"],
+      result: {
+        authCredentials: {
+          username: "testuser",
+          password: "testpass",
+        },
+      },
     },
   };
-  // expecting origin == undefined
-  extension.sendMessage("set-expected", {expect, origin: location.href});
-  await extension.awaitMessage("continue");
 
-  await testXHR(`${baseUrl}?realm=webRequest_auth&user=${authInfo.username}&pass=${authInfo.password}`);
-
-  await extension.awaitMessage("done");
-  await extension.unload();
-});
-
-add_task(async function test_webRequest_cached_credentials2() {
-  let authCredentials = {
-    username: "testuser",
-    password: "testpass",
-  };
-  let ex1 = getAuthHandler();
+  let ex1 = getExtension(config);
+  config.onAuthRequired.result = {cancel: true};
+  let ex2 = getExtension(config);
   await ex1.startup();
-
-  await testXHR(`${baseUrl}?realm=webRequest_auth&user=${authCredentials.username}&pass=${authCredentials.password}`);
-
-  await ex1.awaitMessage("onCompleted");
-  await ex1.unload();
-});
-
-add_task(async function test_webRequest_window() {
-  let authCredentials = {
-    username: "testuser",
-    password: "testpass",
-  };
-  let ex1 = getAuthHandler();
-  await ex1.startup();
-
-  let win = window.open(`${baseUrl}?realm=test_webRequest_window&user=${authCredentials.username}&pass=${authCredentials.password}`);
-
-  await ex1.awaitMessage("onCompleted");
-  await ex1.unload();
-  win.close();
-});
-
-add_task(async function test_webRequest_auth_cancelled() {
-  let authCredentials = {
-    username: "testuser_canceled",
-    password: "testpass_canceled",
-  };
-  let ex1 = getAuthHandler({authCredentials});
-  await ex1.startup();
-  let ex2 = getAuthHandler({cancel: true});
   await ex2.startup();
 
-  await Assert.rejects(testXHR(`${baseUrl}?realm=test_webRequest_auth_cancelled&user=${authCredentials.username}&pass=${authCredentials.password}`), "caught rejected xhr");
-
+  let requestUrl = `${BASE_URL}/authenticate.sjs?realm=${config.realm}`;
+  let contentPage = await ExtensionTestUtils.loadContentPage(requestUrl);
   await Promise.all([
+    ex1.awaitMessage("onBeforeRequest"),
     ex1.awaitMessage("onAuthRequired"),
+    ex1.awaitMessage("onErrorOccurred"),
+    ex2.awaitMessage("onBeforeRequest"),
     ex2.awaitMessage("onAuthRequired"),
-    ex1.awaitMessage("onErrorOccurred"),
     ex2.awaitMessage("onErrorOccurred"),
   ]);
+
+  await contentPage.close();
   await ex1.unload();
   await ex2.unload();
 });
 
+
 add_task(async function test_webRequest_auth_nonblocking() {
-  // The first listener handles the auth request, the second listener
-  // is a non-blocking listener and cannot respond but will get the call.
-  let authCredentials = {
-    username: "foobar",
-    password: "testpass",
+  let config = {
+    path: `${BASE_URL}/*`,
+    realm: `webRequest_auth${Math.random()}`,
+    onBeforeRequest: {
+      extra: ["blocking"],
+    },
+    onAuthRequired: {
+      extra: ["blocking"],
+      result: {
+        authCredentials: {
+          username: "testuser",
+          password: "testpass",
+        },
+      },
+    },
   };
-  let handlingExt = getAuthHandler({authCredentials});
-  await handlingExt.startup();
-  let extension = getAuthHandler({}, false);
-  await extension.startup();
 
-  await testXHR(`${baseUrl}?realm=webRequest_auth_nonblocking&user=${authCredentials.username}&pass=${authCredentials.password}`);
+  let ex1 = getExtension(config);
+  // non-blocking ext tries to cancel but cannot.
+  delete config.onBeforeRequest.extra;
+  delete config.onAuthRequired.extra;
+  config.onAuthRequired.result = {cancel: true};
+  let ex2 = getExtension(config);
+  await ex1.startup();
+  await ex2.startup();
 
+  let requestUrl = `${BASE_URL}/authenticate.sjs?realm=${config.realm}`;
+  let contentPage = await ExtensionTestUtils.loadContentPage(requestUrl);
   await Promise.all([
-    extension.awaitMessage("onAuthRequired"),
-    extension.awaitMessage("onCompleted"),
-    handlingExt.awaitMessage("onAuthRequired"),
-    handlingExt.awaitMessage("onCompleted"),
+    ex1.awaitMessage("onBeforeRequest"),
+    ex1.awaitMessage("onAuthRequired").then(() => {
+      return Promise.all([
+        ex1.awaitMessage("onBeforeRequest"),
+        ex1.awaitMessage("onCompleted"),
+      ]);
+    }),
+    ex2.awaitMessage("onBeforeRequest"),
+    ex2.awaitMessage("onAuthRequired").then(() => {
+      return Promise.all([
+        ex2.awaitMessage("onBeforeRequest"),
+        ex2.awaitMessage("onCompleted"),
+      ]);
+    }),
   ]);
-  await extension.unload();
-  await handlingExt.unload();
+
+  await contentPage.close();
+  Services.obs.notifyObservers(null, "net:clear-active-logins");
+  await ex1.unload();
+  await ex2.unload();
 });
 
-
 add_task(async function test_webRequest_auth_blocking_noreturn() {
   // The first listener is blocking but doesn't return anything.  The second
   // listener cancels the request.
-  let ext = getAuthHandler();
-  await ext.startup();
-  let canceler = getAuthHandler({cancel: true});
-  await canceler.startup();
-
-  await Assert.rejects(testXHR(`${baseUrl}?realm=auth_blocking_noreturn&user=auth_blocking_noreturn&pass=auth_blocking_noreturn`), "caught rejected xhr");
-
-  await Promise.all([
-    ext.awaitMessage("onAuthRequired"),
-    ext.awaitMessage("onErrorOccurred"),
-    canceler.awaitMessage("onAuthRequired"),
-    canceler.awaitMessage("onErrorOccurred"),
-  ]);
-  await ext.unload();
-  await canceler.unload();
-});
-
-add_task(async function test_webRequest_auth_nonblocking_forwardAuthProvider() {
-  // The chrome script sets up a default auth handler on the channel, the
-  // extension does not return anything in the authRequred call.  We should
-  // get the call in the extension first, then in the chrome code where we
-  // cancel the request to avoid dealing with the prompt dialog here.  The test
-  // is to ensure that WebRequest calls the previous notificationCallbacks
-  // if the authorization is not handled by the onAuthRequired handler.
-
-  let chromeScript = SpecialPowers.loadChromeScript(() => {
-    ChromeUtils.import("resource://gre/modules/Services.jsm");
-    ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-    let observer = channel => {
-      if (!(channel instanceof Ci.nsIHttpChannel && channel.URI.host === "mochi.test")) {
-        return;
-      }
-      Services.obs.removeObserver(observer, "http-on-modify-request");
-      channel.notificationCallbacks = {
-        QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor,
-                                               Ci.nsIAuthPromptProvider,
-                                               Ci.nsIAuthPrompt2]),
-        getInterface: XPCOMUtils.generateQI([Ci.nsIAuthPromptProvider,
-                                             Ci.nsIAuthPrompt2]),
-        promptAuth(channel, level, authInfo) {
-          throw Cr.NS_ERROR_NO_INTERFACE;
-        },
-        getAuthPrompt(reason, iid) {
-          return this;
-        },
-        asyncPromptAuth(channel, callback, context, level, authInfo) {
-          // We just cancel here, we're only ensuring that non-webrequest
-          // notificationcallbacks get called if webrequest doesn't handle it.
-          Promise.resolve().then(() => {
-            callback.onAuthCancelled(context, false);
-            channel.cancel(Cr.NS_BINDING_ABORTED);
-            sendAsyncMessage("callback-complete");
-          });
-        },
-      };
-    };
-    Services.obs.addObserver(observer, "http-on-modify-request");
-    sendAsyncMessage("chrome-ready");
-  });
-  await chromeScript.promiseOneMessage("chrome-ready");
-  let callbackComplete = chromeScript.promiseOneMessage("callback-complete");
+  let config = {
+    path: `${BASE_URL}/*`,
+    realm: `webRequest_auth${Math.random()}`,
+    onBeforeRequest: {
+      extra: ["blocking"],
+    },
+    onAuthRequired: {
+      extra: ["blocking"],
+    },
+  };
 
-  let handlingExt = getAuthHandler();
-  await handlingExt.startup();
-
-  await Assert.rejects(testXHR(`${baseUrl}?realm=auth_nonblocking_forwardAuth&user=auth_nonblocking_forwardAuth&pass=auth_nonblocking_forwardAuth`), "caught rejected xhr");
-
-  await callbackComplete;
-  await handlingExt.awaitMessage("onAuthRequired");
-  // We expect onErrorOccurred because the "default" authprompt above cancelled
-  // the auth request to avoid a dialog.
-  await handlingExt.awaitMessage("onErrorOccurred");
-  await handlingExt.unload();
-  chromeScript.destroy();
-});
-
-add_task(async function test_webRequest_auth_nonblocking_forwardAuthPrompt2() {
-  // The chrome script sets up a default auth handler on the channel, the
-  // extension does not return anything in the authRequred call.  We should
-  // get the call in the extension first, then in the chrome code where we
-  // cancel the request to avoid dealing with the prompt dialog here.  The test
-  // is to ensure that WebRequest calls the previous notificationCallbacks
-  // if the authorization is not handled by the onAuthRequired handler.
-
-  let chromeScript = SpecialPowers.loadChromeScript(() => {
-    ChromeUtils.import("resource://gre/modules/Services.jsm");
-    ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+  let ex1 = getExtension(config);
+  config.onAuthRequired.result = {cancel: true};
+  let ex2 = getExtension(config);
+  await ex1.startup();
+  await ex2.startup();
 
-    let observer = channel => {
-      if (!(channel instanceof Ci.nsIHttpChannel && channel.URI.host === "mochi.test")) {
-        return;
-      }
-      Services.obs.removeObserver(observer, "http-on-modify-request");
-      channel.notificationCallbacks = {
-        QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor,
-                                               Ci.nsIAuthPrompt2]),
-        getInterface: XPCOMUtils.generateQI([Ci.nsIAuthPrompt2]),
-        promptAuth(channel, level, authInfo) {
-          throw Cr.NS_ERROR_NO_INTERFACE;
-        },
-        asyncPromptAuth(channel, callback, context, level, authInfo) {
-          // We just cancel here, we're only ensuring that non-webrequest
-          // notificationcallbacks get called if webrequest doesn't handle it.
-          Promise.resolve().then(() => {
-            channel.cancel(Cr.NS_BINDING_ABORTED);
-            sendAsyncMessage("callback-complete");
-          });
-        },
-      };
-    };
-    Services.obs.addObserver(observer, "http-on-modify-request");
-    sendAsyncMessage("chrome-ready");
-  });
-  await chromeScript.promiseOneMessage("chrome-ready");
-  let callbackComplete = chromeScript.promiseOneMessage("callback-complete");
+  let requestUrl = `${BASE_URL}/authenticate.sjs?realm=${config.realm}`;
+  let contentPage = await ExtensionTestUtils.loadContentPage(requestUrl);
+  await Promise.all([
+    ex1.awaitMessage("onBeforeRequest"),
+    ex1.awaitMessage("onAuthRequired"),
+    ex1.awaitMessage("onErrorOccurred"),
+    ex2.awaitMessage("onBeforeRequest"),
+    ex2.awaitMessage("onAuthRequired"),
+    ex2.awaitMessage("onErrorOccurred"),
+  ]);
 
-  let handlingExt = getAuthHandler();
-  await handlingExt.startup();
-
-  await Assert.rejects(testXHR(`${baseUrl}?realm=auth_nonblocking_forwardAuthPromptProvider&user=auth_nonblocking_forwardAuth&pass=auth_nonblocking_forwardAuth`), "caught rejected xhr");
-
-  await callbackComplete;
-  await handlingExt.awaitMessage("onAuthRequired");
-  // We expect onErrorOccurred because the "default" authprompt above cancelled
-  // the auth request to avoid a dialog.
-  await handlingExt.awaitMessage("onErrorOccurred");
-  await handlingExt.unload();
-  chromeScript.destroy();
+  await contentPage.close();
+  await ex1.unload();
+  await ex2.unload();
 });
 
 add_task(async function test_webRequest_duelingAuth() {
-  let exNone = getAuthHandler();
-  await exNone.startup();
-  let authCredentials = {
-    username: "testuser_da1",
-    password: "testpass_da1",
+  let config = {
+    path: `${BASE_URL}/*`,
+    realm: `webRequest_auth${Math.random()}`,
+    onBeforeRequest: {
+      extra: ["blocking"],
+    },
+    onAuthRequired: {
+      extra: ["blocking"],
+    },
   };
-  let ex1 = getAuthHandler({authCredentials});
+  let exNone = getExtension(config);
+  await exNone.startup();
+
+  let authCredentials = {
+    username: `testuser_da1${Math.random()}`,
+    password: `testpass_da1${Math.random()}`,
+  };
+  config.onAuthRequired.result = {authCredentials};
+  let ex1 = getExtension(config);
   await ex1.startup();
-  let exEmpty = getAuthHandler({});
+
+  config.onAuthRequired.result = {};
+  let exEmpty = getExtension(config);
   await exEmpty.startup();
-  let ex2 = getAuthHandler({authCredentials: {
-    username: "testuser_da2",
-    password: "testpass_da2",
-  }});
+
+  config.onAuthRequired.result = {
+    authCredentials: {
+      username: `testuser_da2${Math.random()}`,
+      password: `testpass_da2${Math.random()}`,
+    },
+  };
+  let ex2 = getExtension(config);
   await ex2.startup();
 
-  // XHR should succeed since the first credentials win, and they are correct.
-  await testXHR(`${baseUrl}?realm=test_webRequest_duelingAuth&user=${authCredentials.username}&pass=${authCredentials.password}`);
+
+  let requestUrl = `${BASE_URL}/authenticate.sjs?realm=${config.realm}&user=${authCredentials.username}&pass=${authCredentials.password}`;
+  let contentPage = await ExtensionTestUtils.loadContentPage(requestUrl);
+  await Promise.all([
+    exNone.awaitMessage("onBeforeRequest"),
+    exNone.awaitMessage("onAuthRequired").then(() => {
+      return Promise.all([
+        exNone.awaitMessage("onBeforeRequest"),
+        exNone.awaitMessage("onCompleted"),
+      ]);
+    }),
+    exEmpty.awaitMessage("onBeforeRequest"),
+    exEmpty.awaitMessage("onAuthRequired").then(() => {
+      return Promise.all([
+        exEmpty.awaitMessage("onBeforeRequest"),
+        exEmpty.awaitMessage("onCompleted"),
+      ]);
+    }),
+    ex1.awaitMessage("onBeforeRequest"),
+    ex1.awaitMessage("onAuthRequired").then(() => {
+      return Promise.all([
+        ex1.awaitMessage("onBeforeRequest"),
+        ex1.awaitMessage("onCompleted"),
+      ]);
+    }),
+    ex2.awaitMessage("onBeforeRequest"),
+    ex2.awaitMessage("onAuthRequired").then(() => {
+      return Promise.all([
+        ex2.awaitMessage("onBeforeRequest"),
+        ex2.awaitMessage("onCompleted"),
+      ]);
+    }),
+  ]);
 
   await Promise.all([
-    exNone.awaitMessage("onAuthRequired"),
-    exNone.awaitMessage("onCompleted"),
-    exEmpty.awaitMessage("onAuthRequired"),
-    exEmpty.awaitMessage("onCompleted"),
-    ex1.awaitMessage("onAuthRequired"),
-    ex1.awaitMessage("onCompleted"),
-    ex2.awaitMessage("onAuthRequired"),
-    ex2.awaitMessage("onCompleted"),
-  ]);
-  await Promise.all([
+    await contentPage.close(),
     exNone.unload(),
     exEmpty.unload(),
     ex1.unload(),
     ex2.unload(),
   ]);
 });
 
 add_task(async function test_webRequest_auth_proxy() {
-  function background() {
+  function background(permissionPath) {
     let proxyOk = false;
     browser.webRequest.onAuthRequired.addListener((details) => {
-      browser.test.succeed(`handlingExt onAuthRequired called with ${details.requestId} ${details.url}`);
+      browser.test.log(`handlingExt onAuthRequired called with ${details.requestId} ${details.url}`);
       if (details.isProxy) {
         browser.test.succeed("providing proxy authorization");
         proxyOk = true;
         return {authCredentials: {username: "puser", password: "ppass"}};
       }
       browser.test.assertTrue(proxyOk, "providing www authorization after proxy auth");
       browser.test.sendMessage("done");
       return {authCredentials: {username: "auser", password: "apass"}};
-    }, {urls: ["*://mochi.test/*"]}, ["blocking"]);
+    }, {urls: [permissionPath]}, ["blocking"]);
   }
 
   let handlingExt = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: [
         "webRequest",
         "webRequestBlocking",
-        "*://mochi.test/*",
+        `${BASE_URL}/*`,
       ],
     },
-    background,
+    background: `(${background})("${BASE_URL}/*")`,
   });
 
   await handlingExt.startup();
 
-  await testXHR(`${baseUrl}?realm=auth_proxy&user=auser&pass=apass&proxy_user=puser&proxy_pass=ppass`);
+  let requestUrl = `${BASE_URL}/authenticate.sjs?realm=webRequest_auth${Math.random()}&proxy_realm=proxy_auth${Math.random()}`;
+  let contentPage = await ExtensionTestUtils.loadContentPage(requestUrl);
 
   await handlingExt.awaitMessage("done");
+  await contentPage.close();
   await handlingExt.unload();
 });
-</script>
-</head>
-<body>
-<div id="test">Authorization Test</div>
-
-</body>
-</html>
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -99,16 +99,17 @@ skip-if = os == "android"
 skip-if = os == "android" # checking for telemetry needs to be updated: 1384923
 [test_ext_tab_teardown.js]
 skip-if = os == 'android' # Bug 1258975 on android.
 [test_ext_trustworthy_origin.js]
 [test_ext_topSites.js]
 skip-if = os == "android"
 [test_ext_unload_frame.js]
 skip-if = true # Too frequent intermittent failures
+[test_ext_webRequest_auth.js]
 [test_ext_webRequest_filterResponseData.js]
 [test_ext_webRequest_permission.js]
 [test_ext_webRequest_responseBody.js]
 [test_ext_webRequest_set_cookie.js]
 [test_ext_webRequest_suspend.js]
 [test_ext_webRequest_webSocket.js]
 [test_ext_xhr_capabilities.js]
 [test_native_manifests.js]
--- a/toolkit/components/payments/content/paymentDialogFrameScript.js
+++ b/toolkit/components/payments/content/paymentDialogFrameScript.js
@@ -39,18 +39,35 @@ let PaymentFrameScript = {
   handleEvent(event) {
     this.sendToChrome(event);
   },
 
   receiveMessage({data: {messageType, data}}) {
     this.sendToContent(messageType, data);
   },
 
+  setupContentConsole() {
+    let privilegedLogger = content.window.console.createInstance({
+      maxLogLevelPref: "dom.payments.loglevel",
+      prefix: "paymentDialogContent",
+    });
+
+    let contentLogObject = Cu.waiveXrays(content).log;
+    for (let name of ["error", "warn", "info", "debug"]) {
+      Cu.exportFunction(privilegedLogger[name].bind(privilegedLogger), contentLogObject, {
+        defineAs: name,
+      });
+    }
+  },
+
   sendToChrome({detail}) {
     let {messageType} = detail;
+    if (messageType == "initializeRequest") {
+      this.setupContentConsole();
+    }
     this.log.debug("sendToChrome:", messageType, detail);
     this.sendMessageToChrome(messageType, detail);
   },
 
   sendToContent(messageType, detail = {}) {
     this.log.debug("sendToContent", messageType, detail);
     let response = Object.assign({messageType}, detail);
     let event = new content.CustomEvent("paymentChromeToContent", {
--- a/toolkit/components/payments/jar.mn
+++ b/toolkit/components/payments/jar.mn
@@ -13,11 +13,12 @@ toolkit.jar:
     res/payments                                      (res/paymentRequest.*)
     res/payments/components/                          (res/components/*.css)
     res/payments/components/                          (res/components/*.js)
     res/payments/containers/                          (res/containers/*.js)
     res/payments/containers/                          (res/containers/*.css)
     res/payments/debugging.css                        (res/debugging.css)
     res/payments/debugging.html                       (res/debugging.html)
     res/payments/debugging.js                         (res/debugging.js)
+    res/payments/log.js                               (res/log.js)
     res/payments/mixins/                              (res/mixins/*.js)
     res/payments/PaymentsStore.js                     (res/PaymentsStore.js)
     res/payments/vendor/                              (res/vendor/*)
--- a/toolkit/components/payments/res/containers/payment-dialog.js
+++ b/toolkit/components/payments/res/containers/payment-dialog.js
@@ -167,16 +167,17 @@ class PaymentDialog extends PaymentState
 
   _renderPayButton(state) {
     this._payButton.disabled = state.changesPrevented;
     switch (state.completionState) {
       case "initial":
       case "processing":
       case "success":
       case "fail":
+      case "unknown":
         break;
       default:
         throw new Error("Invalid completionState");
     }
 
     this._payButton.textContent = this._payButton.dataset[state.completionState + "Label"];
   }
 
--- a/toolkit/components/payments/res/debugging.html
+++ b/toolkit/components/payments/res/debugging.html
@@ -34,11 +34,12 @@
       <h1>States</h1>
       <button id="setChangesPrevented">Prevent changes</button>
       <button id="setChangesAllowed">Allow changes</button>
       <button id="setShippingError">Shipping Error</button>
       <button id="setStateDefault">Default</button>
       <button id="setStateProcessing">Processing</button>
       <button id="setStateSuccess">Success</button>
       <button id="setStateFail">Fail</button>
+      <button id="setStateUnknown">Unknown</button>
     </div>
   </body>
 </html>
--- a/toolkit/components/payments/res/debugging.js
+++ b/toolkit/components/payments/res/debugging.js
@@ -318,16 +318,22 @@ let buttonActions = {
     });
   },
 
   setStateFail() {
     requestStore.setState({
       completionState: "fail",
     });
   },
+
+  setStateUnknown() {
+    requestStore.setState({
+      completionState: "unknown",
+    });
+  },
 };
 
 window.addEventListener("click", function onButtonClick(evt) {
   let id = evt.target.id;
   if (!id || typeof(buttonActions[id]) != "function") {
     return;
   }
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/log.js
@@ -0,0 +1,29 @@
+/* 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/. */
+
+/**
+ * This file defines a fallback log object to be used during development outside
+ * of the paymentDialogWrapper. When loaded in the wrapper, a frame script
+ * providing pref-controlled logging overwrites these methods.
+ */
+
+/* eslint-disable no-console */
+/* exported log */
+
+"use strict";
+
+var log = {
+  error(...args) {
+    console.error("log.js", ...args);
+  },
+  warn(...args) {
+    console.warn("log.js", ...args);
+  },
+  info(...args) {
+    console.info("log.js", ...args);
+  },
+  debug(...args) {
+    console.debug("log.js", ...args);
+  },
+};
--- a/toolkit/components/payments/res/paymentRequest.css
+++ b/toolkit/components/payments/res/paymentRequest.css
@@ -78,16 +78,17 @@ payment-dialog > footer {
 
 #pay {
   background-color: #0060df;
   color: white;
   border: none;
 }
 
 payment-dialog[changes-prevented][completion-state="fail"] #pay,
+payment-dialog[changes-prevented][completion-state="unknown"] #pay,
 payment-dialog[changes-prevented][completion-state="processing"] #pay,
 payment-dialog[changes-prevented][completion-state="success"] #pay {
   /* Show the pay button above #disabled-overlay */
   position: relative;
   z-index: 1;
 }
 
 #disabled-overlay {
--- a/toolkit/components/payments/res/paymentRequest.js
+++ b/toolkit/components/payments/res/paymentRequest.js
@@ -3,16 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /**
  * Loaded in the unprivileged frame of each payment dialog.
  *
  * Communicates with privileged code via DOM Events.
  */
 
+/* import-globals-from log.js */
+
 "use strict";
 
 var paymentRequest = {
   domReadyPromise: null,
 
   init() {
     // listen to content
     window.addEventListener("paymentChromeToContent", this);
@@ -51,27 +53,29 @@ var paymentRequest = {
       }
       default: {
         throw new Error("Unexpected event type");
       }
     }
   },
 
   sendMessageToChrome(messageType, detail = {}) {
+    log.debug("sendMessageToChrome: ", messageType, detail);
     let event = new CustomEvent("paymentContentToChrome", {
       bubbles: true,
       detail: Object.assign({
         messageType,
       }, detail),
     });
     document.dispatchEvent(event);
   },
 
   onChromeToContent({detail}) {
     let {messageType} = detail;
+    log.debug("onChromeToContent: ", messageType);
 
     switch (messageType) {
       case "responseSent": {
         document.querySelector("payment-dialog").requestStore.setState({
           changesPrevented: true,
           completionState: "processing",
         });
         break;
@@ -83,29 +87,32 @@ var paymentRequest = {
       case "updateState": {
         document.querySelector("payment-dialog").setStateFromParent(detail);
         break;
       }
     }
   },
 
   onPaymentRequestLoad(requestId) {
+    log.debug("onPaymentRequestLoad:", requestId);
     window.addEventListener("unload", this, {once: true});
     this.sendMessageToChrome("paymentDialogReady");
 
     // Automatically show the debugging console if loaded with a truthy `debug` query parameter.
     if (new URLSearchParams(location.search).get("debug")) {
       document.getElementById("debugging-console").hidden = false;
     }
   },
 
   async onShowPaymentRequest(detail) {
     // Handle getting called before the DOM is ready.
+    log.debug("onShowPaymentRequest:", detail);
     await this.domReadyPromise;
 
+    log.debug("onShowPaymentRequest: domReadyPromise resolved");
     document.querySelector("payment-dialog").setStateFromParent({
       request: detail.request,
       savedAddresses: detail.savedAddresses,
       savedBasicCards: detail.savedBasicCards,
     });
   },
 
   cancel() {
--- a/toolkit/components/payments/res/paymentRequest.xhtml
+++ b/toolkit/components/payments/res/paymentRequest.xhtml
@@ -11,16 +11,17 @@
   <!ENTITY shippingOptionsLabel       "Shipping Options">
   <!ENTITY paymentMethodsLabel        "Payment Method">
   <!ENTITY payerLabel                 "Contact Information">
   <!ENTITY cancelPaymentButton.label   "Cancel">
   <!ENTITY approvePaymentButton.label  "Pay">
   <!ENTITY processingPaymentButton.label "Processing">
   <!ENTITY successPaymentButton.label    "Done">
   <!ENTITY failPaymentButton.label       "Fail">
+  <!ENTITY unknownPaymentButton.label    "Unknown">
   <!ENTITY orderDetailsLabel          "Order Details">
   <!ENTITY orderTotalLabel            "Total">
 ]>
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <meta http-equiv="Content-Security-Policy" content="default-src 'self'"/>
   <title></title>
   <link rel="stylesheet" href="paymentRequest.css"/>
@@ -28,16 +29,18 @@
   <link rel="stylesheet" href="components/address-option.css"/>
   <link rel="stylesheet" href="components/basic-card-option.css"/>
   <link rel="stylesheet" href="components/shipping-option.css"/>
   <link rel="stylesheet" href="components/payment-details-item.css"/>
   <link rel="stylesheet" href="containers/order-details.css"/>
 
   <script src="vendor/custom-elements.min.js"></script>
 
+  <script src="log.js"></script>
+
   <script src="PaymentsStore.js"></script>
 
   <script src="mixins/ObservedPropertiesMixin.js"></script>
   <script src="mixins/PaymentStateSubscriberMixin.js"></script>
 
   <script src="components/currency-amount.js"></script>
   <script src="containers/order-details.js"></script>
   <script src="components/payment-details-item.js"></script>
@@ -92,16 +95,17 @@
         </section>
 
         <footer id="controls-container">
           <button id="cancel">&cancelPaymentButton.label;</button>
           <button id="pay"
                   data-initial-label="&approvePaymentButton.label;"
                   data-processing-label="&processingPaymentButton.label;"
                   data-fail-label="&failPaymentButton.label;"
+                  data-unknown-label="&unknownPaymentButton.label;"
                   data-success-label="&successPaymentButton.label;"></button>
         </footer>
       </section>
       <section id="order-details-overlay" hidden="hidden">
         <h1>&orderDetailsLabel;</h1>
         <order-details></order-details>
       </section>
     </div>
--- a/toolkit/components/payments/test/mochitest/test_payment_dialog.html
+++ b/toolkit/components/payments/test/mochitest/test_payment_dialog.html
@@ -35,16 +35,17 @@ Test the payment-dialog custom element
 /* import-globals-from ../../res/mixins/PaymentStateSubscriberMixin.js */
 
 let el1;
 
 let completionStates = [
     ["processing", "Processing"],
     ["success", "Done"],
     ["fail", "Fail"],
+    ["unknown", "Unknown"],
 ];
 
 /* test that:
   the view-all-items button exists
   that clicking it changes the state on the store
   that clicking it causes render to be called
 
   that order details element's hidden state matches the state on the store
--- a/toolkit/components/telemetry/Telemetry.h
+++ b/toolkit/components/telemetry/Telemetry.h
@@ -320,22 +320,36 @@ public:
   ~RuntimeAutoCounter()
   {
     Accumulate(id, counter);
   }
 
   // Prefix increment only, to encourage good habits.
   void operator++()
   {
+    if (NS_WARN_IF(counter == std::numeric_limits<uint32_t>::max())) {
+      return;
+    }
     ++counter;
   }
 
   // Chaining doesn't make any sense, don't return anything.
   void operator+=(int increment)
   {
+    if (NS_WARN_IF(increment > 0 &&
+                   static_cast<uint32_t>(increment) >
+                     (std::numeric_limits<uint32_t>::max() - counter))) {
+      counter = std::numeric_limits<uint32_t>::max();
+      return;
+    }
+    if (NS_WARN_IF(increment < 0 &&
+                   static_cast<uint32_t>(-increment) > counter)) {
+      counter = std::numeric_limits<uint32_t>::min();
+      return;
+    }
     counter += increment;
   }
 
 private:
   HistogramID id;
   uint32_t counter;
   MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
 };
@@ -349,22 +363,38 @@ public:
     MOZ_GUARD_OBJECT_NOTIFIER_INIT;
   }
 
   ~AutoCounter() {
     Accumulate(id, counter);
   }
 
   // Prefix increment only, to encourage good habits.
-  void operator++() {
+  void operator++()
+  {
+    if (NS_WARN_IF(counter == std::numeric_limits<uint32_t>::max())) {
+      return;
+    }
     ++counter;
   }
 
   // Chaining doesn't make any sense, don't return anything.
-  void operator+=(int increment) {
+  void operator+=(int increment)
+  {
+    if (NS_WARN_IF(increment > 0 &&
+                   static_cast<uint32_t>(increment) >
+                     (std::numeric_limits<uint32_t>::max() - counter))) {
+      counter = std::numeric_limits<uint32_t>::max();
+      return;
+    }
+    if (NS_WARN_IF(increment < 0 &&
+                   static_cast<uint32_t>(-increment) > counter)) {
+      counter = std::numeric_limits<uint32_t>::min();
+      return;
+    }
     counter += increment;
   }
 
 private:
   uint32_t counter;
   MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
 };
 
--- a/toolkit/mozapps/installer/upload-files-APK.mk
+++ b/toolkit/mozapps/installer/upload-files-APK.mk
@@ -14,18 +14,16 @@ include $(MOZILLA_DIR)/config/android-co
 ROOT_FILES := \
   application.ini \
   package-name.txt \
   ua-update.json \
   platform.ini \
   removed-files \
   $(NULL)
 
-GECKO_APP_AP_PATH = $(topobjdir)/mobile/android/base
-
 ifdef ENABLE_TESTS
 INNER_ROBOCOP_PACKAGE=true
 ifeq ($(MOZ_BUILD_APP),mobile/android)
 UPLOAD_EXTRA_FILES += robocop.apk
 
 # Robocop/Robotium tests and Fennec need to be signed with the same
 # key, which means release signing them all.
 
@@ -54,43 +52,40 @@ OMNIJAR_NAME := $(notdir $(OMNIJAR_NAME)
 # Language repacks take advantage of this unchecked dependency ap_ to
 # insert additional resources (translated strings) into the ap_
 # without the build system's participation.  This can do the wrong
 # thing if there are resource changes in between build time and
 # package time.
 PKG_SUFFIX = .apk
 
 INNER_FENNEC_PACKAGE = \
-  $(MAKE) -C $(GECKO_APP_AP_PATH) gecko-nodeps.ap_ && \
   $(PYTHON) -m mozbuild.action.package_fennec_apk \
     --verbose \
-    --inputs \
-      $(GECKO_APP_AP_PATH)/gecko-nodeps.ap_ \
+    --inputs $(GRADLE_ANDROID_APP_APK) \
     --omnijar $(MOZ_PKG_DIR)/$(OMNIJAR_NAME) \
     --lib-dirs $(MOZ_PKG_DIR)/lib \
     --assets-dirs $(MOZ_PKG_DIR)/assets \
     --features-dirs $(MOZ_PKG_DIR)/features \
     --root-files $(foreach f,$(ROOT_FILES),$(MOZ_PKG_DIR)/$(f)) \
     --output $(PACKAGE:.apk=-unsigned-unaligned.apk) && \
   $(call RELEASE_SIGN_ANDROID_APK,$(PACKAGE:.apk=-unsigned-unaligned.apk),$(PACKAGE))
 
 # Packaging produces many optional artifacts.
 package_fennec = \
   $(INNER_FENNEC_PACKAGE) && \
   $(INNER_ROBOCOP_PACKAGE)
 
 # Re-packaging only replaces Android resources and the omnijar before
 # (re-)signing.
 repackage_fennec = \
-  $(MAKE) -C $(GECKO_APP_AP_PATH) gecko-nodeps.ap_ && \
   $(PYTHON) -m mozbuild.action.package_fennec_apk \
     --verbose \
     --inputs \
       $(UNPACKAGE) \
-      $(GECKO_APP_AP_PATH)/gecko-nodeps.ap_ \
+      $(GRADLE_ANDROID_APP_APK) \
     --omnijar $(MOZ_PKG_DIR)/$(OMNIJAR_NAME) \
     --output $(PACKAGE:.apk=-unsigned-unaligned.apk) && \
   $(call RELEASE_SIGN_ANDROID_APK,$(PACKAGE:.apk=-unsigned-unaligned.apk),$(PACKAGE))
 
 INNER_MAKE_PACKAGE = $(if $(UNPACKAGE),$(repackage_fennec),$(package_fennec))
 
 # Language repacks root the resources contained in assets/omni.ja
 # under assets/, but the repacks expect them to be rooted at /.
--- a/tools/lint/eslint/modules.json
+++ b/tools/lint/eslint/modules.json
@@ -27,17 +27,16 @@
   "CertUtils.jsm": ["BadCertHandler", "checkCert", "readCertPrefs", "validateCert"],
   "clients.js": ["ClientEngine", "ClientsRec"],
   "collection_repair.js": ["getRepairRequestor", "getAllRepairRequestors", "CollectionRepairRequestor", "getRepairResponder", "CollectionRepairResponder"],
   "collection_validator.js": ["CollectionValidator", "CollectionProblemData"],
   "Console.jsm": ["console", "ConsoleAPI"],
   "constants.js": ["WEAVE_VERSION", "SYNC_API_VERSION", "STORAGE_VERSION", "PREFS_BRANCH", "DEFAULT_KEYBUNDLE_NAME", "SYNC_KEY_ENCODED_LENGTH", "SYNC_KEY_DECODED_LENGTH", "NO_SYNC_NODE_INTERVAL", "MAX_ERROR_COUNT_BEFORE_BACKOFF", "MINIMUM_BACKOFF_INTERVAL", "MAXIMUM_BACKOFF_INTERVAL", "HMAC_EVENT_INTERVAL", "MASTER_PASSWORD_LOCKED_RETRY_INTERVAL", "DEFAULT_GUID_FETCH_BATCH_SIZE", "DEFAULT_DOWNLOAD_BATCH_SIZE", "SINGLE_USER_THRESHOLD", "MULTI_DEVICE_THRESHOLD", "SCORE_INCREMENT_SMALL", "SCORE_INCREMENT_MEDIUM", "SCORE_INCREMENT_XLARGE", "SCORE_UPDATE_DELAY", "IDLE_OBSERVER_BACK_DELAY", "URI_LENGTH_MAX", "MAX_HISTORY_UPLOAD", "MAX_HISTORY_DOWNLOAD", "STATUS_OK", "SYNC_FAILED", "LOGIN_FAILED", "SYNC_FAILED_PARTIAL", "CLIENT_NOT_CONFIGURED", "STATUS_DISABLED", "MASTER_PASSWORD_LOCKED", "LOGIN_SUCCEEDED", "SYNC_SUCCEEDED", "ENGINE_SUCCEEDED", "LOGIN_FAILED_NO_USERNAME", "LOGIN_FAILED_NO_PASSPHRASE", "LOGIN_FAILED_NETWORK_ERROR", "LOGIN_FAILED_SERVER_ERROR", "LOGIN_FAILED_INVALID_PASSPHRASE", "LOGIN_FAILED_LOGIN_REJECTED", "METARECORD_DOWNLOAD_FAIL", "VERSION_OUT_OF_DATE", "CREDENTIALS_CHANGED", "ABORT_SYNC_COMMAND", "NO_SYNC_NODE_FOUND", "OVER_QUOTA", "SERVER_MAINTENANCE", "RESPONSE_OVER_QUOTA", "ENGINE_UPLOAD_FAIL", "ENGINE_DOWNLOAD_FAIL", "ENGINE_UNKNOWN_FAIL", "ENGINE_APPLY_FAIL", "ENGINE_BATCH_INTERRUPTED", "kSyncMasterPasswordLocked", "kSyncWeaveDisabled", "kSyncNetworkOffline", "kSyncBackoffNotMet", "kFirstSyncChoiceNotMade", "kSyncNotConfigured", "kFirefoxShuttingDown", "DEVICE_TYPE_DESKTOP", "DEVICE_TYPE_MOBILE", "SQLITE_MAX_VARIABLE_NUMBER"],
   "Constants.jsm": ["Roles", "Events", "Relations", "Filters", "States", "Prefilters"],
   "ContactDB.jsm": ["ContactDB", "DB_NAME", "STORE_NAME", "SAVED_GETALL_STORE_NAME", "REVISION_STORE", "DB_VERSION"],
-  "content-server.jsm": ["init"],
   "content.jsm": ["registerContentFrame"],
   "ContentCrashHandlers.jsm": ["TabCrashHandler", "PluginCrashReporter", "UnsubmittedCrashHandler"],
   "ContentObservers.js": [],
   "ContentPrefUtils.jsm": ["ContentPref", "cbHandleResult", "cbHandleError", "cbHandleCompletion", "safeCallback", "_methodsCallableFromChild"],
   "cookies.js": ["Cookies"],
   "CoverageUtils.jsm": ["CoverageCollector"],
   "CrashManagerTest.jsm": ["configureLogging", "getManager", "sleep", "TestingCrashManager"],
   "dbg-client.jsm": ["DebuggerTransport", "DebuggerClient", "RootClient", "LongStringClient", "EnvironmentClient", "ObjectClient"],