Merge inbound to mozilla-central. a=merge default tip
authorGurzau Raul <rgurzau@mozilla.com>
Wed, 12 Jun 2019 00:34:32 +0300
changeset 478287 43b2d57b25cc16d0571e6bef6a414abe24457154
parent 478258 ee33e076cbcbe786125f83079bd8e5b43bb73a99 (current diff)
parent 478286 2f493b1b18cfa0a4b196149bf70309822a85b712 (diff)
push id5
push uservporof@mozilla.com
push dateWed, 12 Jun 2019 10:24:37 +0000
reviewersmerge
milestone69.0a1
Merge inbound to mozilla-central. a=merge
browser/base/content/browser.js
browser/base/content/nsContextMenu.js
browser/base/content/tabbrowser.js
browser/components/BrowserGlue.jsm
toolkit/content/widgets/browser-custom-element.js
--- a/browser/actors/BrowserTabChild.jsm
+++ b/browser/actors/BrowserTabChild.jsm
@@ -1,112 +1,110 @@
 /* vim: set ts=2 sw=2 sts=2 et tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 var EXPORTED_SYMBOLS = ["BrowserTabChild"];
 
-const {ActorChild} = ChromeUtils.import("resource://gre/modules/ActorChild.jsm");
-
 ChromeUtils.defineModuleGetter(this, "E10SUtils",
                                "resource://gre/modules/E10SUtils.jsm");
 
-class BrowserTabChild extends ActorChild {
+class BrowserTabChild extends JSWindowActorChild {
+  constructor() {
+    super();
+    this.handledWindowCreated = false;
+    this.handledFirstPaint = false;
+  }
+
   handleEvent(event) {
     switch (event.type) {
-    case "DOMWindowCreated":
-      let loadContext = this.mm.docShell.QueryInterface(Ci.nsILoadContext);
+    case "DOMWindowCreated": {
+      if (this.handledWindowCreated) {
+        return;
+      }
+      this.handledWindowCreated = true;
+
+      let context = this.manager.browsingContext;
+      let loadContext = context.docShell.QueryInterface(Ci.nsILoadContext);
       let userContextId = loadContext.originAttributes.userContextId;
 
-      this.mm.sendAsyncMessage("Browser:WindowCreated", { userContextId });
+      this.sendAsyncMessage("Browser:WindowCreated", { userContextId });
       break;
+    }
 
     case "MozAfterPaint":
-      this.mm.sendAsyncMessage("Browser:FirstPaint");
+      if (this.handledFirstPaint) {
+        return;
+      }
+      this.handledFirstPaint = true;
+      this.sendAsyncMessage("Browser:FirstPaint", {});
       break;
 
     case "MozDOMPointerLock:Entered":
-      this.mm.sendAsyncMessage("PointerLock:Entered", {
+      this.sendAsyncMessage("PointerLock:Entered", {
         originNoSuffix: event.target.nodePrincipal.originNoSuffix,
       });
       break;
 
     case "MozDOMPointerLock:Exited":
-      this.mm.sendAsyncMessage("PointerLock:Exited");
+      this.sendAsyncMessage("PointerLock:Exited");
       break;
     }
   }
 
-  switchDocumentDirection(window = this.content) {
-   // document.dir can also be "auto", in which case it won't change
-    if (window.document.dir == "ltr" || window.document.dir == "") {
-      window.document.dir = "rtl";
-    } else if (window.document.dir == "rtl") {
-      window.document.dir = "ltr";
-    }
-    for (let i = 0; i < window.frames.length; i++) {
-      this.switchDocumentDirection(window.frames[i]);
-    }
-  }
+  receiveMessage(message) {
+    let context = this.manager.browsingContext;
+    let docShell = context.docShell;
 
-  receiveMessage(message) {
     switch (message.name) {
-      case "AllowScriptsToClose":
-        this.content.windowUtils.allowScriptsToClose();
-        break;
-
       case "Browser:AppTab":
-        if (this.docShell) {
-          this.docShell.isAppTab = message.data.isAppTab;
+        if (docShell) {
+          docShell.isAppTab = message.data.isAppTab;
         }
         break;
 
       case "Browser:HasSiblings":
         try {
-          let browserChild = this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+          let browserChild = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                              .getInterface(Ci.nsIBrowserChild);
           let hasSiblings = message.data;
           browserChild.hasSiblings = hasSiblings;
         } catch (e) {
         }
         break;
 
       // XXX(nika): Should we try to call this in the parent process instead?
       case "Browser:Reload":
         /* First, we'll try to use the session history object to reload so
          * that framesets are handled properly. If we're in a special
          * window (such as view-source) that has no session history, fall
          * back on using the web navigation's reload method.
          */
-
-        let webNav = this.docShell.QueryInterface(Ci.nsIWebNavigation);
+        let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
         try {
           if (webNav.sessionHistory) {
             webNav = webNav.sessionHistory;
           }
         } catch (e) {
         }
 
         let reloadFlags = message.data.flags;
         try {
-          E10SUtils.wrapHandlingUserInput(this.content, message.data.handlingUserInput,
+          E10SUtils.wrapHandlingUserInput(this.document.defaultView,
+                                          message.data.handlingUserInput,
                                           () => webNav.reload(reloadFlags));
         } catch (e) {
         }
         break;
 
       case "MixedContent:ReenableProtection":
-        this.docShell.mixedContentChannel = null;
-        break;
-
-      case "SwitchDocumentDirection":
-        this.switchDocumentDirection();
+        docShell.mixedContentChannel = null;
         break;
 
       case "UpdateCharacterSet":
-        this.docShell.charset = message.data.value;
-        this.docShell.gatherCharsetMenuTelemetry();
+        docShell.charset = message.data.value;
+        docShell.gatherCharsetMenuTelemetry();
         break;
     }
   }
 }
new file mode 100644
--- /dev/null
+++ b/browser/actors/BrowserTabParent.jsm
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var EXPORTED_SYMBOLS = ["BrowserTabParent"];
+
+class BrowserTabParent extends JSWindowActorParent {
+  receiveMessage(message) {
+    let browser = this.manager.browsingContext.embedderElement;
+    if (!browser) {
+      return; // Can happen sometimes if browser is being destroyed
+    }
+
+    let gBrowser = browser.ownerGlobal.gBrowser;
+    if (!gBrowser) {
+      // Note: gBrowser might be null because this message might be received
+      // from the extension process. There's still an embedderElement involved,
+      // but it's the one coming from dummy.xul.
+      // This should probably be fixed by adding support to specifying "group: 'browsers"
+      // in the registerWindowActor options/. See bug 1557118.
+      return;
+    }
+
+    switch (message.name) {
+      case "Browser:WindowCreated": {
+        gBrowser.announceWindowCreated(browser, message.data.userContextId);
+        break;
+      }
+
+      case "Browser:FirstPaint": {
+        browser.ownerGlobal.gBrowserInit._firstBrowserPaintDeferred.resolve();
+        break;
+      }
+
+      case "MozDOMPointerLock:Entered": {
+        browser.ownerGlobal.PointerLock.entered(message.data.originNoSuffix);
+        break;
+      }
+
+      case "MozDOMPointerLock:Exited":
+        browser.ownerGlobal.PointerLock.exited();
+        break;
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/actors/SwitchDocumentDirectionChild.jsm
@@ -0,0 +1,30 @@
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var EXPORTED_SYMBOLS = ["SwitchDocumentDirectionChild"];
+
+class SwitchDocumentDirectionChild extends JSWindowActorChild {
+  receiveMessage(message) {
+    if (message.name == "SwitchDocumentDirection") {
+      let docShell = this.manager.browsingContext.docShell;
+      let document = docShell.QueryInterface(Ci.nsIWebNavigation).document;
+      this.switchDocumentDirection(document);
+    }
+  }
+
+  switchDocumentDirection(document) {
+    // document.dir can also be "auto", in which case it won't change
+    if (document.dir == "ltr" || document.dir == "") {
+      document.dir = "rtl";
+    } else if (document.dir == "rtl") {
+      document.dir = "ltr";
+    }
+
+    for (let frame of document.defaultView.frames) {
+      this.switchDocumentDirection(frame.document);
+    }
+  }
+}
--- a/browser/actors/moz.build
+++ b/browser/actors/moz.build
@@ -21,16 +21,17 @@ with Files("PluginChild.jsm"):
 
 with Files("WebRTCChild.jsm"):
     BUG_COMPONENT = ("Firefox", "Device Permissions")
 
 FINAL_TARGET_FILES.actors += [
     'AboutReaderChild.jsm',
     'BlockedSiteChild.jsm',
     'BrowserTabChild.jsm',
+    'BrowserTabParent.jsm',
     'ClickHandlerChild.jsm',
     'ContentSearchChild.jsm',
     'ContextMenuChild.jsm',
     'ContextMenuParent.jsm',
     'ContextMenuSpecialProcessChild.jsm',
     'DOMFullscreenChild.jsm',
     'FormValidationChild.jsm',
     'LightweightThemeChild.jsm',
@@ -39,11 +40,12 @@ FINAL_TARGET_FILES.actors += [
     'OfflineAppsChild.jsm',
     'PageInfoChild.jsm',
     'PageStyleChild.jsm',
     'PluginChild.jsm',
     'RFPHelperChild.jsm',
     'SearchTelemetryChild.jsm',
     'SubframeCrashChild.jsm',
     'SubframeCrashParent.jsm',
+    'SwitchDocumentDirectionChild.jsm',
     'URIFixupChild.jsm',
     'WebRTCChild.jsm',
 ]
--- a/browser/base/content/browser-fullScreenAndPointerLock.js
+++ b/browser/base/content/browser-fullScreenAndPointerLock.js
@@ -219,34 +219,23 @@ var PointerlockFsWarning = {
       }
       break;
     }
     }
   },
 };
 
 var PointerLock = {
-
-  init() {
-    window.messageManager.addMessageListener("PointerLock:Entered", this);
-    window.messageManager.addMessageListener("PointerLock:Exited", this);
-  },
+ entered(originNoSuffix) {
+   PointerlockFsWarning.showPointerLock(originNoSuffix);
+ },
 
-  receiveMessage(aMessage) {
-    switch (aMessage.name) {
-      case "PointerLock:Entered": {
-        PointerlockFsWarning.showPointerLock(aMessage.data.originNoSuffix);
-        break;
-      }
-      case "PointerLock:Exited": {
-        PointerlockFsWarning.close();
-        break;
-      }
-    }
-  },
+ exited() {
+  PointerlockFsWarning.close();
+ },
 };
 
 var FullScreen = {
   _MESSAGES: [
     "DOMFullscreen:Request",
     "DOMFullscreen:NewOrigin",
     "DOMFullscreen:Exit",
     "DOMFullscreen:Painted",
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -310,19 +310,17 @@
                           label="&showAllTabsCmd.label;"
                           command="Browser:ShowAllTabs"
                           key="key_showAllTabs"/>
                 <menuseparator hidden="true" id="documentDirection-separator"/>
                 <menuitem id="documentDirection-swap"
                           hidden="true"
                           label="&bidiSwitchPageDirectionItem.label;"
                           accesskey="&bidiSwitchPageDirectionItem.accesskey;"
-                          oncommand="gBrowser.selectedBrowser
-                                             .messageManager
-                                             .sendAsyncMessage('SwitchDocumentDirection');"/>
+                          oncommand="gBrowser.selectedBrowser.sendMessageToActor('SwitchDocumentDirection', {}, 'SwitchDocumentDirection', true);"/>
               </menupopup>
             </menu>
 
             <menu id="history-menu"
                   label="&historyMenu.label;"
                   accesskey="&historyMenu.accesskey;">
               <menupopup id="goPopup"
 #ifndef XP_MACOSX
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -320,18 +320,17 @@ var gIdentityHandler = {
     histogram.add(kMIXED_CONTENT_UNBLOCK_EVENT);
     // Reload the page with the content unblocked
     BrowserReloadWithFlags(
       Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT);
     PanelMultiView.hidePopup(this._identityPopup);
   },
 
   enableMixedContentProtection() {
-    gBrowser.selectedBrowser.messageManager.sendAsyncMessage(
-      "MixedContent:ReenableProtection", {});
+    gBrowser.selectedBrowser.sendMessageToActor("MixedContent:ReenableProtection", {}, "BrowserTab");
     BrowserReload();
     PanelMultiView.hidePopup(this._identityPopup);
   },
 
   removeCertException() {
     if (!this._uriHasHost) {
       Cu.reportError("Trying to revoke a cert exception on a URI without a host?");
       return;
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1700,17 +1700,16 @@ var gBrowserInit = {
     if (AppConstants.platform != "macosx") {
       updateEditUIVisibility();
       let placesContext = document.getElementById("placesContext");
       placesContext.addEventListener("popupshowing", updateEditUIVisibility);
       placesContext.addEventListener("popuphiding", updateEditUIVisibility);
     }
 
     FullScreen.init();
-    PointerLock.init();
 
     if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
       MenuTouchModeObserver.init();
     }
 
     if (AppConstants.MOZ_DATA_REPORTING)
       gDataNotificationInfoBar.init();
 
@@ -1769,27 +1768,22 @@ var gBrowserInit = {
 
     Services.obs.notifyObservers(window, "browser-delayed-startup-finished");
     TelemetryTimestamps.add("delayedStartupFinished");
   },
 
   _setInitialFocus() {
     let initiallyFocusedElement = document.commandDispatcher.focusedElement;
 
-    let firstBrowserPaintDeferred = {};
-    firstBrowserPaintDeferred.promise = new Promise(resolve => {
-      firstBrowserPaintDeferred.resolve = resolve;
+    this._firstBrowserPaintDeferred = {};
+    this._firstBrowserPaintDeferred.promise = new Promise(resolve => {
+      this._firstBrowserPaintDeferred.resolve = resolve;
     });
 
     let mm = window.messageManager;
-    mm.addMessageListener("Browser:FirstPaint", function onFirstPaint() {
-      mm.removeMessageListener("Browser:FirstPaint", onFirstPaint);
-      firstBrowserPaintDeferred.resolve();
-    });
-
     let initialBrowser = gBrowser.selectedBrowser;
     mm.addMessageListener("Browser:FirstNonBlankPaint",
                           function onFirstNonBlankPaint() {
       mm.removeMessageListener("Browser:FirstNonBlankPaint", onFirstNonBlankPaint);
       initialBrowser.removeAttribute("blank");
     });
 
     // To prevent flickering of the urlbar-history-dropmarker in the general
@@ -1802,17 +1796,17 @@ var gBrowserInit = {
         focusAndSelectUrlBar();
         shouldRemoveFocusedAttribute = false;
         return;
       }
 
       if (gBrowser.selectedBrowser.isRemoteBrowser) {
         // If the initial browser is remote, in order to optimize for first paint,
         // we'll defer switching focus to that browser until it has painted.
-        firstBrowserPaintDeferred.promise.then(() => {
+        this._firstBrowserPaintDeferred.promise.then(() => {
           // If focus didn't move while we were waiting for first paint, we're okay
           // to move to the browser.
           if (document.commandDispatcher.focusedElement == initiallyFocusedElement) {
             gBrowser.selectedBrowser.focus();
           }
         });
       } else {
         // If the initial browser is not remote, we can focus the browser
@@ -3479,20 +3473,18 @@ function BrowserReloadWithFlags(reloadFl
   function loadBrowserURI(browser, url) {
     browser.loadURI(url, {
       flags: reloadFlags,
       triggeringPrincipal: browser.contentPrincipal,
     });
   }
 
   function sendReloadMessage(tab) {
-    tab.linkedBrowser
-         .messageManager
-         .sendAsyncMessage("Browser:Reload",
-                           { flags: reloadFlags, handlingUserInput });
+    tab.linkedBrowser.sendMessageToActor("Browser:Reload",
+                                         { flags: reloadFlags, handlingUserInput }, "BrowserTab");
   }
 }
 
 function getSecurityInfo(securityInfoAsString) {
   if (!securityInfoAsString)
     return null;
 
   let securityInfo = gSerializationHelper.deserializeObject(securityInfoAsString);
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -1453,17 +1453,17 @@ nsContextMenu.prototype = {
     saveBrowser(this.browser);
   },
 
   printFrame: function CM_printFrame() {
     PrintUtils.printWindow(this.frameOuterWindowID, this.browser);
   },
 
   switchPageDirection: function CM_switchPageDirection() {
-    this.browser.messageManager.sendAsyncMessage("SwitchDocumentDirection");
+    gBrowser.selectedBrowser.sendMessageToActor("SwitchDocumentDirection", {}, "SwitchDocumentDirection", true);
   },
 
   mediaCommand: function CM_mediaCommand(command, data) {
     this.actor.mediaCommand(this.targetIdentifier, command, data);
   },
 
   copyMediaLocation() {
     var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -48,17 +48,16 @@ window._gBrowser = {
       messageManager.addMessageListener("DOMTitleChanged", this);
       messageManager.addMessageListener("DOMWindowClose", this);
       messageManager.addMessageListener("Browser:Init", this);
     } else {
       this._outerWindowIDBrowserMap.set(this.selectedBrowser.outerWindowID,
         this.selectedBrowser);
     }
     messageManager.addMessageListener("RefreshBlocker:Blocked", this);
-    messageManager.addMessageListener("Browser:WindowCreated", this);
 
     // To correctly handle keypresses for potential FindAsYouType, while
     // the tab's find bar is not yet initialized.
     messageManager.addMessageListener("Findbar:Keypress", this);
     this._setFindbarData();
 
     XPCOMUtils.defineLazyModuleGetters(this, {
       E10SUtils: "resource://gre/modules/E10SUtils.jsm",
@@ -596,17 +595,17 @@ window._gBrowser = {
 
   _updateTabBarForPinnedTabs() {
     this.tabContainer._unlockTabSizing();
     this.tabContainer._positionPinnedTabs();
     this.tabContainer._updateCloseButtons();
   },
 
   _notifyPinnedStatus(aTab) {
-    this.getBrowserForTab(aTab).messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: aTab.pinned });
+    aTab.linkedBrowser.sendMessageToActor("Browser:AppTab", { isAppTab: aTab.pinned }, "BrowserTab");
 
     let event = document.createEvent("Events");
     event.initEvent(aTab.pinned ? "TabPinned" : "TabUnpinned", true, false);
     aTab.dispatchEvent(event);
   },
 
   pinTab(aTab) {
     if (aTab.pinned)
@@ -1775,33 +1774,31 @@ window._gBrowser = {
                                 true, false);
 
     if (shouldBeRemote) {
       // Switching the browser to be remote will connect to a new child
       // process so the browser can no longer be considered to be
       // crashed.
       tab.removeAttribute("crashed");
     } else {
-      aBrowser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned });
+      aBrowser.sendMessageToActor("Browser:AppTab", { isAppTab: tab.pinned }, "BrowserTab");
 
       // Register the new outerWindowID.
       this._outerWindowIDBrowserMap.set(aBrowser.outerWindowID, aBrowser);
     }
 
     if (wasActive)
       aBrowser.focus();
 
     // If the findbar has been initialised, reset its browser reference.
     if (this.isFindBarInitialized(tab)) {
       this.getCachedFindBar(tab).browser = aBrowser;
     }
 
-    tab.linkedBrowser
-       .messageManager
-       .sendAsyncMessage("Browser:HasSiblings", this.tabs.length > 1);
+    tab.linkedBrowser.sendMessageToActor("Browser:HasSiblings", this.tabs.length > 1, "BrowserTab");
 
     evt = document.createEvent("Events");
     evt.initEvent("TabRemotenessChange", true, false);
     tab.dispatchEvent(evt);
 
     return true;
   },
 
@@ -2137,22 +2134,20 @@ window._gBrowser = {
     // for non-remote browsers after we have called browser.loadURI().
     if (remoteType == E10SUtils.NOT_REMOTE) {
       this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser);
     }
 
     // If we transitioned from one browser to two browsers, we need to set
     // hasSiblings=false on both the existing browser and the new browser.
     if (this.tabs.length == 2) {
-      window.messageManager
-            .broadcastAsyncMessage("Browser:HasSiblings", true);
+      this.tabs[0].linkedBrowser.sendMessageToActor("Browser:HasSiblings", true, "BrowserTab");
+      this.tabs[1].linkedBrowser.sendMessageToActor("Browser:HasSiblings", true, "BrowserTab");
     } else {
-      aTab.linkedBrowser
-          .messageManager
-          .sendAsyncMessage("Browser:HasSiblings", this.tabs.length > 1);
+      aTab.linkedBrowser.sendMessageToActor("Browser:HasSiblings", this.tabs.length > 1, "BrowserTab");
     }
 
     var evt = new CustomEvent("TabBrowserInserted", { bubbles: true, detail: { insertedOnTabCreation: aInsertedOnTabCreation } });
     aTab.dispatchEvent(evt);
   },
 
   _mayDiscardBrowser(aTab, aForceDiscard) {
     let browser = aTab.linkedBrowser;
@@ -3001,18 +2996,18 @@ window._gBrowser = {
     // We dispatch it before any teardown so that event listeners can
     // inspect the tab that's about to close.
     let evt = new CustomEvent("TabClose", { bubbles: true, detail: { adoptedBy: adoptedByTab } });
     aTab.dispatchEvent(evt);
 
     if (this.tabs.length == 2) {
       // We're closing one of our two open tabs, inform the other tab that its
       // sibling is going away.
-      window.messageManager
-            .broadcastAsyncMessage("Browser:HasSiblings", false);
+      this.tabs[0].linkedBrowser.sendMessageToActor("Browser:HasSiblings", false, "BrowserTab");
+      this.tabs[1].linkedBrowser.sendMessageToActor("Browser:HasSiblings", false, "BrowserTab");
     }
 
     if (aTab.linkedPanel) {
       if (!adoptedByTab && !gMultiProcessBrowser) {
         // Prevent this tab from showing further dialogs, since we're closing it
         browser.contentWindow.windowUtils.disableDialogs();
       }
 
@@ -3470,16 +3465,30 @@ window._gBrowser = {
       aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI;
       delete aOtherBrowser.registeredOpenURI;
     }
     if (tmp) {
       aOtherBrowser.registeredOpenURI = tmp;
     }
   },
 
+  announceWindowCreated(browser, userContextId) {
+    let tab = this.getTabForBrowser(browser);
+    if (tab && userContextId) {
+      ContextualIdentityService.telemetry(userContextId);
+      tab.setUserContextId(userContextId);
+    }
+
+    // We don't want to update the container icon and identifier if
+    // this is not the selected browser.
+    if (browser == gBrowser.selectedBrowser) {
+      updateUserContextUIIndicator();
+    }
+  },
+
   reloadMultiSelectedTabs() {
     this.reloadTabs(this.selectedTabs);
   },
 
   reloadTabs(tabs) {
     for (let tab of tabs) {
       try {
         this.getBrowserForTab(tab).reload();
@@ -4343,33 +4352,17 @@ window._gBrowser = {
       }
       case "Browser:Init":
       {
         let tab = this.getTabForBrowser(browser);
         if (!tab)
           return undefined;
 
         this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser);
-        browser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned });
-        break;
-      }
-      case "Browser:WindowCreated":
-      {
-        let tab = this.getTabForBrowser(browser);
-        if (tab && data.userContextId) {
-          ContextualIdentityService.telemetry(data.userContextId);
-          tab.setUserContextId(data.userContextId);
-        }
-
-        // We don't want to update the container icon and identifier if
-        // this is not the selected browser.
-        if (browser == gBrowser.selectedBrowser) {
-          updateUserContextUIIndicator();
-        }
-
+        browser.sendMessageToActor("Browser:AppTab", { isAppTab: tab.pinned }, "BrowserTab");
         break;
       }
       case "Findbar:Keypress":
       {
         let tab = this.getTabForBrowser(browser);
         if (!this.isFindBarInitialized(tab)) {
           let fakeEvent = data;
           this.getFindBar(tab).then(findbar => {
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -11,31 +11,66 @@ const {Services} = ChromeUtils.import("r
 const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 
 ChromeUtils.defineModuleGetter(this, "ActorManagerParent",
                                "resource://gre/modules/ActorManagerParent.jsm");
 
 const PREF_PDFJS_ENABLED_CACHE_STATE = "pdfjs.enabledCache.state";
 
 let ACTORS = {
+  BrowserTab: {
+    parent: {
+      moduleURI: "resource:///actors/BrowserTabParent.jsm",
+    },
+    child: {
+      moduleURI: "resource:///actors/BrowserTabChild.jsm",
+
+      events: {
+        "DOMWindowCreated": {},
+        "MozAfterPaint": {},
+        "MozDOMPointerLock:Entered": {},
+        "MozDOMPointerLock:Exited": {},
+      },
+      messages: [
+        "Browser:Reload",
+        "Browser:AppTab",
+        "Browser:HasSiblings",
+        "MixedContent:ReenableProtection",
+        "UpdateCharacterSet",
+      ],
+    },
+  },
+
   ContextMenu: {
     parent: {
       moduleURI: "resource:///actors/ContextMenuParent.jsm",
     },
 
     child: {
       moduleURI: "resource:///actors/ContextMenuChild.jsm",
       events: {
         "contextmenu": { mozSystemGroup: true },
       },
     },
 
     allFrames: true,
   },
 
+  SwitchDocumentDirection: {
+    child: {
+      moduleURI: "resource:///actors/SwitchDocumentDirectionChild.jsm",
+
+      messages: [
+        "SwitchDocumentDirection",
+      ],
+    },
+
+    allFrames: true,
+  },
+
   SubframeCrash: {
     parent: {
       moduleURI: "resource:///actors/SubframeCrashParent.jsm",
     },
 
     child: {
       moduleURI: "resource:///actors/SubframeCrashChild.jsm",
     },
@@ -94,40 +129,16 @@ let LEGACY_ACTORS = {
       matches: ["about:blocked?*"],
       allFrames: true,
       messages: [
         "DeceptiveBlockedDetails",
       ],
     },
   },
 
-  BrowserTab: {
-    child: {
-      module: "resource:///actors/BrowserTabChild.jsm",
-      group: "browsers",
-
-      events: {
-        "DOMWindowCreated": {once: true},
-        "MozAfterPaint": {once: true},
-        "MozDOMPointerLock:Entered": {},
-        "MozDOMPointerLock:Exited": {},
-      },
-
-      messages: [
-        "AllowScriptsToClose",
-        "Browser:AppTab",
-        "Browser:HasSiblings",
-        "Browser:Reload",
-        "MixedContent:ReenableProtection",
-        "SwitchDocumentDirection",
-        "UpdateCharacterSet",
-      ],
-    },
-  },
-
   ClickHandler: {
     child: {
       module: "resource:///actors/ClickHandlerChild.jsm",
       events: {
         "click": {capture: true, mozSystemGroup: true},
         "auxclick": {capture: true, mozSystemGroup: true},
       },
     },
--- a/gfx/ipc/GPUProcessManager.cpp
+++ b/gfx/ipc/GPUProcessManager.cpp
@@ -194,19 +194,19 @@ void GPUProcessManager::DisableGPUProces
   // know that it is disabled in the config above.
   EnsureProtocolsReady();
 
   // If we disable the GPU process during reinitialization after a previous
   // crash, then we need to tell the content processes again, because they
   // need to rebind to the UI process.
   HandleProcessLost();
 
-  // On Windows, always fallback to software.
+  // On Windows and Linux, always fallback to software.
   // The assumption is that something in the graphics driver is crashing.
-#if XP_WIN
+#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK)
   FallbackToSoftware("GPU Process is disabled, fallback to software solution.");
 #endif
 }
 
 bool GPUProcessManager::EnsureGPUReady() {
   if (mProcess && !mProcess->IsConnected()) {
     if (!mProcess->WaitForLaunch()) {
       // If this fails, we should have fired OnProcessLaunchComplete and
@@ -489,19 +489,21 @@ void GPUProcessManager::OnRemoteProcessD
 
   RebuildRemoteSessions();
   NotifyListenersOnCompositeDeviceReset();
 }
 
 void GPUProcessManager::FallbackToSoftware(const char* aMessage) {
   gfxConfig::SetFailed(Feature::HW_COMPOSITING, FeatureStatus::Blocked,
                        aMessage);
+#ifdef XP_WIN
   gfxConfig::SetFailed(Feature::D3D11_COMPOSITING, FeatureStatus::Blocked,
                        aMessage);
   gfxConfig::SetFailed(Feature::DIRECT2D, FeatureStatus::Blocked, aMessage);
+#endif
 }
 
 void GPUProcessManager::NotifyListenersOnCompositeDeviceReset() {
   for (const auto& listener : mListeners) {
     listener->OnCompositorDeviceReset();
   }
 }
 
--- a/gfx/thebes/gfxPlatform.cpp
+++ b/gfx/thebes/gfxPlatform.cpp
@@ -3267,22 +3267,34 @@ void gfxPlatform::GetCMSSupportInfo(mozi
   void* profile = nullptr;
   size_t size = 0;
 
   GetCMSOutputProfileData(profile, size);
   if (!profile) {
     return;
   }
 
-  char* encodedProfile = nullptr;
-  nsresult rv =
-      Base64Encode(reinterpret_cast<char*>(profile), size, &encodedProfile);
-  if (NS_SUCCEEDED(rv)) {
-    aObj.DefineProperty("CMSOutputProfile", encodedProfile);
-    free(encodedProfile);
+  // Some profiles can be quite large. We don't want to include giant profiles
+  // by default in about:support. For now, we only accept less than 8kiB.
+  const size_t kMaxProfileSize = 8192;
+  if (size < kMaxProfileSize) {
+    char* encodedProfile = nullptr;
+    nsresult rv =
+        Base64Encode(reinterpret_cast<char*>(profile), size, &encodedProfile);
+    if (NS_SUCCEEDED(rv)) {
+      aObj.DefineProperty("CMSOutputProfile", encodedProfile);
+      free(encodedProfile);
+    } else {
+      nsPrintfCString msg("base64 encode failed 0x%08x",
+                          static_cast<uint32_t>(rv));
+      aObj.DefineProperty("CMSOutputProfile", msg.get());
+    }
+  } else {
+    nsPrintfCString msg("%zu bytes, too large", size);
+    aObj.DefineProperty("CMSOutputProfile", msg.get());
   }
 
   free(profile);
 }
 
 class FrameStatsComparator {
  public:
   bool Equals(const FrameStats& aA, const FrameStats& aB) const {
--- a/image/decoders/nsPNGDecoder.cpp
+++ b/image/decoders/nsPNGDecoder.cpp
@@ -111,17 +111,16 @@ nsPNGDecoder::nsPNGDecoder(RasterImage* 
       interlacebuf(nullptr),
       mFormat(SurfaceFormat::UNKNOWN),
       mCMSMode(0),
       mChannels(0),
       mPass(0),
       mFrameIsHidden(false),
       mDisablePremultipliedAlpha(false),
       mGotInfoCallback(false),
-      mUsePipeTransform(false),
       mNumFrames(0) {}
 
 nsPNGDecoder::~nsPNGDecoder() {
   if (mPNG) {
     png_destroy_read_struct(&mPNG, mInfo ? &mInfo : nullptr, nullptr);
   }
   if (mCMSLine) {
     free(mCMSLine);
@@ -208,20 +207,19 @@ nsresult nsPNGDecoder::CreateFrame(const
                                    ? SurfacePipeFlags::ADAM7_INTERPOLATE
                                    : SurfacePipeFlags();
 
   if (mNumFrames == 0) {
     // The first frame may be displayed progressively.
     pipeFlags |= SurfacePipeFlags::PROGRESSIVE_DISPLAY;
   }
 
-  qcms_transform* pipeTransform = mUsePipeTransform ? mTransform : nullptr;
   Maybe<SurfacePipe> pipe = SurfacePipeFactory::CreateSurfacePipe(
       this, Size(), OutputSize(), aFrameInfo.mFrameRect, mFormat, animParams,
-      pipeTransform, pipeFlags);
+      /*aTransform*/ nullptr, pipeFlags);
 
   if (!pipe) {
     mPipe = SurfacePipe();
     return NS_ERROR_FAILURE;
   }
 
   mPipe = std::move(*pipe);
 
@@ -410,17 +408,18 @@ static void PNGDoGammaCorrection(png_str
     png_set_gamma(png_ptr, 2.2, aGamma);
   } else {
     png_set_gamma(png_ptr, 2.2, 0.45455);
   }
 }
 
 // Adapted from http://www.littlecms.com/pngchrm.c example code
 static qcms_profile* PNGGetColorProfile(png_structp png_ptr, png_infop info_ptr,
-                                        int color_type, uint32_t* intent) {
+                                        int color_type, qcms_data_type* inType,
+                                        uint32_t* intent) {
   qcms_profile* profile = nullptr;
   *intent = QCMS_INTENT_PERCEPTUAL;  // Our default
 
   // First try to see if iCCP chunk is present
   if (png_get_valid(png_ptr, info_ptr, PNG_INFO_iCCP)) {
     png_uint_32 profileLen;
     png_bytep profileData;
     png_charp profileName;
@@ -488,16 +487,34 @@ static qcms_profile* PNGGetColorProfile(
     profile = qcms_profile_create_rgb_with_gamma(whitePoint, primaries,
                                                  1.0 / gammaOfFile);
 
     if (profile) {
       png_set_gray_to_rgb(png_ptr);
     }
   }
 
+  if (profile) {
+    uint32_t profileSpace = qcms_profile_get_color_space(profile);
+    if (profileSpace == icSigGrayData) {
+      if (color_type & PNG_COLOR_MASK_ALPHA) {
+        *inType = QCMS_DATA_GRAYA_8;
+      } else {
+        *inType = QCMS_DATA_GRAY_8;
+      }
+    } else {
+      if (color_type & PNG_COLOR_MASK_ALPHA ||
+          png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
+        *inType = QCMS_DATA_RGBA_8;
+      } else {
+        *inType = QCMS_DATA_RGB_8;
+      }
+    }
+  }
+
   return profile;
 }
 
 void nsPNGDecoder::info_callback(png_structp png_ptr, png_infop info_ptr) {
   png_uint_32 width, height;
   int bit_depth, color_type, interlace_type, compression_type, filter_type;
   unsigned int channels;
 
@@ -565,70 +582,63 @@ void nsPNGDecoder::info_callback(png_str
       png_set_expand(png_ptr);
     }
   }
 
   if (bit_depth == 16) {
     png_set_scale_16(png_ptr);
   }
 
-  // Let libpng expand interlaced images.
-  const bool isInterlaced = interlace_type == PNG_INTERLACE_ADAM7;
-  if (isInterlaced) {
-    png_set_interlace_handling(png_ptr);
-  }
-
+  qcms_data_type inType = QCMS_DATA_RGBA_8;
   uint32_t intent = -1;
   uint32_t pIntent;
   if (decoder->mCMSMode != eCMSMode_Off) {
     intent = gfxPlatform::GetRenderingIntent();
     decoder->mInProfile =
-        PNGGetColorProfile(png_ptr, info_ptr, color_type, &pIntent);
+        PNGGetColorProfile(png_ptr, info_ptr, color_type, &inType, &pIntent);
     // If we're not mandating an intent, use the one from the image.
     if (intent == uint32_t(-1)) {
       intent = pIntent;
     }
   }
   if (decoder->mInProfile && gfxPlatform::GetCMSOutputProfile()) {
-    qcms_data_type inType;
     qcms_data_type outType;
 
-    uint32_t profileSpace = qcms_profile_get_color_space(decoder->mInProfile);
-    decoder->mUsePipeTransform = profileSpace != icSigGrayData;
-    if (decoder->mUsePipeTransform) {
-      // If the transform happens with SurfacePipe, it will always be in BGRA.
-      inType = QCMS_DATA_BGRA_8;
-      outType = QCMS_DATA_BGRA_8;
+    if (color_type & PNG_COLOR_MASK_ALPHA || num_trans) {
+      outType = QCMS_DATA_RGBA_8;
     } else {
-      if (color_type & PNG_COLOR_MASK_ALPHA) {
-        inType = QCMS_DATA_GRAYA_8;
-        outType = QCMS_DATA_RGBA_8;
-      } else {
-        inType = QCMS_DATA_GRAY_8;
-        outType = QCMS_DATA_RGB_8;
-      }
+      outType = QCMS_DATA_RGB_8;
     }
 
     decoder->mTransform = qcms_transform_create(
         decoder->mInProfile, inType, gfxPlatform::GetCMSOutputProfile(),
         outType, (qcms_intent)intent);
   } else {
     png_set_gray_to_rgb(png_ptr);
 
     // only do gamma correction if CMS isn't entirely disabled
     if (decoder->mCMSMode != eCMSMode_Off) {
       PNGDoGammaCorrection(png_ptr, info_ptr);
     }
 
     if (decoder->mCMSMode == eCMSMode_All) {
-      decoder->mTransform = gfxPlatform::GetCMSBGRATransform();
-      decoder->mUsePipeTransform = true;
+      if (color_type & PNG_COLOR_MASK_ALPHA || num_trans) {
+        decoder->mTransform = gfxPlatform::GetCMSRGBATransform();
+      } else {
+        decoder->mTransform = gfxPlatform::GetCMSRGBTransform();
+      }
     }
   }
 
+  // Let libpng expand interlaced images.
+  const bool isInterlaced = interlace_type == PNG_INTERLACE_ADAM7;
+  if (isInterlaced) {
+    png_set_interlace_handling(png_ptr);
+  }
+
   // now all of those things we set above are used to update various struct
   // members and whatnot, after which we can get channels, rowbytes, etc.
   png_read_update_info(png_ptr, info_ptr);
   decoder->mChannels = channels = png_get_channels(png_ptr, info_ptr);
 
   //---------------------------------------------------------------//
   // copy PNG info into imagelib structs (formerly png_set_dims()) //
   //---------------------------------------------------------------//
@@ -683,18 +693,18 @@ void nsPNGDecoder::info_callback(png_str
     if (NS_FAILED(rv)) {
       png_error(decoder->mPNG, "CreateFrame failed");
     }
     MOZ_ASSERT(decoder->mImageData, "Should have a buffer now");
 #ifdef PNG_APNG_SUPPORTED
   }
 #endif
 
-  if (decoder->mTransform && !decoder->mUsePipeTransform) {
-    uint32_t bpp[] = {0, 3, 4};
+  if (decoder->mTransform && (channels <= 2 || isInterlaced)) {
+    uint32_t bpp[] = {0, 3, 4, 3, 4};
     decoder->mCMSLine =
         static_cast<uint8_t*>(malloc(bpp[channels] * frameRect.Width()));
     if (!decoder->mCMSLine) {
       png_error(decoder->mPNG, "malloc of mCMSLine failed");
     }
   }
 
   if (interlace_type == PNG_INTERLACE_ADAM7) {
@@ -825,21 +835,31 @@ void nsPNGDecoder::row_callback(png_stru
 
 void nsPNGDecoder::WriteRow(uint8_t* aRow) {
   MOZ_ASSERT(aRow);
 
   uint8_t* rowToWrite = aRow;
   uint32_t width = uint32_t(mFrameRect.Width());
 
   // Apply color management to the row, if necessary, before writing it out.
-  // This is only needed for grayscale images.
-  if (mTransform && !mUsePipeTransform) {
-    MOZ_ASSERT(mCMSLine);
-    qcms_transform_data(mTransform, rowToWrite, mCMSLine, width);
-    rowToWrite = mCMSLine;
+  if (mTransform) {
+    if (mCMSLine) {
+      qcms_transform_data(mTransform, rowToWrite, mCMSLine, width);
+
+      // Copy alpha over.
+      if (HasAlphaChannel()) {
+        for (uint32_t i = 0; i < width; ++i) {
+          mCMSLine[4 * i + 3] = rowToWrite[mChannels * i + mChannels - 1];
+        }
+      }
+
+      rowToWrite = mCMSLine;
+    } else {
+      qcms_transform_data(mTransform, rowToWrite, rowToWrite, width);
+    }
   }
 
   // Write this row to the SurfacePipe.
   DebugOnly<WriteState> result;
   if (HasAlphaChannel()) {
     if (mDisablePremultipliedAlpha) {
       result = mPipe.WritePixelsToRow<uint32_t>(
           [&] { return PackUnpremultipliedRGBAPixelAndAdvance(rowToWrite); });
--- a/image/decoders/nsPNGDecoder.h
+++ b/image/decoders/nsPNGDecoder.h
@@ -96,17 +96,16 @@ class nsPNGDecoder : public Decoder {
   // whether CMS or premultiplied alpha are forced off
   uint32_t mCMSMode;
 
   uint8_t mChannels;
   uint8_t mPass;
   bool mFrameIsHidden;
   bool mDisablePremultipliedAlpha;
   bool mGotInfoCallback;
-  bool mUsePipeTransform;
 
   struct AnimFrameInfo {
     AnimFrameInfo();
 #ifdef PNG_APNG_SUPPORTED
     AnimFrameInfo(png_structp aPNG, png_infop aInfo);
 #endif
 
     DisposalMethod mDispose;
--- a/image/decoders/nsWebPDecoder.cpp
+++ b/image/decoders/nsWebPDecoder.cpp
@@ -223,18 +223,18 @@ nsresult nsWebPDecoder::CreateFrame(cons
   SurfacePipeFlags pipeFlags = SurfacePipeFlags();
 
   Maybe<AnimationParams> animParams;
   if (!IsFirstFrameDecode()) {
     animParams.emplace(aFrameRect, mTimeout, mCurrentFrame, mBlend, mDisposal);
   }
 
   Maybe<SurfacePipe> pipe = SurfacePipeFactory::CreateSurfacePipe(
-      this, Size(), OutputSize(), aFrameRect, mFormat, animParams, mTransform,
-      pipeFlags);
+      this, Size(), OutputSize(), aFrameRect, mFormat, animParams,
+      /*aTransform*/ nullptr, pipeFlags);
   if (!pipe) {
     MOZ_LOG(sWebPLog, LogLevel::Error,
             ("[this=%p] nsWebPDecoder::CreateFrame -- no pipe\n", this));
     return NS_ERROR_FAILURE;
   }
 
   mFrameRect = aFrameRect;
   mPipe = std::move(*pipe);
@@ -276,17 +276,17 @@ void nsWebPDecoder::ApplyColorProfile(co
     return;
   }
 
   if (!aProfile) {
     MOZ_LOG(sWebPLog, LogLevel::Debug,
             ("[this=%p] nsWebPDecoder::ApplyColorProfile -- not tagged, use "
              "sRGB transform\n",
              this));
-    mTransform = gfxPlatform::GetCMSBGRATransform();
+    mTransform = gfxPlatform::GetCMSRGBATransform();
     return;
   }
 
   mInProfile = qcms_profile_from_memory(aProfile, aLength);
   if (!mInProfile) {
     MOZ_LOG(
         sWebPLog, LogLevel::Error,
         ("[this=%p] nsWebPDecoder::ApplyColorProfile -- bad color profile\n",
@@ -306,19 +306,19 @@ void nsWebPDecoder::ApplyColorProfile(co
 
   // Calculate rendering intent.
   int intent = gfxPlatform::GetRenderingIntent();
   if (intent == -1) {
     intent = qcms_profile_get_rendering_intent(mInProfile);
   }
 
   // Create the color management transform.
-  mTransform = qcms_transform_create(mInProfile, QCMS_DATA_BGRA_8,
+  mTransform = qcms_transform_create(mInProfile, QCMS_DATA_RGBA_8,
                                      gfxPlatform::GetCMSOutputProfile(),
-                                     QCMS_DATA_BGRA_8, (qcms_intent)intent);
+                                     QCMS_DATA_RGBA_8, (qcms_intent)intent);
   MOZ_LOG(sWebPLog, LogLevel::Debug,
           ("[this=%p] nsWebPDecoder::ApplyColorProfile -- use tagged "
            "transform\n",
            this));
 }
 
 LexerResult nsWebPDecoder::ReadHeader(WebPDemuxer* aDemuxer, bool aIsComplete) {
   MOZ_ASSERT(aDemuxer);
@@ -458,16 +458,19 @@ LexerResult nsWebPDecoder::ReadSingle(co
       return LexerResult(TerminalState::FAILURE);
     }
 
     const bool noPremultiply =
         bool(GetSurfaceFlags() & SurfaceFlags::NO_PREMULTIPLY_ALPHA);
 
     for (int row = mLastRow; row < lastRow; row++) {
       uint8_t* src = rowStart + row * stride;
+      if (mTransform) {
+        qcms_transform_data(mTransform, src, src, width);
+      }
 
       WriteState result;
       if (mFormat == SurfaceFormat::B8G8R8A8) {
         if (noPremultiply) {
           result =
               mPipe.WritePixelsToRow<uint32_t>([&]() -> NextPixel<uint32_t> {
                 const uint32_t pixel =
                     gfxPackedPixelNoPreMultiply(src[3], src[0], src[1], src[2]);
--- a/js/public/GCAPI.h
+++ b/js/public/GCAPI.h
@@ -422,28 +422,28 @@ namespace JS {
   D(DELAYED_ATOMS_GC, 12)                  \
   D(SHARED_MEMORY_LIMIT, 13)               \
   D(IDLE_TIME_COLLECTION, 14)              \
   D(INCREMENTAL_TOO_SLOW, 15)              \
   D(ABORT_GC, 16)                          \
   D(FULL_WHOLE_CELL_BUFFER, 17)            \
   D(FULL_GENERIC_BUFFER, 18)               \
   D(FULL_VALUE_BUFFER, 19)                 \
-  D(FULL_CELL_PTR_BUFFER, 20)              \
+  D(FULL_CELL_PTR_OBJ_BUFFER, 20)          \
   D(FULL_SLOT_BUFFER, 21)                  \
   D(FULL_SHAPE_BUFFER, 22)                 \
   D(TOO_MUCH_WASM_MEMORY, 23)              \
   D(DISABLE_GENERATIONAL_GC, 24)           \
   D(FINISH_GC, 25)                         \
   D(PREPARE_FOR_TRACING, 26)               \
   D(INCREMENTAL_ALLOC_TRIGGER, 27)         \
+  D(FULL_CELL_PTR_STR_BUFFER, 28)          \
+  D(INCREMENTAL_MALLOC_TRIGGER, 29)        \
                                            \
   /* These are reserved for future use. */ \
-  D(RESERVED4, 28)                         \
-  D(RESERVED5, 29)                         \
   D(RESERVED6, 30)                         \
   D(RESERVED7, 31)                         \
   D(RESERVED8, 32)                         \
                                            \
   /* Reasons from Firefox */               \
   D(DOM_WINDOW_UTILS, 33)                  \
   D(COMPONENT_UTILS, 34)                   \
   D(MEM_PRESSURE, 35)                      \
--- a/js/src/gc/Allocator.cpp
+++ b/js/src/gc/Allocator.cpp
@@ -366,18 +366,19 @@ bool GCRuntime::gcIfNeededAtAllocation(J
   // handle that here. Just check in case we need to collect instead.
   if (cx->hasAnyPendingInterrupt()) {
     gcIfRequested();
   }
 
   // If we have grown past our GC heap threshold while in the middle of
   // an incremental GC, we're growing faster than we're GCing, so stop
   // the world and do a full, non-incremental GC right now, if possible.
+  Zone* zone = cx->zone();
   if (isIncrementalGCInProgress() &&
-      cx->zone()->totalBytes() > cx->zone()->threshold.gcTriggerBytes()) {
+      zone->zoneSize.gcBytes() > zone->threshold.gcTriggerBytes()) {
     PrepareZoneForGC(cx->zone());
     gc(GC_NORMAL, JS::GCReason::INCREMENTAL_TOO_SLOW);
   }
 
   return true;
 }
 
 template <typename T>
--- a/js/src/gc/GC.cpp
+++ b/js/src/gc/GC.cpp
@@ -1455,18 +1455,17 @@ bool GCRuntime::setParameter(JSGCParamKe
     case JSGC_COMPACTING_ENABLED:
       compactingEnabled = value != 0;
       break;
     default:
       if (!tunables.setParameter(key, value, lock)) {
         return false;
       }
       for (ZonesIter zone(rt, WithAtoms); !zone.done(); zone.next()) {
-        zone->threshold.updateAfterGC(zone->totalBytes(), GC_NORMAL, tunables,
-                                      schedulingState, lock);
+        zone->updateAllGCThresholds(*this, lock);
       }
   }
 
   return true;
 }
 
 bool GCSchedulingTunables::setParameter(JSGCParamKey key, uint32_t value,
                                         const AutoLockGC& lock) {
@@ -1715,18 +1714,17 @@ void GCRuntime::resetParameter(JSGCParam
       mode = TuningDefaults::Mode;
       break;
     case JSGC_COMPACTING_ENABLED:
       compactingEnabled = TuningDefaults::CompactingEnabled;
       break;
     default:
       tunables.resetParameter(key, lock);
       for (ZonesIter zone(rt, WithAtoms); !zone.done(); zone.next()) {
-        zone->threshold.updateAfterGC(zone->totalBytes(), GC_NORMAL, tunables,
-                                      schedulingState, lock);
+        zone->updateAllGCThresholds(*this, lock);
       }
   }
 }
 
 void GCSchedulingTunables::resetParameter(JSGCParamKey key,
                                           const AutoLockGC& lock) {
   switch (key) {
     case JSGC_MAX_BYTES:
@@ -2066,17 +2064,17 @@ extern JS_FRIEND_API void js::RemoveRawV
 void GCRuntime::setMaxMallocBytes(size_t value, const AutoLockGC& lock) {
   tunables.setMaxMallocBytes(value);
   mallocCounter.setMax(value, lock);
   for (ZonesIter zone(rt, WithAtoms); !zone.done(); zone.next()) {
     zone->setGCMaxMallocBytes(value, lock);
   }
 }
 
-float ZoneHeapThreshold::eagerAllocTrigger(bool highFrequencyGC) const {
+float ZoneThreshold::eagerAllocTrigger(bool highFrequencyGC) const {
   float eagerTriggerFactor = highFrequencyGC
                                  ? HighFrequencyEagerAllocTriggerFactor
                                  : LowFrequencyEagerAllocTriggerFactor;
   return eagerTriggerFactor * gcTriggerBytes();
 }
 
 /* static */
 float ZoneHeapThreshold::computeZoneHeapGrowthFactorForHeapSize(
@@ -2162,16 +2160,32 @@ void ZoneHeapThreshold::updateForRemoved
       (gcTriggerBytes_ - amount <
        tunables.gcZoneAllocThresholdBase() * gcHeapGrowthFactor_)) {
     return;
   }
 
   gcTriggerBytes_ -= amount;
 }
 
+/* static */
+size_t ZoneMallocThreshold::computeZoneTriggerBytes(
+    float growthFactor, size_t lastBytes, const GCSchedulingTunables& tunables,
+    const AutoLockGC& lock) {
+  size_t base = Max(lastBytes, tunables.maxMallocBytes());
+  float trigger = float(base) * growthFactor;
+  return size_t(trigger);
+}
+
+void ZoneMallocThreshold::updateAfterGC(size_t lastBytes,
+                                        const GCSchedulingTunables& tunables,
+                                        const GCSchedulingState& state,
+                                        const AutoLockGC& lock) {
+  gcTriggerBytes_ = computeZoneTriggerBytes(2.0, lastBytes, tunables, lock);
+}
+
 MemoryCounter::MemoryCounter()
     : bytes_(0), maxBytes_(0), triggered_(NoTrigger) {}
 
 void MemoryCounter::updateOnGCStart() {
   // Record the current byte count at the start of GC.
   bytesAtStartOfGC_ = bytes_;
 }
 
@@ -3417,32 +3431,28 @@ bool GCRuntime::triggerGC(JS::GCReason r
     return false;
   }
 
   JS::PrepareForFullGC(rt->mainContextFromOwnThread());
   requestMajorGC(reason);
   return true;
 }
 
-void js::gc::MaybeAllocTriggerZoneGC(JSRuntime* rt, ZoneAllocator* zoneAlloc) {
-  rt->gc.maybeAllocTriggerZoneGC(Zone::from(zoneAlloc));
-}
-
 void GCRuntime::maybeAllocTriggerZoneGC(Zone* zone, size_t nbytes) {
   if (!CurrentThreadCanAccessRuntime(rt)) {
     // Zones in use by a helper thread can't be collected.
     MOZ_ASSERT(zone->usedByHelperThread() || zone->isAtomsZone());
     return;
   }
 
   MOZ_ASSERT(!JS::RuntimeHeapIsCollecting());
 
-  size_t usedBytes = zone->totalBytes();  // This already includes |nbytes|.
+  size_t usedBytes =
+      zone->zoneSize.gcBytes();  // This already includes |nbytes|.
   size_t thresholdBytes = zone->threshold.gcTriggerBytes();
-
   if (usedBytes >= thresholdBytes) {
     // The threshold has been surpassed, immediately trigger a GC, which
     // will be done non-incrementally.
     triggerZoneGC(zone, JS::GCReason::ALLOC_TRIGGER, usedBytes, thresholdBytes);
     return;
   }
 
   bool wouldInterruptCollection =
@@ -3473,16 +3483,49 @@ void GCRuntime::maybeAllocTriggerZoneGC(
       // Delay the next slice until a certain amount of allocation
       // has been performed.
       zone->gcDelayBytes = tunables.zoneAllocDelayBytes();
       return;
     }
   }
 }
 
+void js::gc::MaybeMallocTriggerZoneGC(JSRuntime* rt, ZoneAllocator* zoneAlloc) {
+  rt->gc.maybeMallocTriggerZoneGC(Zone::from(zoneAlloc));
+}
+
+void GCRuntime::maybeMallocTriggerZoneGC(Zone* zone) {
+  if (!CurrentThreadCanAccessRuntime(rt)) {
+    // Zones in use by a helper thread can't be collected.
+    MOZ_ASSERT(zone->usedByHelperThread() || zone->isAtomsZone());
+    return;
+  }
+
+  MOZ_ASSERT(!JS::RuntimeHeapIsCollecting());
+
+  size_t usedBytes = zone->gcMallocBytes.gcBytes();
+  size_t thresholdBytes = zone->gcMallocThreshold.gcTriggerBytes();
+  if (usedBytes >= thresholdBytes) {
+    // The threshold has been surpassed, immediately trigger a GC, which
+    // will be done non-incrementally.
+    triggerZoneGC(zone, JS::GCReason::TOO_MUCH_MALLOC, usedBytes,
+                  thresholdBytes);
+    return;
+  }
+
+  float zoneGCThresholdFactor = tunables.allocThresholdFactor();
+  size_t igcThresholdBytes = thresholdBytes * zoneGCThresholdFactor;
+  if (usedBytes >= igcThresholdBytes) {
+    // Start or continue an in progress incremental GC.
+    triggerZoneGC(zone, JS::GCReason::INCREMENTAL_MALLOC_TRIGGER, usedBytes,
+                  igcThresholdBytes);
+    return;
+  }
+}
+
 bool GCRuntime::triggerZoneGC(Zone* zone, JS::GCReason reason, size_t used,
                               size_t threshold) {
   MOZ_ASSERT(CurrentThreadCanAccessRuntime(rt));
 
   /* GC is already running. */
   if (JS::RuntimeHeapIsBusy()) {
     return false;
   }
@@ -3527,27 +3570,40 @@ void GCRuntime::maybeGC(Zone* zone) {
     return;
   }
 #endif
 
   if (gcIfRequested()) {
     return;
   }
 
-  float threshold = zone->threshold.eagerAllocTrigger(
-      schedulingState.inHighFrequencyGCMode());
-  float usedBytes = zone->totalBytes();
-  if (usedBytes > 1024 * 1024 && usedBytes >= threshold &&
-      !isIncrementalGCInProgress() && !isBackgroundSweeping()) {
-    stats().recordTrigger(usedBytes, threshold);
+  if (isIncrementalGCInProgress() || isBackgroundSweeping()) {
+    return;
+  }
+
+  if (checkEagerAllocTrigger(zone->zoneSize, zone->threshold) ||
+      checkEagerAllocTrigger(zone->gcMallocBytes, zone->gcMallocThreshold)) {
     PrepareZoneForGC(zone);
     startGC(GC_NORMAL, JS::GCReason::EAGER_ALLOC_TRIGGER);
   }
 }
 
+bool GCRuntime::checkEagerAllocTrigger(const HeapSize& size,
+                                       const ZoneThreshold& threshold) {
+  float thresholdBytes =
+      threshold.eagerAllocTrigger(schedulingState.inHighFrequencyGCMode());
+  float usedBytes = size.gcBytes();
+  if (usedBytes <= 1024 * 1024 || usedBytes < thresholdBytes) {
+    return false;
+  }
+
+  stats().recordTrigger(usedBytes, thresholdBytes);
+  return true;
+}
+
 void GCRuntime::triggerFullGCForAtoms(JSContext* cx) {
   MOZ_ASSERT(fullGCForAtomsRequested_);
   MOZ_ASSERT(CurrentThreadCanAccessRuntime(rt));
   MOZ_ASSERT(!JS::RuntimeHeapIsCollecting());
   MOZ_ASSERT(cx->canCollectAtoms());
   fullGCForAtomsRequested_ = false;
   MOZ_RELEASE_ASSERT(triggerGC(JS::GCReason::DELAYED_ATOMS_GC));
 }
@@ -5933,18 +5989,17 @@ IncrementalProgress GCRuntime::endSweepi
 
   /* Free LIFO blocks on a background thread if possible. */
   startBackgroundFree();
 
   /* Update the GC state for zones we have swept. */
   for (SweepGroupZonesIter zone(rt); !zone.done(); zone.next()) {
     AutoLockGC lock(rt);
     zone->changeGCState(Zone::Sweep, Zone::Finished);
-    zone->threshold.updateAfterGC(zone->totalBytes(), invocationKind, tunables,
-                                  schedulingState, lock);
+    zone->updateAllGCThresholds(*this, lock);
     zone->updateAllGCMallocCountersOnGCEnd(lock);
     zone->arenas.unmarkPreMarkedFreeCells();
   }
 
   /*
    * Start background thread to sweep zones if required, sweeping the atoms
    * zone last if present.
    */
@@ -7435,25 +7490,35 @@ GCRuntime::IncrementalResult GCRuntime::
   }
 
   AbortReason resetReason = AbortReason::None;
   for (ZonesIter zone(rt, WithAtoms); !zone.done(); zone.next()) {
     if (!zone->canCollect()) {
       continue;
     }
 
-    if (zone->totalBytes() >= zone->threshold.gcTriggerBytes()) {
+    if (zone->zoneSize.gcBytes() >= zone->threshold.gcTriggerBytes()) {
       CheckZoneIsScheduled(zone, reason, "GC bytes");
       budget.makeUnlimited();
       stats().nonincremental(AbortReason::GCBytesTrigger);
       if (zone->wasGCStarted() && zone->gcState() > Zone::Sweep) {
         resetReason = AbortReason::GCBytesTrigger;
       }
     }
 
+    if (zone->gcMallocBytes.gcBytes() >=
+        zone->gcMallocThreshold.gcTriggerBytes()) {
+      CheckZoneIsScheduled(zone, reason, "malloc bytes");
+      budget.makeUnlimited();
+      stats().nonincremental(AbortReason::MallocBytesTrigger);
+      if (zone->wasGCStarted() && zone->gcState() > Zone::Sweep) {
+        resetReason = AbortReason::MallocBytesTrigger;
+      }
+    }
+
     if (zone->shouldTriggerGCForTooMuchMalloc() == NonIncrementalTrigger) {
       CheckZoneIsScheduled(zone, reason, "malloc bytes");
       budget.makeUnlimited();
       stats().nonincremental(AbortReason::MallocBytesTrigger);
       if (zone->wasGCStarted() && zone->gcState() > Zone::Sweep) {
         resetReason = AbortReason::MallocBytesTrigger;
       }
     }
@@ -7488,20 +7553,24 @@ static void ScheduleZones(GCRuntime* gc)
     // To avoid resets, continue to collect any zones that were being
     // collected in a previous slice.
     if (gc->isIncrementalGCInProgress() && zone->wasGCStarted()) {
       zone->scheduleGC();
     }
 
     // This is a heuristic to reduce the total number of collections.
     bool inHighFrequencyMode = gc->schedulingState.inHighFrequencyGCMode();
-    if (zone->totalBytes() >=
+    if (zone->zoneSize.gcBytes() >=
         zone->threshold.eagerAllocTrigger(inHighFrequencyMode)) {
       zone->scheduleGC();
     }
+    if (zone->gcMallocBytes.gcBytes() >=
+        zone->gcMallocThreshold.eagerAllocTrigger(inHighFrequencyMode)) {
+      zone->scheduleGC();
+    }
 
     // This ensures we collect zones that have reached the malloc limit.
     if (zone->shouldTriggerGCForTooMuchMalloc()) {
       zone->scheduleGC();
     }
   }
 }
 
@@ -8020,16 +8089,17 @@ void GCRuntime::minorGC(JS::GCReason rea
 #ifdef JS_GC_ZEAL
   if (hasZealMode(ZealMode::CheckHeapAfterGC)) {
     CheckHeapAfterGC(rt);
   }
 #endif
 
   for (ZonesIter zone(rt, WithAtoms); !zone.done(); zone.next()) {
     maybeAllocTriggerZoneGC(zone);
+    maybeMallocTriggerZoneGC(zone);
   }
 }
 
 void GCRuntime::startBackgroundFreeAfterMinorGC() {
   MOZ_ASSERT(nursery().isEmpty());
 
   {
     AutoLockHelperThreadState lock;
@@ -8204,16 +8274,17 @@ Realm* js::NewRealm(JSContext* cx, JSPri
 
   return realm.release();
 }
 
 void gc::MergeRealms(Realm* source, Realm* target) {
   JSRuntime* rt = source->runtimeFromMainThread();
   rt->gc.mergeRealms(source, target);
   rt->gc.maybeAllocTriggerZoneGC(target->zone());
+  rt->gc.maybeMallocTriggerZoneGC(target->zone());
 }
 
 void GCRuntime::mergeRealms(Realm* source, Realm* target) {
   // The source realm must be specifically flagged as mergable.  This
   // also implies that the realm is not visible to the debugger.
   MOZ_ASSERT(source->creationOptions().mergeable());
   MOZ_ASSERT(source->creationOptions().invisibleToDebugger());
 
--- a/js/src/gc/GCRuntime.h
+++ b/js/src/gc/GCRuntime.h
@@ -253,23 +253,28 @@ class GCRuntime {
   void setMarkStackLimit(size_t limit, AutoLockGC& lock);
 
   MOZ_MUST_USE bool setParameter(JSGCParamKey key, uint32_t value,
                                  AutoLockGC& lock);
   void resetParameter(JSGCParamKey key, AutoLockGC& lock);
   uint32_t getParameter(JSGCParamKey key, const AutoLockGC& lock);
 
   MOZ_MUST_USE bool triggerGC(JS::GCReason reason);
-  // Check whether to trigger a zone GC. During an incremental GC, optionally
-  // count |nbytes| towards the threshold for performing the next slice.
+  // Check whether to trigger a zone GC after allocating GC cells. During an
+  // incremental GC, optionally count |nbytes| towards the threshold for
+  // performing the next slice.
   void maybeAllocTriggerZoneGC(Zone* zone, size_t nbytes = 0);
+  // Check whether to trigger a zone GC after malloc memory.
+  void maybeMallocTriggerZoneGC(Zone* zone);
   // The return value indicates if we were able to do the GC.
   bool triggerZoneGC(Zone* zone, JS::GCReason reason, size_t usedBytes,
                      size_t thresholdBytes);
   void maybeGC(Zone* zone);
+  bool checkEagerAllocTrigger(const HeapSize& size,
+                              const ZoneThreshold& threshold);
   // The return value indicates whether a major GC was performed.
   bool gcIfRequested();
   void gc(JSGCInvocationKind gckind, JS::GCReason reason);
   void startGC(JSGCInvocationKind gckind, JS::GCReason reason,
                int64_t millis = 0);
   void gcSlice(JS::GCReason reason, int64_t millis = 0);
   void finishGC(JS::GCReason reason);
   void abortGC();
--- a/js/src/gc/Heap.h
+++ b/js/src/gc/Heap.h
@@ -826,64 +826,16 @@ static_assert(
     js::gc::ChunkLocationOffset ==
         offsetof(Chunk, trailer) + offsetof(ChunkTrailer, location),
     "The hardcoded API location offset must match the actual offset.");
 static_assert(
     js::gc::ChunkStoreBufferOffset ==
         offsetof(Chunk, trailer) + offsetof(ChunkTrailer, storeBuffer),
     "The hardcoded API storeBuffer offset must match the actual offset.");
 
-/*
- * Tracks the used sizes for owned heap data and automatically maintains the
- * memory usage relationship between GCRuntime and Zones.
- */
-class HeapSize {
-  /*
-   * A heap usage that contains our parent's heap usage, or null if this is
-   * the top-level usage container.
-   */
-  HeapSize* const parent_;
-
-  /*
-   * The approximate number of bytes in use on the GC heap, to the nearest
-   * ArenaSize. This does not include any malloc data. It also does not
-   * include not-actively-used addresses that are still reserved at the OS
-   * level for GC usage. It is atomic because it is updated by both the active
-   * and GC helper threads.
-   */
-  mozilla::Atomic<size_t, mozilla::ReleaseAcquire,
-                  mozilla::recordreplay::Behavior::DontPreserve>
-      gcBytes_;
-
- public:
-  explicit HeapSize(HeapSize* parent) : parent_(parent), gcBytes_(0) {}
-
-  size_t gcBytes() const { return gcBytes_; }
-
-  void addGCArena() {
-    gcBytes_ += ArenaSize;
-    if (parent_) {
-      parent_->addGCArena();
-    }
-  }
-  void removeGCArena() {
-    MOZ_ASSERT(gcBytes_ >= ArenaSize);
-    gcBytes_ -= ArenaSize;
-    if (parent_) {
-      parent_->removeGCArena();
-    }
-  }
-
-  /* Pair to adoptArenas. Adopts the attendant usage statistics. */
-  void adopt(HeapSize& other) {
-    gcBytes_ += other.gcBytes_;
-    other.gcBytes_ = 0;
-  }
-};
-
 inline void Arena::checkAddress() const {
   mozilla::DebugOnly<uintptr_t> addr = uintptr_t(this);
   MOZ_ASSERT(addr);
   MOZ_ASSERT(!(addr & ArenaMask));
   MOZ_ASSERT(Chunk::withinValidRange(addr));
 }
 
 inline Chunk* Arena::chunk() const { return Chunk::fromAddress(address()); }
--- a/js/src/gc/Nursery.cpp
+++ b/js/src/gc/Nursery.cpp
@@ -773,21 +773,24 @@ bool js::Nursery::shouldCollect() const 
   //
   // With defaults that's:
   //
   //   1MB = 256KB / 0.25
   //
   return belowBytesThreshold && belowFractionThreshold;
 }
 
-static inline bool IsFullStoreBufferReason(JS::GCReason reason) {
-  return reason == JS::GCReason::FULL_WHOLE_CELL_BUFFER ||
+// typeReason is the gcReason for specified type, for example,
+// FULL_CELL_PTR_OBJ_BUFFER is the gcReason for JSObject.
+static inline bool IsFullStoreBufferReason(JS::GCReason reason,
+                                           JS::GCReason typeReason) {
+  return reason == typeReason ||
+         reason == JS::GCReason::FULL_WHOLE_CELL_BUFFER ||
          reason == JS::GCReason::FULL_GENERIC_BUFFER ||
          reason == JS::GCReason::FULL_VALUE_BUFFER ||
-         reason == JS::GCReason::FULL_CELL_PTR_BUFFER ||
          reason == JS::GCReason::FULL_SLOT_BUFFER ||
          reason == JS::GCReason::FULL_SHAPE_BUFFER;
 }
 
 void js::Nursery::collect(JS::GCReason reason) {
   JSRuntime* rt = runtime();
   MOZ_ASSERT(!rt->mainContextFromOwnThread()->suppressGC);
 
@@ -835,78 +838,17 @@ void js::Nursery::collect(JS::GCReason r
     previousGC.nurseryCommitted = committed();
     previousGC.tenuredBytes = 0;
     previousGC.tenuredCells = 0;
   }
 
   // Resize the nursery.
   maybeResizeNursery(reason);
 
-  // If we are promoting the nursery, or exhausted the store buffer with
-  // pointers to nursery things, which will force a collection well before
-  // the nursery is full, look for object groups that are getting promoted
-  // excessively and try to pretenure them.
-  startProfile(ProfileKey::Pretenure);
-  bool validPromotionRate;
-  const float promotionRate = calcPromotionRate(&validPromotionRate);
-  uint32_t pretenureCount = 0;
-  bool shouldPretenure =
-      tunables().attemptPretenuring() &&
-      ((validPromotionRate && promotionRate > tunables().pretenureThreshold() &&
-        previousGC.nurseryUsedBytes >= 4 * 1024 * 1024) ||
-       IsFullStoreBufferReason(reason));
-
-  if (shouldPretenure) {
-    JSContext* cx = rt->mainContextFromOwnThread();
-    for (auto& entry : tenureCounts.entries) {
-      if (entry.count >= tunables().pretenureGroupThreshold()) {
-        ObjectGroup* group = entry.group;
-        AutoMaybeLeaveAtomsZone leaveAtomsZone(cx);
-        AutoRealm ar(cx, group);
-        AutoSweepObjectGroup sweep(group);
-        if (group->canPreTenure(sweep)) {
-          group->setShouldPreTenure(sweep, cx);
-          pretenureCount++;
-        }
-      }
-    }
-  }
-  stats().setStat(gcstats::STAT_OBJECT_GROUPS_PRETENURED, pretenureCount);
-
-  mozilla::Maybe<AutoGCSession> session;
-  uint32_t numStringsTenured = 0;
-  uint32_t numNurseryStringRealmsDisabled = 0;
-  for (ZonesIter zone(rt, SkipAtoms); !zone.done(); zone.next()) {
-    if (shouldPretenure && zone->allocNurseryStrings &&
-        zone->tenuredStrings >= 30 * 1000) {
-      if (!session.isSome()) {
-        session.emplace(rt, JS::HeapState::MinorCollecting);
-      }
-      CancelOffThreadIonCompile(zone);
-      bool preserving = zone->isPreservingCode();
-      zone->setPreservingCode(false);
-      zone->discardJitCode(rt->defaultFreeOp());
-      zone->setPreservingCode(preserving);
-      for (RealmsInZoneIter r(zone); !r.done(); r.next()) {
-        if (jit::JitRealm* jitRealm = r->jitRealm()) {
-          jitRealm->discardStubs();
-          jitRealm->setStringsCanBeInNursery(false);
-          numNurseryStringRealmsDisabled++;
-        }
-      }
-      zone->allocNurseryStrings = false;
-    }
-    numStringsTenured += zone->tenuredStrings;
-    zone->tenuredStrings = 0;
-  }
-  session.reset();  // End the minor GC session, if running one.
-  stats().setStat(gcstats::STAT_NURSERY_STRING_REALMS_DISABLED,
-                  numNurseryStringRealmsDisabled);
-  stats().setStat(gcstats::STAT_STRINGS_TENURED, numStringsTenured);
-  endProfile(ProfileKey::Pretenure);
+  const float promotionRate = doPretenuring(rt, reason, tenureCounts);
 
   // We ignore gcMaxBytes when allocating for minor collection. However, if we
   // overflowed, we disable the nursery. The next time we allocate, we'll fail
   // because gcBytes >= gcMaxBytes.
   if (rt->gc.heapSize.gcBytes() >= tunables().gcMaxBytes()) {
     disable();
   }
 
@@ -915,18 +857,16 @@ void js::Nursery::collect(JS::GCReason r
 
   TimeDuration totalTime = profileDurations_[ProfileKey::Total];
   rt->addTelemetry(JS_TELEMETRY_GC_MINOR_US, totalTime.ToMicroseconds());
   rt->addTelemetry(JS_TELEMETRY_GC_MINOR_REASON, uint32_t(reason));
   if (totalTime.ToMilliseconds() > 1.0) {
     rt->addTelemetry(JS_TELEMETRY_GC_MINOR_REASON_LONG, uint32_t(reason));
   }
   rt->addTelemetry(JS_TELEMETRY_GC_NURSERY_BYTES, committed());
-  rt->addTelemetry(JS_TELEMETRY_GC_PRETENURE_COUNT, pretenureCount);
-  rt->addTelemetry(JS_TELEMETRY_GC_NURSERY_PROMOTION_RATE, promotionRate * 100);
 
   stats().endNurseryCollection(reason);
   gcTracer.traceMinorGCEnd();
   timeInChunkAlloc_ = mozilla::TimeDuration();
 
   if (enableProfiling_ && totalTime >= profileThreshold_) {
     stats().maybePrintProfileHeaders();
 
@@ -1058,16 +998,103 @@ void js::Nursery::doCollection(JS::GCRea
   previousGC.reason = reason;
   previousGC.nurseryCapacity = initialNurseryCapacity;
   previousGC.nurseryCommitted = spaceToEnd(allocatedChunkCount());
   previousGC.nurseryUsedBytes = initialNurseryUsedBytes;
   previousGC.tenuredBytes = mover.tenuredSize;
   previousGC.tenuredCells = mover.tenuredCells;
 }
 
+float js::Nursery::doPretenuring(JSRuntime* rt, JS::GCReason reason,
+                                 TenureCountCache& tenureCounts) {
+  // If we are promoting the nursery, or exhausted the store buffer with
+  // pointers to nursery things, which will force a collection well before
+  // the nursery is full, look for object groups that are getting promoted
+  // excessively and try to pretenure them.
+  startProfile(ProfileKey::Pretenure);
+  bool validPromotionRate;
+  const float promotionRate = calcPromotionRate(&validPromotionRate);
+  uint32_t pretenureCount = 0;
+  bool attempt = tunables().attemptPretenuring();
+
+  bool pretenureObj, pretenureStr;
+  if (attempt) {
+    // Should we do pretenuring regardless of gcreason?
+    bool shouldPretenure = validPromotionRate &&
+                           promotionRate > tunables().pretenureThreshold() &&
+                           previousGC.nurseryUsedBytes >= 4 * 1024 * 1024;
+    pretenureObj =
+        shouldPretenure ||
+        IsFullStoreBufferReason(reason, JS::GCReason::FULL_CELL_PTR_OBJ_BUFFER);
+    pretenureStr =
+        shouldPretenure ||
+        IsFullStoreBufferReason(reason, JS::GCReason::FULL_CELL_PTR_STR_BUFFER);
+  } else {
+    pretenureObj = false;
+    pretenureStr = false;
+  }
+
+  if (pretenureObj) {
+    JSContext* cx = rt->mainContextFromOwnThread();
+    uint32_t threshold = tunables().pretenureGroupThreshold();
+    for (auto& entry : tenureCounts.entries) {
+      if (entry.count < threshold) {
+        continue;
+      }
+
+      ObjectGroup* group = entry.group;
+      AutoMaybeLeaveAtomsZone leaveAtomsZone(cx);
+      AutoRealm ar(cx, group);
+      AutoSweepObjectGroup sweep(group);
+      if (group->canPreTenure(sweep)) {
+        group->setShouldPreTenure(sweep, cx);
+        pretenureCount++;
+      }
+    }
+  }
+  stats().setStat(gcstats::STAT_OBJECT_GROUPS_PRETENURED, pretenureCount);
+
+  mozilla::Maybe<AutoGCSession> session;
+  uint32_t numStringsTenured = 0;
+  uint32_t numNurseryStringRealmsDisabled = 0;
+  for (ZonesIter zone(rt, SkipAtoms); !zone.done(); zone.next()) {
+    if (pretenureStr && zone->allocNurseryStrings &&
+        zone->tenuredStrings >= 30 * 1000) {
+      if (!session.isSome()) {
+        session.emplace(rt, JS::HeapState::MinorCollecting);
+      }
+      CancelOffThreadIonCompile(zone);
+      bool preserving = zone->isPreservingCode();
+      zone->setPreservingCode(false);
+      zone->discardJitCode(rt->defaultFreeOp());
+      zone->setPreservingCode(preserving);
+      for (RealmsInZoneIter r(zone); !r.done(); r.next()) {
+        if (jit::JitRealm* jitRealm = r->jitRealm()) {
+          jitRealm->discardStubs();
+          jitRealm->setStringsCanBeInNursery(false);
+          numNurseryStringRealmsDisabled++;
+        }
+      }
+      zone->allocNurseryStrings = false;
+    }
+    numStringsTenured += zone->tenuredStrings;
+    zone->tenuredStrings = 0;
+  }
+  session.reset();  // End the minor GC session, if running one.
+  stats().setStat(gcstats::STAT_NURSERY_STRING_REALMS_DISABLED,
+                  numNurseryStringRealmsDisabled);
+  stats().setStat(gcstats::STAT_STRINGS_TENURED, numStringsTenured);
+  endProfile(ProfileKey::Pretenure);
+
+  rt->addTelemetry(JS_TELEMETRY_GC_PRETENURE_COUNT, pretenureCount);
+  rt->addTelemetry(JS_TELEMETRY_GC_NURSERY_PROMOTION_RATE, promotionRate * 100);
+
+  return promotionRate;
+}
+
 bool js::Nursery::registerMallocedBuffer(void* buffer) {
   MOZ_ASSERT(buffer);
   return mallocedBuffers.putNew(buffer);
 }
 
 void js::Nursery::sweep(JSTracer* trc) {
   // Sweep unique IDs first before we sweep any tables that may be keyed based
   // on them.
--- a/js/src/gc/Nursery.h
+++ b/js/src/gc/Nursery.h
@@ -603,16 +603,19 @@ class Nursery {
 
   const js::gc::GCSchedulingTunables& tunables() const;
 
   /* Common internal allocator function. */
   void* allocate(size_t size);
 
   void doCollection(JS::GCReason reason, gc::TenureCountCache& tenureCounts);
 
+  float doPretenuring(JSRuntime* rt, JS::GCReason reason,
+                      gc::TenureCountCache& tenureCounts);
+
   /*
    * Move the object at |src| in the Nursery to an already-allocated cell
    * |dst| in Tenured.
    */
   void collectToFixedPoint(TenuringTracer& trc,
                            gc::TenureCountCache& tenureCounts);
 
   /* Handle relocation of slots/elements pointers stored in Ion frames. */
--- a/js/src/gc/Scheduling.h
+++ b/js/src/gc/Scheduling.h
@@ -613,61 +613,133 @@ class MemoryCounter {
 
   void recordTrigger(TriggerKind trigger);
 
   void updateOnGCStart();
   void updateOnGCEnd(const GCSchedulingTunables& tunables,
                      const AutoLockGC& lock);
 };
 
-// This class encapsulates the data that determines when we need to do a zone
-// GC.
-class ZoneHeapThreshold {
-  // The "growth factor" for computing our next thresholds after a GC.
-  GCLockData<float> gcHeapGrowthFactor_;
+/*
+ * Tracks the used sizes for owned heap data and automatically maintains the
+ * memory usage relationship between GCRuntime and Zones.
+ */
+class HeapSize {
+  /*
+   * A heap usage that contains our parent's heap usage, or null if this is
+   * the top-level usage container.
+   */
+  HeapSize* const parent_;
+
+  /*
+   * The approximate number of bytes in use on the GC heap, to the nearest
+   * ArenaSize. This does not include any malloc data. It also does not
+   * include not-actively-used addresses that are still reserved at the OS
+   * level for GC usage. It is atomic because it is updated by both the active
+   * and GC helper threads.
+   */
+  mozilla::Atomic<size_t, mozilla::ReleaseAcquire,
+                  mozilla::recordreplay::Behavior::DontPreserve>
+      gcBytes_;
+
+ public:
+  explicit HeapSize(HeapSize* parent) : parent_(parent), gcBytes_(0) {}
+
+  size_t gcBytes() const { return gcBytes_; }
 
-  // GC trigger threshold for allocations on the GC heap.
+  void addGCArena() { addBytes(ArenaSize); }
+  void removeGCArena() { removeBytes(ArenaSize); }
+
+  void addBytes(size_t nbytes) {
+    mozilla::DebugOnly<size_t> initialBytes(gcBytes_);
+    MOZ_ASSERT(initialBytes + nbytes > initialBytes);
+    gcBytes_ += nbytes;
+    if (parent_) {
+      parent_->addBytes(nbytes);
+    }
+  }
+  void removeBytes(size_t nbytes) {
+    MOZ_ASSERT(gcBytes_ >= nbytes);
+    gcBytes_ -= nbytes;
+    if (parent_) {
+      parent_->removeBytes(nbytes);
+    }
+  }
+
+  /* Pair to adoptArenas. Adopts the attendant usage statistics. */
+  void adopt(HeapSize& other) {
+    gcBytes_ += other.gcBytes_;
+    other.gcBytes_ = 0;
+  }
+};
+
+// Base class for GC heap and malloc thresholds.
+class ZoneThreshold {
+ protected:
+  // GC trigger threshold.
   mozilla::Atomic<size_t, mozilla::Relaxed,
                   mozilla::recordreplay::Behavior::DontPreserve>
       gcTriggerBytes_;
 
  public:
-  ZoneHeapThreshold() : gcHeapGrowthFactor_(3.0f), gcTriggerBytes_(0) {}
+  size_t gcTriggerBytes() const { return gcTriggerBytes_; }
+  float eagerAllocTrigger(bool highFrequencyGC) const;
+};
+
+// This class encapsulates the data that determines when we need to do a zone GC
+// base on GC heap size.
+class ZoneHeapThreshold : public ZoneThreshold {
+  // The "growth factor" for computing our next thresholds after a GC.
+  GCLockData<float> gcHeapGrowthFactor_;
+
+ public:
+  ZoneHeapThreshold() : gcHeapGrowthFactor_(3.0f) {}
 
   float gcHeapGrowthFactor() const { return gcHeapGrowthFactor_; }
-  size_t gcTriggerBytes() const { return gcTriggerBytes_; }
-  float eagerAllocTrigger(bool highFrequencyGC) const;
 
   void updateAfterGC(size_t lastBytes, JSGCInvocationKind gckind,
                      const GCSchedulingTunables& tunables,
                      const GCSchedulingState& state, const AutoLockGC& lock);
   void updateForRemovedArena(const GCSchedulingTunables& tunables);
 
  private:
   static float computeZoneHeapGrowthFactorForHeapSize(
       size_t lastBytes, const GCSchedulingTunables& tunables,
       const GCSchedulingState& state);
   static size_t computeZoneTriggerBytes(float growthFactor, size_t lastBytes,
                                         JSGCInvocationKind gckind,
                                         const GCSchedulingTunables& tunables,
                                         const AutoLockGC& lock);
 };
 
+// This class encapsulates the data that determines when we need to do a zone
+// GC based on malloc data.
+class ZoneMallocThreshold : public ZoneThreshold {
+ public:
+  void updateAfterGC(size_t lastBytes, const GCSchedulingTunables& tunables,
+                     const GCSchedulingState& state, const AutoLockGC& lock);
+
+ private:
+  static size_t computeZoneTriggerBytes(float growthFactor, size_t lastBytes,
+                                        const GCSchedulingTunables& tunables,
+                                        const AutoLockGC& lock);
+};
+
 #ifdef DEBUG
 
 // Counts memory associated with GC things in a zone.
 //
 // In debug builds, this records details of the cell the memory allocations is
 // associated with to check the correctness of the information provided. In opt
 // builds it's just a counter.
 class MemoryTracker {
  public:
   MemoryTracker();
-  ~MemoryTracker();
   void fixupAfterMovingGC();
+  void checkEmptyOnDestroy();
 
   void adopt(MemoryTracker& other);
 
   void trackMemory(Cell* cell, size_t nbytes, MemoryUse use);
   void untrackMemory(Cell* cell, size_t nbytes, MemoryUse use);
   void swapMemory(Cell* a, Cell* b, MemoryUse use);
   void registerPolicy(ZoneAllocPolicy* policy);
   void unregisterPolicy(ZoneAllocPolicy* policy);
--- a/js/src/gc/StoreBuffer.cpp
+++ b/js/src/gc/StoreBuffer.cpp
@@ -50,20 +50,20 @@ void StoreBuffer::GenericBuffer::trace(J
   for (LifoAlloc::Enum e(*storage_); !e.empty();) {
     unsigned size = *e.read<unsigned>();
     BufferableRef* edge = e.read<BufferableRef>(size);
     edge->trace(trc);
   }
 }
 
 StoreBuffer::StoreBuffer(JSRuntime* rt, const Nursery& nursery)
-    : bufferVal(this),
-      bufStrCell(this),
-      bufObjCell(this),
-      bufferSlot(this),
+    : bufferVal(this, JS::GCReason::FULL_VALUE_BUFFER),
+      bufStrCell(this, JS::GCReason::FULL_CELL_PTR_STR_BUFFER),
+      bufObjCell(this, JS::GCReason::FULL_CELL_PTR_OBJ_BUFFER),
+      bufferSlot(this, JS::GCReason::FULL_SLOT_BUFFER),
       bufferWholeCell(this),
       bufferGeneric(this),
       cancelIonCompilations_(false),
       runtime_(rt),
       nursery_(nursery),
       aboutToOverflow_(false),
       enabled_(false)
 #ifdef DEBUG
--- a/js/src/gc/StoreBuffer.h
+++ b/js/src/gc/StoreBuffer.h
@@ -75,20 +75,23 @@ class StoreBuffer {
     /*
      * A one element cache in front of the canonical set to speed up
      * temporary instances of HeapPtr.
      */
     T last_;
 
     StoreBuffer* owner_;
 
+    JS::GCReason gcReason_;
+
     /* Maximum number of entries before we request a minor GC. */
     const static size_t MaxEntries = 48 * 1024 / sizeof(T);
 
-    explicit MonoTypeBuffer(StoreBuffer* owner) : last_(T()), owner_(owner) {}
+    explicit MonoTypeBuffer(StoreBuffer* owner, JS::GCReason reason)
+        : last_(T()), owner_(owner), gcReason_(reason) {}
 
     void clear() {
       last_ = T();
       stores_.clear();
     }
 
     /* Add one item to the buffer. */
     void put(const T& t) {
@@ -112,17 +115,17 @@ class StoreBuffer {
         AutoEnterOOMUnsafeRegion oomUnsafe;
         if (!stores_.put(last_)) {
           oomUnsafe.crash("Failed to allocate for MonoTypeBuffer::put.");
         }
       }
       last_ = T();
 
       if (MOZ_UNLIKELY(stores_.count() > MaxEntries)) {
-        owner_->setAboutToOverflow(T::FullBufferReason);
+        owner_->setAboutToOverflow(gcReason_);
       }
     }
 
     /* Trace the source of all edges in the store buffer. */
     void trace(TenuringTracer& mover);
 
     template <typename CellType>
     void traceTyped(TenuringTracer& mover);
@@ -271,18 +274,16 @@ class StoreBuffer {
     CellPtrEdge untagged() const {
       return CellPtrEdge((Cell**)(uintptr_t(edge) & ~1));
     }
     bool isTagged() const { return bool(uintptr_t(edge) & 1); }
 
     explicit operator bool() const { return edge != nullptr; }
 
     typedef PointerEdgeHasher<CellPtrEdge> Hasher;
-
-    static const auto FullBufferReason = JS::GCReason::FULL_CELL_PTR_BUFFER;
   };
 
   struct ValueEdge {
     JS::Value* edge;
 
     ValueEdge() : edge(nullptr) {}
     explicit ValueEdge(JS::Value* v) : edge(v) {}
     bool operator==(const ValueEdge& other) const { return edge == other.edge; }
@@ -306,18 +307,16 @@ class StoreBuffer {
     ValueEdge untagged() const {
       return ValueEdge((JS::Value*)(uintptr_t(edge) & ~1));
     }
     bool isTagged() const { return bool(uintptr_t(edge) & 1); }
 
     explicit operator bool() const { return edge != nullptr; }
 
     typedef PointerEdgeHasher<ValueEdge> Hasher;
-
-    static const auto FullBufferReason = JS::GCReason::FULL_VALUE_BUFFER;
   };
 
   struct SlotsEdge {
     // These definitions must match those in HeapSlot::Kind.
     const static int SlotKind = 0;
     const static int ElementKind = 1;
 
     uintptr_t objectAndKind_;  // NativeObject* | Kind
@@ -389,18 +388,16 @@ class StoreBuffer {
 
     typedef struct {
       typedef SlotsEdge Lookup;
       static HashNumber hash(const Lookup& l) {
         return mozilla::HashGeneric(l.objectAndKind_, l.start_, l.count_);
       }
       static bool match(const SlotsEdge& k, const Lookup& l) { return k == l; }
     } Hasher;
-
-    static const auto FullBufferReason = JS::GCReason::FULL_SLOT_BUFFER;
   };
 
   template <typename Buffer, typename Edge>
   void unput(Buffer& buffer, const Edge& edge) {
     MOZ_ASSERT(!JS::RuntimeHeapIsBusy());
     MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime_));
     if (!isEnabled()) {
       return;
--- a/js/src/gc/Zone.cpp
+++ b/js/src/gc/Zone.cpp
@@ -27,27 +27,36 @@
 #include "vm/Realm-inl.h"
 
 using namespace js;
 using namespace js::gc;
 
 Zone* const Zone::NotOnList = reinterpret_cast<Zone*>(1);
 
 ZoneAllocator::ZoneAllocator(JSRuntime* rt)
-    : JS::shadow::Zone(rt, &rt->gc.marker), zoneSize(&rt->gc.heapSize) {
+    : JS::shadow::Zone(rt, &rt->gc.marker),
+      zoneSize(&rt->gc.heapSize),
+      gcMallocBytes(nullptr) {
   AutoLockGC lock(rt);
   threshold.updateAfterGC(8192, GC_NORMAL, rt->gc.tunables,
                           rt->gc.schedulingState, lock);
+  gcMallocThreshold.updateAfterGC(8192, rt->gc.tunables, rt->gc.schedulingState,
+                                  lock);
   setGCMaxMallocBytes(rt->gc.tunables.maxMallocBytes(), lock);
   jitCodeCounter.setMax(jit::MaxCodeBytesPerProcess * 0.8, lock);
 }
 
 ZoneAllocator::~ZoneAllocator() {
-  MOZ_ASSERT_IF(runtimeFromAnyThread()->gc.shutdownCollectedEverything(),
-                gcMallocBytes == 0);
+#ifdef DEBUG
+  if (runtimeFromAnyThread()->gc.shutdownCollectedEverything()) {
+    gcMallocTracker.checkEmptyOnDestroy();
+    MOZ_ASSERT(zoneSize.gcBytes() == 0);
+    MOZ_ASSERT(gcMallocBytes.gcBytes() == 0);
+  }
+#endif
 }
 
 void ZoneAllocator::fixupAfterMovingGC() {
 #ifdef DEBUG
   gcMallocTracker.fixupAfterMovingGC();
 #endif
 }
 
@@ -58,16 +67,24 @@ void js::ZoneAllocator::updateAllGCMallo
 
 void js::ZoneAllocator::updateAllGCMallocCountersOnGCEnd(
     const js::AutoLockGC& lock) {
   auto& gc = runtimeFromAnyThread()->gc;
   gcMallocCounter.updateOnGCEnd(gc.tunables, lock);
   jitCodeCounter.updateOnGCEnd(gc.tunables, lock);
 }
 
+void js::ZoneAllocator::updateAllGCThresholds(GCRuntime& gc,
+                                              const js::AutoLockGC& lock) {
+  threshold.updateAfterGC(zoneSize.gcBytes(), GC_NORMAL, gc.tunables,
+                          gc.schedulingState, lock);
+  gcMallocThreshold.updateAfterGC(gcMallocBytes.gcBytes(), gc.tunables,
+                                  gc.schedulingState, lock);
+}
+
 js::gc::TriggerKind js::ZoneAllocator::shouldTriggerGCForTooMuchMalloc() {
   auto& gc = runtimeFromAnyThread()->gc;
   return std::max(gcMallocCounter.shouldTriggerGC(gc.tunables),
                   jitCodeCounter.shouldTriggerGC(gc.tunables));
 }
 
 JS::Zone::Zone(JSRuntime* rt)
     : ZoneAllocator(rt),
@@ -653,22 +670,17 @@ static const char* MemoryUseName(MemoryU
 #  undef DEFINE_CASE
   }
 
   MOZ_CRASH("Unknown memory use");
 }
 
 MemoryTracker::MemoryTracker() : mutex(mutexid::MemoryTracker) {}
 
-MemoryTracker::~MemoryTracker() {
-  if (!TlsContext.get()->runtime()->gc.shutdownCollectedEverything()) {
-    // Memory leak, suppress crashes.
-    return;
-  }
-
+void MemoryTracker::checkEmptyOnDestroy() {
   bool ok = true;
 
   if (!map.empty()) {
     ok = false;
     fprintf(stderr, "Missing calls to JS::RemoveAssociatedMemory:\n");
     for (auto r = map.all(); !r.empty(); r.popFront()) {
       fprintf(stderr, "  %p 0x%zx %s\n", r.front().key().cell(),
               r.front().value(), MemoryUseName(r.front().key().use()));
--- a/js/src/gc/ZoneAllocator.h
+++ b/js/src/gc/ZoneAllocator.h
@@ -16,17 +16,17 @@
 
 namespace JS {
 class Zone;
 }  // namespace JS
 
 namespace js {
 
 namespace gc {
-void MaybeAllocTriggerZoneGC(JSRuntime* rt, ZoneAllocator* zoneAlloc);
+void MaybeMallocTriggerZoneGC(JSRuntime* rt, ZoneAllocator* zoneAlloc);
 }
 
 // Base class of JS::Zone that provides malloc memory allocation and accounting.
 class ZoneAllocator : public JS::shadow::Zone,
                       public js::MallocProvider<JS::Zone> {
  protected:
   explicit ZoneAllocator(JSRuntime* rt);
   ~ZoneAllocator();
@@ -46,55 +46,51 @@ class ZoneAllocator : public JS::shadow:
   void setGCMaxMallocBytes(size_t value, const js::AutoLockGC& lock) {
     gcMallocCounter.setMax(value, lock);
   }
   void updateMallocCounter(size_t nbytes) {
     updateMemoryCounter(gcMallocCounter, nbytes);
   }
   void adoptMallocBytes(ZoneAllocator* other) {
     gcMallocCounter.adopt(other->gcMallocCounter);
-    gcMallocBytes += other->gcMallocBytes;
-    other->gcMallocBytes = 0;
+    gcMallocBytes.adopt(other->gcMallocBytes);
 #ifdef DEBUG
     gcMallocTracker.adopt(other->gcMallocTracker);
 #endif
   }
   size_t GCMaxMallocBytes() const { return gcMallocCounter.maxBytes(); }
   size_t GCMallocBytes() const { return gcMallocCounter.bytes(); }
 
   void updateJitCodeMallocBytes(size_t nbytes) {
     updateMemoryCounter(jitCodeCounter, nbytes);
   }
 
   void updateAllGCMallocCountersOnGCStart();
   void updateAllGCMallocCountersOnGCEnd(const js::AutoLockGC& lock);
+  void updateAllGCThresholds(gc::GCRuntime& gc, const js::AutoLockGC& lock);
   js::gc::TriggerKind shouldTriggerGCForTooMuchMalloc();
 
   // Memory accounting APIs for malloc memory owned by GC cells.
 
   void addCellMemory(js::gc::Cell* cell, size_t nbytes, js::MemoryUse use) {
     MOZ_ASSERT(cell);
     MOZ_ASSERT(nbytes);
-    mozilla::DebugOnly<size_t> initialBytes(gcMallocBytes);
-    MOZ_ASSERT(initialBytes + nbytes > initialBytes);
+    gcMallocBytes.addBytes(nbytes);
 
-    gcMallocBytes += nbytes;
     // We don't currently check GC triggers here.
 
 #ifdef DEBUG
     gcMallocTracker.trackMemory(cell, nbytes, use);
 #endif
   }
 
   void removeCellMemory(js::gc::Cell* cell, size_t nbytes, js::MemoryUse use) {
     MOZ_ASSERT(cell);
     MOZ_ASSERT(nbytes);
-    MOZ_ASSERT(gcMallocBytes >= nbytes);
-
-    gcMallocBytes -= nbytes;
+    gcMallocBytes.removeBytes(nbytes);
 
 #ifdef DEBUG
     gcMallocTracker.untrackMemory(cell, nbytes, use);
 #endif
   }
 
   void swapCellMemory(js::gc::Cell* a, js::gc::Cell* b, js::MemoryUse use) {
 #ifdef DEBUG
@@ -108,46 +104,39 @@ class ZoneAllocator : public JS::shadow:
   }
   void unregisterPolicy(js::ZoneAllocPolicy* policy) {
     return gcMallocTracker.unregisterPolicy(policy);
   }
 #endif
 
   void incPolicyMemory(js::ZoneAllocPolicy* policy, size_t nbytes) {
     MOZ_ASSERT(nbytes);
-    mozilla::DebugOnly<size_t> initialBytes(gcMallocBytes);
-    MOZ_ASSERT(initialBytes + nbytes > initialBytes);
-
-    gcMallocBytes += nbytes;
+    gcMallocBytes.addBytes(nbytes);
 
 #ifdef DEBUG
     gcMallocTracker.incPolicyMemory(policy, nbytes);
 #endif
 
-    maybeAllocTriggerZoneGC();
+    maybeMallocTriggerZoneGC();
   }
   void decPolicyMemory(js::ZoneAllocPolicy* policy, size_t nbytes) {
     MOZ_ASSERT(nbytes);
-    MOZ_ASSERT(gcMallocBytes >= nbytes);
-
-    gcMallocBytes -= nbytes;
+    gcMallocBytes.removeBytes(nbytes);
 
 #ifdef DEBUG
     gcMallocTracker.decPolicyMemory(policy, nbytes);
 #endif
   }
 
-  size_t totalBytes() const { return zoneSize.gcBytes() + gcMallocBytes; }
-
-  // Check allocation threshold and trigger a zone GC if necessary.
-  void maybeAllocTriggerZoneGC() {
+  // Check malloc allocation threshold and trigger a zone GC if necessary.
+  void maybeMallocTriggerZoneGC() {
     JSRuntime* rt = runtimeFromAnyThread();
-    if (totalBytes() >= threshold.gcTriggerBytes() &&
+    if (gcMallocBytes.gcBytes() >= threshold.gcTriggerBytes() &&
         rt->heapState() == JS::HeapState::Idle) {
-      gc::MaybeAllocTriggerZoneGC(rt, this);
+      gc::MaybeMallocTriggerZoneGC(rt, this);
     }
   }
 
  private:
   void updateMemoryCounter(js::gc::MemoryCounter& counter, size_t nbytes) {
     JSRuntime* rt = runtimeFromAnyThread();
 
     counter.update(nbytes);
@@ -159,38 +148,41 @@ class ZoneAllocator : public JS::shadow:
 
     maybeTriggerGCForTooMuchMalloc(counter, trigger);
   }
 
   void maybeTriggerGCForTooMuchMalloc(js::gc::MemoryCounter& counter,
                                       js::gc::TriggerKind trigger);
 
  public:
-  // Track heap size under this Zone.
+  // Track GC heap size under this Zone.
   js::gc::HeapSize zoneSize;
 
-  // Thresholds used to trigger GC.
+  // Thresholds used to trigger GC based on heap size.
   js::gc::ZoneHeapThreshold threshold;
 
   // Amount of data to allocate before triggering a new incremental slice for
   // the current GC.
   js::MainThreadData<size_t> gcDelayBytes;
 
  private:
   // Malloc counter to measure memory pressure for GC scheduling. This counter
   // is used for allocations where the size of the allocation is not known on
   // free. Currently this is used for all internal malloc allocations.
   js::gc::MemoryCounter gcMallocCounter;
 
+ public:
   // Malloc counter used for allocations where size information is
   // available. Used for some internal and all tracked external allocations.
-  mozilla::Atomic<size_t, mozilla::Relaxed,
-                  mozilla::recordreplay::Behavior::DontPreserve>
-      gcMallocBytes;
+  js::gc::HeapSize gcMallocBytes;
 
+  // Thresholds used to trigger GC based on malloc allocations.
+  js::gc::ZoneMallocThreshold gcMallocThreshold;
+
+ private:
 #ifdef DEBUG
   // In debug builds, malloc allocations can be tracked to make debugging easier
   // (possible?) if allocation and free sizes don't balance.
   js::gc::MemoryTracker gcMallocTracker;
 #endif
 
   // Counter of JIT code executable memory for GC scheduling. Also imprecise,
   // since wasm can generate code that outlives a zone.
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -1167,17 +1167,17 @@ JS_PUBLIC_API void JS::AddAssociatedMemo
                                            JS::MemoryUse use) {
   MOZ_ASSERT(obj);
   if (!nbytes) {
     return;
   }
 
   Zone* zone = obj->zone();
   zone->addCellMemory(obj, nbytes, js::MemoryUse(use));
-  zone->maybeAllocTriggerZoneGC();
+  zone->maybeMallocTriggerZoneGC();
 }
 
 JS_PUBLIC_API void JS::RemoveAssociatedMemory(JSObject* obj, size_t nbytes,
                                               JS::MemoryUse use) {
   MOZ_ASSERT(obj);
   if (!nbytes) {
     return;
   }
--- a/js/src/util/Unicode.h
+++ b/js/src/util/Unicode.h
@@ -223,26 +223,30 @@ inline bool IsSpace(char16_t ch) {
 
   if (ch == NO_BREAK_SPACE) {
     return true;
   }
 
   return CharInfo(ch).isSpace();
 }
 
-inline bool IsSpaceOrBOM2(char16_t ch) {
+inline bool IsSpaceOrBOM2(char32_t ch) {
   if (ch < 128) {
     return js_isspace[ch];
   }
 
   /* We accept BOM2 (0xFFFE) for compatibility reasons in the parser. */
   if (ch == NO_BREAK_SPACE || ch == BYTE_ORDER_MARK2) {
     return true;
   }
 
+  if (ch >= NonBMPMin) {
+    return false;
+  }
+
   return CharInfo(ch).isSpace();
 }
 
 /*
  * Returns the simple upper case mapping (possibly the identity mapping; see
  * ChangesWhenUpperCasedSpecialCasing for details) of the given UTF-16 code
  * unit.
  */
--- a/js/src/vm/JSScript.cpp
+++ b/js/src/vm/JSScript.cpp
@@ -1645,33 +1645,30 @@ class ScriptSource::LoadSourceMatcher {
   JSContext* const cx_;
   ScriptSource* const ss_;
   bool* const loaded_;
 
  public:
   explicit LoadSourceMatcher(JSContext* cx, ScriptSource* ss, bool* loaded)
       : cx_(cx), ss_(ss), loaded_(loaded) {}
 
-  template <typename Unit>
-  bool operator()(const Compressed<Unit>&) const {
+  template <typename Unit, SourceRetrievable CanRetrieve>
+  bool operator()(const Compressed<Unit, CanRetrieve>&) const {
     *loaded_ = true;
     return true;
   }
 
-  template <typename Unit>
-  bool operator()(const Uncompressed<Unit>&) const {
+  template <typename Unit, SourceRetrievable CanRetrieve>
+  bool operator()(const Uncompressed<Unit, CanRetrieve>&) const {
     *loaded_ = true;
     return true;
   }
 
   template <typename Unit>
   bool operator()(const Retrievable<Unit>&) {
-    MOZ_ASSERT(ss_->sourceRetrievable(),
-               "should be retrievable if Retrievable");
-
     if (!cx_->runtime()->sourceHook.ref()) {
       *loaded_ = false;
       return true;
     }
 
     size_t length;
 
     // The first argument is just for overloading -- its value doesn't matter.
@@ -1680,25 +1677,21 @@ class ScriptSource::LoadSourceMatcher {
     }
 
     cx_->updateMallocCounter(length * sizeof(Unit));
 
     return true;
   }
 
   bool operator()(const Missing&) const {
-    MOZ_ASSERT(!ss_->sourceRetrievable(),
-               "should have Retrievable<Unit> source, not Missing source, if "
-               "retrievable");
     *loaded_ = false;
     return true;
   }
 
   bool operator()(const BinAST&) const {
-    MOZ_ASSERT(!ss_->sourceRetrievable(), "binast source is never retrievable");
     *loaded_ = false;
     return true;
   }
 
  private:
   bool tryLoadAndSetSource(const Utf8Unit&, size_t* length) const {
     char* utf8Source;
     if (!cx_->runtime()->sourceHook->load(cx_, ss_->filename(), nullptr,
@@ -1772,17 +1765,17 @@ void UncompressedSourceCache::releaseEnt
   MOZ_ASSERT(holder_ == &holder);
   holder_ = nullptr;
 }
 
 template <typename Unit>
 const Unit* UncompressedSourceCache::lookup(const ScriptSourceChunk& ssc,
                                             AutoHoldEntry& holder) {
   MOZ_ASSERT(!holder_);
-  MOZ_ASSERT(ssc.ss->compressedSourceIs<Unit>());
+  MOZ_ASSERT(ssc.ss->isCompressed<Unit>());
 
   if (!map_) {
     return nullptr;
   }
 
   if (Map::Ptr p = map_->lookup(ssc)) {
     holdEntry(holder, ssc);
     return static_cast<const Unit*>(p->value().get());
@@ -1836,17 +1829,17 @@ size_t UncompressedSourceCache::sizeOfEx
   }
   return n;
 }
 
 template <typename Unit>
 const Unit* ScriptSource::chunkUnits(
     JSContext* cx, UncompressedSourceCache::AutoHoldEntry& holder,
     size_t chunk) {
-  const Compressed<Unit>& c = data.as<Compressed<Unit>>();
+  const CompressedData<Unit>& c = *compressedData<Unit>();
 
   ScriptSourceChunk ssc(this, chunk);
   if (const Unit* decompressed =
           cx->caches().uncompressedSourceCache.lookup<Unit>(ssc, holder)) {
     return decompressed;
   }
 
   size_t totalLengthInBytes = length() * sizeof(Unit);
@@ -1874,66 +1867,81 @@ const Unit* ScriptSource::chunkUnits(
           ssc, ToSourceData(std::move(decompressed)), holder)) {
     JS_ReportOutOfMemory(cx);
     return nullptr;
   }
   return ret;
 }
 
 template <typename Unit>
-void ScriptSource::movePendingCompressedSource() {
+void ScriptSource::convertToCompressedSource(SharedImmutableString compressed,
+                                             size_t uncompressedLength) {
+  MOZ_ASSERT(isUncompressed<Unit>());
+  MOZ_ASSERT(uncompressedData<Unit>()->length() == uncompressedLength);
+
+  if (data.is<Uncompressed<Unit, SourceRetrievable::Yes>>()) {
+    data = SourceType(Compressed<Unit, SourceRetrievable::Yes>(
+        std::move(compressed), uncompressedLength));
+  } else {
+    data = SourceType(Compressed<Unit, SourceRetrievable::No>(
+        std::move(compressed), uncompressedLength));
+  }
+}
+
+template <typename Unit>
+void ScriptSource::performDelayedConvertToCompressedSource() {
+  // There might not be a conversion to compressed source happening at all.
   if (pendingCompressed_.empty()) {
     return;
   }
 
-  Compressed<Unit>& pending = pendingCompressed_.ref<Compressed<Unit>>();
-
-  MOZ_ASSERT(!hasCompressedSource());
-  MOZ_ASSERT_IF(hasUncompressedSource(),
-                pending.uncompressedLength == length());
-
-  data = SourceType(std::move(pending));
+  CompressedData<Unit>& pending =
+      pendingCompressed_.ref<CompressedData<Unit>>();
+
+  convertToCompressedSource<Unit>(std::move(pending.raw),
+                                  pending.uncompressedLength);
+
   pendingCompressed_.destroy();
 }
 
 template <typename Unit>
 ScriptSource::PinnedUnits<Unit>::~PinnedUnits() {
   if (units_) {
     MOZ_ASSERT(*stack_ == this);
     *stack_ = prev_;
     if (!prev_) {
-      source_->movePendingCompressedSource<Unit>();
+      source_->performDelayedConvertToCompressedSource<Unit>();
     }
   }
 }
 
 template <typename Unit>
 const Unit* ScriptSource::units(JSContext* cx,
                                 UncompressedSourceCache::AutoHoldEntry& holder,
                                 size_t begin, size_t len) {
   MOZ_ASSERT(begin <= length());
   MOZ_ASSERT(begin + len <= length());
 
-  if (data.is<Uncompressed<Unit>>()) {
-    const Unit* units = data.as<Uncompressed<Unit>>().units();
+  if (isUncompressed<Unit>()) {
+    const Unit* units = uncompressedData<Unit>()->units();
     if (!units) {
       return nullptr;
     }
     return units + begin;
   }
 
   if (data.is<Missing>()) {
     MOZ_CRASH("ScriptSource::units() on ScriptSource with missing source");
   }
 
   if (data.is<Retrievable<Unit>>()) {
     MOZ_CRASH("ScriptSource::units() on ScriptSource with retrievable source");
   }
 
-  MOZ_ASSERT(data.is<Compressed<Unit>>());
+  MOZ_ASSERT(isCompressed<Unit>());
 
   // Determine first/last chunks, the offset (in bytes) into the first chunk
   // of the requested units, and the number of bytes in the last chunk.
   //
   // Note that first and last chunk sizes are miscomputed and *must not be
   // used* when the first chunk is the last chunk.
   size_t firstChunk, firstChunkOffset, firstChunkSize;
   size_t lastChunk, lastChunkSize;
@@ -2125,39 +2133,46 @@ JSFlatString* ScriptSource::functionBody
   size_t start =
       parameterListEnd_ + (sizeof(FunctionConstructorMedialSigils) - 1);
   size_t stop = length() - (sizeof(FunctionConstructorFinalBrace) - 1);
   return substring(cx, start, stop);
 }
 
 template <typename Unit>
 MOZ_MUST_USE bool ScriptSource::setUncompressedSourceHelper(
-    JSContext* cx, EntryUnits<Unit>&& source, size_t length) {
+    JSContext* cx, EntryUnits<Unit>&& source, size_t length,
+    SourceRetrievable retrievable) {
   auto& cache = cx->zone()->runtimeFromAnyThread()->sharedImmutableStrings();
 
   auto uniqueChars = SourceTypeTraits<Unit>::toCacheable(std::move(source));
   auto deduped = cache.getOrCreate(std::move(uniqueChars), length);
   if (!deduped) {
     ReportOutOfMemory(cx);
     return false;
   }
 
-  data = SourceType(Uncompressed<Unit>(std::move(*deduped)));
+  if (retrievable == SourceRetrievable::Yes) {
+    data = SourceType(
+        Uncompressed<Unit, SourceRetrievable::Yes>(std::move(*deduped)));
+  } else {
+    data = SourceType(
+        Uncompressed<Unit, SourceRetrievable::No>(std::move(*deduped)));
+  }
   return true;
 }
 
 template <typename Unit>
 MOZ_MUST_USE bool ScriptSource::setRetrievedSource(JSContext* cx,
                                                    EntryUnits<Unit>&& source,
                                                    size_t length) {
-  MOZ_ASSERT(sourceRetrievable_);
   MOZ_ASSERT(data.is<Retrievable<Unit>>(),
              "retrieved source can only overwrite the corresponding "
              "retrievable source");
-  return setUncompressedSourceHelper(cx, std::move(source), length);
+  return setUncompressedSourceHelper(cx, std::move(source), length,
+                                     SourceRetrievable::Yes);
 }
 
 #if defined(JS_BUILD_BINAST)
 
 MOZ_MUST_USE bool ScriptSource::setBinASTSourceCopy(JSContext* cx,
                                                     const uint8_t* buf,
                                                     size_t len) {
   MOZ_ASSERT(data.is<Missing>());
@@ -2221,69 +2236,76 @@ bool ScriptSource::tryCompressOffThread(
   if (!task) {
     ReportOutOfMemory(cx);
     return false;
   }
   return EnqueueOffThreadCompression(cx, std::move(task));
 }
 
 template <typename Unit>
-void ScriptSource::convertToCompressedSource(SharedImmutableString compressed,
-                                             size_t uncompressedLength) {
-  MOZ_ASSERT(data.is<Uncompressed<Unit>>(),
-             "should only be converting uncompressed source to compressed "
-             "source identically encoded");
-  MOZ_ASSERT(length() == uncompressedLength);
-
-  if (pinnedUnitsStack_) {
-    MOZ_ASSERT(pendingCompressed_.empty());
-    pendingCompressed_.construct<Compressed<Unit>>(std::move(compressed),
-                                                   uncompressedLength);
-  } else {
-    data =
-        SourceType(Compressed<Unit>(std::move(compressed), uncompressedLength));
-  }
+void ScriptSource::triggerConvertToCompressedSource(
+    SharedImmutableString compressed, size_t uncompressedLength) {
+  MOZ_ASSERT(isUncompressed<Unit>(),
+             "should only be triggering compressed source installation to "
+             "overwrite identically-encoded uncompressed source");
+  MOZ_ASSERT(uncompressedData<Unit>()->length() == uncompressedLength);
+
+  // If units aren't pinned -- and they probably won't be, we'd have to have a
+  // GC in the small window of time where a |PinnedUnits| was live -- then we
+  // can immediately convert.
+  if (MOZ_LIKELY(!pinnedUnitsStack_)) {
+    convertToCompressedSource<Unit>(std::move(compressed), uncompressedLength);
+    return;
+  }
+
+  // Otherwise, set aside the compressed-data info.  The conversion is performed
+  // when the last |PinnedUnits| dies.
+  MOZ_ASSERT(pendingCompressed_.empty(),
+             "shouldn't be multiple conversions happening");
+  pendingCompressed_.construct<CompressedData<Unit>>(std::move(compressed),
+                                                     uncompressedLength);
 }
 
 template <typename Unit>
-MOZ_MUST_USE bool ScriptSource::initializeWithCompressedSource(
+MOZ_MUST_USE bool ScriptSource::initializeWithUnretrievableCompressedSource(
     JSContext* cx, UniqueChars&& compressed, size_t rawLength,
     size_t sourceLength) {
   MOZ_ASSERT(data.is<Missing>(), "shouldn't be double-initializing");
   MOZ_ASSERT(compressed != nullptr);
 
   auto& cache = cx->zone()->runtimeFromAnyThread()->sharedImmutableStrings();
   auto deduped = cache.getOrCreate(std::move(compressed), rawLength);
   if (!deduped) {
     ReportOutOfMemory(cx);
     return false;
   }
 
   MOZ_ASSERT(pinnedUnitsStack_ == nullptr,
              "shouldn't be initializing a ScriptSource while its characters "
              "are pinned -- that only makes sense with a ScriptSource actively "
              "being inspected");
-  data = SourceType(Compressed<Unit>(std::move(*deduped), sourceLength));
+
+  data = SourceType(Compressed<Unit, SourceRetrievable::No>(std::move(*deduped),
+                                                            sourceLength));
 
   return true;
 }
 
 template <typename Unit>
 bool ScriptSource::assignSource(JSContext* cx,
                                 const ReadOnlyCompileOptions& options,
                                 SourceText<Unit>& srcBuf) {
   MOZ_ASSERT(data.is<Missing>(),
              "source assignment should only occur on fresh ScriptSources");
 
   if (cx->realm()->behaviors().discardSource()) {
     return true;
   }
 
   if (options.sourceIsLazy) {
-    sourceRetrievable_ = true;
     data = SourceType(Retrievable<Unit>());
     return true;
   }
 
   JSRuntime* runtime = cx->zone()->runtimeFromAnyThread();
   auto& cache = runtime->sharedImmutableStrings();
   auto deduped = cache.getOrCreate(srcBuf.get(), srcBuf.length(), [&srcBuf]() {
     using CharT = typename SourceTypeTraits<Unit>::CharT;
@@ -2291,17 +2313,18 @@ bool ScriptSource::assignSource(JSContex
                ? UniquePtr<CharT[], JS::FreePolicy>(srcBuf.takeChars())
                : DuplicateString(srcBuf.get(), srcBuf.length());
   });
   if (!deduped) {
     ReportOutOfMemory(cx);
     return false;
   }
 
-  data = SourceType(Uncompressed<Unit>(std::move(*deduped)));
+  data = SourceType(
+      Uncompressed<Unit, SourceRetrievable::No>(std::move(*deduped)));
   return true;
 }
 
 template bool ScriptSource::assignSource(JSContext* cx,
                                          const ReadOnlyCompileOptions& options,
                                          SourceText<char16_t>& srcBuf);
 template bool ScriptSource::assignSource(JSContext* cx,
                                          const ReadOnlyCompileOptions& options,
@@ -2329,29 +2352,28 @@ static MOZ_MUST_USE bool reallocUniquePt
   mozilla::Unused << unique.release();
   unique.reset(newPtr);
   return true;
 }
 
 template <typename Unit>
 void SourceCompressionTask::workEncodingSpecific() {
   ScriptSource* source = sourceHolder_.get();
-  MOZ_ASSERT(source->data.is<ScriptSource::Uncompressed<Unit>>());
+  MOZ_ASSERT(source->isUncompressed<Unit>());
 
   // Try to keep the maximum memory usage down by only allocating half the
   // size of the string, first.
   size_t inputBytes = source->length() * sizeof(Unit);
   size_t firstSize = inputBytes / 2;
   UniqueChars compressed(js_pod_malloc<char>(firstSize));
   if (!compressed) {
     return;
   }
 
-  const Unit* chars =
-      source->data.as<ScriptSource::Uncompressed<Unit>>().units();
+  const Unit* chars = source->uncompressedData<Unit>()->units();
   Compressor comp(reinterpret_cast<const unsigned char*>(chars), inputBytes);
   if (!comp.init()) {
     return;
   }
 
   comp.setOutput(reinterpret_cast<unsigned char*>(compressed.get()), firstSize);
   bool cont = true;
   bool reallocated = false;
@@ -2405,18 +2427,18 @@ void SourceCompressionTask::workEncoding
   resultString_ = strings.getOrCreate(std::move(compressed), totalBytes);
 }
 
 struct SourceCompressionTask::PerformTaskWork {
   SourceCompressionTask* const task_;
 
   explicit PerformTaskWork(SourceCompressionTask* task) : task_(task) {}
 
-  template <typename Unit>
-  void operator()(const ScriptSource::Uncompressed<Unit>&) {
+  template <typename Unit, SourceRetrievable CanRetrieve>
+  void operator()(const ScriptSource::Uncompressed<Unit, CanRetrieve>&) {
     task_->workEncodingSpecific<Unit>();
   }
 
   template <typename T>
   void operator()(const T&) {
     MOZ_CRASH(
         "why are we compressing missing, missing-but-retrievable, "
         "already-compressed, or BinAST source?");
@@ -2434,25 +2456,25 @@ void SourceCompressionTask::runTask() {
   }
 
   ScriptSource* source = sourceHolder_.get();
   MOZ_ASSERT(source->hasUncompressedSource());
 
   source->performTaskWork(this);
 }
 
-void ScriptSource::convertToCompressedSourceFromTask(
+void ScriptSource::triggerConvertToCompressedSourceFromTask(
     SharedImmutableString compressed) {
-  data.match(ConvertToCompressedSourceFromTask(this, compressed));
+  data.match(TriggerConvertToCompressedSourceFromTask(this, compressed));
 }
 
 void SourceCompressionTask::complete() {
   if (!shouldCancel() && resultString_.isSome()) {
     ScriptSource* source = sourceHolder_.get();
-    source->convertToCompressedSourceFromTask(std::move(*resultString_));
+    source->triggerConvertToCompressedSourceFromTask(std::move(*resultString_));
   }
 }
 
 void ScriptSource::addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf,
                                           JS::ScriptSourceInfo* info) const {
   info->misc += mallocSizeOf(this) + mallocSizeOf(filename_.get()) +
                 mallocSizeOf(introducerFilename_.get());
   info->numScripts++;
@@ -2519,203 +2541,201 @@ bool ScriptSource::xdrFinalizeEncoder(JS
 
   auto cleanup = mozilla::MakeScopeExit([&] { xdrEncoder_.reset(nullptr); });
 
   XDRResult res = xdrEncoder_->linearize(buffer);
   return res.isOk();
 }
 
 template <typename Unit>
-MOZ_MUST_USE bool ScriptSource::initializeUncompressedSource(
+MOZ_MUST_USE bool ScriptSource::initializeUnretrievableUncompressedSource(
     JSContext* cx, EntryUnits<Unit>&& source, size_t length) {
   MOZ_ASSERT(data.is<Missing>(), "must be initializing a fresh ScriptSource");
-  return setUncompressedSourceHelper(cx, std::move(source), length);
+  return setUncompressedSourceHelper(cx, std::move(source), length,
+                                     SourceRetrievable::No);
 }
 
 template <typename Unit>
-struct SourceDecoder {
+struct UnretrievableSourceDecoder {
   XDRState<XDR_DECODE>* const xdr_;
   ScriptSource* const scriptSource_;
   const uint32_t uncompressedLength_;
 
  public:
-  SourceDecoder(XDRState<XDR_DECODE>* xdr, ScriptSource* scriptSource,
-                uint32_t uncompressedLength)
+  UnretrievableSourceDecoder(XDRState<XDR_DECODE>* xdr,
+                             ScriptSource* scriptSource,
+                             uint32_t uncompressedLength)
       : xdr_(xdr),
         scriptSource_(scriptSource),
         uncompressedLength_(uncompressedLength) {}
 
   XDRResult decode() {
     auto sourceUnits =
         xdr_->cx()->make_pod_array<Unit>(Max<size_t>(uncompressedLength_, 1));
     if (!sourceUnits) {
       return xdr_->fail(JS::TranscodeResult_Throw);
     }
 
     MOZ_TRY(xdr_->codeChars(sourceUnits.get(), uncompressedLength_));
 
-    if (!scriptSource_->initializeUncompressedSource(
+    if (!scriptSource_->initializeUnretrievableUncompressedSource(
             xdr_->cx(), std::move(sourceUnits), uncompressedLength_)) {
       return xdr_->fail(JS::TranscodeResult_Throw);
     }
 
     return Ok();
   }
 };
 
 namespace js {
 
 template <>
-XDRResult ScriptSource::xdrUncompressedSource<XDR_DECODE>(
+XDRResult ScriptSource::xdrUnretrievableUncompressedSource<XDR_DECODE>(
     XDRState<XDR_DECODE>* xdr, uint8_t sourceCharSize,
     uint32_t uncompressedLength) {
   MOZ_ASSERT(sourceCharSize == 1 || sourceCharSize == 2);
 
   if (sourceCharSize == 1) {
-    SourceDecoder<Utf8Unit> decoder(xdr, this, uncompressedLength);
+    UnretrievableSourceDecoder<Utf8Unit> decoder(xdr, this, uncompressedLength);
     return decoder.decode();
   }
 
-  SourceDecoder<char16_t> decoder(xdr, this, uncompressedLength);
+  UnretrievableSourceDecoder<char16_t> decoder(xdr, this, uncompressedLength);
   return decoder.decode();
 }
 
 }  // namespace js
 
 template <typename Unit>
-struct SourceEncoder {
+struct UnretrievableSourceEncoder {
   XDRState<XDR_ENCODE>* const xdr_;
   ScriptSource* const source_;
   const uint32_t uncompressedLength_;
 
-  SourceEncoder(XDRState<XDR_ENCODE>* xdr, ScriptSource* source,
-                uint32_t uncompressedLength)
+  UnretrievableSourceEncoder(XDRState<XDR_ENCODE>* xdr, ScriptSource* source,
+                             uint32_t uncompressedLength)
       : xdr_(xdr), source_(source), uncompressedLength_(uncompressedLength) {}
 
   XDRResult encode() {
-    Unit* sourceUnits = const_cast<Unit*>(source_->uncompressedData<Unit>());
+    Unit* sourceUnits =
+        const_cast<Unit*>(source_->uncompressedData<Unit>()->units());
 
     return xdr_->codeChars(sourceUnits, uncompressedLength_);
   }
 };
 
 namespace js {
 
 template <>
-XDRResult ScriptSource::xdrUncompressedSource<XDR_ENCODE>(
+XDRResult ScriptSource::xdrUnretrievableUncompressedSource<XDR_ENCODE>(
     XDRState<XDR_ENCODE>* xdr, uint8_t sourceCharSize,
     uint32_t uncompressedLength) {
   MOZ_ASSERT(sourceCharSize == 1 || sourceCharSize == 2);
 
   if (sourceCharSize == 1) {
-    SourceEncoder<Utf8Unit> encoder(xdr, this, uncompressedLength);
+    UnretrievableSourceEncoder<Utf8Unit> encoder(xdr, this, uncompressedLength);
     return encoder.encode();
   }
 
-  SourceEncoder<char16_t> encoder(xdr, this, uncompressedLength);
+  UnretrievableSourceEncoder<char16_t> encoder(xdr, this, uncompressedLength);
   return encoder.encode();
 }
 
 }  // namespace js
 
 template <typename Unit, XDRMode mode>
 /* static */
 XDRResult ScriptSource::codeUncompressedData(XDRState<mode>* const xdr,
-                                             ScriptSource* const ss,
-                                             bool retrievable) {
+                                             ScriptSource* const ss) {
   static_assert(std::is_same<Unit, Utf8Unit>::value ||
                     std::is_same<Unit, char16_t>::value,
                 "should handle UTF-8 and UTF-16");
 
   if (mode == XDR_ENCODE) {
-    MOZ_ASSERT(ss->data.is<Uncompressed<Unit>>());
+    MOZ_ASSERT(ss->isUncompressed<Unit>());
   } else {
     MOZ_ASSERT(ss->data.is<Missing>());
   }
 
-  MOZ_ASSERT(retrievable == ss->sourceRetrievable());
-
-  if (retrievable) {
-    // It's unnecessary to code uncompressed data if it can just be retrieved
-    // using the source hook.
-    if (mode == XDR_DECODE) {
-      ss->data = SourceType(Retrievable<Unit>());
-    }
-    return Ok();
-  }
-
   uint32_t uncompressedLength;
   if (mode == XDR_ENCODE) {
-    uncompressedLength = ss->data.as<Uncompressed<Unit>>().length();
+    uncompressedLength = ss->uncompressedData<Unit>()->length();
   }
   MOZ_TRY(xdr->codeUint32(&uncompressedLength));
 
-  return ss->xdrUncompressedSource(xdr, sizeof(Unit), uncompressedLength);
+  return ss->xdrUnretrievableUncompressedSource(xdr, sizeof(Unit),
+                                                uncompressedLength);
 }
 
 template <typename Unit, XDRMode mode>
 /* static */
 XDRResult ScriptSource::codeCompressedData(XDRState<mode>* const xdr,
-                                           ScriptSource* const ss,
-                                           bool retrievable) {
+                                           ScriptSource* const ss) {
   static_assert(std::is_same<Unit, Utf8Unit>::value ||
                     std::is_same<Unit, char16_t>::value,
                 "should handle UTF-8 and UTF-16");
 
   if (mode == XDR_ENCODE) {
-    MOZ_ASSERT(ss->data.is<Compressed<Unit>>());
+    MOZ_ASSERT(ss->isCompressed<Unit>());
   } else {
     MOZ_ASSERT(ss->data.is<Missing>());
   }
 
-  MOZ_ASSERT(retrievable == ss->sourceRetrievable());
-
-  if (retrievable) {
-    // It's unnecessary to code compressed data if it can just be retrieved
-    // using the source hook.
-    if (mode == XDR_DECODE) {
-      ss->data = SourceType(Retrievable<Unit>());
-    }
-    return Ok();
-  }
-
   uint32_t uncompressedLength;
   if (mode == XDR_ENCODE) {
-    uncompressedLength = ss->data.as<Compressed<Unit>>().uncompressedLength;
+    uncompressedLength = ss->data.as<Compressed<Unit, SourceRetrievable::No>>()
+                             .uncompressedLength;
   }
   MOZ_TRY(xdr->codeUint32(&uncompressedLength));
 
   uint32_t compressedLength;
   if (mode == XDR_ENCODE) {
-    compressedLength = ss->data.as<Compressed<Unit>>().raw.length();
+    compressedLength =
+        ss->data.as<Compressed<Unit, SourceRetrievable::No>>().raw.length();
   }
   MOZ_TRY(xdr->codeUint32(&compressedLength));
 
   if (mode == XDR_DECODE) {
     // Compressed data is always single-byte chars.
     auto bytes = xdr->cx()->template make_pod_array<char>(compressedLength);
     if (!bytes) {
       return xdr->fail(JS::TranscodeResult_Throw);
     }
     MOZ_TRY(xdr->codeBytes(bytes.get(), compressedLength));
 
-    if (!ss->initializeWithCompressedSource<Unit>(xdr->cx(), std::move(bytes),
-                                                  compressedLength,
-                                                  uncompressedLength)) {
+    if (!ss->initializeWithUnretrievableCompressedSource<Unit>(
+            xdr->cx(), std::move(bytes), compressedLength,
+            uncompressedLength)) {
       return xdr->fail(JS::TranscodeResult_Throw);
     }
   } else {
-    void* bytes =
-        const_cast<char*>(ss->data.as<Compressed<Unit>>().raw.chars());
+    void* bytes = const_cast<char*>(ss->compressedData<Unit>()->raw.chars());
     MOZ_TRY(xdr->codeBytes(bytes, compressedLength));
   }
 
   return Ok();
 }
 
+template <typename Unit,
+          template <typename U, SourceRetrievable CanRetrieve> class Data,
+          XDRMode mode>
+/* static */
+void ScriptSource::codeRetrievable(ScriptSource* const ss) {
+  static_assert(std::is_same<Unit, Utf8Unit>::value ||
+                    std::is_same<Unit, char16_t>::value,
+                "should handle UTF-8 and UTF-16");
+
+  if (mode == XDR_ENCODE) {
+    MOZ_ASSERT((ss->data.is<Data<Unit, SourceRetrievable::Yes>>()));
+  } else {
+    MOZ_ASSERT(ss->data.is<Missing>());
+    ss->data = SourceType(Retrievable<Unit>());
+  }
+}
+
 template <XDRMode mode>
 /* static */
 XDRResult ScriptSource::codeBinASTData(XDRState<mode>* const xdr,
                                        ScriptSource* const ss) {
 #if !defined(JS_BUILD_BINAST)
   return xdr->fail(JS::TranscodeResult_Throw);
 #else
   if (mode == XDR_ENCODE) {
@@ -2875,61 +2895,69 @@ void ScriptSource::codeRetrievableData(S
     ss->data = SourceType(Retrievable<Unit>());
   }
 }
 
 template <XDRMode mode>
 /* static */
 XDRResult ScriptSource::xdrData(XDRState<mode>* const xdr,
                                 ScriptSource* const ss) {
-  // Retrievability is kept outside |ScriptSource::data| (and not solely as
-  // distinct variant types within it) because retrievable compressed or
-  // uncompressed data need not be XDR'd.
-  uint8_t retrievable;
-  if (mode == XDR_ENCODE) {
-    retrievable = ss->sourceRetrievable_;
-  }
-  MOZ_TRY(xdr->codeUint8(&retrievable));
-  if (mode == XDR_DECODE) {
-    ss->sourceRetrievable_ = retrievable != 0;
-  }
-
   // The order here corresponds to the type order in |ScriptSource::SourceType|
-  // for simplicity, but it isn't truly necessary that it do so.
+  // so number->internal Variant tag is a no-op.
   enum class DataType {
-    CompressedUtf8,
-    UncompressedUtf8,
-    CompressedUtf16,
-    UncompressedUtf16,
+    CompressedUtf8Retrievable,
+    UncompressedUtf8Retrievable,
+    CompressedUtf8NotRetrievable,
+    UncompressedUtf8NotRetrievable,
+    CompressedUtf16Retrievable,
+    UncompressedUtf16Retrievable,
+    CompressedUtf16NotRetrievable,
+    UncompressedUtf16NotRetrievable,
     RetrievableUtf8,
     RetrievableUtf16,
     Missing,
     BinAST,
   };
 
   DataType tag;
   {
     // This is terrible, but we can't do better.  When |mode == XDR_DECODE| we
     // don't have a |ScriptSource::data| |Variant| to match -- the entire XDR
     // idiom for tagged unions depends on coding a tag-number, then the
     // corresponding tagged data.  So we must manually define a tag-enum, code
     // it, then switch on it (and ignore the |Variant::match| API).
     class XDRDataTag {
      public:
-      DataType operator()(const Compressed<Utf8Unit>&) {
-        return DataType::CompressedUtf8;
+      DataType operator()(const Compressed<Utf8Unit, SourceRetrievable::Yes>&) {
+        return DataType::CompressedUtf8Retrievable;
+      }
+      DataType operator()(
+          const Uncompressed<Utf8Unit, SourceRetrievable::Yes>&) {
+        return DataType::UncompressedUtf8Retrievable;
       }
-      DataType operator()(const Uncompressed<Utf8Unit>&) {
-        return DataType::UncompressedUtf8;
+      DataType operator()(const Compressed<Utf8Unit, SourceRetrievable::No>&) {
+        return DataType::CompressedUtf8NotRetrievable;
+      }
+      DataType operator()(
+          const Uncompressed<Utf8Unit, SourceRetrievable::No>&) {
+        return DataType::UncompressedUtf8NotRetrievable;
       }
-      DataType operator()(const Compressed<char16_t>&) {
-        return DataType::CompressedUtf16;
+      DataType operator()(const Compressed<char16_t, SourceRetrievable::Yes>&) {
+        return DataType::CompressedUtf16Retrievable;
+      }
+      DataType operator()(
+          const Uncompressed<char16_t, SourceRetrievable::Yes>&) {
+        return DataType::UncompressedUtf16Retrievable;
       }
-      DataType operator()(const Uncompressed<char16_t>&) {
-        return DataType::UncompressedUtf16;
+      DataType operator()(const Compressed<char16_t, SourceRetrievable::No>&) {
+        return DataType::CompressedUtf16NotRetrievable;
+      }
+      DataType operator()(
+          const Uncompressed<char16_t, SourceRetrievable::No>&) {
+        return DataType::UncompressedUtf16NotRetrievable;
       }
       DataType operator()(const Retrievable<Utf8Unit>&) {
         return DataType::RetrievableUtf8;
       }
       DataType operator()(const Retrievable<char16_t>&) {
         return DataType::RetrievableUtf16;
       }
       DataType operator()(const Missing&) { return DataType::Missing; }
@@ -2947,43 +2975,59 @@ XDRResult ScriptSource::xdrData(XDRState
       MOZ_ASSERT_UNREACHABLE("bad tag");
       return xdr->fail(JS::TranscodeResult_Failure_BadDecode);
     }
 
     tag = static_cast<DataType>(type);
   }
 
   switch (tag) {
-    case DataType::CompressedUtf8:
-      return ScriptSource::codeCompressedData<Utf8Unit>(xdr, ss, retrievable);
-
-    case DataType::UncompressedUtf8:
-      return ScriptSource::codeUncompressedData<Utf8Unit>(xdr, ss, retrievable);
-
-    case DataType::CompressedUtf16:
-      return ScriptSource::codeCompressedData<char16_t>(xdr, ss, retrievable);
-
-    case DataType::UncompressedUtf16:
-      return ScriptSource::codeUncompressedData<char16_t>(xdr, ss, retrievable);
+    case DataType::CompressedUtf8Retrievable:
+      ScriptSource::codeRetrievable<Utf8Unit, Compressed, mode>(ss);
+      return Ok();
+
+    case DataType::CompressedUtf8NotRetrievable:
+      return ScriptSource::codeCompressedData<Utf8Unit>(xdr, ss);
+
+    case DataType::UncompressedUtf8Retrievable:
+      ScriptSource::codeRetrievable<Utf8Unit, Uncompressed, mode>(ss);
+      return Ok();
+
+    case DataType::UncompressedUtf8NotRetrievable:
+      return ScriptSource::codeUncompressedData<Utf8Unit>(xdr, ss);
+
+    case DataType::CompressedUtf16Retrievable:
+      ScriptSource::codeRetrievable<char16_t, Compressed, mode>(ss);
+      return Ok();
+
+    case DataType::CompressedUtf16NotRetrievable:
+      return ScriptSource::codeCompressedData<char16_t>(xdr, ss);
+
+    case DataType::UncompressedUtf16Retrievable:
+      ScriptSource::codeRetrievable<char16_t, Uncompressed, mode>(ss);
+      return Ok();
+
+    case DataType::UncompressedUtf16NotRetrievable:
+      return ScriptSource::codeUncompressedData<char16_t>(xdr, ss);
 
     case DataType::Missing: {
       MOZ_ASSERT(ss->data.is<Missing>(),
                  "ScriptSource::data is initialized as missing, so neither "
                  "encoding nor decoding has to change anything");
 
       // There's no data to XDR for missing source.
       break;
     }
 
     case DataType::RetrievableUtf8:
-      codeRetrievableData<Utf8Unit, mode>(ss);
+      ScriptSource::codeRetrievableData<Utf8Unit, mode>(ss);
       return Ok();
 
     case DataType::RetrievableUtf16:
-      codeRetrievableData<char16_t, mode>(ss);
+      ScriptSource::codeRetrievableData<char16_t, mode>(ss);
       return Ok();
 
     case DataType::BinAST:
       return codeBinASTData(xdr, ss);
   }
 
   // The range-check on |type| far above ought ensure the above |switch| is
   // exhaustive and all cases will return, but not all compilers understand
--- a/js/src/vm/JSScript.h
+++ b/js/src/vm/JSScript.h
@@ -480,16 +480,21 @@ struct SourceTypeTraits<char16_t> {
 
   static UniqueTwoByteChars toCacheable(EntryUnits<char16_t> str) {
     return UniqueTwoByteChars(std::move(str));
   }
 };
 
 class ScriptSourceHolder;
 
+// Retrievable source can be retrieved using the source hook (and therefore
+// need not be XDR'd, can be discarded if desired because it can always be
+// reconstituted later, etc.).
+enum class SourceRetrievable { Yes, No };
+
 class ScriptSource {
   friend class SourceCompressionTask;
 
   class PinnedUnitsBase {
    protected:
     PinnedUnitsBase** stack_ = nullptr;
     PinnedUnitsBase* prev_ = nullptr;
 
@@ -528,43 +533,59 @@ class ScriptSource {
       refs;
 
   // Note: while ScriptSources may be compressed off thread, they are only
   // modified by the main thread, and all members are always safe to access
   // on the main thread.
 
   // Indicate which field in the |data| union is active.
 
-  // Uncompressed source text.
   template <typename Unit>
-  class Uncompressed {
+  class UncompressedData {
     typename SourceTypeTraits<Unit>::SharedImmutableString string_;
 
    public:
-    explicit Uncompressed(
+    explicit UncompressedData(
         typename SourceTypeTraits<Unit>::SharedImmutableString str)
         : string_(std::move(str)) {}
 
     const Unit* units() const { return SourceTypeTraits<Unit>::units(string_); }
 
     size_t length() const { return string_.length(); }
   };
 
-  // Compressed source text.
+  // Uncompressed source text.
+  template <typename Unit, SourceRetrievable CanRetrieve>
+  class Uncompressed : public UncompressedData<Unit> {
+    using Base = UncompressedData<Unit>;
+
+   public:
+    using Base::Base;
+  };
+
   template <typename Unit>
-  struct Compressed {
+  struct CompressedData {
     // Single-byte compressed text, regardless whether the original text
     // was single-byte or two-byte.
     SharedImmutableString raw;
     size_t uncompressedLength;
 
-    Compressed(SharedImmutableString raw, size_t uncompressedLength)
+    CompressedData(SharedImmutableString raw, size_t uncompressedLength)
         : raw(std::move(raw)), uncompressedLength(uncompressedLength) {}
   };
 
+  // Compressed source text.
+  template <typename Unit, SourceRetrievable CanRetrieve>
+  struct Compressed : public CompressedData<Unit> {
+    using Base = CompressedData<Unit>;
+
+   public:
+    using Base::Base;
+  };
+
   // Source that can be retrieved using the registered source hook.  |Unit|
   // records the source type so that source-text coordinates in functions and
   // scripts that depend on this |ScriptSource| are correct.
   template <typename Unit>
   struct Retrievable {
     // The source hook and script URL required to retrieve source are stored
     // elsewhere, so nothing is needed here.  It'd be better hygiene to store
     // something source-hook-like in each |ScriptSource| that needs it, but that
@@ -583,27 +604,37 @@ class ScriptSource {
     UniquePtr<frontend::BinASTSourceMetadata> metadata;
 
     BinAST(SharedImmutableString&& str,
            UniquePtr<frontend::BinASTSourceMetadata> metadata)
         : string(std::move(str)), metadata(std::move(metadata)) {}
   };
 
   using SourceType =
-      mozilla::Variant<Compressed<mozilla::Utf8Unit>,
-                       Uncompressed<mozilla::Utf8Unit>, Compressed<char16_t>,
-                       Uncompressed<char16_t>, Retrievable<mozilla::Utf8Unit>,
-                       Retrievable<char16_t>, Missing, BinAST>;
+      mozilla::Variant<Compressed<mozilla::Utf8Unit, SourceRetrievable::Yes>,
+                       Uncompressed<mozilla::Utf8Unit, SourceRetrievable::Yes>,
+                       Compressed<mozilla::Utf8Unit, SourceRetrievable::No>,
+                       Uncompressed<mozilla::Utf8Unit, SourceRetrievable::No>,
+                       Compressed<char16_t, SourceRetrievable::Yes>,
+                       Uncompressed<char16_t, SourceRetrievable::Yes>,
+                       Compressed<char16_t, SourceRetrievable::No>,
+                       Uncompressed<char16_t, SourceRetrievable::No>,
+                       Retrievable<mozilla::Utf8Unit>, Retrievable<char16_t>,
+                       Missing, BinAST>;
   SourceType data;
 
-  // If the GC attempts to call convertToCompressedSource with PinnedUnits
-  // present, the first PinnedUnits (that is, bottom of the stack) will set
-  // the compressed chars upon destruction.
+  // If the GC calls triggerConvertToCompressedSource with PinnedUnits present,
+  // the first PinnedUnits (that is, bottom of the stack) will install the
+  // compressed chars upon destruction.
+  //
+  // Retrievability isn't part of the type here because uncompressed->compressed
+  // transitions must preserve existing retrievability.
   PinnedUnitsBase* pinnedUnitsStack_;
-  mozilla::MaybeOneOf<Compressed<mozilla::Utf8Unit>, Compressed<char16_t>>
+  mozilla::MaybeOneOf<CompressedData<mozilla::Utf8Unit>,
+                      CompressedData<char16_t>>
       pendingCompressed_;
 
   // The filename of this script.
   UniqueChars filename_;
 
   UniqueTwoByteChars displayURL_;
   UniqueTwoByteChars sourceMapURL_;
   bool mutedErrors_;
@@ -666,47 +697,33 @@ class ScriptSource {
   // unique anymore.
   uint32_t id_;
 
   // How many ids have been handed out to sources.
   static mozilla::Atomic<uint32_t, mozilla::SequentiallyConsistent,
                          mozilla::recordreplay::Behavior::DontPreserve>
       idCount_;
 
-  // If this field is true, we can call JSRuntime::sourceHook to load the source
-  // on demand.  Thus if this contains compressed/uncompressed data, we don't
-  // have to preserve it while we're not using it, because we can use the source
-  // hook to load it when we need it.
-  //
-  // This field is always true for retrievable source.  It *may* be true for
-  // compressed/uncompressed source (if retrievable source was rewritten to
-  // compressed/uncompressed source loaded using the source hook).  It is always
-  // false for missing or BinAST source.
-  bool sourceRetrievable_ : 1;
-
   bool hasIntroductionOffset_ : 1;
   bool containsAsmJS_ : 1;
 
   template <typename Unit>
   const Unit* chunkUnits(JSContext* cx,
                          UncompressedSourceCache::AutoHoldEntry& holder,
                          size_t chunk);
 
   // Return a string containing the chars starting at |begin| and ending at
   // |begin + len|.
   //
-  // Warning: this is *not* GC-safe! Any chars to be handed out should use
+  // Warning: this is *not* GC-safe! Any chars to be handed out must use
   // PinnedUnits. See comment below.
   template <typename Unit>
   const Unit* units(JSContext* cx, UncompressedSourceCache::AutoHoldEntry& asp,
                     size_t begin, size_t len);
 
-  template <typename Unit>
-  void movePendingCompressedSource();
-
  public:
   // When creating a JSString* from TwoByte source characters, we don't try to
   // to deflate to Latin1 for longer strings, because this can be slow.
   static const size_t SourceDeflateLimit = 100;
 
   explicit ScriptSource()
       : refs(0),
         data(SourceType(Missing())),
@@ -716,17 +733,16 @@ class ScriptSource {
         sourceMapURL_(nullptr),
         mutedErrors_(false),
         introductionOffset_(0),
         parameterListEnd_(0),
         introducerFilename_(nullptr),
         introductionType_(nullptr),
         xdrEncoder_(nullptr),
         id_(++idCount_),
-        sourceRetrievable_(false),
         hasIntroductionOffset_(false),
         containsAsmJS_(false) {}
 
   ~ScriptSource() { MOZ_ASSERT(refs == 0); }
 
   void incref() { refs++; }
   void decref() {
     MOZ_ASSERT(refs != 0);
@@ -755,17 +771,16 @@ class ScriptSource {
   static bool loadSource(JSContext* cx, ScriptSource* ss, bool* loaded);
 
   // Assign source data from |srcBuf| to this recently-created |ScriptSource|.
   template <typename Unit>
   MOZ_MUST_USE bool assignSource(JSContext* cx,
                                  const JS::ReadOnlyCompileOptions& options,
                                  JS::SourceText<Unit>& srcBuf);
 
-  bool sourceRetrievable() const { return sourceRetrievable_; }
   bool hasSourceText() const {
     return hasUncompressedSource() || hasCompressedSource();
   }
   bool hasBinASTSource() const { return data.is<BinAST>(); }
 
   void setBinASTSourceMetadata(frontend::BinASTSourceMetadata* metadata) {
     MOZ_ASSERT(hasBinASTSource());
     data.as<BinAST>().metadata.reset(metadata);
@@ -773,52 +788,58 @@ class ScriptSource {
   frontend::BinASTSourceMetadata* binASTSourceMetadata() const {
     MOZ_ASSERT(hasBinASTSource());
     return data.as<BinAST>().metadata.get();
   }
 
  private:
   template <typename Unit>
   struct UncompressedDataMatcher {
-    const Unit* operator()(const Uncompressed<Unit>& u) { return u.units(); }
+    template <SourceRetrievable CanRetrieve>
+    const UncompressedData<Unit>* operator()(
+        const Uncompressed<Unit, CanRetrieve>& u) {
+      return &u;
+    }
 
     template <typename T>
-    const Unit* operator()(const T&) {
+    const UncompressedData<Unit>* operator()(const T&) {
       MOZ_CRASH(
           "attempting to access uncompressed data in a ScriptSource not "
           "containing it");
       return nullptr;
     }
   };
 
  public:
   template <typename Unit>
-  const Unit* uncompressedData() {
+  const UncompressedData<Unit>* uncompressedData() {
     return data.match(UncompressedDataMatcher<Unit>());
   }
 
  private:
   template <typename Unit>
   struct CompressedDataMatcher {
-    char* operator()(const Compressed<Unit>& c) {
-      return const_cast<char*>(c.raw.chars());
+    template <SourceRetrievable CanRetrieve>
+    const CompressedData<Unit>* operator()(
+        const Compressed<Unit, CanRetrieve>& c) {
+      return &c;
     }
 
     template <typename T>
-    char* operator()(const T&) {
+    const CompressedData<Unit>* operator()(const T&) {
       MOZ_CRASH(
           "attempting to access compressed data in a ScriptSource not "
           "containing it");
       return nullptr;
     }
   };
 
  public:
   template <typename Unit>
-  char* compressedData() {
+  const CompressedData<Unit>* compressedData() {
     return data.match(CompressedDataMatcher<Unit>());
   }
 
  private:
   struct BinASTDataMatcher {
     void* operator()(const BinAST& b) {
       return const_cast<char*>(b.string.chars());
     }
@@ -832,23 +853,23 @@ class ScriptSource {
     }
   };
 
  public:
   void* binASTData() { return data.match(BinASTDataMatcher()); }
 
  private:
   struct HasUncompressedSource {
-    template <typename Unit>
-    bool operator()(const Uncompressed<Unit>&) {
+    template <typename Unit, SourceRetrievable CanRetrieve>
+    bool operator()(const Uncompressed<Unit, CanRetrieve>&) {
       return true;
     }
 
-    template <typename Unit>
-    bool operator()(const Compressed<Unit>&) {
+    template <typename Unit, SourceRetrievable CanRetrieve>
+    bool operator()(const Compressed<Unit, CanRetrieve>&) {
       return false;
     }
 
     template <typename Unit>
     bool operator()(const Retrievable<Unit>&) {
       return false;
     }
 
@@ -857,63 +878,94 @@ class ScriptSource {
     bool operator()(const Missing&) { return false; }
   };
 
  public:
   bool hasUncompressedSource() const {
     return data.match(HasUncompressedSource());
   }
 
+ private:
   template <typename Unit>
-  bool uncompressedSourceIs() const {
-    MOZ_ASSERT(hasUncompressedSource());
-    return data.is<Uncompressed<Unit>>();
+  struct IsUncompressed {
+    template <SourceRetrievable CanRetrieve>
+    bool operator()(const Uncompressed<Unit, CanRetrieve>&) {
+      return true;
+    }
+
+    template <typename T>
+    bool operator()(const T&) {
+      return false;
+    }
+  };
+
+ public:
+  template <typename Unit>
+  bool isUncompressed() const {
+    return data.match(IsUncompressed<Unit>());
   }
 
  private:
   struct HasCompressedSource {
-    template <typename Unit>
-    bool operator()(const Compressed<Unit>&) {
+    template <typename Unit, SourceRetrievable CanRetrieve>
+    bool operator()(const Compressed<Unit, CanRetrieve>&) {
       return true;
     }
 
-    template <typename Unit>
-    bool operator()(const Uncompressed<Unit>&) {
+    template <typename T>
+    bool operator()(const T&) {
       return false;
     }
-
-    template <typename Unit>
-    bool operator()(const Retrievable<Unit>&) {
-      return false;
-    }
-
-    bool operator()(const BinAST&) { return false; }
-
-    bool operator()(const Missing&) { return false; }
   };
 
  public:
   bool hasCompressedSource() const { return data.match(HasCompressedSource()); }
 
+ private:
   template <typename Unit>
-  bool compressedSourceIs() const {
-    MOZ_ASSERT(hasCompressedSource());
-    return data.is<Compressed<Unit>>();
+  struct IsCompressed {
+    template <SourceRetrievable CanRetrieve>
+    bool operator()(const Compressed<Unit, CanRetrieve>&) {
+      return true;
+    }
+
+    template <typename T>
+    bool operator()(const T&) {
+      return false;
+    }
+  };
+
+ public:
+  template <typename Unit>
+  bool isCompressed() const {
+    return data.match(IsCompressed<Unit>());
   }
 
  private:
   template <typename Unit>
   struct SourceTypeMatcher {
-    template <template <typename C> class Data>
-    bool operator()(const Data<Unit>&) {
+    template <template <typename C, SourceRetrievable R> class Data,
+              SourceRetrievable CanRetrieve>
+    bool operator()(const Data<Unit, CanRetrieve>&) {
       return true;
     }
 
-    template <template <typename C> class Data, typename NotUnit>
-    bool operator()(const Data<NotUnit>&) {
+    template <template <typename C, SourceRetrievable R> class Data,
+              typename NotUnit, SourceRetrievable CanRetrieve>
+    bool operator()(const Data<NotUnit, CanRetrieve>&) {
+      return false;
+    }
+
+    bool operator()(const Retrievable<Unit>&) {
+      MOZ_CRASH("source type only applies where actual text is available");
+      return false;
+    }
+
+    template <typename NotUnit>
+    bool operator()(const Retrievable<NotUnit>&) {
       return false;
     }
 
     bool operator()(const BinAST&) {
       MOZ_CRASH("doesn't make sense to ask source type of BinAST data");
       return false;
     }
 
@@ -926,23 +978,23 @@ class ScriptSource {
  public:
   template <typename Unit>
   bool hasSourceType() const {
     return data.match(SourceTypeMatcher<Unit>());
   }
 
  private:
   struct UncompressedLengthMatcher {
-    template <typename Unit>
-    size_t operator()(const Uncompressed<Unit>& u) {
+    template <typename Unit, SourceRetrievable CanRetrieve>
+    size_t operator()(const Uncompressed<Unit, CanRetrieve>& u) {
       return u.length();
     }
 
-    template <typename Unit>
-    size_t operator()(const Compressed<Unit>& u) {
+    template <typename Unit, SourceRetrievable CanRetrieve>
+    size_t operator()(const Compressed<Unit, CanRetrieve>& u) {
       return u.uncompressedLength;
     }
 
     template <typename Unit>
     size_t operator()(const Retrievable<Unit>&) {
       MOZ_CRASH("ScriptSource::length on a missing-but-retrievable source");
       return 0;
     }
@@ -977,47 +1029,51 @@ class ScriptSource {
   // Overwrites |data| with the uncompressed data from |source|.
   //
   // This function asserts nothing about |data|.  Users should use assertions to
   // double-check their own understandings of the |data| state transition being
   // performed.
   template <typename Unit>
   MOZ_MUST_USE bool setUncompressedSourceHelper(JSContext* cx,
                                                 EntryUnits<Unit>&& source,
-                                                size_t length);
+                                                size_t length,
+                                                SourceRetrievable retrievable);
 
  public:
-  // Initialize a fresh |ScriptSource| with uncompressed source.
+  // Initialize a fresh |ScriptSource| with unretrievable, uncompressed source.
   template <typename Unit>
-  MOZ_MUST_USE bool initializeUncompressedSource(JSContext* cx,
-                                                 EntryUnits<Unit>&& source,
-                                                 size_t length);
+  MOZ_MUST_USE bool initializeUnretrievableUncompressedSource(
+      JSContext* cx, EntryUnits<Unit>&& source, size_t length);
 
   // Set the retrieved source for a |ScriptSource| whose source was recorded as
   // missing but retrievable.
   template <typename Unit>
   MOZ_MUST_USE bool setRetrievedSource(JSContext* cx, EntryUnits<Unit>&& source,
                                        size_t length);
 
   MOZ_MUST_USE bool tryCompressOffThread(JSContext* cx);
 
-  // Convert this ScriptSource from storing uncompressed source of the given
-  // type, to storing compressed source.  (Raw compressed source is always
-  // single-byte; |Unit| just records the encoding of the uncompressed source.)
+  // *Trigger* the conversion of this ScriptSource from containing uncompressed
+  // |Unit|-encoded source to containing compressed source.  Conversion may not
+  // be complete when this function returns: it'll be delayed if there's ongoing
+  // use of the uncompressed source via |PinnedUnits|, in which case conversion
+  // won't occur until the outermost |PinnedUnits| is destroyed.
+  //
+  // Compressed source is in bytes, no matter that |Unit| might be |char16_t|.
+  // |sourceLength| is the length in code units (not bytes) of the uncompressed
+  // source.
   template <typename Unit>
-  void convertToCompressedSource(SharedImmutableString compressed,
-                                 size_t sourceLength);
-
-  // Initialize a fresh ScriptSource as containing compressed source of the
-  // indicated original encoding.
+  void triggerConvertToCompressedSource(SharedImmutableString compressed,
+                                        size_t sourceLength);
+
+  // Initialize a fresh ScriptSource as containing unretrievable compressed
+  // source of the indicated original encoding.
   template <typename Unit>
-  MOZ_MUST_USE bool initializeWithCompressedSource(JSContext* cx,
-                                                   UniqueChars&& raw,
-                                                   size_t rawLength,
-                                                   size_t sourceLength);
+  MOZ_MUST_USE bool initializeWithUnretrievableCompressedSource(
+      JSContext* cx, UniqueChars&& raw, size_t rawLength, size_t sourceLength);
 
 #if defined(JS_BUILD_BINAST)
 
   /*
    * Do not take ownership of the given `buf`. Store the canonical, shared
    * and de-duplicated version. If there is no extant shared version of
    * `buf`, make a copy.
    */
@@ -1026,32 +1082,32 @@ class ScriptSource {
 
   const uint8_t* binASTSource();
 
 #endif /* JS_BUILD_BINAST */
 
  private:
   void performTaskWork(SourceCompressionTask* task);
 
-  struct ConvertToCompressedSourceFromTask {
+  struct TriggerConvertToCompressedSourceFromTask {
     ScriptSource* const source_;
     SharedImmutableString& compressed_;
 
-    ConvertToCompressedSourceFromTask(ScriptSource* source,
-                                      SharedImmutableString& compressed)
+    TriggerConvertToCompressedSourceFromTask(ScriptSource* source,
+                                             SharedImmutableString& compressed)
         : source_(source), compressed_(compressed) {}
 
-    template <typename Unit>
-    void operator()(const Uncompressed<Unit>&) {
-      source_->convertToCompressedSource<Unit>(std::move(compressed_),
-                                               source_->length());
+    template <typename Unit, SourceRetrievable CanRetrieve>
+    void operator()(const Uncompressed<Unit, CanRetrieve>&) {
+      source_->triggerConvertToCompressedSource<Unit>(std::move(compressed_),
+                                                      source_->length());
     }
 
-    template <typename Unit>
-    void operator()(const Compressed<Unit>&) {
+    template <typename Unit, SourceRetrievable CanRetrieve>
+    void operator()(const Compressed<Unit, CanRetrieve>&) {
       MOZ_CRASH(
           "can't set compressed source when source is already compressed -- "
           "ScriptSource::tryCompressOffThread shouldn't have queued up this "
           "task?");
     }
 
     template <typename Unit>
     void operator()(const Retrievable<Unit>&) {
@@ -1065,28 +1121,35 @@ class ScriptSource {
     void operator()(const Missing&) {
       MOZ_CRASH(
           "doesn't make sense to set compressed source for missing source -- "
           "ScriptSource::tryCompressOffThread shouldn't have queued up this "
           "task?");
     }
   };
 
-  void convertToCompressedSourceFromTask(SharedImmutableString compressed);
+  template <typename Unit>
+  void convertToCompressedSource(SharedImmutableString compressed,
+                                 size_t uncompressedLength);
+
+  template <typename Unit>
+  void performDelayedConvertToCompressedSource();
+
+  void triggerConvertToCompressedSourceFromTask(
+      SharedImmutableString compressed);
 
  private:
   // It'd be better to make this function take <XDRMode, Unit>, as both
   // specializations of this function contain nested Unit-parametrized
   // helper classes that do everything the function needs to do.  But then
   // we'd need template function partial specialization to hold XDRMode
   // constant while varying Unit, so that idea's no dice.
   template <XDRMode mode>
-  MOZ_MUST_USE XDRResult xdrUncompressedSource(XDRState<mode>* xdr,
-                                               uint8_t sourceCharSize,
-                                               uint32_t uncompressedLength);
+  MOZ_MUST_USE XDRResult xdrUnretrievableUncompressedSource(
+      XDRState<mode>* xdr, uint8_t sourceCharSize, uint32_t uncompressedLength);
 
  public:
   MOZ_MUST_USE bool setFilename(JSContext* cx, const char* filename);
   const char* introducerFilename() const {
     return introducerFilename_ ? introducerFilename_.get() : filename_.get();
   }
   bool hasIntroductionType() const { return introductionType_; }
   const char* introductionType() const {
@@ -1157,25 +1220,28 @@ class ScriptSource {
   const mozilla::TimeStamp parseEnded() const { return parseEnded_; }
   // Inform `this` source that it has been fully parsed.
   void recordParseEnded() {
     MOZ_ASSERT(parseEnded_.IsNull());
     parseEnded_ = ReallyNow();
   }
 
  private:
+  template <typename Unit,
+            template <typename U, SourceRetrievable CanRetrieve> class Data,
+            XDRMode mode>
+  static void codeRetrievable(ScriptSource* ss);
+
   template <typename Unit, XDRMode mode>
   static MOZ_MUST_USE XDRResult codeUncompressedData(XDRState<mode>* const xdr,
-                                                     ScriptSource* const ss,
-                                                     bool retrievable);
+                                                     ScriptSource* const ss);
 
   template <typename Unit, XDRMode mode>
   static MOZ_MUST_USE XDRResult codeCompressedData(XDRState<mode>* const xdr,
-                                                   ScriptSource* const ss,
-                                                   bool retrievable);
+                                                   ScriptSource* const ss);
 
   template <XDRMode mode>
   static MOZ_MUST_USE XDRResult codeBinASTData(XDRState<mode>* const xdr,
                                                ScriptSource* const ss);
 
   template <typename Unit, XDRMode mode>
   static void codeRetrievableData(ScriptSource* ss);
 
--- a/layout/printing/nsPrintJob.h
+++ b/layout/printing/nsPrintJob.h
@@ -115,126 +115,112 @@ class nsPrintJob final : public nsIObser
   bool IsDoingPrintPreview() const { return mIsDoingPrintPreview; }
   bool IsIFrameSelected();
   bool IsRangeSelection();
   /// If the returned value is not greater than zero, an error occurred.
   int32_t GetPrintPreviewNumPages();
   nsresult GetDocumentName(nsAString& aDocName);
   already_AddRefed<nsIPrintSettings> GetCurrentPrintSettings();
 
+  // The setters here also update the DocViewer
+  void SetIsPrinting(bool aIsPrinting);
+  bool GetIsPrinting() { return mIsDoingPrinting; }
+  void SetIsPrintPreview(bool aIsPrintPreview);
+  bool GetIsPrintPreview() { return mIsDoingPrintPreview; }
+  bool GetIsCreatingPrintPreview() { return mIsCreatingPrintPreview; }
+
   // This enum tells indicates what the default should be for the title
   // if the title from the document is null
   enum eDocTitleDefault { eDocTitleDefBlank, eDocTitleDefURLDoc };
 
-  void Destroy();
-  void DestroyPrintingData();
-
   nsresult GetSeqFrameAndCountPages(nsIFrame*& aSeqFrame, int32_t& aCount);
 
-  //
-  // The following three methods are used for printing...
-  //
-  nsresult DocumentReadyForPrinting();
-  nsresult GetSelectionDocument(nsIDeviceContextSpec* aDevSpec,
-                                mozilla::dom::Document** aNewDoc);
-
-  nsresult SetupToPrintContent();
-  nsresult EnablePOsForPrinting();
-  nsPrintObject* FindSmallestSTF();
-
-  bool PrintDocContent(const mozilla::UniquePtr<nsPrintObject>& aPO,
-                       nsresult& aStatus);
-  nsresult DoPrint(const mozilla::UniquePtr<nsPrintObject>& aPO);
-
-  void SetPrintPO(nsPrintObject* aPO, bool aPrint);
-
   void TurnScriptingOn(bool aDoTurnOn);
-  bool CheckDocumentForPPCaching();
-
-  /**
-   * Filters out certain user events while Print Preview is open to prevent
-   * the user from interacting with the Print Preview document and breaking
-   * printing invariants.
-   */
-  void SuppressPrintPreviewUserEvents();
-
-  // nsIDocumentViewerPrint Printing Methods:
 
   /**
    * Checks to see if the document this print engine is associated with has any
    * canvases that have a mozPrintCallback.
    * https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement#Properties
    */
   bool HasPrintCallbackCanvas() { return mHasMozPrintCallback; }
   bool PrePrintPage();
   bool PrintPage(nsPrintObject* aPOect, bool& aInRange);
   bool DonePrintingPages(nsPrintObject* aPO, nsresult aResult);
 
-  //---------------------------------------------------------------------
+  nsresult CleanupOnFailure(nsresult aResult, bool aIsPrinting);
+  // If FinishPrintPreview() fails, caller may need to reset the state of the
+  // object, for example by calling CleanupOnFailure().
+  nsresult FinishPrintPreview();
+  void FirePrintingErrorEvent(nsresult aPrintError);
+  bool CheckBeforeDestroy() const { return mPrt && mPrt->mPreparingForPrint; }
+
+  mozilla::PresShell* GetPrintPreviewPresShell() {
+    return mPrtPreview->mPrintObject->mPresShell;
+  }
+
+  float GetPrintPreviewScale() {
+    return mPrtPreview->mPrintObject->mPresContext->GetPrintPreviewScale();
+  }
+
+  nsresult Cancel();
+  void Destroy();
+  void DestroyPrintingData();
+
+ private:
+  nsPrintJob& operator=(const nsPrintJob& aOther) = delete;
+
+  ~nsPrintJob();
+
+  nsresult DocumentReadyForPrinting();
+  nsresult SetupToPrintContent();
+  nsresult EnablePOsForPrinting();
+  nsPrintObject* FindSmallestSTF();
+
+  bool PrintDocContent(const mozilla::UniquePtr<nsPrintObject>& aPO,
+                       nsresult& aStatus);
+  nsresult DoPrint(const mozilla::UniquePtr<nsPrintObject>& aPO);
+
+  void SetPrintPO(nsPrintObject* aPO, bool aPrint);
+
+  /**
+   * Filters out certain user events while Print Preview is open to prevent
+   * the user from interacting with the Print Preview document and breaking
+   * printing invariants.
+   */
+  void SuppressPrintPreviewUserEvents();
+
   nsresult ReflowDocList(const mozilla::UniquePtr<nsPrintObject>& aPO,
                          bool aSetPixelScale);
 
   MOZ_CAN_RUN_SCRIPT_BOUNDARY
   nsresult ReflowPrintObject(const mozilla::UniquePtr<nsPrintObject>& aPO);
 
   void CalcNumPrintablePages(int32_t& aNumPages);
   void ShowPrintProgress(bool aIsForPrinting, bool& aDoNotify);
-  nsresult CleanupOnFailure(nsresult aResult, bool aIsPrinting);
-  // If FinishPrintPreview() fails, caller may need to reset the state of the
-  // object, for example by calling CleanupOnFailure().
-  nsresult FinishPrintPreview();
   void SetURLAndTitleOnProgressParams(
       const mozilla::UniquePtr<nsPrintObject>& aPO,
       nsIPrintProgressParams* aParams);
   void EllipseLongString(nsAString& aStr, const uint32_t aLen, bool aDoFront);
 
   bool IsThereARangeSelection(nsPIDOMWindowOuter* aDOMWin);
 
-  void FirePrintingErrorEvent(nsresult aPrintError);
-  //---------------------------------------------------------------------
-
-  // Timer Methods
   nsresult StartPagePrintTimer(const mozilla::UniquePtr<nsPrintObject>& aPO);
 
   bool IsWindowsInOurSubTree(nsPIDOMWindowOuter* aDOMWindow) const;
   bool IsThereAnIFrameSelected(nsIDocShell* aDocShell,
                                nsPIDOMWindowOuter* aDOMWin,
                                bool& aIsParentFrameSet);
 
   // get the currently infocus frame for the document viewer
   already_AddRefed<nsPIDOMWindowOuter> FindFocusedDOMWindow() const;
 
   void GetDisplayTitleAndURL(const mozilla::UniquePtr<nsPrintObject>& aPO,
                              nsAString& aTitle, nsAString& aURLStr,
                              eDocTitleDefault aDefType);
 
-  bool CheckBeforeDestroy() const { return mPrt && mPrt->mPreparingForPrint; }
-
-  nsresult Cancel();
-
-  mozilla::PresShell* GetPrintPreviewPresShell() {
-    return mPrtPreview->mPrintObject->mPresShell;
-  }
-
-  float GetPrintPreviewScale() {
-    return mPrtPreview->mPrintObject->mPresContext->GetPrintPreviewScale();
-  }
-
-  // These calls also update the DocViewer
-  void SetIsPrinting(bool aIsPrinting);
-  bool GetIsPrinting() { return mIsDoingPrinting; }
-  void SetIsPrintPreview(bool aIsPrintPreview);
-  bool GetIsPrintPreview() { return mIsDoingPrintPreview; }
-  bool GetIsCreatingPrintPreview() { return mIsCreatingPrintPreview; }
-
- private:
-  nsPrintJob& operator=(const nsPrintJob& aOther) = delete;
-
-  ~nsPrintJob();
-
   nsresult CommonPrint(bool aIsPrintPreview, nsIPrintSettings* aPrintSettings,
                        nsIWebProgressListener* aWebProgressListener,
                        mozilla::dom::Document* aSourceDoc);
 
   nsresult DoCommonPrint(bool aIsPrintPreview, nsIPrintSettings* aPrintSettings,
                          nsIWebProgressListener* aWebProgressListener,
                          mozilla::dom::Document* aSourceDoc);
 
--- a/toolkit/content/widgets/browser-custom-element.js
+++ b/toolkit/content/widgets/browser-custom-element.js
@@ -595,17 +595,17 @@ class MozBrowser extends MozElements.Moz
   }
 
   get contentTitle() {
     return this.isRemoteBrowser ? this._contentTitle : this.contentDocument.title;
   }
 
   set characterSet(val) {
     if (this.isRemoteBrowser) {
-      this.messageManager.sendAsyncMessage("UpdateCharacterSet", { value: val });
+      this.sendMessageToActor("UpdateCharacterSet", { value: val }, "BrowserTab");
       this._characterSet = val;
     } else {
       this.docShell.charset = val;
       this.docShell.gatherCharsetMenuTelemetry();
     }
   }
 
   get characterSet() {
@@ -1857,13 +1857,48 @@ class MozBrowser extends MozElements.Moz
   getContentBlockingLog() {
     if (this.isRemoteBrowser) {
       return this.frameLoader.remoteTab.getContentBlockingLog();
     }
     return this.docShell ?
       this.docShell.getContentBlockingLog() :
       Promise.reject("docshell isn't available");
   }
+
+  // Send an asynchronous message to the remote child via an actor.
+  // Note: use this only for messages through an actor. For old-style
+  // messages, use the message manager. If 'all' is true, then send
+  // a message to all descendant processes.
+  sendMessageToActor(messageName, args, actorName, all) {
+    if (!this.frameLoader) {
+      return;
+    }
+
+    let windowGlobal = this.browsingContext.currentWindowGlobal;
+    if (!windowGlobal) {
+      // Workaround for bug 1523638 where about:blank is loaded in a tab.
+      if (messageName == "Browser:AppTab") {
+        setTimeout(() => { this.sendMessageToActor(messageName, args, actorName); }, 0);
+      }
+      return;
+    }
+
+    function sendToChildren(browsingContext, checkRoot) {
+      let windowGlobal = browsingContext.currentWindowGlobal;
+      if (windowGlobal && (!checkRoot || windowGlobal.isProcessRoot)) {
+        windowGlobal.getActor(actorName).sendAsyncMessage(messageName, args);
+      }
+
+      if (all) {
+        let contexts = browsingContext.getChildren();
+        for (let context of contexts) {
+          sendToChildren(context, true);
+        }
+      }
+    }
+
+    sendToChildren(this.browsingContext, false);
+  }
 }
 
 MozXULElement.implementCustomInterface(MozBrowser, [Ci.nsIBrowser]);
 customElements.define("browser", MozBrowser);
 }
--- a/xpcom/base/nsCycleCollectionParticipant.h
+++ b/xpcom/base/nsCycleCollectionParticipant.h
@@ -173,25 +173,25 @@ class NS_NO_VTABLE nsCycleCollectionPart
   // CanSkipInCC is allowed to remove other objects from the purple buffer but
   // should not remove aPtr and should not mark JS things black.  It should also
   // not modify any reference counts.
   //
   // Things can return true from CanSkipInCC if either they know they have no
   // outgoing edges at all in the cycle collection graph or they know for sure
   // they're alive _and_ none of their outgoing edges are to gray (in the GC
   // sense) gcthings.  See also nsWrapperCache::HasNothingToTrace and
-  // nsWrapperCache::IsBlackAndDoesNotNeedTracing.  The restriction on not
-  // having outgoing edges to gray gcthings is because if we _do_ have them that
-  // means we have a "strong" edge to a JS thing and since we're alive we need
-  // to trace through it and mark keep them alive.  Outgoing edges to C++ things
-  // don't matter here, because the criteria for when a CC participant is
-  // considered alive are slightly different for JS and C++ things: JS things
-  // are only considered alive when reachable via an edge from a live thing,
-  // while C++ things are also considered alive when their refcount exceeds the
-  // number of edges via which they are reachable.
+  // nsWrapperCache::HasKnownLiveWrapperAndDoesNotNeedTracing. The restriction
+  // on not having outgoing edges to gray gcthings is because if we _do_ have
+  // them that means we have a "strong" edge to a JS thing and since we're alive
+  // we need to trace through it and mark keep them alive.  Outgoing edges to
+  // C++ things don't matter here, because the criteria for when a CC
+  // participant is considered alive are slightly different for JS and C++
+  // things: JS things are only considered alive when reachable via an edge from
+  // a live thing, while C++ things are also considered alive when their
+  // refcount exceeds the number of edges via which they are reachable.
   bool CanSkipInCC(void* aPtr) {
     return mMightSkip ? CanSkipInCCReal(aPtr) : false;
   }
 
   // CanSkipThis is called during construction of the cycle collector graph,
   // when we traverse an edge to aPtr and consider adding it to the graph.  If
   // it returns true, aPtr is not added to the graph.
   //