Merge mozilla-central to mozilla-inbound. a=merge on a CLOSED TREE
authorRazvan Maries <rmaries@mozilla.com>
Fri, 02 Aug 2019 01:52:58 +0300
changeset 485831 2cc2e0210a28dba5566fe5e57c51a195f66bf71b
parent 485830 f742215abea862a82599cc6869b970d487cf8f29 (current diff)
parent 485711 5f8aeb02af2259acfceed9e1abe987849fbb67ca (diff)
child 485832 09384c51f12729d8daecc67af15453f70d3acce1
push id36374
push userrmaries@mozilla.com
push dateFri, 02 Aug 2019 03:53:17 +0000
treeherdermozilla-central@4748c006ae7b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone70.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound. a=merge on a CLOSED TREE
dom/browser-element/mochitest/mochitest.ini
dom/media/encoder/EncodedFrameContainer.h
netwerk/base/nsNetUtil.cpp
--- a/.cron.yml
+++ b/.cron.yml
@@ -188,8 +188,25 @@ jobs:
           target-tasks-method: pipfile_update
       run-on-projects:
           - mozilla-central
       when:
           by-project:
               # No default branch
               mozilla-central:
                   - {weekday: 'Monday', hour: 10, minute: 0}
+
+    - name: raptor-tp6m
+      job:
+          type: decision-task
+          treeherder-symbol: tp6m
+          target-tasks-method: raptor_tp6m
+          include-push-tasks: true
+      run-on-projects:
+          - mozilla-central
+      when:
+          - {weekday: 'Monday', hour: 3, minute: 0}
+          - {weekday: 'Tuesday', hour: 3, minute: 0}
+          - {weekday: 'Wednesday', hour: 3, minute: 0}
+          - {weekday: 'Thursday', hour: 3, minute: 0}
+          - {weekday: 'Friday', hour: 3, minute: 0}
+          - {weekday: 'Saturday', hour: 3, minute: 0}
+          - {weekday: 'Sunday', hour: 3, minute: 0}
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1315,16 +1315,18 @@ pref("browser.newtabpage.activity-stream
 #else
 pref("browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", false);
 #endif
 
 pref("trailhead.firstrun.branches", "join-privacy");
 
 // The pref that controls if the What's New panel is enabled.
 pref("browser.messaging-system.whatsNewPanel.enabled", false);
+// Whether to use Messaging System to add a badge to the FxA toolbar button
+pref("browser.messaging-system.fxatoolbarbadge.enabled", true);
 
 // Enable the DOM fullscreen API.
 pref("full-screen-api.enabled", true);
 
 // Startup Crash Tracking
 // number of startup crashes that can occur before starting into safe mode automatically
 // (this pref has no effect if more than 6 hours have passed since the last crash)
 pref("toolkit.startup.max_resumed_crashes", 3);
--- a/browser/base/content/browser-siteProtections.js
+++ b/browser/base/content/browser-siteProtections.js
@@ -5,16 +5,23 @@
 /* eslint-env mozilla/browser-window */
 
 ChromeUtils.defineModuleGetter(
   this,
   "ContentBlockingAllowList",
   "resource://gre/modules/ContentBlockingAllowList.jsm"
 );
 
+XPCOMUtils.defineLazyServiceGetter(
+  this,
+  "TrackingDBService",
+  "@mozilla.org/tracking-db-service;1",
+  "nsITrackingDBService"
+);
+
 var Fingerprinting = {
   PREF_ENABLED: "privacy.trackingprotection.fingerprinting.enabled",
   reportBreakageLabel: "fingerprinting",
 
   strings: {
     get subViewBlocked() {
       delete this.subViewBlocked;
       return (this.subViewBlocked = gNavigatorBundle.getString(
@@ -1073,31 +1080,37 @@ var gProtectionsHandler = {
     ));
   },
   get _protectionsPopupTPSwitch() {
     delete this._protectionsPopupTPSwitch;
     return (this._protectionsPopupTPSwitch = document.getElementById(
       "protections-popup-tp-switch"
     ));
   },
-  get _protectionPopupSettingsButton() {
-    delete this._protectionPopupSettingsButton;
-    return (this._protectionPopupSettingsButton = document.getElementById(
+  get _protectionsPopupSettingsButton() {
+    delete this._protectionsPopupSettingsButton;
+    return (this._protectionsPopupSettingsButton = document.getElementById(
       "protections-popup-settings-button"
     ));
   },
-  get _protectionPopupFooter() {
-    delete this._protectionPopupFooter;
-    return (this._protectionPopupFooter = document.getElementById(
+  get _protectionsPopupFooter() {
+    delete this._protectionsPopupFooter;
+    return (this._protectionsPopupFooter = document.getElementById(
       "protections-popup-footer"
     ));
   },
-  get _protectionPopupTrackersCounterDescription() {
-    delete this._protectionPopupTrackersCounterDescription;
-    return (this._protectionPopupTrackersCounterDescription = document.getElementById(
+  get _protectionsPopupTrackersCounterBox() {
+    delete this._protectionsPopupTrackersCounterBox;
+    return (this._protectionsPopupTrackersCounterBox = document.getElementById(
+      "protections-popup-trackers-blocked-counter-box"
+    ));
+  },
+  get _protectionsPopupTrackersCounterDescription() {
+    delete this._protectionsPopupTrackersCounterDescription;
+    return (this._protectionsPopupTrackersCounterDescription = document.getElementById(
       "protections-popup-trackers-blocked-counter-description"
     ));
   },
   get _protectionsPopupSiteNotWorkingTPSwitch() {
     delete this._protectionsPopupSiteNotWorkingTPSwitch;
     return (this._protectionsPopupSiteNotWorkingTPSwitch = document.getElementById(
       "protections-popup-siteNotWorking-tp-switch"
     ));
@@ -1218,38 +1231,44 @@ var gProtectionsHandler = {
     );
 
     let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
     gProtectionsHandler._protectionsPopupSendReportLearnMore.href =
       baseURL + "blocking-breakage";
 
     this.appMenuLabel.setAttribute("value", this.strings.appMenuTitle);
     this.appMenuLabel.setAttribute("tooltiptext", this.strings.appMenuTooltip);
+
+    // Add an observer to observe that the history has been cleared.
+    Services.obs.addObserver(this, "browser:purge-session-history");
   },
 
   uninit() {
     for (let blocker of this.blockers) {
       if (blocker.uninit) {
         blocker.uninit();
       }
     }
 
     Services.prefs.removeObserver(
       this.PREF_ANIMATIONS_ENABLED,
       this.updateAnimationsEnabled
     );
+
+    Services.obs.removeObserver(this, "browser:purge-session-history");
   },
 
   openPreferences(origin) {
     openPreferences("privacy-trackingprotection", { origin });
   },
 
-  openProtections() {
+  openProtections(relatedToCurrent = false) {
     switchToTabHavingURI("about:protections", true, {
       replaceQueryString: true,
+      relatedToCurrent,
       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
     });
   },
 
   async showTrackersSubview() {
     await TrackingProtection.updateSubView();
     this._protectionsPopupMultiView.showSubView(
       "protections-popup-trackersView"
@@ -1324,30 +1343,55 @@ var gProtectionsHandler = {
       window.addEventListener("focus", this, true);
     }
   },
 
   onPopupHidden(event) {
     if (event.target == this._protectionsPopup) {
       window.removeEventListener("focus", this, true);
       gIdentityHandler._trackingProtectionIconContainer.removeAttribute("open");
+
+      // Hide the tracker counter when the popup get hidden.
+      this._protectionsPopupTrackersCounterBox.toggleAttribute(
+        "showing",
+        false
+      );
     }
   },
 
   onHeaderClicked(event) {
     // Display the whole protections panel if the toast has been clicked.
     if (this._protectionsPopup.hasAttribute("toast")) {
       // Hide the toast first.
       PanelMultiView.hidePopup(this._protectionsPopup);
 
       // Open the full protections panel.
       this.showProtectionsPopup({ event });
     }
   },
 
+  async onTrackingProtectionIconHoveredOrFocused() {
+    // We would try to pre-fetch the data whenever the shield icon is hovered or
+    // focused. We check focus event here due to the keyboard navigation.
+    if (this._updatingFooter) {
+      return;
+    }
+    this._updatingFooter = true;
+
+    // Get the tracker count and set it to the counter in the footer.
+    const trackerCount = await TrackingDBService.sumAllEvents();
+    this.setTrackersBlockedCounter(trackerCount);
+
+    // Try to get the earliest recorded date in case that there was no record
+    // during the initiation but new records come after that.
+    await this.maybeUpdateEarliestRecordedDateTooltip();
+
+    this._updatingFooter = false;
+  },
+
   // This triggers from top level location changes.
   onLocationChange() {
     if (this._showToastAfterRefresh) {
       this._showToastAfterRefresh = false;
 
       // We only display the toast if we're still on the same page.
       if (
         this._previousURI == gBrowser.currentURI.spec &&
@@ -1492,16 +1536,27 @@ var gProtectionsHandler = {
     ) {
       // Hide the panel when focusing an element that is
       // neither an ancestor nor descendant unless the panel has
       // @noautohide (e.g. for a tour).
       PanelMultiView.hidePopup(this._protectionsPopup);
     }
   },
 
+  observe(subject, topic, data) {
+    switch (topic) {
+      case "browser:purge-session-history":
+        // We need to update the earliest recorded date if history has been
+        // cleared.
+        this._hasEarliestRecord = false;
+        this.maybeUpdateEarliestRecordedDateTooltip();
+        break;
+    }
+  },
+
   refreshProtectionsPopup() {
     let host = gIdentityHandler.getHostForDisplay();
 
     // Push the appropriate strings out to the UI.
     this._protectionsPopupMainViewHeaderLabel.textContent = gNavigatorBundle.getFormattedString(
       "protections.header",
       [host]
     );
@@ -1524,21 +1579,18 @@ var gProtectionsHandler = {
     // height of TP switch section, or it will change when toggling the switch,
     // which is not desirable for us. So, we need to use a different attribute
     // here.
     this._protectionsPopupTPSwitchSection.toggleAttribute(
       "short",
       !currentlyEnabled
     );
 
-    // Set the counter of the 'Trackers blocked This Week'.
-    // We need to get the statistics of trackers. So far, we haven't implemented
-    // this yet. So we use a fake number here. Should be resolved in
-    // Bug 1555231.
-    this.setTrackersBlockedCounter(244051);
+    // Update the tooltip of the blocked tracker counter.
+    this.maybeUpdateEarliestRecordedDateTooltip();
   },
 
   disableForCurrentPage() {
     ContentBlockingAllowList.add(gBrowser.selectedBrowser);
     PanelMultiView.hidePopup(this._protectionsPopup);
     BrowserReload();
   },
 
@@ -1588,20 +1640,29 @@ var gProtectionsHandler = {
     } else {
       this.enableForCurrentPage();
     }
 
     delete this._TPSwitchCommanding;
   },
 
   setTrackersBlockedCounter(trackerCount) {
-    this._protectionPopupTrackersCounterDescription.textContent =
-      // gNavigatorBundle.getFormattedString(
-      //   "protections.trackers_counter", [cnt]);
-      `Trackers blocked this week: ${trackerCount.toLocaleString()}`;
+    let forms = gNavigatorBundle.getString(
+      "protections.footer.blockedTrackerCounter.description"
+    );
+    this._protectionsPopupTrackersCounterDescription.textContent = PluralForm.get(
+      trackerCount,
+      forms
+    ).replace("#1", trackerCount);
+
+    // Show the counter if the number of tracker is not zero.
+    this._protectionsPopupTrackersCounterBox.toggleAttribute(
+      "showing",
+      trackerCount != 0
+    );
   },
 
   /**
    * Showing the protections popup.
    *
    * @param {Object} options
    *                 The object could have two properties.
    *                 event:
@@ -1795,9 +1856,40 @@ var gProtectionsHandler = {
       })
       .catch(Cu.reportError);
   },
 
   onSendReportClicked() {
     this._protectionsPopup.hidePopup();
     this.submitBreakageReport(this.reportURI);
   },
+
+  async maybeUpdateEarliestRecordedDateTooltip() {
+    if (this._hasEarliestRecord) {
+      return;
+    }
+
+    let date = await TrackingDBService.getEarliestRecordedDate();
+
+    // If there is no record for any blocked tracker, we don't have to do anything
+    // since the tracker counter won't be shown.
+    if (!date) {
+      return;
+    }
+    this._hasEarliestRecord = true;
+
+    const dateLocaleStr = new Date(date).toLocaleDateString("default", {
+      month: "long",
+      day: "numeric",
+      year: "numeric",
+    });
+
+    const tooltipStr = gNavigatorBundle.getFormattedString(
+      "protections.footer.blockedTrackerCounter.tooltip",
+      [dateLocaleStr]
+    );
+
+    this._protectionsPopupTrackersCounterDescription.setAttribute(
+      "tooltiptext",
+      tooltipStr
+    );
+  },
 };
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -486,16 +486,26 @@ XPCOMUtils.defineLazyPreferenceGetter(
   false,
   (aPref, aOldVal, aNewVal) => {
     showFxaToolbarMenu(gFxaToolbarEnabled);
   }
 );
 
 XPCOMUtils.defineLazyPreferenceGetter(
   this,
+  "gMsgingSystemFxABadge",
+  "browser.messaging-system.fxatoolbarbadge.enabled",
+  true,
+  (aPref, aOldVal, aNewVal) => {
+    showFxaToolbarMenu(gFxaToolbarEnabled);
+  }
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+  this,
   "gHtmlAboutAddonsEnabled",
   "extensions.htmlaboutaddons.enabled",
   false
 );
 
 XPCOMUtils.defineLazyPreferenceGetter(
   this,
   "gAddonAbuseReportEnabled",
@@ -626,17 +636,19 @@ function showFxaToolbarMenu(enable) {
     // because it could show an invalid icon if the user is logged in and no sync
     // event was performed yet.
     gSync.maybeUpdateUIState();
 
     Services.telemetry.setEventRecordingEnabled("fxa_avatar_menu", true);
 
     // We set an attribute here so that we can toggle the custom
     // badge depending on whether the FxA menu was ever accessed.
-    if (!gFxaToolbarAccessed) {
+    // If badging is handled by Messaging System we shouldn't set
+    // the attribute.
+    if (!gFxaToolbarAccessed && !gMsgingSystemFxABadge) {
       mainWindowEl.setAttribute("fxa_avatar_badged", "badged");
     } else {
       mainWindowEl.removeAttribute("fxa_avatar_badged");
     }
   } else {
     mainWindowEl.removeAttribute("fxatoolbarmenu");
   }
 }
--- a/browser/base/content/browser.xhtml
+++ b/browser/base/content/browser.xhtml
@@ -844,17 +844,19 @@
                   defaultPlaceholder="&urlbar.placeholder2;"
                   focused="true"
                   pageproxystate="invalid">
               <!-- Use onclick instead of normal popup= syntax since the popup
                    code fires onmousedown, and hence eats our favicon drag events. -->
               <box id="tracking-protection-icon-container" align="center"
                    role="button"
                    onclick="gProtectionsHandler.handleProtectionsButtonEvent(event);"
-                   onkeypress="gProtectionsHandler.handleProtectionsButtonEvent(event);">
+                   onkeypress="gProtectionsHandler.handleProtectionsButtonEvent(event);"
+                   onmouseover="gProtectionsHandler.onTrackingProtectionIconHoveredOrFocused();"
+                   onfocus="gProtectionsHandler.onTrackingProtectionIconHoveredOrFocused();">
                 <box id="tracking-protection-icon-box" animationsenabled="true">
                   <image id="tracking-protection-icon"/>
                   <box id="tracking-protection-icon-animatable-box" flex="1">
                     <image id="tracking-protection-icon-animatable-image" flex="1"/>
                   </box>
                 </box>
               </box>
               <box id="identity-box" role="button"
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -1016,25 +1016,28 @@ nsContextMenu.prototype = {
       );
     }
 
     if (!showFill || disableFill) {
       return;
     }
 
     let documentURI = gContextMenuContentData.documentURIObject;
+    let formOrigin = LoginHelper.getLoginOrigin(documentURI.spec);
     let fragment = LoginManagerContextMenu.addLoginsToMenu(
       this.targetIdentifier,
       this.browser,
-      documentURI
+      formOrigin
     );
     let isGeneratedPasswordEnabled =
       LoginHelper.generationAvailable && LoginHelper.generationEnabled;
     let canFillGeneratedPassword =
-      this.onPassword && isGeneratedPasswordEnabled;
+      this.onPassword &&
+      isGeneratedPasswordEnabled &&
+      Services.logins.getLoginSavingEnabled(formOrigin);
 
     this.showItem("fill-login-no-logins", !fragment);
     this.showItem("fill-login-generated-password", canFillGeneratedPassword);
     this.showItem("generated-password-separator", canFillGeneratedPassword);
 
     this.setItemAttr(
       "fill-login-generated-password",
       "disabled",
--- a/browser/base/content/test/performance/browser.ini
+++ b/browser/base/content/test/performance/browser.ini
@@ -2,16 +2,17 @@
 # to avoid overhead when running the browser normally, startupRecorder.js will
 # do almost nothing unless browser.startup.record is true.
 # gfx.canvas.willReadFrequently.enable is just an optimization, but needs to be
 # set during early startup to have an impact as a canvas will be used by
 # startupRecorder.js
 prefs =
   # Skip migration work in BG__migrateUI for browser_startup.js since it isn't
   # representative of common startup.
+  browser.messaging-system.fxatoolbarbadge.enabled=false # Bug 1570336
   browser.migration.version=9999999
   browser.urlbar.quantumbar=true
   browser.startup.record=true
   gfx.canvas.willReadFrequently.enable=true
   # The form autofill framescript is only used in certain locales if this
   # pref is set to 'detect', which is the default value on non-Nightly.
   extensions.formautofill.available='on'
 support-files =
--- a/browser/base/content/test/siteProtections/browser_protections_UI.js
+++ b/browser/base/content/test/siteProtections/browser_protections_UI.js
@@ -125,53 +125,53 @@ add_task(async function testSettingsButt
   let popuphiddenPromise = BrowserTestUtils.waitForEvent(
     protectionsPopup,
     "popuphidden"
   );
   let newTabPromise = BrowserTestUtils.waitForNewTab(
     gBrowser,
     "about:preferences#privacy"
   );
-  gProtectionsHandler._protectionPopupSettingsButton.click();
+  gProtectionsHandler._protectionsPopupSettingsButton.click();
 
   // The protection popup should be hidden after clicking settings button.
   await popuphiddenPromise;
   // Wait until the about:preferences has been opened correctly.
   let newTab = await newTabPromise;
 
   ok(true, "about:preferences has been opened successfully");
 
   BrowserTestUtils.removeTab(newTab);
   BrowserTestUtils.removeTab(tab);
 });
 
 /**
- * A test for the 'Show Full Report' button in the footer seciton.
+ * A test for the 'Show Full Report' button in the footer section.
  */
-add_task(async function testShowFullReportLink() {
+add_task(async function testShowFullReportButton() {
   // Open a tab and its protection panel.
   let tab = await BrowserTestUtils.openNewForegroundTab(
     gBrowser,
     "https://example.com"
   );
   await openProtectionsPanel();
 
   let popuphiddenPromise = BrowserTestUtils.waitForEvent(
     protectionsPopup,
     "popuphidden"
   );
   let newTabPromise = BrowserTestUtils.waitForNewTab(
     gBrowser,
     "about:protections"
   );
-  let showFullReportLink = document.getElementById(
-    "protections-popup-show-full-report-link"
+  let showFullReportButton = document.getElementById(
+    "protections-popup-show-report-button"
   );
 
-  showFullReportLink.click();
+  showFullReportButton.click();
 
   // The protection popup should be hidden after clicking the link.
   await popuphiddenPromise;
   // Wait until the 'about:protections' has been opened correctly.
   let newTab = await newTabPromise;
 
   ok(true, "about:protections has been opened successfully");
 
--- a/browser/base/content/test/siteProtections/head.js
+++ b/browser/base/content/test/siteProtections/head.js
@@ -12,16 +12,20 @@ var protectionsPopupHeader = document.ge
 async function openProtectionsPanel(toast) {
   let popupShownPromise = BrowserTestUtils.waitForEvent(
     protectionsPopup,
     "popupshown"
   );
   let shieldIconContainer = document.getElementById(
     "tracking-protection-icon-container"
   );
+
+  // Focus to the icon container in order to fetch tracker count.
+  shieldIconContainer.focus();
+
   if (!toast) {
     EventUtils.synthesizeMouseAtCenter(shieldIconContainer, {});
   } else {
     gProtectionsHandler.showProtectionsPopup({ toast });
   }
 
   await popupShownPromise;
 }
--- a/browser/base/content/test/sync/browser_sync.js
+++ b/browser/base/content/test/sync/browser_sync.js
@@ -17,16 +17,19 @@ add_task(async function test_ui_state_no
 
   Services.obs.notifyObservers(null, UIState.ON_UPDATE);
   ok(called);
 
   gSync.updateAllUI = updateAllUI;
 });
 
 add_task(async function test_ui_state_signedin() {
+  const msBadgeEnabled = Services.prefs.getBoolPref(
+    "browser.messaging-system.fxatoolbarbadge.enabled"
+  );
   const relativeDateAnchor = new Date();
   let state = {
     status: UIState.STATUS_SIGNED_IN,
     email: "foo@bar.com",
     displayName: "Foo Bar",
     avatarURL: "https://foo.bar",
     lastSync: new Date(),
     syncing: false,
@@ -47,17 +50,19 @@ add_task(async function test_ui_state_si
     label: "Foo Bar",
     fxastatus: "signedin",
     syncing: false,
     syncNowTooltip: lastSyncTooltip,
   });
   checkRemoteTabsPanel("PanelUI-remotetabs-main", false);
   checkMenuBarItem("sync-syncnowitem");
   checkFxaToolbarButtonPanel("PanelUI-fxa-menu");
-  checkFxAAvatar("signedin");
+  if (!msBadgeEnabled) {
+    checkFxAAvatar("signedin");
+  }
   gSync.relativeTimeFormat = origRelativeTimeFormat;
 });
 
 add_task(async function test_ui_state_syncing() {
   let state = {
     status: UIState.STATUS_SIGNED_IN,
     email: "foo@bar.com",
     displayName: "Foo Bar",
@@ -77,30 +82,35 @@ add_task(async function test_ui_state_sy
     lastSync: new Date(),
     syncing: false,
   });
   // Because we switch from syncing to non-syncing, and there's a timeout involved.
   await promiseObserver("test:browser-sync:activity-stop");
 });
 
 add_task(async function test_ui_state_unconfigured() {
+  const msBadgeEnabled = Services.prefs.getBoolPref(
+    "browser.messaging-system.fxatoolbarbadge.enabled"
+  );
   let state = {
     status: UIState.STATUS_NOT_CONFIGURED,
   };
 
   gSync.updateAllUI(state);
 
   let signedOffLabel = gSync.appMenuStatus.getAttribute("defaultlabel");
   checkPanelUIStatusBar({
     label: signedOffLabel,
   });
   checkRemoteTabsPanel("PanelUI-remotetabs-setupsync");
   checkMenuBarItem("sync-setup");
   checkFxaToolbarButtonPanel("PanelUI-fxa-signin");
-  checkFxAAvatar("not_configured");
+  if (!msBadgeEnabled) {
+    checkFxAAvatar("not_configured");
+  }
 });
 
 add_task(async function test_ui_state_unverified() {
   let state = {
     status: UIState.STATUS_NOT_VERIFIED,
     email: "foo@bar.com",
     lastSync: new Date(),
     syncing: false,
@@ -254,16 +264,26 @@ async function checkFxaToolbarButtonPane
   document.getElementById("PanelUI-fxa-menu").click();
   await promisePanelOpen;
   checkItemsDisplayed(
     ["PanelUI-fxa-signin", "PanelUI-fxa-unverified", "PanelUI-fxa-menu"],
     expectedShownItemId
   );
 }
 
+async function checkFxABadged() {
+  const button = document.getElementById("fxa-toolbar-menu-button");
+  await BrowserTestUtils.waitForCondition(() => {
+    return button.querySelector("label.feature-callout");
+  });
+  const badge = button.querySelector("label.feature-callout");
+  ok(badge, "expected feature-callout style badge");
+  ok(BrowserTestUtils.is_visible(badge), "expected the badge to be visible");
+}
+
 // fxaStatus is one of 'not_configured', 'unverified', or 'signedin'.
 function checkFxAAvatar(fxaStatus) {
   const avatarContainers = [
     document.getElementById("appMenu-fxa-avatar"),
     document.getElementById("fxa-avatar-image"),
   ];
   for (const avatar of avatarContainers) {
     const avatarURL = getComputedStyle(avatar).listStyleImage;
--- a/browser/components/about/AboutProtectionsHandler.jsm
+++ b/browser/components/about/AboutProtectionsHandler.jsm
@@ -311,20 +311,37 @@ var AboutProtectionsHandler = {
           dataToSend[timestamp] = dataToSend[timestamp] || { total: 0 };
           dataToSend[timestamp][idToTextMap.get(type)] = count;
           dataToSend[timestamp].total += count;
           // Record the largest amount of tracking events found per day,
           // to create the tallest column on the graph and compare other days to.
           if (largest < dataToSend[timestamp].total) {
             largest = dataToSend[timestamp].total;
           }
-          dataToSend.largest = largest;
         }
+        dataToSend.largest = largest;
         dataToSend.earliestDate = earliestDate;
         dataToSend.sumEvents = sumEvents;
+
+        let weekdays = Services.intl.getDisplayNames(undefined, {
+          style: "short",
+          keys: [
+            "dates/gregorian/weekdays/sunday",
+            "dates/gregorian/weekdays/monday",
+            "dates/gregorian/weekdays/tuesday",
+            "dates/gregorian/weekdays/wednesday",
+            "dates/gregorian/weekdays/thursday",
+            "dates/gregorian/weekdays/friday",
+            "dates/gregorian/weekdays/saturday",
+            "dates/gregorian/weekdays/sunday",
+          ],
+        });
+        weekdays = Object.values(weekdays.values);
+        dataToSend.weekdays = weekdays;
+
         this.sendMessage(
           aMessage.target,
           "SendContentBlockingRecords",
           dataToSend
         );
         break;
       case "FetchMonitorData":
         this.sendMessage(
--- a/browser/components/aboutlogins/content/aboutLogins.html
+++ b/browser/components/aboutlogins/content/aboutLogins.html
@@ -84,17 +84,17 @@
         <span class="count" data-l10n-id="login-list-count" data-l10n-args='{"count": 0}'></span>
       </div>
       <ol role="listbox" tabindex="0" data-l10n-id="login-list">
       </ol>
       <button class="create-login-button" data-l10n-id="create-login-button"></button>
     </template>
 
     <template id="login-list-item-template">
-      <li class="login-list-item">
+      <li class="login-list-item" role="option">
         <span class="title"></span>
         <span class="username"></span>
       </li>
     </template>
 
     <template id="login-item-template">
       <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/common.css">
--- a/browser/components/aboutlogins/content/components/login-list-item.js
+++ b/browser/components/aboutlogins/content/components/login-list-item.js
@@ -5,46 +5,54 @@
 /**
  * LoginListItemFactory is used instead of the "login-list-item" custom element
  * since there may be 100s of login-list-items on about:logins and each
  * custom element carries with it significant overhead when used in large
  * numbers.
  */
 export default class LoginListItemFactory {
   static create(login) {
-    let loginListItemTemplate = document.querySelector(
-      "#login-list-item-template"
-    );
-    let loginListItem = loginListItemTemplate.content.cloneNode(true);
-    let listItem = loginListItem.querySelector("li");
-    let title = loginListItem.querySelector(".title");
-    let username = loginListItem.querySelector(".username");
+    let template = document.querySelector("#login-list-item-template");
+    let fragment = template.content.cloneNode(true);
+    let listItem = fragment.firstElementChild;
+
+    LoginListItemFactory.update(listItem, login);
 
-    listItem.setAttribute("role", "option");
+    return listItem;
+  }
 
+  static update(listItem, login) {
+    let title = listItem.querySelector(".title");
+    let username = listItem.querySelector(".username");
     if (!login.guid) {
       listItem.id = "new-login-list-item";
       document.l10n.setAttributes(title, "login-list-item-title-new-login");
       document.l10n.setAttributes(
         username,
         "login-list-item-subtitle-new-login"
       );
-      return listItem;
+      return;
     }
 
     // Prepend the ID with a string since IDs must not begin with a number.
-    listItem.id = "lli-" + login.guid;
-    listItem.dataset.guid = login.guid;
+    if (!listItem.id) {
+      listItem.id = "lli-" + login.guid;
+      listItem.dataset.guid = login.guid;
+    }
     listItem._login = login;
-    title.textContent = login.title;
-    if (login.username.trim()) {
-      username.removeAttribute("data-l10n-id");
-      username.textContent = login.username.trim();
+    if (title.textContent != login.title) {
+      title.textContent = login.title;
+    }
+
+    let trimmedUsernameValue = login.username.trim();
+    if (trimmedUsernameValue) {
+      if (username.textContent != trimmedUsernameValue) {
+        username.removeAttribute("data-l10n-id");
+        username.textContent = trimmedUsernameValue;
+      }
     } else {
       document.l10n.setAttributes(
         username,
         "login-list-item-subtitle-missing-username"
       );
     }
-
-    return listItem;
   }
 }
--- a/browser/components/aboutlogins/content/components/login-list.js
+++ b/browser/components/aboutlogins/content/components/login-list.js
@@ -10,17 +10,20 @@ const sortFnOptions = {
   name: (a, b) => collator.compare(a.title, b.title),
   "last-used": (a, b) => a.timeLastUsed < b.timeLastUsed,
   "last-changed": (a, b) => a.timePasswordChanged < b.timePasswordChanged,
 };
 
 export default class LoginList extends HTMLElement {
   constructor() {
     super();
-    this._logins = [];
+    // An array of login GUIDs, stored in sorted order.
+    this._loginGuidsSortedOrder = [];
+    // A map of login GUID -> {login, listItem}.
+    this._logins = {};
     this._filter = "";
     this._selectedGuid = null;
     this._blankLoginListItem = LoginListItemFactory.create({});
   }
 
   connectedCallback() {
     if (this.shadowRoot) {
       return;
@@ -28,141 +31,124 @@ export default class LoginList extends H
     let loginListTemplate = document.querySelector("#login-list-template");
     let shadowRoot = this.attachShadow({ mode: "open" });
     document.l10n.connectRoot(shadowRoot);
     shadowRoot.appendChild(loginListTemplate.content.cloneNode(true));
 
     this._count = shadowRoot.querySelector(".count");
     this._createLoginButton = shadowRoot.querySelector(".create-login-button");
     this._list = shadowRoot.querySelector("ol");
+    this._list.appendChild(this._blankLoginListItem);
     this._sortSelect = shadowRoot.querySelector("#login-sort");
 
     this.render();
 
     this.shadowRoot
       .getElementById("login-sort")
       .addEventListener("change", this);
     window.addEventListener("AboutLoginsClearSelection", this);
     window.addEventListener("AboutLoginsCreateLogin", this);
     window.addEventListener("AboutLoginsLoginSelected", this);
     window.addEventListener("AboutLoginsFilterLogins", this);
     this._list.addEventListener("click", this);
     this.addEventListener("keydown", this);
     this._createLoginButton.addEventListener("click", this);
   }
 
-  /**
-   *
-   * @param {object} options optional
-   *                         createLogin: When set to true will show and select
-   *                                      a blank login-list-item.
-   */
-  async render(options = {}) {
-    this._list.textContent = "";
+  async render() {
+    let visibleLoginGuids = this._applyFilter();
+    this._updateVisibleLoginCount(visibleLoginGuids.size);
 
-    if (options.createLogin) {
-      this._blankLoginListItem.classList.add("selected");
-      this._blankLoginListItem.setAttribute("aria-selected", "true");
-      this._list.setAttribute(
-        "aria-activedescendant",
-        this._blankLoginListItem.id
-      );
-      this._list.appendChild(this._blankLoginListItem);
-    } else {
-      this._blankLoginListItem.remove();
+    // Add all of the logins that are not in the DOM yet.
+    let fragment = document.createDocumentFragment();
+    for (let guid of this._loginGuidsSortedOrder) {
+      if (this._logins[guid].listItem) {
+        continue;
+      }
+      let login = this._logins[guid].login;
+      let listItem = LoginListItemFactory.create(login);
+      this._logins[login.guid] = Object.assign(this._logins[login.guid], {
+        listItem,
+      });
+      fragment.appendChild(listItem);
     }
-
-    if (!this._logins.length) {
-      document.l10n.setAttributes(this._count, "login-list-count", {
-        count: 0,
-      });
-      return;
-    }
+    this._list.appendChild(fragment);
 
-    let visibleLogins = this._applyFilter();
-    document.l10n.setAttributes(this._count, "login-list-count", {
-      count: visibleLogins.length,
-    });
-
-    let fragment = document.createDocumentFragment();
-    let chunkSize = 5;
-    for (let i = 0; i < this._logins.length; i++) {
-      let login = this._logins[i];
-      let listItem = LoginListItemFactory.create(login);
-      if (login.guid == this._selectedGuid) {
+    // Show, hide, and update state of the list items per the applied search filter.
+    for (let guid of this._loginGuidsSortedOrder) {
+      let { listItem } = this._logins[guid];
+      if (guid == this._selectedGuid) {
         this._setListItemAsSelected(listItem);
       }
       if (
         this._breachesByLoginGUID &&
-        this._breachesByLoginGUID.has(login.guid)
+        this._breachesByLoginGUID.has(listItem.dataset.guid)
       ) {
         listItem.classList.add("breached");
       }
-      if (!visibleLogins.includes(login.guid)) {
-        listItem.hidden = true;
-      }
 
-      fragment.appendChild(listItem);
+      listItem.hidden = !visibleLoginGuids.has(listItem.dataset.guid);
+    }
+    this._blankLoginListItem.hidden = this._selectedGuid != null;
 
-      // Display a first chunk of logins ASAP to improve perceived performance,
-      // then append progressively larger chunks until complete.
-      if (i == chunkSize) {
-        this._list.appendChild(fragment);
-        await new Promise(resolve => requestAnimationFrame(resolve));
-        chunkSize *= chunkSize;
-      }
+    // Re-arrange the login-list-items according to their sort
+    for (let i = this._loginGuidsSortedOrder.length - 1; i >= 0; i--) {
+      let guid = this._loginGuidsSortedOrder[i];
+      let { listItem } = this._logins[guid];
+      this._list.insertBefore(
+        listItem,
+        this._blankLoginListItem.nextElementSibling
+      );
     }
-    this._list.appendChild(fragment);
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "click": {
         if (event.originalTarget == this._createLoginButton) {
           window.dispatchEvent(new CustomEvent("AboutLoginsCreateLogin"));
           recordTelemetryEvent({ object: "new_login", method: "new" });
           return;
         }
 
-        let loginListItem = event.originalTarget.closest(".login-list-item");
-        if (!loginListItem || !loginListItem.dataset.guid) {
+        let listItem = event.originalTarget.closest(".login-list-item");
+        if (!listItem || !listItem.dataset.guid) {
           return;
         }
 
         this.dispatchEvent(
           new CustomEvent("AboutLoginsLoginSelected", {
             bubbles: true,
             composed: true,
-            detail: loginListItem._login,
+            detail: listItem._login,
           })
         );
 
         recordTelemetryEvent({ object: "existing_login", method: "select" });
         break;
       }
       case "change": {
-        const sort = this._sortSelect.value;
-        this._logins = this._logins.sort((a, b) => sortFnOptions[sort](a, b));
+        this._applySort();
         this.render();
         break;
       }
       case "AboutLoginsClearSelection": {
-        if (!this._logins.length) {
+        if (!this._loginGuidsSortedOrder.length) {
           return;
         }
         window.dispatchEvent(
           new CustomEvent("AboutLoginsLoginSelected", {
             detail: this._logins[0],
           })
         );
         break;
       }
       case "AboutLoginsCreateLogin": {
         this._selectedGuid = null;
-        this.render({ createLogin: true });
+        this._setListItemAsSelected(this._blankLoginListItem);
         break;
       }
       case "AboutLoginsFilterLogins": {
         this._filter = event.detail.toLocaleLowerCase();
         this.render();
         break;
       }
       case "AboutLoginsLoginSelected": {
@@ -186,26 +172,28 @@ export default class LoginList extends H
       }
     }
   }
 
   /**
    * @param {login[]} logins An array of logins used for displaying in the list.
    */
   setLogins(logins) {
-    this._logins = logins;
-    const sort = this._sortSelect.value;
-    this._logins = this._logins.sort((a, b) => sortFnOptions[sort](a, b));
-
+    this._loginGuidsSortedOrder = [];
+    this._logins = logins.reduce((map, login) => {
+      this._loginGuidsSortedOrder.push(login.guid);
+      map[login.guid] = { login };
+      return map;
+    }, {});
+    this._applySort();
+    this._list.textContent = "";
+    this._list.appendChild(this._blankLoginListItem);
     this.render();
 
-    if (
-      !this._selectedGuid ||
-      !this._logins.findIndex(login => login.guid == this._selectedGuid) != -1
-    ) {
+    if (!this._selectedGuid || !this._logins[this._selectedGuid]) {
       // Select the first visible login after any possible filter is applied.
       let firstVisibleListItem = this._list.querySelector(
         ".login-list-item[data-guid]:not([hidden])"
       );
       if (firstVisibleListItem) {
         this._selectedGuid = firstVisibleListItem.dataset.guid;
         this._setListItemAsSelected(firstVisibleListItem);
         window.dispatchEvent(
@@ -213,77 +201,128 @@ export default class LoginList extends H
             detail: firstVisibleListItem._login,
           })
         );
       }
     }
   }
 
   /**
-   * @param {Map} breachesByLoginGUID A Map of breaches by login GUIDs used for displaying breached login indicators.
+   * @param {Map} breachesByLoginGUID A Map of breaches by login GUIDs used
+   *                                  for displaying breached login indicators.
    */
   updateBreaches(breachesByLoginGUID) {
     this._breachesByLoginGUID = breachesByLoginGUID;
     this.render();
   }
 
   /**
    * @param {login} login A login that was added to storage.
    */
   loginAdded(login) {
-    this._logins.push(login);
+    this._logins[login.guid] = { login };
+    this._loginGuidsSortedOrder.push(login.guid);
+    this._applySort();
+
+    // Add the list item and update any other related state that may pertain
+    // to the list item such as breach alerts.
     this.render();
   }
 
   /**
-   * @param {login} login A login that was modified in storage. The related login-list-item
-   *                       will get updated.
+   * @param {login} login A login that was modified in storage. The related
+   *                      login-list-item will get updated.
    */
   loginModified(login) {
-    for (let i = 0; i < this._logins.length; i++) {
-      if (this._logins[i].guid == login.guid) {
-        this._logins[i] = login;
-        break;
-      }
-    }
+    this._logins[login.guid] = Object.assign(this._logins[login.guid], {
+      login,
+    });
+    this._applySort();
+    let { listItem } = this._logins[login.guid];
+    LoginListItemFactory.update(listItem, login);
+
+    // Update any other related state that may pertain to the list item
+    // such as breach alerts that may or may not now apply.
     this.render();
   }
 
   /**
-   * @param {login} login A login that was removed from storage. The related login-list-item
-   *                      will get removed. The login object is a plain JS object
-   *                      representation of nsILoginInfo/nsILoginMetaInfo.
+   * @param {login} login A login that was removed from storage. The related
+   *                      login-list-item will get removed. The login object
+   *                      is a plain JS object representation of
+   *                      nsILoginInfo/nsILoginMetaInfo.
    */
   loginRemoved(login) {
-    this._logins = this._logins.filter(l => l.guid != login.guid);
-    this.render();
+    this._logins[login.guid].listItem.remove();
+
+    // Update the selected list item to the previous item in the list
+    // if one exists, otherwise the next item. If no logins remain
+    // the login-intro text will be shown instead of the login-list.
+    if (this._selectedGuid == login.guid) {
+      let index = this._loginGuidsSortedOrder.indexOf(login.guid);
+      if (this._loginGuidsSortedOrder.length > 1) {
+        let newlySelectedIndex = index > 0 ? index - 1 : index + 1;
+        let newlySelectedListItem = this._logins[
+          this._loginGuidsSortedOrder[newlySelectedIndex]
+        ].listItem;
+        this._setListItemAsSelected(newlySelectedListItem);
+      }
+    }
+
+    delete this._logins[login.guid];
+    this._loginGuidsSortedOrder = this._loginGuidsSortedOrder.filter(guid => {
+      return guid != login.guid;
+    });
+
+    let visibleLoginGuids = this._applyFilter();
+    this._updateVisibleLoginCount(visibleLoginGuids.size);
+
+    // Since the login has been removed, we don't need to call render
+    // as nothing related to the login needs updating.
   }
 
   /**
-   * Filters the displayed logins in the list to only those matching the
-   * cached filter value.
+   * @returns {Set} Set of login guids that match the filter.
    */
   _applyFilter() {
     let matchingLoginGuids;
     if (this._filter) {
-      matchingLoginGuids = this._logins
-        .filter(login => {
+      matchingLoginGuids = new Set(
+        this._loginGuidsSortedOrder.filter(guid => {
+          let { login } = this._logins[guid];
           return (
             login.origin.toLocaleLowerCase().includes(this._filter) ||
             login.username.toLocaleLowerCase().includes(this._filter)
           );
         })
-        .map(login => login.guid);
+      );
     } else {
-      matchingLoginGuids = this._logins.map(login => login.guid);
+      matchingLoginGuids = new Set([...this._loginGuidsSortedOrder]);
     }
 
     return matchingLoginGuids;
   }
 
+  _applySort() {
+    const sort = this._sortSelect.value;
+    this._loginGuidsSortedOrder = this._loginGuidsSortedOrder.sort((a, b) => {
+      let loginA = this._logins[a].login;
+      let loginB = this._logins[b].login;
+      return sortFnOptions[sort](loginA, loginB);
+    });
+  }
+
+  _updateVisibleLoginCount(count) {
+    if (count != document.l10n.getAttributes(this._count).args.count) {
+      document.l10n.setAttributes(this._count, "login-list-count", {
+        count,
+      });
+    }
+  }
+
   _handleKeyboardNav(event) {
     if (
       this._createLoginButton == this.shadowRoot.activeElement &&
       event.key == "Tab"
     ) {
       // Bug 1562716: Pressing Tab from the create-login-button cycles back to the
       // login-sort dropdown due to the login-list having `overflow`
       // CSS property set. Explicitly forward focus here until
@@ -361,18 +400,16 @@ export default class LoginList extends H
   }
 
   _setListItemAsSelected(listItem) {
     let oldSelectedItem = this._list.querySelector(".selected");
     if (oldSelectedItem) {
       oldSelectedItem.classList.remove("selected");
       oldSelectedItem.removeAttribute("aria-selected");
     }
-    if (listItem.dataset.guid) {
-      this._blankLoginListItem.remove();
-    }
+    this._blankLoginListItem.hidden = !!listItem.dataset.guid;
     listItem.classList.add("selected");
     listItem.setAttribute("aria-selected", "true");
     this._list.setAttribute("aria-activedescendant", listItem.id);
     this._selectedGuid = listItem.dataset.guid;
   }
 }
 customElements.define("login-list", LoginList);
--- a/browser/components/aboutlogins/tests/browser/browser_aaa_eventTelemetry_run_first.js
+++ b/browser/components/aboutlogins/tests/browser/browser_aaa_eventTelemetry_run_first.js
@@ -1,16 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 requestLongerTimeout(2);
 
 ChromeUtils.import("resource://testing-common/TelemetryTestUtils.jsm", this);
 
 function waitForTelemetryEventCount(count) {
+  info("waiting for telemetry event count of " + count);
   return TestUtils.waitForCondition(() => {
     let events = Services.telemetry.snapshotEvents(
       Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
       false
     ).content;
     return events && events.length == count;
   }, "waiting for telemetry event count of: " + count);
 }
@@ -72,27 +73,27 @@ add_task(async function test_telemetry_e
       ".copy-password-button"
     );
     copyButton.click();
   });
   await waitForTelemetryEventCount(3);
 
   let promiseNewTab = BrowserTestUtils.waitForNewTab(
     gBrowser,
-    TEST_LOGIN1.origin
+    TEST_LOGIN2.origin
   );
   await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
     let loginItem = content.document.querySelector("login-item");
     let openSiteButton = loginItem.shadowRoot.querySelector(
       ".open-site-button"
     );
     openSiteButton.click();
   });
   let newTab = await promiseNewTab;
-  ok(true, "New tab opened to " + TEST_LOGIN1.origin);
+  ok(true, "New tab opened to " + TEST_LOGIN2.origin);
   BrowserTestUtils.removeTab(newTab);
   await waitForTelemetryEventCount(4);
 
   // Show the password
   await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
     let loginItem = content.document.querySelector("login-item");
     let revealCheckbox = loginItem.shadowRoot.querySelector(
       ".reveal-password-checkbox"
--- a/browser/components/aboutlogins/tests/browser/browser_createLogin.js
+++ b/browser/components/aboutlogins/tests/browser/browser_createLogin.js
@@ -76,23 +76,23 @@ add_task(async function test_create_logi
     await ContentTask.spawn(
       browser,
       { expectedCount, originTuple },
       async ({ expectedCount: aExpectedCount, originTuple: aOriginTuple }) => {
         let loginList = Cu.waiveXrays(
           content.document.querySelector("login-list")
         );
         let loginFound = await ContentTaskUtils.waitForCondition(() => {
-          return loginList._logins.length == aExpectedCount;
+          return loginList._loginGuidsSortedOrder.length == aExpectedCount;
         }, "Waiting for login to be displayed");
         ok(loginFound, "Expected number of logins found in login-list");
 
         let loginListItem = [
           ...loginList.shadowRoot.querySelectorAll(".login-list-item"),
-        ].find(l => l._login.origin == aOriginTuple[1]);
+        ].find(l => l._login && l._login.origin == aOriginTuple[1]);
         ok(
           !!loginListItem,
           `Stored login should only include the origin of the URL provided during creation (${
             aOriginTuple[1]
           })`
         );
         is(
           loginListItem._login.username,
@@ -136,17 +136,19 @@ add_task(async function test_create_logi
     info("waiting for login to get modified in storage");
     await storageChangedPromised;
     info("login modified in storage");
 
     await ContentTask.spawn(browser, originTuple, async aOriginTuple => {
       let loginList = Cu.waiveXrays(
         content.document.querySelector("login-list")
       );
-      let login = loginList._logins.find(l => l.origin == aOriginTuple[1]);
+      let login = Object.values(loginList._logins).find(
+        obj => obj.login.origin == aOriginTuple[1]
+      ).login;
       ok(
         !!login,
         "Stored login should only include the origin of the URL provided during creation"
       );
       is(
         login.username,
         "testuser2",
         "Stored login should have modified username"
--- a/browser/components/aboutlogins/tests/browser/browser_deleteLogin.js
+++ b/browser/components/aboutlogins/tests/browser/browser_deleteLogin.js
@@ -15,19 +15,19 @@ add_task(async function setup() {
 
 add_task(async function test_show_logins() {
   let browser = gBrowser.selectedBrowser;
 
   await ContentTask.spawn(browser, [TEST_LOGIN1, TEST_LOGIN2], async logins => {
     let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
     let loginFound = await ContentTaskUtils.waitForCondition(() => {
       return (
-        loginList._logins.length == 2 &&
-        loginList._logins[0].guid == logins[0].guid &&
-        loginList._logins[1].guid == logins[1].guid
+        loginList._loginGuidsSortedOrder.length == 2 &&
+        loginList._loginGuidsSortedOrder.includes(logins[0].guid) &&
+        loginList._loginGuidsSortedOrder.includes(logins[1].guid)
       );
     }, "Waiting for logins to be displayed");
     ok(loginFound, "Newly added logins should be added to the page");
   });
 });
 
 add_task(async function test_login_item() {
   let browser = gBrowser.selectedBrowser;
--- a/browser/components/aboutlogins/tests/browser/browser_loginListChanges.js
+++ b/browser/components/aboutlogins/tests/browser/browser_loginListChanges.js
@@ -20,18 +20,18 @@ add_task(async function test_login_added
   };
   let browser = gBrowser.selectedBrowser;
   browser.messageManager.sendAsyncMessage("AboutLogins:LoginAdded", login);
 
   await ContentTask.spawn(browser, login, async addedLogin => {
     let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
     let loginFound = await ContentTaskUtils.waitForCondition(() => {
       return (
-        loginList._logins.length == 1 &&
-        loginList._logins[0].guid == addedLogin.guid
+        loginList._loginGuidsSortedOrder.length == 1 &&
+        loginList._loginGuidsSortedOrder[0] == addedLogin.guid
       );
     }, "Waiting for login to be added");
     ok(loginFound, "Newly added logins should be added to the page");
   });
 });
 
 add_task(async function test_login_modified() {
   let login = {
@@ -42,19 +42,20 @@ add_task(async function test_login_modif
   };
   let browser = gBrowser.selectedBrowser;
   browser.messageManager.sendAsyncMessage("AboutLogins:LoginModified", login);
 
   await ContentTask.spawn(browser, login, async modifiedLogin => {
     let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
     let loginFound = await ContentTaskUtils.waitForCondition(() => {
       return (
-        loginList._logins.length == 1 &&
-        loginList._logins[0].guid == modifiedLogin.guid &&
-        loginList._logins[0].username == modifiedLogin.username
+        loginList._loginGuidsSortedOrder.length == 1 &&
+        loginList._loginGuidsSortedOrder[0] == modifiedLogin.guid &&
+        loginList._logins[loginList._loginGuidsSortedOrder[0]].login.username ==
+          modifiedLogin.username
       );
     }, "Waiting for username to get updated");
     ok(loginFound, "The login should get updated on the page");
   });
 });
 
 add_task(async function test_login_removed() {
   let login = {
@@ -64,13 +65,13 @@ add_task(async function test_login_remov
     origin: "https://www.example.com",
   };
   let browser = gBrowser.selectedBrowser;
   browser.messageManager.sendAsyncMessage("AboutLogins:LoginRemoved", login);
 
   await ContentTask.spawn(browser, login, async removedLogin => {
     let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
     let loginRemoved = await ContentTaskUtils.waitForCondition(() => {
-      return loginList._logins.length == 0;
+      return loginList._loginGuidsSortedOrder.length == 0;
     }, "Waiting for login to get removed");
     ok(loginRemoved, "The login should be removed from the page");
   });
 });
--- a/browser/components/aboutlogins/tests/browser/browser_masterPassword.js
+++ b/browser/components/aboutlogins/tests/browser/browser_masterPassword.js
@@ -28,19 +28,19 @@ function waitForMPDialog(action) {
     return BrowserTestUtils.waitForEvent(window, "DOMModalDialogClosed");
   });
 }
 
 function waitForLoginCountToReach(browser, loginCount) {
   return ContentTask.spawn(browser, loginCount, async expectedLoginCount => {
     let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
     await ContentTaskUtils.waitForCondition(() => {
-      return loginList._logins.length == expectedLoginCount;
+      return loginList._loginGuidsSortedOrder.length == expectedLoginCount;
     });
-    return loginList._logins.length;
+    return loginList._loginGuidsSortedOrder.length;
   });
 }
 
 add_task(async function test() {
   TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
   LoginTestUtils.masterPassword.enable();
 
   let mpDialogShown = waitForMPDialog("cancel");
--- a/browser/components/aboutlogins/tests/browser/browser_openFiltered.js
+++ b/browser/components/aboutlogins/tests/browser/browser_openFiltered.js
@@ -38,17 +38,17 @@ add_task(async function test_query_param
   let browser = gBrowser.selectedBrowser;
   let vanillaLogins = [
     LoginHelper.loginToVanillaObject(TEST_LOGIN1),
     LoginHelper.loginToVanillaObject(TEST_LOGIN2),
   ];
   await ContentTask.spawn(browser, vanillaLogins, async logins => {
     let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
     await ContentTaskUtils.waitForCondition(() => {
-      return loginList._logins.length == 2;
+      return loginList._loginGuidsSortedOrder.length == 2;
     }, "Waiting for logins to be cached");
 
     let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
     await ContentTaskUtils.waitForCondition(
       () => loginItem._login.guid == logins[0].guid,
       "Waiting for TEST_LOGIN1 to be selected for the login-item view"
     );
 
@@ -77,16 +77,16 @@ add_task(async function test_query_param
       ".login-list-item:not([hidden])"
     );
     is(visibleLoginListItems.length, 1, "The one login should be visible");
     is(
       visibleLoginListItems[0].dataset.guid,
       logins[0].guid,
       "TEST_LOGIN1 should be visible"
     );
-    is(hiddenLoginListItems.length, 1, "One login should be hidden");
+    is(hiddenLoginListItems.length, 2, "One login should be hidden");
     is(
-      hiddenLoginListItems[0].dataset.guid,
+      hiddenLoginListItems[1].dataset.guid,
       logins[1].guid,
       "TEST_LOGIN2 should be hidden"
     );
   });
 });
--- a/browser/components/aboutlogins/tests/browser/browser_updateLogin.js
+++ b/browser/components/aboutlogins/tests/browser/browser_updateLogin.js
@@ -14,17 +14,18 @@ add_task(async function setup() {
 });
 
 add_task(async function test_show_logins() {
   let browser = gBrowser.selectedBrowser;
   await ContentTask.spawn(browser, TEST_LOGIN1.guid, async loginGuid => {
     let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
     let loginFound = await ContentTaskUtils.waitForCondition(() => {
       return (
-        loginList._logins.length == 1 && loginList._logins[0].guid == loginGuid
+        loginList._loginGuidsSortedOrder.length == 1 &&
+        loginList._loginGuidsSortedOrder[0] == loginGuid
       );
     }, "Waiting for login to be displayed");
     ok(loginFound, "Stored logins should be displayed upon loading the page");
   });
 });
 
 add_task(async function test_login_item() {
   let browser = gBrowser.selectedBrowser;
@@ -98,19 +99,20 @@ add_task(async function test_login_item(
       usernameInput = loginItem.shadowRoot.querySelector(
         "input[name='username']"
       );
       passwordInput = loginItem.shadowRoot.querySelector(
         "input[name='password']"
       );
       await ContentTaskUtils.waitForCondition(() => {
         loginListItem = Cu.waiveXrays(
-          loginList.shadowRoot.querySelector(".login-list-item")
+          loginList.shadowRoot.querySelector(".login-list-item[data-guid]")
         );
         return (
+          loginListItem._login &&
           loginListItem._login.username == usernameInput.value &&
           loginListItem._login.password == passwordInput.value
         );
       }, "Waiting for corresponding login in login list to update");
 
       ok(
         !loginItem.dataset.editing,
         "LoginItem should not be in 'edit' mode after saving"
@@ -126,18 +128,18 @@ add_task(async function test_login_item(
         content.document.querySelector("confirmation-dialog")
       );
       let confirmDeleteButton = confirmDeleteDialog.shadowRoot.querySelector(
         ".confirm-button"
       );
       confirmDeleteButton.click();
 
       await ContentTaskUtils.waitForCondition(() => {
-        loginListItem = Cu.waiveXrays(
-          loginList.shadowRoot.querySelector(".login-list-item")
+        loginListItem = loginList.shadowRoot.querySelector(
+          ".login-list-item[data-guid]"
         );
         return !loginListItem;
       }, "Waiting for login to be removed from list");
 
       ok(
         !loginItem.dataset.editing,
         "LoginItem should not be in 'edit' mode after deleting"
       );
--- a/browser/components/aboutlogins/tests/chrome/test_login_list.html
+++ b/browser/components/aboutlogins/tests/chrome/test_login_list.html
@@ -96,213 +96,248 @@ add_task(async function test_keyboard_na
   sendKey("TAB");
   let ol = gLoginList.shadowRoot.querySelector("ol");
   await SimpleTest.promiseWaitForCondition(() => ol.matches(":focus"),
     "waiting for 'ol' to get focus");
   ok(ol.matches(":focus"), "'ol' should be focused after tabbing to it");
 
   for (let [keyFwd, keyRev] of [["LEFT", "RIGHT"], ["DOWN", "UP"]]) {
     sendKey(keyFwd);
-    await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.children[1].id,
+    await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.querySelectorAll(".login-list-item:not([hidden])")[1].id,
       `waiting for second item in list to get focused (${keyFwd})`);
-    ok(ol.children[1].classList.contains("keyboard-selected"), `second item should be marked as keyboard-selected (${keyFwd})`);
+    ok(ol.querySelectorAll(".login-list-item:not([hidden])")[1].classList.contains("keyboard-selected"), `second item should be marked as keyboard-selected (${keyFwd})`);
 
     sendKey(keyRev);
-    await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.children[0].id,
+    await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.querySelectorAll(".login-list-item:not([hidden])")[0].id,
       `waiting for first item in list to get focused (${keyRev})`);
-    ok(ol.children[0].classList.contains("keyboard-selected"), `first item should be marked as keyboard-selected (${keyRev})`);
+    ok(ol.querySelectorAll(".login-list-item:not([hidden])")[0].classList.contains("keyboard-selected"), `first item should be marked as keyboard-selected (${keyRev})`);
   }
 
   sendKey("DOWN");
-  await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.children[1].id,
+  await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.querySelectorAll(".login-list-item:not([hidden])")[1].id,
     `waiting for second item in list to get focused (DOWN)`);
-  ok(ol.children[1].classList.contains("keyboard-selected"), `second item should be marked as keyboard-selected (DOWN)`);
-  let selectedGuid = ol.children[1].dataset.guid;
+  ok(ol.querySelectorAll(".login-list-item:not([hidden])")[1].classList.contains("keyboard-selected"), `second item should be marked as keyboard-selected (DOWN)`);
+  let selectedGuid = ol.querySelectorAll(".login-list-item:not([hidden])")[1].dataset.guid;
 
   let loginSelectedEvent = null;
   gLoginList.addEventListener("AboutLoginsLoginSelected", event => loginSelectedEvent = event, {once: true});
   sendKey("RETURN");
   is(ol.querySelector(".selected").dataset.guid, selectedGuid, "item should be marked as selected");
   ok(loginSelectedEvent, "AboutLoginsLoginSelected event should be dispatched on pressing Enter");
   is(loginSelectedEvent.detail.guid, selectedGuid, "event should have expected login attached");
 });
 
 add_task(async function test_empty_login_username_in_list() {
   // Clear the selection so the 'new' login will be in the list too.
   window.dispatchEvent(new CustomEvent("AboutLoginsLoginSelected", {
     detail: {},
   }));
 
   gLoginList.setLogins([TEST_LOGIN_3]);
-  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
+  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not([hidden])");
   is(loginListItems.length, 1, "The one stored login should be displayed");
   is(loginListItems[0].dataset.guid, TEST_LOGIN_3.guid, "login-list-item should have correct guid attribute");
   let loginUsername = loginListItems[0].querySelector(".username");
   is(loginUsername.getAttribute("data-l10n-id"), "login-list-item-subtitle-missing-username", "login should show missing username text");
 });
 
 add_task(async function test_populated_list() {
   gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2]);
-  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
+  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not([hidden])");
   is(loginListItems.length, 2, "The two stored logins should be displayed");
   is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item should have correct guid attribute");
   is(loginListItems[0].querySelector(".title").textContent, TEST_LOGIN_1.title,
      "login-list-item origin should match");
   is(loginListItems[0].querySelector(".username").textContent, TEST_LOGIN_1.username,
      "login-list-item username should match");
   ok(loginListItems[0].classList.contains("selected"), "The first item should be selected by default");
   ok(!loginListItems[1].classList.contains("selected"), "The second item should not be selected by default");
   loginListItems[0].click();
-  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
+  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not([hidden])");
   is(loginListItems.length, 2, "After selecting one, only the two stored logins should be displayed");
   ok(loginListItems[0].classList.contains("selected"), "The first item should be selected");
   ok(!loginListItems[1].classList.contains("selected"), "The second item should still not be selected");
 });
 
 add_task(async function test_breach_indicator() {
   gLoginList.updateBreaches(TEST_BREACHES_MAP);
-  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
+  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not([hidden])");
   ok(loginListItems[0].classList.contains("breached"), "The first login should have the .breached class.");
   ok(!loginListItems[1].classList.contains("breached"), "The second login should not have the .breached class");
 });
 
 add_task(async function test_filtered_list() {
   is(gLoginList.shadowRoot.querySelectorAll(".login-list-item:not([hidden])").length, 2, "Both logins should be visible");
   let countSpan = gLoginList.shadowRoot.querySelector(".count");
   is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should match full list length");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "user1",
   }));
   is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should match result amount");
-  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
+  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
   is(loginListItems[0].querySelector(".username").textContent, "user1", "user1 is expected first");
   ok(!loginListItems[0].hidden, "user1 should remain visible");
   ok(loginListItems[1].hidden, "user2 should be hidden");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "user2",
   }));
   is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should match result amount");
-  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
+  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
   ok(loginListItems[0].hidden, "user1 should be hidden");
   ok(!loginListItems[1].hidden, "user2 should be visible");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "user",
   }));
   is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should match result amount");
-  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
+  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
   ok(!loginListItems[0].hidden, "user1 should be visible");
   ok(!loginListItems[1].hidden, "user2 should be visible");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "foo",
   }));
   is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 0, "Count should match result amount");
-  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
+  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
   ok(loginListItems[0].hidden, "user1 should be hidden");
   ok(loginListItems[1].hidden, "user2 should be hidden");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "",
   }));
   is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should be reset to full list length");
-  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
+  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
   ok(!loginListItems[0].hidden, "user1 should be visible");
   ok(!loginListItems[1].hidden, "user2 should be visible");
 });
 
 add_task(async function test_login_modified() {
   let modifiedLogin = Object.assign(TEST_LOGIN_1, {username: "user11"});
   gLoginList.loginModified(modifiedLogin);
   await asyncElementRendered();
-  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
+  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]:not([hidden])");
   is(loginListItems.length, 2, "Both logins should be displayed");
   is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item should have correct guid attribute");
   is(loginListItems[0].querySelector(".title").textContent, TEST_LOGIN_1.title,
      "login-list-item origin should match");
   is(loginListItems[0].querySelector(".username").textContent, modifiedLogin.username,
      "login-list-item username should have been updated");
   is(loginListItems[1].querySelector(".username").textContent, TEST_LOGIN_2.username,
      "login-list-item2 username should remain unchanged");
 });
 
 add_task(async function test_login_added() {
-  let newLogin = Object.assign({}, TEST_LOGIN_1, {username: "user22", guid: "111222"});
+  info("selected sort: " + gLoginList.shadowRoot.getElementById("login-sort").selectedIndex);
+
+  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not([hidden])");
+  is(loginListItems.length, 2, "Should have two logins at start of test");
+  let newLogin = Object.assign({}, TEST_LOGIN_1, {title: "example2.example.com", guid: "111222"});
   gLoginList.loginAdded(newLogin);
   await asyncElementRendered();
-  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
+  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not([hidden])");
   is(loginListItems.length, 3, "New login should be added to the list");
   is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item1 should have correct guid attribute");
   is(loginListItems[1].dataset.guid, TEST_LOGIN_2.guid, "login-list-item2 should have correct guid attribute");
   is(loginListItems[2].dataset.guid, newLogin.guid, "login-list-item3 should have correct guid attribute");
   is(loginListItems[2].querySelector(".title").textContent, newLogin.title,
      "login-list-item origin should match");
   is(loginListItems[2].querySelector(".username").textContent, newLogin.username,
      "login-list-item username should have been updated");
 });
 
 add_task(async function test_login_removed() {
   gLoginList.loginRemoved({guid: "111222"});
   await asyncElementRendered();
-  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
+  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not([hidden])");
   is(loginListItems.length, 2, "New login should be removed from the list");
   is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item1 should have correct guid attribute");
   is(loginListItems[1].dataset.guid, TEST_LOGIN_2.guid, "login-list-item2 should have correct guid attribute");
 });
 
 add_task(async function test_login_added_filtered() {
   let countSpan = gLoginList.shadowRoot.querySelector(".count");
-  is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should match full list length");
+  is(document.l10n.getAttributes(countSpan).args.count, 2, "Count should match full list length");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
-    bubbles: true,
-    composed: true,
     detail: "user1",
   }));
   is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should match result amount");
 
-  let newLogin = Object.assign({}, TEST_LOGIN_1, {username: "user22", guid: "111222"});
+  let newLogin = Object.assign({}, TEST_LOGIN_1, {title: "example2.example.com", username: "user22", guid: "111222"});
   gLoginList.loginAdded(newLogin);
   await asyncElementRendered();
-  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
+  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
   is(loginListItems.length, 3, "New login should be added to the list");
   is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item1 should have correct guid attribute");
   is(loginListItems[1].dataset.guid, TEST_LOGIN_2.guid, "login-list-item2 should have correct guid attribute");
   is(loginListItems[2].dataset.guid, newLogin.guid, "login-list-item3 should have correct guid attribute");
   ok(!loginListItems[0].hidden, "login-list-item1 should be visible");
   ok(loginListItems[1].hidden, "login-list-item2 should be hidden");
   ok(loginListItems[2].hidden, "login-list-item3 should be hidden");
   is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should remain unchanged");
 });
 
 add_task(async function test_sorted_list() {
+  function dispatchChangeEvent(target) {
+    let event = document.createEvent("UIEvent");
+    event.initEvent("change", true, true);
+    target.dispatchEvent(event);
+  }
+
+  // Clear the filter
+  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+    detail: "",
+  }));
+
   // Clear the selection so the 'new' login will be in the list too.
   window.dispatchEvent(new CustomEvent("AboutLoginsLoginSelected", {
     detail: {},
   }));
 
+  // make sure that the logins have distinct orderings based on sort order
+  let [guid1, guid2, guid3] = gLoginList._loginGuidsSortedOrder;
+  gLoginList._logins[guid1].login.timeLastUsed = 0;
+  gLoginList._logins[guid2].login.timeLastUsed = 1;
+  gLoginList._logins[guid3].login.timeLastUsed = 2;
+  gLoginList._logins[guid1].login.title = "a";
+  gLoginList._logins[guid2].login.title = "b";
+  gLoginList._logins[guid3].login.title = "c";
+  gLoginList._logins[guid1].login.timePasswordChanged = 1;
+  gLoginList._logins[guid2].login.timePasswordChanged = 2;
+  gLoginList._logins[guid3].login.timePasswordChanged = 0;
+
   // sort by last used
-  gLoginList.shadowRoot.getElementById("login-sort").selectedIndex = 1;
-  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
+  let loginSort = gLoginList.shadowRoot.getElementById("login-sort");
+  loginSort.selectedIndex = 1;
+  dispatchChangeEvent(loginSort);
+  let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not([hidden])");
   is(loginListItems.length, 3, "The list should contain the three stored logins");
-  let timeUsed = loginListItems[0]._login.timeLastUsed;
+  let timeUsed1 = loginListItems[0]._login.timeLastUsed;
   let timeUsed2 = loginListItems[1]._login.timeLastUsed;
-  is(timeUsed2 > timeUsed, true, "Last used login should be displayed at top of list");
+  let timeUsed3 = loginListItems[2]._login.timeLastUsed;
+  is(timeUsed1 > timeUsed2, true, "Logins sorted by timeLastUsed. First: " + timeUsed1 + "; Second: " + timeUsed2);
+  is(timeUsed2 > timeUsed3, true, "Logins sorted by timeLastUsed. Second: " + timeUsed2 + "; Third: " + timeUsed3);
 
-  // sort by name
-  gLoginList.shadowRoot.getElementById("login-sort").selectedIndex = 0;
-  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
-  let title = loginListItems[0]._login.title;
+  // sort by title
+  loginSort.selectedIndex = 0;
+  dispatchChangeEvent(loginSort);
+  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not([hidden])");
+  let title1 = loginListItems[0]._login.title;
   let title2 = loginListItems[1]._login.title;
-  is(title.localeCompare(title2), -1, "Logins should be sorted alphabetically by hostname");
+  let title3 = loginListItems[2]._login.title;
+  is(title1.localeCompare(title2), -1, "Logins sorted by title. First: " + title1 + "; Second: " + title2);
+  is(title2.localeCompare(title3), -1, "Logins sorted by title. Second: " + title2 + "; Third: " + title3);
 
   // sort by last changed
-  gLoginList.shadowRoot.getElementById("login-sort").selectedIndex = 2;
-  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
-  let pwChanged = loginListItems[0]._login.timePasswordChanged;
+  loginSort.selectedIndex = 2;
+  dispatchChangeEvent(loginSort);
+  loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not([hidden])");
+  let pwChanged1 = loginListItems[0]._login.timePasswordChanged;
   let pwChanged2 = loginListItems[1]._login.timePasswordChanged;
-  is(pwChanged2 > pwChanged, true, "Login with most recently changed password should be displayed at top of list");
+  let pwChanged3 = loginListItems[2]._login.timePasswordChanged;
+  is(pwChanged1 > pwChanged2, true, "Logins sorted by timePasswordChanged. First: " + pwChanged1 + "; Second: " + pwChanged2);
+  is(pwChanged2 > pwChanged3, true, "Logins sorted by timePasswordChanged. Second: " + pwChanged2 + "; Third: " + pwChanged3);
 });
 </script>
 
 </body>
 </html>
--- a/browser/components/aboutlogins/tests/unit/test_getBreachesForLogins.js
+++ b/browser/components/aboutlogins/tests/unit/test_getBreachesForLogins.js
@@ -17,32 +17,46 @@ ChromeUtils.defineModuleGetter(
 
 const TEST_BREACHES = [
   {
     AddedDate: "2018-12-20T23:56:26Z",
     BreachDate: "2018-12-16",
     Domain: "breached.com",
     Name: "Breached",
     PwnCount: 1643100,
+    DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
     _status: "synced",
     id: "047940fe-d2fd-4314-b636-b4a952ee0043",
     last_modified: "1541615610052",
     schema: "1541615609018",
   },
   {
     AddedDate: "2018-12-20T23:56:26Z",
     BreachDate: "2018-12-16",
     Domain: "breached-subdomain.host.com",
     Name: "Only a Sub-Domain was Breached",
     PwnCount: 2754200,
+    DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
     _status: "synced",
     id: "047940fe-d2fd-4314-b636-b4a952ee0044",
     last_modified: "1541615610052",
     schema: "1541615609018",
   },
+  {
+    AddedDate: "2018-12-20T23:56:26Z",
+    BreachDate: "2018-12-16",
+    Domain: "breached-site-without-passwords.com",
+    Name: "Breached Site without passwords",
+    PwnCount: 987654,
+    DataClasses: ["Email addresses", "Usernames", "IP addresses"],
+    _status: "synced",
+    id: "047940fe-d2fd-4314-b636-b4a952ee0045",
+    last_modified: "1541615610052",
+    schema: "1541615609018",
+  },
 ];
 
 const NOT_BREACHED_LOGIN = LoginTestUtils.testData.formLogin({
   origin: "https://www.example.com",
   formActionOrigin: "https://www.example.com",
   username: "username",
   password: "password",
   timePasswordChanged: Date.now(),
@@ -62,16 +76,25 @@ const NOT_BREACHED_SUBDOMAIN_LOGIN = Log
 });
 const BREACHED_SUBDOMAIN_LOGIN = LoginTestUtils.testData.formLogin({
   origin: "https://breached-subdomain.host.com",
   formActionOrigin: "https://breached-subdomain.host.com",
   username: "username",
   password: "password",
   timePasswordChanged: new Date("2018-12-15").getTime(),
 });
+const LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS = LoginTestUtils.testData.formLogin(
+  {
+    origin: "https://breached-site-without-passwords.com",
+    formActionOrigin: "https://breached-site-without-passwords.com",
+    username: "username",
+    password: "password",
+    timePasswordChanged: new Date("2018-12-15").getTime(),
+  }
+);
 
 add_task(async function test_getBreachesForLogins_notBreachedLogin() {
   Services.logins.addLogin(NOT_BREACHED_LOGIN);
 
   const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
     [NOT_BREACHED_LOGIN],
     TEST_BREACHES
   );
@@ -118,8 +141,25 @@ add_task(async function test_getBreaches
     TEST_BREACHES
   );
   Assert.strictEqual(
     breachesByLoginGUID.size,
     1,
     "Should be 1 breached login: " + BREACHED_SUBDOMAIN_LOGIN.origin
   );
 });
+
+add_task(
+  async function test_getBreachesForLogins_breachedSiteWithoutPasswords() {
+    Services.logins.addLogin(LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS);
+
+    const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
+      [LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS],
+      TEST_BREACHES
+    );
+    Assert.strictEqual(
+      breachesByLoginGUID.size,
+      0,
+      "Should be 0 breached login: " +
+        LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS.origin
+    );
+  }
+);
--- a/browser/components/controlcenter/content/protectionsPanel.inc.xul
+++ b/browser/components/controlcenter/content/protectionsPanel.inc.xul
@@ -44,73 +44,81 @@
       </hbox>
 
       <!-- Tracking Protection Section -->
       <hbox id="tracking-protection-container" class="protections-popup-section">
         <vbox id="protections-popup-content" flex="1">
           <vbox id="protections-popup-category-list">
             <toolbarbutton id="protections-popup-category-tracking-protection"
                            onclick="gProtectionsHandler.showTrackersSubview();"
-                           class="protections-popup-category" align="center">
+                           class="protections-popup-category" align="center"
+                           wrap="true">
               <image class="protections-popup-category-icon tracking-protection-icon"/>
               <label flex="1" class="protections-popup-category-label">&contentBlocking.trackingProtection3.label;</label>
               <label flex="1" id="protections-popup-tracking-protection-state-label" class="protections-popup-category-state-label"/>
             </toolbarbutton>
             <toolbarbutton id="protections-popup-category-socialblock"
                            onclick="gProtectionsHandler.showSocialblockerSubview();"
                            class="protections-popup-category" align="center">
               <image class="protections-popup-category-icon socialblock-icon"/>
               <label flex="1"
               class="protections-popup-category-label">&contentBlocking.socialblock.label;</label>
               <label flex="1" id="protections-popup-socialblock-state-label" class="protections-popup-category-state-label"/>
             </toolbarbutton>
             <toolbarbutton id="protections-popup-category-cookies"
                            onclick="gProtectionsHandler.showCookiesSubview();"
-                           class="protections-popup-category" align="center">
+                           class="protections-popup-category" align="center"
+                           wrap="true">
               <image class="protections-popup-category-icon thirdpartycookies-icon"/>
               <label flex="1" id="protections-popup-category-label-default"
                      class="protections-popup-category-label">&contentBlocking.cookies.label;</label>
               <label flex="1" id="protections-popup-cookies-state-label" class="protections-popup-category-state-label"/>
             </toolbarbutton>
             <toolbarbutton id="protections-popup-category-cryptominers"
                            onclick="gProtectionsHandler.showCryptominersSubview();"
-                           class="protections-popup-category" align="center">
+                           class="protections-popup-category" align="center"
+                           wrap="true">
               <image class="protections-popup-category-icon cryptominers-icon"/>
               <label flex="1" class="protections-popup-category-label">&contentBlocking.cryptominers.label;</label>
               <label flex="1" id="protections-popup-cryptominers-state-label" class="protections-popup-category-state-label"/>
             </toolbarbutton>
             <toolbarbutton id="protections-popup-category-fingerprinters"
                            onclick="gProtectionsHandler.showFingerprintersSubview();"
-                           class="protections-popup-category" align="center">
+                           class="protections-popup-category" align="center"
+                           wrap="true">
               <image class="protections-popup-category-icon fingerprinters-icon"/>
               <label flex="1" class="protections-popup-category-label">&contentBlocking.fingerprinters.label;</label>
               <label flex="1" id="protections-popup-fingerprinters-state-label" class="protections-popup-category-state-label"/>
             </toolbarbutton>
           </vbox>
         </vbox>
       </hbox>
 
-
-      <vbox id="protections-popup-settings-section"
-            class="protections-popup-section">
+      <vbox id="protections-popup-footer" class="protections-popup-section">
         <toolbarbutton id="protections-popup-settings-button"
+                       class="protections-popup-footer-button"
                        oncommand="gProtectionsHandler.openPreferences();">
-          <image class="protection-settings-icon"/>
-          <label class="protections-popup-settings-label" flex="1">&protections.settings.label;</label>
+          <image class="protection-popup-footer-icon protections-popup-settings-icon"/>
+          <label class="protections-popup-footer-button-label" flex="1">&protections.settings.label;</label>
         </toolbarbutton>
+        <stack id="protections-popup-show-report-stack">
+          <toolbarbutton id="protections-popup-show-report-button"
+                         class="protections-popup-footer-button"
+                         oncommand="gProtectionsHandler.openProtections(true);">
+            <image class="protection-popup-footer-icon protections-popup-show-report-icon"/>
+            <label class="protections-popup-footer-button-label" flex="1">&protections.report.label;</label>
+          </toolbarbutton>
+          <hbox id="protections-popup-trackers-blocked-counter-box"
+                align="center"
+                right="0">
+            <description id="protections-popup-trackers-blocked-counter-description"
+                         onclick="gProtectionsHandler.openProtections(true);"/>
+          </hbox>
+        </stack>
       </vbox>
-
-      <hbox id="protections-popup-footer">
-        <description id="protections-popup-trackers-blocked-counter-description"
-                     flex="1"/>
-        <label id="protections-popup-show-full-report-link"
-               is="text-link"
-               useoriginprincipal="true"
-               href="about:protections">&protections.report.label;</label>
-      </hbox>
     </panelview>
 
     <!-- Site Not Working? SubView -->
     <panelview id="protections-popup-siteNotWorkingView"
                title="&protections.siteNotWorkingView.title;"
                descriptionheightworkaround="true"
                flex="1">
         <hbox id="protections-popup-siteNotWorkingView-header">
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -189,17 +189,16 @@ skip-if = os == "linux" && debug && bits
 [browser_ext_tabs_create_invalid_url.js]
 [browser_ext_tabs_detectLanguage.js]
 [browser_ext_tabs_discard.js]
 skip-if = !e10s
 [browser_ext_tabs_discarded.js]
 fail-if = fission
 [browser_ext_tabs_duplicate.js]
 [browser_ext_tabs_events.js]
-skip-if = true # Bug 1521363
 [browser_ext_tabs_events_order.js]
 [browser_ext_tabs_executeScript.js]
 skip-if = (verify && !debug && (os == 'mac'))
 [browser_ext_tabs_executeScript_good.js]
 [browser_ext_tabs_executeScript_bad.js]
 [browser_ext_tabs_executeScript_multiple.js]
 [browser_ext_tabs_executeScript_no_create.js]
 [browser_ext_tabs_executeScript_runAt.js]
--- a/browser/components/extensions/test/browser/browser_ext_tabs_events.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_events.js
@@ -72,23 +72,27 @@ add_task(async function test_tab_events_
           i in events && events[i].type,
           `Got expected ${name} event`
         );
       }
       return events.splice(0);
     }
 
     try {
-      let windows = await Promise.all([
-        browser.windows.create({ url: "about:blank", incognito }),
-        browser.windows.create({ url: "about:blank", incognito }),
-      ]);
+      let firstWindow = await browser.windows.create({
+        url: "about:blank",
+        incognito,
+      });
+      let otherWindow = await browser.windows.create({
+        url: "about:blank",
+        incognito,
+      });
 
-      let windowId = windows[0].id;
-      let otherWindowId = windows[1].id;
+      let windowId = firstWindow.id;
+      let otherWindowId = otherWindow.id;
 
       let created = await expectEvents(["onCreated", "onCreated"]);
       let initialTab = created[1].tab;
 
       browser.test.log("Create tab in window 1");
       let tab = await browser.tabs.create({
         windowId,
         index: 0,
--- a/browser/components/extensions/test/xpcshell/test_ext_urlbar.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_urlbar.js
@@ -31,16 +31,17 @@ add_task(async function startup() {
   Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
   await AddonTestUtils.promiseStartupManager();
 
   // Add a test engine and make it default so that when we do searches below,
   // Firefox doesn't try to include search suggestions from the actual default
   // engine from over the network.
   let engine = await Services.search.addEngineWithDetails("Test engine", {
     template: "http://example.com/?s=%S",
+    alias: "@testengine",
   });
   Services.search.defaultEngine = engine;
 
   // Set the notification timeout to a really high value to avoid intermittent
   // failures due to the mock extensions not responding in time.
   UrlbarProviderExtension.notificationTimeout = 5000;
 });
 
@@ -293,16 +294,196 @@ add_task(async function test_onProviderR
     heuristic: r.heuristic,
   }));
 
   Assert.deepEqual(actualResults, expectedResults);
 
   await ext.unload();
 });
 
+// Extensions can specify search engines using engine names, aliases, and URLs.
+add_task(async function test_onProviderResultsRequested_searchEngines() {
+  let ext = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["urlbar"],
+    },
+    isPrivileged: true,
+    incognitoOverride: "spanning",
+    background() {
+      browser.urlbar.onBehaviorRequested.addListener(query => {
+        return "restricting";
+      }, "test");
+      browser.urlbar.onResultsRequested.addListener(query => {
+        return [
+          {
+            type: "search",
+            source: "search",
+            payload: {
+              engine: "Test engine",
+              suggestion: "engine specified",
+            },
+          },
+          {
+            type: "search",
+            source: "search",
+            payload: {
+              keyword: "@testengine",
+              suggestion: "keyword specified",
+            },
+          },
+          {
+            type: "search",
+            source: "search",
+            payload: {
+              url: "http://example.com/?s",
+              suggestion: "url specified",
+            },
+          },
+          {
+            type: "search",
+            source: "search",
+            payload: {
+              engine: "Test engine",
+              keyword: "@testengine",
+              url: "http://example.com/?s",
+              suggestion: "engine, keyword, and url specified",
+            },
+          },
+          {
+            type: "search",
+            source: "search",
+            payload: {
+              keyword: "@testengine",
+              url: "http://example.com/?s",
+              suggestion: "keyword and url specified",
+            },
+          },
+          {
+            type: "search",
+            source: "search",
+            payload: {
+              suggestion: "no engine",
+            },
+          },
+          {
+            type: "search",
+            source: "search",
+            payload: {
+              engine: "bogus",
+              suggestion: "no matching engine",
+            },
+          },
+          {
+            type: "search",
+            source: "search",
+            payload: {
+              keyword: "@bogus",
+              suggestion: "no matching keyword",
+            },
+          },
+          {
+            type: "search",
+            source: "search",
+            payload: {
+              url: "http://bogus-no-search-engine.com/",
+              suggestion: "no matching url",
+            },
+          },
+          {
+            type: "search",
+            source: "search",
+            payload: {
+              url: "bogus",
+              suggestion: "invalid url",
+            },
+          },
+          {
+            type: "search",
+            source: "search",
+            payload: {
+              url: "foo:bar",
+              suggestion: "url with no hostname",
+            },
+          },
+        ];
+      }, "test");
+    },
+  });
+  await ext.startup();
+
+  // Run a query.
+  let context = new UrlbarQueryContext({
+    allowAutofill: false,
+    isPrivate: false,
+    maxResults: 10,
+    searchString: "test",
+  });
+  let controller = new UrlbarController({
+    browserWindow: {
+      location: {
+        href: AppConstants.BROWSER_CHROME_URL,
+      },
+    },
+  });
+  await controller.startQuery(context);
+
+  // Check the results.  The first several are valid and should include "Test
+  // engine" as the engine.  The others don't specify an engine and are
+  // therefore invalid, so they should be ignored.
+  let expectedResults = [
+    {
+      type: UrlbarUtils.RESULT_TYPE.SEARCH,
+      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+      engine: "Test engine",
+      title: "engine specified",
+      heuristic: false,
+    },
+    {
+      type: UrlbarUtils.RESULT_TYPE.SEARCH,
+      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+      engine: "Test engine",
+      title: "keyword specified",
+      heuristic: false,
+    },
+    {
+      type: UrlbarUtils.RESULT_TYPE.SEARCH,
+      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+      engine: "Test engine",
+      title: "url specified",
+      heuristic: false,
+    },
+    {
+      type: UrlbarUtils.RESULT_TYPE.SEARCH,
+      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+      engine: "Test engine",
+      title: "engine, keyword, and url specified",
+      heuristic: false,
+    },
+    {
+      type: UrlbarUtils.RESULT_TYPE.SEARCH,
+      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+      engine: "Test engine",
+      title: "keyword and url specified",
+      heuristic: false,
+    },
+  ];
+
+  let actualResults = context.results.map(r => ({
+    type: r.type,
+    source: r.source,
+    engine: r.payload.engine || null,
+    title: r.title,
+    heuristic: r.heuristic,
+  }));
+
+  Assert.deepEqual(actualResults, expectedResults);
+
+  await ext.unload();
+});
+
 // Adds two providers, one active and one inactive.  Only the active provider
 // should be asked to return results.
 add_task(async function test_activeAndInactiveProviders() {
   let ext = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["urlbar"],
     },
     isPrivileged: true,
--- a/browser/components/newtab/content-src/asrouter/docs/targeting-attributes.md
+++ b/browser/components/newtab/content-src/asrouter/docs/targeting-attributes.md
@@ -32,16 +32,17 @@ Please note that some targeting attribut
 * [trailheadTriplet](#trailheadtriplet)
 * [usesFirefoxSync](#usesfirefoxsync)
 * [isFxAEnabled](#isFxAEnabled)
 * [xpinstallEnabled](#xpinstallEnabled)
 * [hasPinnedTabs](#haspinnedtabs)
 * [hasAccessedFxAPanel](#hasaccessedfxapanel)
 * [isWhatsNewPanelEnabled](#iswhatsnewpanelenabled)
 * [earliestFirefoxVersion](#earliestfirefoxversion)
+* [isFxABadgeEnabled](#isfxabadgeenabled)
 
 ## Detailed usage
 
 ### `addonsInfo`
 Provides information about the add-ons the user has installed.
 
 Note that the `name`, `userDisabled`, and `installDate` is only available if `isFullData` is `true` (this is usually not the case right at start-up).
 
@@ -502,8 +503,18 @@ declare const isWhatsNewPanelEnabled: bo
 
 Integer value of the first Firefox version the profile ran on
 
 #### Definition
 
 ```ts
 declare const earliestFirefoxVersion: boolean;
 ```
+
+### `isFxABadgeEnabled`
+
+Boolean pref that controls if the FxA toolbar button is badged by Messaging System.
+
+#### Definition
+
+```ts
+declare const isFxABadgeEnabled: boolean;
+```
--- a/browser/components/newtab/lib/ASRouterTargeting.jsm
+++ b/browser/components/newtab/lib/ASRouterTargeting.jsm
@@ -411,16 +411,22 @@ const TargetingGetters = {
       const earliestFirefoxVersion = UpdateManager.getUpdateAt(
         UpdateManager.updateCount - 1
       ).previousAppVersion;
       return parseInt(earliestFirefoxVersion.match(/\d+/), 10);
     }
 
     return null;
   },
+  get isFxABadgeEnabled() {
+    return Services.prefs.getBoolPref(
+      "browser.messaging-system.fxatoolbarbadge.enabled",
+      false
+    );
+  },
 };
 
 this.ASRouterTargeting = {
   Environment: TargetingGetters,
 
   ERROR_TYPES: {
     MALFORMED_EXPRESSION: "MALFORMED_EXPRESSION",
     OTHER_ERROR: "OTHER_ERROR",
--- a/browser/components/newtab/lib/OnboardingMessageProvider.jsm
+++ b/browser/components/newtab/lib/OnboardingMessageProvider.jsm
@@ -361,16 +361,27 @@ const ONBOARDING_MESSAGES = () => [
       length: 3,
       template: "onboarding",
       trigger: { id: "showOnboarding" },
     },
     targeting:
       "attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
     trigger: { id: "firstRun" },
   },
+  {
+    id: "FXA_ACCOUNTS_BADGE",
+    template: "toolbar_badge",
+    content: {
+      delay: 10000, // delay for 10 seconds
+      target: "fxa-toolbar-menu-button",
+    },
+    // Never accessed the FxA panel && doesn't use Firefox sync & has FxA enabled
+    targeting: `isFxABadgeEnabled && !hasAccessedFxAPanel && !usesFirefoxSync && isFxAEnabled == true`,
+    trigger: { id: "toolbarBadgeUpdate" },
+  },
 ];
 
 const OnboardingMessageProvider = {
   async getExtraAttributes() {
     const [header, button_label] = await L10N.formatMessages([
       { id: "onboarding-welcome-header" },
       { id: "onboarding-start-browsing-button-label" },
     ]);
--- a/browser/components/newtab/lib/PanelTestProvider.jsm
+++ b/browser/components/newtab/lib/PanelTestProvider.jsm
@@ -46,26 +46,16 @@ const MESSAGES = () => [
       },
       close_button: {
         tooltiptext: "Close tooltip",
       },
     },
     trigger: { id: "bookmark-panel" },
   },
   {
-    id: "FXA_ACCOUNTS_BADGE",
-    template: "toolbar_badge",
-    content: {
-      target: "fxa-toolbar-menu-button",
-    },
-    // Never accessed the FxA panel && doesn't use Firefox sync & has FxA enabled
-    targeting: `!hasAccessedFxAPanel && !usesFirefoxSync && isFxAEnabled == true`,
-    trigger: { id: "toolbarBadgeUpdate" },
-  },
-  {
     id: `WHATS_NEW_BADGE_${FIREFOX_VERSION}`,
     template: "toolbar_badge",
     content: {
       // delay: 5 * 3600 * 1000,
       delay: 5000,
       target: "whats-new-menu-button",
       action: { id: "show-whatsnew-button" },
     },
--- a/browser/components/newtab/test/unit/asrouter/PanelTestProvider.test.js
+++ b/browser/components/newtab/test/unit/asrouter/PanelTestProvider.test.js
@@ -1,12 +1,12 @@
 import { PanelTestProvider } from "lib/PanelTestProvider.jsm";
 import schema from "content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json";
 const messages = PanelTestProvider.getMessages();
 
 describe("PanelTestProvider", () => {
   it("should have a message", () => {
-    assert.lengthOf(messages, 7);
+    assert.lengthOf(messages, 6);
   });
   it("should be a valid message", () => {
     assert.jsonSchema(messages[0].content, schema);
   });
 });
--- a/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js
+++ b/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js
@@ -1,11 +1,12 @@
 import { _ToolbarBadgeHub } from "lib/ToolbarBadgeHub.jsm";
 import { GlobalOverrider } from "test/unit/utils";
 import { PanelTestProvider } from "lib/PanelTestProvider.jsm";
+import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
 import { _ToolbarPanelHub } from "lib/ToolbarPanelHub.jsm";
 
 describe("ToolbarBadgeHub", () => {
   let sandbox;
   let instance;
   let fakeAddImpression;
   let fakeDispatch;
   let isBrowserPrivateStub;
@@ -20,19 +21,22 @@ describe("ToolbarBadgeHub", () => {
   let removeObserverStub;
   beforeEach(async () => {
     globals = new GlobalOverrider();
     sandbox = sinon.createSandbox();
     instance = new _ToolbarBadgeHub();
     fakeAddImpression = sandbox.stub();
     fakeDispatch = sandbox.stub();
     isBrowserPrivateStub = sandbox.stub();
-    const msgs = await PanelTestProvider.getMessages();
-    fxaMessage = msgs.find(({ id }) => id === "FXA_ACCOUNTS_BADGE");
-    whatsnewMessage = msgs.find(({ id }) => id.includes("WHATS_NEW_BADGE_"));
+    const panelTestMsgs = await PanelTestProvider.getMessages();
+    const onboardingMsgs = await OnboardingMessageProvider.getUntranslatedMessages();
+    fxaMessage = onboardingMsgs.find(({ id }) => id === "FXA_ACCOUNTS_BADGE");
+    whatsnewMessage = panelTestMsgs.find(({ id }) =>
+      id.includes("WHATS_NEW_BADGE_")
+    );
     fakeElement = {
       classList: {
         add: sandbox.stub(),
         remove: sandbox.stub(),
       },
       setAttribute: sandbox.stub(),
       removeAttribute: sandbox.stub(),
       querySelector: sandbox.stub(),
--- a/browser/components/protections/content/lockwise-card.js
+++ b/browser/components/protections/content/lockwise-card.js
@@ -48,24 +48,25 @@ export default class LockwiseCard {
     // Get the container for the content to display.
     const container = isLoggedIn
       ? lockwiseBodyContent.querySelector(".has-logins")
       : lockwiseBodyContent.querySelector(".no-logins");
     // Display the content
     container.classList.remove("hidden");
 
     if (isLoggedIn) {
-      title.textContent = "Firefox Lockwise";
-      headerContent.textContent =
-        "Securely store and sync your passwords to all your devices.";
+      title.setAttribute("data-l10n-id", "lockwise-title-logged-in");
+      headerContent.setAttribute(
+        "data-l10n-id",
+        "lockwise-header-content-logged-in"
+      );
       this.renderContentForLoggedInUser(container, numLogins, numSyncedDevices);
     } else {
-      title.textContent = "Never forget a password again";
-      headerContent.textContent =
-        "Firefox Lockwise securely stores your passwords in your browser.";
+      title.setAttribute("data-l10n-id", "lockwise-title");
+      headerContent.setAttribute("data-l10n-id", "lockwise-header-content");
     }
   }
 
   /**
    * Displays the number of stored logins and synced devices for a user.
    *
    * @param {Element} container
    *        The containing element for the content.
@@ -76,28 +77,44 @@ export default class LockwiseCard {
    */
   renderContentForLoggedInUser(container, storedLogins, syncedDevices) {
     // Set the text for number of stored logins.
     const numberOfLoginsBlock = container.querySelector(
       ".number-of-logins.block"
     );
     numberOfLoginsBlock.textContent = storedLogins;
 
+    const lockwisePasswordsStored = this.doc.getElementById(
+      "lockwise-passwords-stored"
+    );
+    lockwisePasswordsStored.setAttribute(
+      "data-l10n-args",
+      JSON.stringify({ count: storedLogins })
+    );
+    lockwisePasswordsStored.setAttribute(
+      "data-l10n-id",
+      "lockwise-passwords-stored"
+    );
+
     // Set the text for the number of synced devices.
     const syncedDevicesBlock = container.querySelector(
       ".number-of-synced-devices.block"
     );
     syncedDevicesBlock.textContent = syncedDevices;
 
     const syncedDevicesText = container.querySelector(".synced-devices-text");
     const textEl = syncedDevicesText.querySelector("span");
-    textEl.textContent =
-      syncedDevices > 0
-        ? `Syncing to ${syncedDevices} other devices.`
-        : "Not syncing to other devices.";
-
+    if (syncedDevices) {
+      textEl.setAttribute(
+        "data-l10n-args",
+        JSON.stringify({ count: syncedDevices })
+      );
+      textEl.setAttribute("data-l10n-id", "lockwise-sync-status");
+    } else {
+      textEl.setAttribute("data-l10n-id", "lockwise-sync-not-syncing");
+    }
     // Display the link for enabling sync if no synced devices are detected.
     if (syncedDevices === 0) {
       const syncLink = syncedDevicesText.querySelector("a");
       syncLink.classList.remove("hidden");
     }
   }
 }
--- a/browser/components/protections/content/monitor-card.js
+++ b/browser/components/protections/content/monitor-card.js
@@ -33,38 +33,36 @@ export default class MonitorClass {
 
       // Show the Monitor card.
       const monitorCard = this.doc.querySelector(".card.monitor-card.hidden");
       monitorCard.classList.remove("hidden");
     });
   }
 
   buildContent(loginData, monitorData) {
-    const { hasFxa, numLogins } = loginData;
-    const isLoggedIn = numLogins > 0 || hasFxa;
+    const { numLogins } = loginData;
     const headerContent = this.doc.querySelector(
       "#monitor-header-content span"
     );
     const monitorCard = this.doc.querySelector(".card.monitor-card");
-    if (isLoggedIn && !monitorData.error) {
+    if (numLogins > 0 && !monitorData.error) {
       monitorCard.classList.add("has-logins");
-      headerContent.textContent =
-        "Firefox Monitor warns you if your info has appeared in a known data breach";
+      headerContent.setAttribute(
+        "data-l10n-id",
+        "monitor-header-content-logged-in"
+      );
       this.renderContentForUserWithLogins(monitorData);
     } else {
       monitorCard.classList.add("no-logins");
       const signUpForMonitorLink = this.doc.getElementById(
         "sign-up-for-monitor-link"
       );
-      signUpForMonitorLink.textContent = hasFxa
-        ? "Turn on Monitor"
-        : "Sign up for Monitor";
       signUpForMonitorLink.href = this.buildMonitorUrl(monitorData.userEmail);
-      headerContent.textContent =
-        "Check Firefox Monitor to see if you've been part of a data breach and get alerts about new breaches.";
+      signUpForMonitorLink.setAttribute("data-l10n-id", "monitor-sign-up");
+      headerContent.setAttribute("data-l10n-id", "monitor-header-content");
     }
   }
 
   /**
    * Builds the appropriate URL that takes the user to the Monitor website's
    * sign-up/sign-in page.
    *
    * @param {String|null} email
@@ -96,24 +94,60 @@ export default class MonitorClass {
     const exposedPasswords = this.doc.querySelector(
       "span[data-type='exposed-passwords']"
     );
 
     storedEmail.textContent = monitorData.monitoredEmails;
     knownBreaches.textContent = monitorData.numBreaches;
     exposedPasswords.textContent = monitorData.passwords;
 
+    const infoMonitoredAddresses = this.doc.getElementById(
+      "info-monitored-addresses"
+    );
+    infoMonitoredAddresses.setAttribute(
+      "data-l10n-args",
+      JSON.stringify({ count: monitorData.monitoredEmails })
+    );
+    infoMonitoredAddresses.setAttribute(
+      "data-l10n-id",
+      "info-monitored-addresses"
+    );
+
+    const infoKnownBreaches = this.doc.getElementById("info-known-breaches");
+    infoKnownBreaches.setAttribute(
+      "data-l10n-args",
+      JSON.stringify({ count: monitorData.numBreaches })
+    );
+    infoKnownBreaches.setAttribute("data-l10n-id", "info-known-breaches");
+
+    const infoExposedPasswords = this.doc.getElementById(
+      "info-exposed-passwords"
+    );
+    infoExposedPasswords.setAttribute(
+      "data-l10n-args",
+      JSON.stringify({ count: monitorData.passwords })
+    );
+    infoExposedPasswords.setAttribute("data-l10n-id", "info-exposed-passwords");
+
     // Display Lockwise section if there are any potential breached logins to report.
     if (monitorData.potentiallyBreachedLogins > 0) {
       const lockwiseSection = this.doc.querySelector(
         ".monitor-breached-passwords"
       );
       const exposedLockwisePasswords = this.doc.querySelector(
         "span[data-type='breached-lockwise-passwords']"
       );
 
       exposedLockwisePasswords.textContent =
         monitorData.potentiallyBreachedLogins;
 
+      let breachedPasswordWarning = this.doc.getElementById("password-warning");
+
+      breachedPasswordWarning.setAttribute(
+        "data-l10n-args",
+        JSON.stringify({ count: monitorData.potentiallyBreachedLogins })
+      );
+      breachedPasswordWarning.setAttribute("data-l10n-id", "password-warning");
+
       lockwiseSection.classList.remove("hidden");
     }
   }
 }
--- a/browser/components/protections/content/protections.ftl
+++ b/browser/components/protections/content/protections.ftl
@@ -20,13 +20,110 @@ graph-week-summary =
 # earliest date recorded in the database.
 graph-total-summary =
   { $count ->
      [one] { $count } tracker blocked since { DATETIME($earliestDate, day: "numeric", month: "long", year: "numeric") }
     *[other] { $count } trackers blocked since { DATETIME($earliestDate, day: "numeric", month: "long", year: "numeric") }
   }
 
 # The terminology used to refer to categories of Content Blocking is also used in chrome/browser/browser.properties and should be translated consistently.
+# "Standard" in this case is an adjective, meaning "default" or "normal".
 # The category name in the <b> tag will be bold.
-# "Standard" in this case is an adjective, meaning "default" or "normal".
 protection-header-details-standard = Protection Level is set to <b>Standard</b>
 protection-header-details-strict = Protection Level is set to <b>Strict</b>
 protection-header-details-custom = Protection Level is set to <b>Custom</b>
+protection-report-page-title = Privacy Protections
+protection-report-content-title = Privacy Protections
+
+etp-card-title = Enhanced Tracking Protection
+etp-card-content = Trackers follow you around online to collect information about your browsing habits and interests. { -brand-short-name } blocks many of these trackers and other malicious scripts.
+
+# This string is used to label the X axis of a graph. Other days of the week are generated via Intl.DateTimeFormat,
+# capitalization for this string should match the output for your locale.
+graph-today = Today
+
+social-tab-title = Social Media Trackers
+social-tab-contant = Social media like, post, and comment buttons on other websites can track you — even if you don’t use them. Logging in to sites using your Facebook or Twitter account is another way they can track what you do on those sites. We remove these trackers so Facebook and Twitter see less of what you do online.
+
+cookie-tab-title = Cross-Site Tracking Cookies
+cookie-tab-content = Cross-site tracking cookies follow you from site to site to collect data about your browsing habits. Advertisers and analytics companies gather this data to create a profile of your interests across many sites. Blocking them reduces the number of personalized ads that follow you around.
+
+tracker-tab-title = Tracking Content
+tracker-tab-content = Websites may load outside ads, videos, and other content that contain hidden trackers. Blocking tracking content can make websites load faster, but some buttons, forms, and login fields might not work.
+
+fingerprinter-tab-title = Fingerprinters
+fingerprinter-tab-content = Fingerprinting is a form of online tracking that’s different from your real fingerprints. Companies use it to create a unique profile of you using data about your browser, device, and other settings. We block ad trackers from fingerprinting your device.
+
+cryptominer-tab-title = Cryptominers
+cryptominer-tab-content = Some websites host hidden malware that secretly uses your system’s computing power to mine cryptocurrency, or digital money. It drains your battery, slows down your computer, and increases your energy bill. We block known cryptominers from using your computing resources to make money.
+
+lockwise-title = Never forget a password again
+lockwise-title-logged-in = { -lockwise-brand-name }
+lockwise-header-content = { -lockwise-brand-name } securely stores your passwords in your browser.
+lockwise-header-content-logged-in = Securely store and sync your passwords to all your devices.
+open-about-logins-button = Open in { -brand-short-name }
+lockwise-no-logins-content = Get the <a data-l10n-name="lockwise-inline-link">{ -lockwise-brand-name }</a> app to take your passwords everywhere.
+
+# This string is displayed after a large numeral that indicates the total number
+# of email addresses being monitored. Don’t add $count to
+# your localization, because it would result in the number showing twice.
+lockwise-passwords-stored =
+  { $count ->
+     [one] Password stored securely. <a data-l10n-name="lockwise-how-it-works">How it works</a>
+    *[other] Passwords stored securely. <a data-l10n-name="lockwise-how-it-works">How it works</a>
+  }
+
+turn-on-sync = Turn on { -sync-brand-short-name }…
+  .title = Go to sync preferences
+
+# Variables:
+#   $count (Number) - Number of devices connected with sync.
+lockwise-sync-status =
+  { $count ->
+     [one] Syncing to { $count } other device.
+    *[other] Syncing to { $count } other devices.
+  }
+lockwise-sync-not-syncing = Not syncing to other devices.
+
+monitor-title = Look out for data breaches
+monitor-link = How it works
+monitor-header-content = Check { -monitor-brand-name } to see if you’ve been part of a data breach and get alerts about new breaches.
+monitor-header-content-logged-in = { -monitor-brand-name } warns you if your info has appeared in a known data breach
+monitor-sign-up = Sign up for Breach Alerts
+auto-scan = Automatically scanned today
+
+# This string is displayed after a large numeral that indicates the total number
+# of email addresses being monitored. Don’t add $count to
+# your localization, because it would result in the number showing twice.
+info-monitored-addresses = 
+  { $count ->
+     [one] Email address being monitored.
+    *[other] Email addresses being monitored.
+  }
+
+# This string is displayed after a large numeral that indicates the total number
+# of known data breaches. Don’t add $count to
+# your localization, because it would result in the number showing twice.
+info-known-breaches =
+  { $count ->
+     [one] Known data breach has exposed your information.
+    *[other] Known data breaches have exposed your information.
+  }
+
+# This string is displayed after a large numeral that indicates the total number
+# of exposed passwords. Don’t add $count to
+# your localization, because it would result in the number showing twice.
+info-exposed-passwords =
+  { $count ->
+     [one] Password exposed across all breaches.
+    *[other] Passwords exposed across all breaches.
+  }
+
+full-report-link = View full report on <a data-l10n-name="monitor-inline-link">{ -monitor-brand-name }</a>
+
+# This string is displayed after a large numeral that indicates the total number
+# of saved logins which may have been exposed. Don’t add $count to
+# your localization, because it would result in the number showing twice.
+password-warning =
+  { $count ->
+     [one] Saved login may have been exposed in a data breach. Change this password for better online security. <a data-l10n-name="lockwise-link">View Saved Logins</a>
+    *[other] Saved logins may have been exposed in a data breach. Change these passwords for better online security. <a data-l10n-name="lockwise-link">View Saved Logins</a>
+  }
--- a/browser/components/protections/content/protections.html
+++ b/browser/components/protections/content/protections.html
@@ -2,41 +2,39 @@
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy" content="default-src chrome: blob:">
+    <link rel="localization" href="browser/branding/brandings.ftl"/>
     <link rel="localization" href="branding/brand.ftl"/>
+    <link rel="localization" href="browser/branding/sync-brand.ftl">
     <!-- rename to browser/protections.ftl when exposing to l10n -->
     <link rel="localization" href="preview/protections.ftl">
     <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
     <link rel="stylesheet" href="chrome://browser/content/protections.css">
     <link rel="icon" href="chrome://browser/skin/controlcenter/dashboard.svg">
     <script type="module" src="chrome://browser/content/protections.js"></script>
     <script type="module" src="chrome://browser/content/lockwise-card.js"></script>
     <script type="module" src="chrome://browser/content/monitor-card.js"></script>
-    <title>Protection Report</title>
+    <title data-l10n-id="protection-report-page-title"></title>
   </head>
 
   <body>
     <div id="report-content">
-      <h2 id="report-title">Privacy Protections</h2>
+      <h2 id="report-title" data-l10n-id="protection-report-content-title"></h2>
       <div class="card card-no-hover etp-card">
         <div class="card-header">
           <div class="icon"></div>
           <div class="wrapper">
-            <h3 class="card-title">
-              Enhanced Tracking Protection
-            </h3>
-            <p class="content">
-              Trackers follow you around online to collect information about your browsing habits and interests. Firefox blocks many of these trackers and other malicious scripts.
-            </p>
+            <h3 class="card-title" data-l10n-id="etp-card-title"></h3>
+            <p class="content" data-l10n-id="etp-card-content"></p>
             <p id="protection-details"></p>
           </div>
         </div>
         <div class="card-body">
           <div class="body-wrapper">
             <p id="graph-week-summary"></p>
             <div id="graph-wrapper">
               <div id="graph"></div>
@@ -52,104 +50,98 @@
 
                 <input id="tab-fingerprinter" data-type="fingerprinter" type="radio" name="tabs">
                 <label for="tab-fingerprinter" data-type="fingerprinter"></label>
 
                 <input id="tab-cryptominer" data-type="cryptominer" type="radio" name="tabs">
                 <label for="tab-cryptominer" data-type="cryptominer"></label>
 
                 <div id="social" class="tab-content">
-                  <p class="content-title">Social Media Trackers</p>
-                  <p>Social media like, post, and comment buttons on other websites can track you — even if you don’t use them. Logging in to sites using your Facebook or Twitter account is another way they can track what you do on those sites. We remove these trackers so Facebook and Twitter see less of what you do online.</p>
+                  <p class="content-title" data-l10n-id="social-tab-title"></p>
+                  <p data-l10n-id="social-tab-contant"></p>
                 </div>
                 <div id="cookie" class="tab-content">
-                  <p class="content-title">Cross-Site Tracking Cookies</p>
-                  <p>Cross-site tracking cookies follow you from site to site to collect data about your browsing habits. Advertisers and analytics companies gather this data to create a profile of your interests across many sites. Blocking them reduces the number of personalized ads that follow you around.</p>
+                  <p class="content-title" data-l10n-id="cookie-tab-title"></p>
+                  <p data-l10n-id="cookie-tab-content"></p>
                 </div>
                 <div id="tracker" class="tab-content">
-                  <p class="content-title">Tracking Content</p>
-                  <p>Websites may load outside ads, videos, and other content that contain hidden trackers. Blocking tracking content can make websites load faster, but some buttons, forms, and login fields might not work.</p>
+                  <p class="content-title" data-l10n-id="tracker-tab-title"></p>
+                  <p data-l10n-id="tracker-tab-content"></p>
                 </div>
                 <div id="fingerprinter" class="tab-content">
-                  <p class="content-title">Fingerprinters</p>
-                  <p>Fingerprinting is a form of online tracking that’s different from your real fingerprints. Companies use it to create a unique profile of you using data about your browser, device, and other settings. We block ad trackers from fingerprinting your device.</p>
+                  <p class="content-title" data-l10n-id="fingerprinter-tab-title"></p>
+                  <p data-l10n-id="fingerprinter-tab-content"></p>
                 </div>
                 <div id="cryptominer" class="tab-content">
-                  <p class="content-title">Cryptominers</p>
-                  <p>Some websites host hidden malware that secretly uses your system’s computing power to mine cryptocurrency, or digital money. It drains your battery, slows down your computer, and increases your energy bill. We block known cryptominers from using your computing resources to make money.</p>
+                  <p class="content-title" data-l10n-id="cryptominer-tab-title"></p>
+                  <p data-l10n-id="cryptominer-tab-content"></p>
                 </div>
               </div>
             </div>
             <div id="graph-total-summary"></div>
           </div>
         </div>
       </div>
      <!-- Markup for Monitor card. -->
       <section class="card card-no-hover monitor-card hidden">
           <div class="card-header">
             <div class="icon"></div>
             <div class="wrapper">
-              <h3 id="monitor-title" class="card-title">
-                Look out for data breaches
-              </h3>
+              <h3 id="monitor-title" class="card-title" data-l10n-id="monitor-title"></h3>
               <p id="monitor-header-content" class="content">
                 <span>
                   <!-- Insert Monitor header content here. -->
                 </span>
-                <a href="">How it works</a>
+                <a href="" data-l10n-id="monitor-link"></a>
               </p>
-              <span class="inline-text-icon monitor-scanned-text">
-                Automatically scanned today
-              </span>
+              <span class="inline-text-icon monitor-scanned-text" data-l10n-id="auto-scan"></span>
             </div>
             <a target="_blank" id="sign-up-for-monitor-link">
               <!-- Insert Monitor link content here. -->
             </a>
           </div>
           <div class="card-body">
             <div class="body-wrapper">
               <div id="monitor-body-content">
                 <div class="monitor-block email">
                   <span class="monitor-stat">
                     <span class="monitor-icon"></span>
                     <span data-type="stored-emails">
                       <!-- Display number of stored emails here. -->
                     </span>
                   </span>
-                  <span class="info-text">Email addresses being monitored</span>
+                  <span id="info-monitored-addresses" class="info-text"></span>
                 </div>
                 <div class="monitor-block breaches">
                   <span class="monitor-stat">
                     <span class="monitor-icon"></span>
                     <span data-type="known-breaches">
                       <!-- Display number of known breaches here. -->
                     </span>
                   </span>
-                  <span class="info-text">Known data breaches have exposed your information</span>
+                  <span id="info-known-breaches" class="info-text"></span>
                 </div>
                 <div class="monitor-block passwords">
                   <span class="monitor-stat">
                       <span class="monitor-icon"></span>
                       <span data-type="exposed-passwords">
                         <!-- Display number of exposed passwords here. -->
                       </span>
                   </span>
-                  <span class="info-text">Passwords exposed across all breaches</span>
+                  <span id="info-exposed-passwords" class="info-text"></span>
                 </div>
-                <div class="monitor-view-full-report">
-                  <span>View full report on</span>
-                  <a href="">Firefox Monitor</a>
+                <div class="monitor-view-full-report" data-l10n-id="full-report-link">
+                    <a data-l10n-name="monitor-inline-link" href=""></a>
                 </div>
                 <div class="monitor-breached-passwords hidden">
                   <span data-type="breached-lockwise-passwords" class="number-of-breaches block">
                     <!-- Display number of exposed stored passwords here. -->
                   </span>
-                  <span>
-                    Passwords saved in Firefox may have been exposed in a data breach. Change these passwords to protect your accounts.
-                    <a href="">View Saved Logins</a>
+                  <span id="password-warning">
+                    <a href="" data-l10n-name="lockwise-link"></a>
                   </span>
                 </div>
               </div>
             </div>
           </div>
       </section>
       <!-- Markup for Lockwise card. -->
       <section class="card card-no-hover lockwise-card hidden">
@@ -158,44 +150,42 @@
           <div class="wrapper">
             <h3 id="lockwise-title" class="card-title">
               <!-- Insert Lockwise card title here. -->
             </h3>
             <p id="lockwise-header-content" class="content">
               <!-- Insert Lockwise header content here. -->
             </p>
           </div>
-          <button id="open-about-logins-button" class="primary">Open in Firefox</button>
+          <button id="open-about-logins-button" class="primary" data-l10n-id="open-about-logins-button"></button>
         </div>
         <div class="card-body">
           <div id="lockwise-body-content" class="body-wrapper">
             <div class="no-logins hidden">
                 <div class="lockwise-mobile-app-icon"></div>
-                <span>
-                  Get the <a target="_blank" href="https://lockwise.firefox.com/">Firefox Lockwise</a>
-                  app to take your passwords everywhere.
+                <span data-l10n-id="lockwise-no-logins-content">
+                  <a data-l10n-name="lockwise-inline-link" href=""></a>
                 </span>
             </div>
             <div class="has-logins hidden">
               <span class="number-of-logins block">
                 <!-- Display number of stored logins here. -->
               </span>
-              <span class="inline-text-icon passwords-stored-text">
-                Passwords stored securely.
-                <a href="">How it works</a>
+              <span id="lockwise-passwords-stored" class="inline-text-icon passwords-stored-text">
+                 <!-- Display message for stored logins here. -->
+                <a data-l10n-name="lockwise-how-it-works" href=""></a>
               </span>
               <span class="number-of-synced-devices block">
                 <!-- Display number of synced devices here. -->
               </span>
               <span class="inline-text-icon synced-devices-text">
                 <span>
                   <!-- Display message for status of synced devices here. -->
                 </span>
-                <a class="hidden" href="" title="Go to sync preferences">Turn on sync…</a>
+                <a class="hidden" href="" data-l10n-id="turn-on-sync"></a>
               </span>
             </div>
-           </div>
           </div>
         </div>
       </section>
     </div>
   </body>
 </html>
--- a/browser/components/protections/content/protections.js
+++ b/browser/components/protections/content/protections.js
@@ -9,25 +9,23 @@ import MonitorCard from "./monitor-card.
 
 document.addEventListener("DOMContentLoaded", e => {
   let todayInMs = Date.now();
   let weekAgoInMs = todayInMs - 7 * 24 * 60 * 60 * 1000;
   RPMSendAsyncMessage("FetchContentBlockingEvents", {
     from: weekAgoInMs,
     to: todayInMs,
   });
-
   let dataTypes = [
     "cryptominer",
     "fingerprinter",
     "tracker",
     "cookie",
     "social",
   ];
-  let weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
 
   let protectionDetails = document.getElementById("protection-details");
   protectionDetails.addEventListener("click", () => {
     RPMSendAsyncMessage("OpenContentBlockingPreferences");
   });
 
   let cbCategory = RPMGetStringPref("browser.contentblocking.category");
   if (cbCategory == "custom") {
@@ -72,22 +70,21 @@ document.addEventListener("DOMContentLoa
     let weekTypeCounts = {
       social: 0,
       cookie: 0,
       tracker: 0,
       fingerprinter: 0,
       cryptominer: 0,
     };
 
+    let date = new Date();
     let graph = document.getElementById("graph");
-    for (let i = weekdays.length - 1; i >= 0; i--) {
-      // Start 7 days ago and count down to today.
-      let date = new Date();
-      date.setDate(date.getDate() - i);
+    for (let i = 0; i <= 6; i++) {
       let dateString = date.toISOString().split("T")[0];
+
       let bar = document.createElement("div");
       bar.className = "graph-bar";
       if (data[dateString]) {
         let content = data[dateString];
         let count = document.createElement("div");
         count.className = "bar-count";
         count.textContent = content.total;
         bar.appendChild(count);
@@ -104,37 +101,39 @@ document.addEventListener("DOMContentLoa
             weekTypeCounts[type] += content[type];
             bar.appendChild(div);
           }
         }
       } else {
         // There were no content blocking events on this day.
         bar.classList.add("empty");
       }
-      graph.appendChild(bar);
+      graph.prepend(bar);
       let weekSummary = document.getElementById("graph-week-summary");
       weekSummary.setAttribute(
         "data-l10n-args",
         JSON.stringify({ count: weekCount })
       );
       weekSummary.setAttribute("data-l10n-id", "graph-week-summary");
 
+      // Set the total number of each type of tracker on the tabs
       for (let type of dataTypes) {
         document.querySelector(`label[data-type=${type}]`).textContent =
           weekTypeCounts[type];
       }
 
       let label = document.createElement("span");
       label.className = "column-label";
       if (i == 6) {
-        label.textContent = "Today";
+        label.setAttribute("data-l10n-id", "graph-today");
       } else {
-        label.textContent = weekdays[(i + 1 + new Date().getDay()) % 7];
+        label.textContent = data.weekdays[(i + 1 + new Date().getDay()) % 7];
       }
-      graph.prepend(label);
+      graph.append(label);
+      date.setDate(date.getDate() - 1);
     }
 
     addListeners();
   };
 
   let addListeners = () => {
     let wrapper = document.querySelector(".body-wrapper");
     wrapper.addEventListener("mouseover", ev => {
--- a/browser/components/protections/test/browser/browser_protections_lockwise.js
+++ b/browser/components/protections/test/browser/browser_protections_lockwise.js
@@ -116,17 +116,21 @@ add_task(async function() {
     is(numberOfLogins.textContent, 1, "One stored login should be displayed");
 
     info("Also check that content for no synced devices is correct.");
     is(
       numberOfSyncedDevices.textContent,
       0,
       "Zero synced devices are displayed."
     );
-    is(syncedDevicesStatusText.textContent, "Not syncing to other devices.");
+    is(
+      syncedDevicesStatusText.getAttribute("data-l10n-id"),
+      "lockwise-sync-not-syncing",
+      "Not syncing to other devices."
+    );
   });
 
   info(
     "Add another login and check the number of stored logins is updated after reload."
   );
   Services.logins.addLogin(TEST_LOGIN2);
   await reloadTab(tab);
 
@@ -155,26 +159,22 @@ add_task(async function() {
         "#lockwise-body-content .has-logins"
       );
       return ContentTaskUtils.is_visible(hasLogins);
     }, "Lockwise card for user with logins is shown.");
 
     const numberOfSyncedDevices = content.document.querySelector(
       ".number-of-synced-devices.block"
     );
-    const syncedDevicesStatusText = content.document.querySelector(
-      ".synced-devices-text span"
-    );
 
     is(
       numberOfSyncedDevices.textContent,
       5,
       "Five synced devices should be displayed"
     );
-    is(syncedDevicesStatusText.textContent, "Syncing to 5 other devices.");
   });
 
   info("Disable showing the Lockwise card.");
   Services.prefs.setBoolPref(
     "browser.contentblocking.report.lockwise.enabled",
     false
   );
   await reloadTab(tab);
--- a/browser/components/protections/test/browser/browser_protections_monitor.js
+++ b/browser/components/protections/test/browser/browser_protections_monitor.js
@@ -58,17 +58,17 @@ add_task(async function() {
     gBrowser,
   });
   const { getLoginData } = AboutProtectionsHandler;
   const { getMonitorData } = AboutProtectionsHandler;
 
   await reloadTab(tab);
 
   info("Check that the correct content is displayed for users with no logins.");
-  await checkNoLoginsContentIsDisplayed(tab, "Sign up for Monitor");
+  await checkNoLoginsContentIsDisplayed(tab, "monitor-sign-up");
 
   info(
     "Check that the correct content is displayed for users with monitor data."
   );
   Services.logins.addLogin(TEST_LOGIN1);
   AboutProtectionsHandler.getMonitorData = mockGetMonitorData(
     fakeDataWithNoError
   );
@@ -153,17 +153,17 @@ add_task(async function() {
   info(
     "Check that correct content is displayed when monitor data contains an error message."
   );
   AboutProtectionsHandler.getMonitorData = mockGetMonitorData(
     fakeDataWithError
   );
   AboutProtectionsHandler.getLoginData = mockGetLoginData;
   await reloadTab(tab);
-  await checkNoLoginsContentIsDisplayed(tab, "Turn on Monitor");
+  await checkNoLoginsContentIsDisplayed(tab);
 
   info("Disable showing the Monitor card.");
   Services.prefs.setBoolPref(
     "browser.contentblocking.report.monitor.enabled",
     false
   );
   await reloadTab(tab);
 
@@ -189,40 +189,37 @@ add_task(async function() {
   // restore original getLoginData & getMonitorData methods to AboutProtectionsHandler
   AboutProtectionsHandler.getLoginData = getLoginData;
   AboutProtectionsHandler.getMonitorData = getMonitorData;
 
   await BrowserTestUtils.removeTab(tab);
 });
 
 async function checkNoLoginsContentIsDisplayed(tab, expectedLinkContent) {
-  await ContentTask.spawn(
-    tab.linkedBrowser,
-    { linkText: expectedLinkContent },
-    async function({ linkText }) {
-      await ContentTaskUtils.waitForCondition(() => {
-        const noLogins = content.document.querySelector(
-          ".monitor-card.no-logins"
-        );
-        return ContentTaskUtils.is_visible(noLogins);
-      }, "Monitor card for user with no logins is shown.");
-
-      const noLoginsHeaderContent = content.document.querySelector(
-        "#monitor-header-content span"
+  await ContentTask.spawn(tab.linkedBrowser, null, async function() {
+    await ContentTaskUtils.waitForCondition(() => {
+      const noLogins = content.document.querySelector(
+        ".monitor-card.no-logins"
       );
-      const cardBody = content.document.querySelector(
-        ".monitor-card .card-body"
-      );
-      const link = content.document.getElementById("sign-up-for-monitor-link");
+      return ContentTaskUtils.is_visible(noLogins);
+    }, "Monitor card for user with no logins is shown.");
+
+    const noLoginsHeaderContent = content.document.querySelector(
+      "#monitor-header-content span"
+    );
+    const cardBody = content.document.querySelector(".monitor-card .card-body");
 
-      ok(
-        ContentTaskUtils.is_hidden(cardBody),
-        "Card body is hidden for users with no logins."
-      );
-      is(
-        noLoginsHeaderContent.textContent,
-        "Check Firefox Monitor to see if you've been part of a data breach and get alerts about new breaches.",
-        "Header content for user with no logins is correct"
-      );
-      is(link.textContent, linkText, "Text content for link is correct");
-    }
-  );
+    ok(
+      ContentTaskUtils.is_hidden(cardBody),
+      "Card body is hidden for users with no logins."
+    );
+    is(
+      noLoginsHeaderContent.getAttribute("data-l10n-id"),
+      "monitor-header-content",
+      "Header content for user with no logins is correct"
+    );
+    is(
+      noLoginsHeaderContent.getAttribute("data-l10n-id"),
+      "monitor-header-content",
+      "Header content for user with no logins is correct"
+    );
+  });
 }
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -1312,17 +1312,21 @@ class UrlbarInput {
    *
    * @param {Event} event the event triggering the opening.
    * @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
    */
   _whereToOpen(event) {
     let isMouseEvent = event instanceof MouseEvent;
     let reuseEmpty = !isMouseEvent;
     let where = undefined;
-    if (!isMouseEvent && event && event.altKey) {
+    if (
+      !isMouseEvent &&
+      event &&
+      (event.altKey || event.getModifierState("AltGraph"))
+    ) {
       // We support using 'alt' to open in a tab, because ctrl/shift
       // might be used for canonizing URLs:
       where = event.shiftKey ? "tabshifted" : "tab";
     } else if (
       !isMouseEvent &&
       event &&
       event.ctrlKey &&
       UrlbarPrefs.get("ctrlCanonizesURLs")
--- a/browser/components/urlbar/UrlbarProviderExtension.jsm
+++ b/browser/components/urlbar/UrlbarProviderExtension.jsm
@@ -10,16 +10,19 @@
  */
 
 var EXPORTED_SYMBOLS = ["UrlbarProviderExtension"];
 
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 XPCOMUtils.defineLazyModuleGetters(this, {
+  PlacesSearchAutocompleteProvider:
+    "resource://gre/modules/PlacesSearchAutocompleteProvider.jsm",
+  Services: "resource://gre/modules/Services.jsm",
   SkippableTimer: "resource:///modules/UrlbarUtils.jsm",
   UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
   UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
   UrlbarResult: "resource:///modules/UrlbarResult.jsm",
   UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
 });
 
 // When we send events to extensions, we wait this amount of time in ms for them
@@ -184,23 +187,22 @@ class UrlbarProviderExtension extends Ur
    *   The query context.
    * @param {function} addCallback
    *   The callback invoked by this method to add each result.
    */
   async startQuery(context, addCallback) {
     let extResults = await this._notifyListener("resultsRequested", context);
     if (extResults) {
       for (let extResult of extResults) {
-        let result;
-        try {
-          result = this._makeUrlbarResult(context, extResult);
-        } catch (err) {
-          continue;
+        let result = await this._makeUrlbarResult(context, extResult).catch(
+          Cu.reportError
+        );
+        if (result) {
+          addCallback(this, result);
         }
-        addCallback(this, result);
       }
     }
   }
 
   /**
    * This method is called by the providers manager when an ongoing query is
    * canceled.  It fires the queryCanceled event.
    *
@@ -256,17 +258,51 @@ class UrlbarProviderExtension extends Ur
    *
    * @param {UrlbarQueryContext} context
    *   The query context.
    * @param {object} extResult
    *   A plain JS object representing a result created by the extension.
    * @returns {UrlbarResult}
    *   The UrlbarResult object.
    */
-  _makeUrlbarResult(context, extResult) {
+  async _makeUrlbarResult(context, extResult) {
+    // If the result is a search result, make sure its payload has a valid
+    // `engine` property, which is the name of an engine, and which we use later
+    // on to look up the nsISearchEngine.  We allow the extension to specify the
+    // engine by its name, alias, or domain.  Prefer aliases over domains since
+    // one domain can have many engines.
+    if (extResult.type == "search") {
+      let engine;
+      if (extResult.payload.engine) {
+        // Validate the engine name by looking it up.
+        engine = Services.search.getEngineByName(extResult.payload.engine);
+      } else if (extResult.payload.keyword) {
+        // Look up the engine by its alias.
+        engine = await PlacesSearchAutocompleteProvider.engineForAlias(
+          extResult.payload.keyword
+        );
+      } else if (extResult.payload.url) {
+        // Look up the engine by its domain.
+        let host;
+        try {
+          host = new URL(extResult.payload.url).hostname;
+        } catch (err) {}
+        if (host) {
+          engine = await PlacesSearchAutocompleteProvider.engineForDomainPrefix(
+            host
+          );
+        }
+      }
+      if (!engine) {
+        // No engine found.
+        throw new Error("Invalid or missing engine specified by extension");
+      }
+      extResult.payload.engine = engine.name;
+    }
+
     return new UrlbarResult(
       UrlbarProviderExtension.RESULT_TYPES[extResult.type],
       UrlbarProviderExtension.SOURCE_TYPES[extResult.source],
       ...UrlbarResult.payloadAndSimpleHighlights(
         context.tokens,
         extResult.payload || {}
       )
     );
--- a/browser/components/urlbar/UrlbarResult.jsm
+++ b/browser/components/urlbar/UrlbarResult.jsm
@@ -163,19 +163,19 @@ class UrlbarResult {
    *        it's a string, then the payloadHighlights in the return value will
    *        be an array of match highlights as described in
    *        UrlbarUtils.getTokenMatches().  If it's an array, then
    *        payloadHighlights will be an array of arrays of match highlights,
    *        one element per element in payloadPropertyValue.
    * @returns {array} An array [payload, payloadHighlights].
    */
   static payloadAndSimpleHighlights(tokens, payloadInfo) {
-    // Convert string values in payloadInfo to [value, false] arrays.
+    // Convert scalar values in payloadInfo to [value] arrays.
     for (let [name, info] of Object.entries(payloadInfo)) {
-      if (typeof info == "string") {
+      if (!Array.isArray(info)) {
         payloadInfo[name] = [info];
       }
     }
 
     if (
       (!payloadInfo.title || !payloadInfo.title[0]) &&
       payloadInfo.url &&
       typeof payloadInfo.url[0] == "string"
--- a/browser/components/urlbar/tests/browser/browser_locationBarCommand.js
+++ b/browser/components/urlbar/tests/browser/browser_locationBarCommand.js
@@ -119,16 +119,21 @@ add_task(async function load_in_current_
       type: "keypress",
       details: { accelKey: true },
     },
     {
       desc: "Alt+Return keypress in a blank tab",
       type: "keypress",
       details: { altKey: true },
     },
+    {
+      desc: "AltGr+Return keypress in a blank tab",
+      type: "keypress",
+      details: { altGraphKey: true },
+    },
   ];
 
   for (let { desc, type, details } of tests) {
     info(`Running test: ${desc}`);
 
     // Add a new tab.
     let tab = await promiseOpenNewTab();
 
@@ -161,25 +166,31 @@ add_task(async function load_in_new_tab_
       url: "about:blank",
     },
     {
       desc: "Alt+Return keypress in a dirty tab",
       type: "keypress",
       details: { altKey: true },
       url: START_VALUE,
     },
+    {
+      desc: "AltGr+Return keypress in a dirty tab",
+      type: "keypress",
+      details: { altGraphKey: true },
+      url: START_VALUE,
+    },
   ];
 
   for (let { desc, type, details, url } of tests) {
     info(`Running test: ${desc}`);
 
     // Add a new tab.
     let tab = await promiseOpenNewTab(url);
 
-    // Trigger a load and check it occurs in the current tab.
+    // Trigger a load and check it occurs in a new tab.
     let tabSwitchedPromise = promiseNewTabSwitched();
     await triggerCommand(type, details);
     await tabSwitchedPromise;
 
     // Check the load occurred in a new tab.
     info("URL should be loaded in a new focused tab");
     is(gURLBar.textValue, TEST_VALUE, "Urlbar still has the value we entered");
     await promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser);
--- a/browser/components/urlbar/tests/browser/browser_urlbarEnter.js
+++ b/browser/components/urlbar/tests/browser/browser_urlbarEnter.js
@@ -47,8 +47,35 @@ add_task(async function altReturnKeypres
     "Urlbar should preserve the value on return keypress"
   );
   isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
 
   // Cleanup.
   BrowserTestUtils.removeTab(tab);
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
+
+add_task(async function altGrReturnKeypress() {
+  info("AltGr+Return keypress");
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE);
+
+  let tabOpenPromise = BrowserTestUtils.waitForEvent(
+    gBrowser.tabContainer,
+    "TabOpen"
+  );
+  gURLBar.focus();
+  EventUtils.synthesizeKey("KEY_Enter", { altGraphKey: true });
+
+  // wait for the new tab to appear.
+  await tabOpenPromise;
+
+  // Check url bar and selected tab.
+  is(
+    gURLBar.textValue,
+    TEST_VALUE,
+    "Urlbar should preserve the value on return keypress"
+  );
+  isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
+
+  // Cleanup.
+  BrowserTestUtils.removeTab(tab);
+  BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
--- a/browser/components/urlbar/tests/browser/browser_urlbar_whereToOpen.js
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_whereToOpen.js
@@ -3,16 +3,17 @@
 
 "use strict";
 
 const NON_EMPTY_TAB = "example.com/non-empty";
 const EMPTY_TAB = "about:blank";
 const META_KEY = AppConstants.platform == "macosx" ? "metaKey" : "ctrlKey";
 const ENTER = new KeyboardEvent("keydown", {});
 const ALT_ENTER = new KeyboardEvent("keydown", { altKey: true });
+const ALTGR_ENTER = new KeyboardEvent("keydown", { modifierAltGraph: true });
 const CLICK = new MouseEvent("click", { button: 0 });
 const META_CLICK = new MouseEvent("click", { button: 0, [META_KEY]: true });
 const MIDDLE_CLICK = new MouseEvent("click", { button: 1 });
 
 let old_openintab = Preferences.get("browser.urlbar.openintab");
 registerCleanupFunction(async function() {
   Preferences.set("browser.urlbar.openintab", old_openintab);
 });
@@ -27,16 +28,21 @@ add_task(async function openInTab() {
   for (let test of [
     {
       pref: false,
       event: ALT_ENTER,
       desc: "Alt+Enter, non-empty tab, default prefs",
     },
     {
       pref: false,
+      event: ALTGR_ENTER,
+      desc: "AltGr+Enter, non-empty tab, default prefs",
+    },
+    {
+      pref: false,
       event: META_CLICK,
       desc: "Meta+click, non-empty tab, default prefs",
     },
     {
       pref: false,
       event: MIDDLE_CLICK,
       desc: "Middle click, non-empty tab, default prefs",
     },
@@ -96,16 +102,21 @@ add_task(async function reuseEmptyTab() 
   ));
 
   for (let test of [
     {
       pref: false,
       event: ALT_ENTER,
       desc: "Alt+Enter, empty tab, default prefs",
     },
+    {
+      pref: false,
+      event: ALTGR_ENTER,
+      desc: "AltGr+Enter, empty tab, default prefs",
+    },
     { pref: true, event: ENTER, desc: "Enter, empty tab, openInTab" },
     { pref: true, event: CLICK, desc: "Normal click, empty tab, openInTab" },
   ]) {
     info(test.desc);
     Preferences.set("browser.urlbar.openintab", test.pref);
     let where = gURLBar._whereToOpen(test.event);
     is(where, "current", "New URL would reuse the current empty tab");
   }
@@ -144,16 +155,22 @@ add_task(async function openInCurrentTab
       pref: true,
       url: NON_EMPTY_TAB,
       event: ALT_ENTER,
       desc: "Alt+Enter, non-empty tab, openInTab",
     },
     {
       pref: true,
       url: NON_EMPTY_TAB,
+      event: ALTGR_ENTER,
+      desc: "AltGr+Enter, non-empty tab, openInTab",
+    },
+    {
+      pref: true,
+      url: NON_EMPTY_TAB,
       event: META_CLICK,
       desc: "Meta+click, non-empty tab, openInTab",
     },
     {
       pref: true,
       url: NON_EMPTY_TAB,
       event: MIDDLE_CLICK,
       desc: "Middle click, non-empty tab, openInTab",
--- a/browser/locales/en-US/browser/branding/brandings.ftl
+++ b/browser/locales/en-US/browser/branding/brandings.ftl
@@ -6,10 +6,11 @@
 ## They cannot be:
 ## - Declined to adapt to grammatical case.
 ## - Transliterated.
 ## - Translated.
 
 -facebook-container-brand-name = Facebook Container
 -lockwise-brand-name = Firefox Lockwise
 -monitor-brand-name = Firefox Monitor
+-monitor-brand-short-name = Monitor
 -pocket-brand-name = Pocket
 -send-brand-name = Firefox Send
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -642,16 +642,30 @@ protections.blocking.crossSiteTrackingCo
 protections.blocking.trackingContent.title=Tracking Content Blocked
 protections.blocking.socialMediaTrackers.title=Social Media Trackers Blocked
 protections.notBlocking.fingerprinters.title=Not Blocking Fingerprinters
 protections.notBlocking.cryptominers.title=Not Blocking Cryptominers
 protections.notBlocking.crossSiteTrackingCookies.title=Not Blocking Cross-Site Tracking Cookies
 protections.notBlocking.trackingContent.title=Not Blocking Tracking Content
 protections.notBlocking.socialMediaTrackers.title=Not Blocking Social Media Trackers
 
+# Footer section in the Protections Panel
+# LOCALIZATION NOTE (protections.footer.blockedTrackerCounter.description,
+# protections.footer.blockedTrackerCounter.tooltip):
+#   This text indicates the total number of trackers blocked on all sites. In
+#   its tooltip, we show the date when we started counting this number.
+# LOCALIZATION NOTE (protections.footer.blockedTrackerCounter.description):
+#   Semicolon-separated list of plural forms.
+#   See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+#   Replacement for #1 is a locale-string converted positive integer.
+protections.footer.blockedTrackerCounter.description=1 Blocked;#1 Blocked
+# LOCALIZATION NOTE (protections.footer.blockedTrackerCounter.tooltip):
+#   %S is the date on which we started counting (e.g., July 17, 2019).
+protections.footer.blockedTrackerCounter.tooltip=Since %S
+
 # Edit Bookmark UI
 editBookmarkPanel.newBookmarkTitle=New Bookmark
 editBookmarkPanel.editBookmarkTitle=Edit This Bookmark
 editBookmarkPanel.cancel.label=Cancel
 editBookmarkPanel.cancel.accesskey=C
 
 # LOCALIZATION NOTE (editBookmark.removeBookmarks.label): Semicolon-separated list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -187,19 +187,18 @@
 .protections-popup-category-state-label,
 .identity-popup-permission-label,
 .identity-popup-permission-state-label,
 .identity-popup-security-content > description,
 #identity-popup-security-descriptions > description,
 #identity-popup-securityView-body > description,
 #identity-popup-permissions-content > description,
 #protections-popup-content > description,
-.protections-popup-settings-label,
-#protections-popup-footer > description,
-#protections-popup-footer > label {
+.protections-popup-footer-button-label,
+#protections-popup-trackers-blocked-counter-description {
   font-size: 110%;
   margin: 0;
 }
 
 #identity-popup-mainView-panel-header,
 #protections-popup-mainView-panel-header,
 #protections-popup-siteNotWorkingView-footer {
   padding: 4px 1em;
@@ -428,23 +427,27 @@ description#identity-popup-content-verif
   /* Overwrite toolbarbutton styles */
   -moz-appearance: none;
   margin: 0;
   padding-inline-start: 0;
 }
 
 .protections-popup-category:-moz-focusring,
 .protections-popup-category:hover,
-#protections-popup-settings-button:hover {
+.protections-popup-footer-button:-moz-focusring,
+.protections-popup-footer-button:hover,
+#protections-popup-show-report-stack:hover > .protections-popup-footer-button {
   border-radius: 2px;
   background-color: var(--arrowpanel-dimmed);
   outline: none;
 }
 
-.protections-popup-category:hover:active {
+.protections-popup-category:hover:active,
+.protections-popup-footer-button:hover:active,
+#protections-popup-show-report-stack:hover:active > .protections-popup-footer-button {
   background-color: var(--arrowpanel-dimmed-further);
 }
 
 .protections-popup-category::after {
   content: url(chrome://browser/skin/back-12.svg);
   -moz-context-properties: fill, fill-opacity;
   transform: scaleX(-1) translateY(1px);
   float: right;
@@ -713,32 +716,33 @@ description#identity-popup-content-verif
 @keyframes in-use-blink {
   50% { opacity: 0; }
 }
 
 .protections-popup-category-label,
 .protections-popup-category-state-label,
 .identity-popup-permission-label,
 .identity-popup-permission-state-label,
-.protections-popup-settings-label {
+.protections-popup-footer-button-label,
+#protections-popup-trackers-blocked-counter-description {
   /* We need to align the action buttons and permission icons with the text.
      This is tricky because the icon height is defined in pixels, while the
      font height can vary with platform and system settings, and at least on
      Windows the default font metrics reserve more extra space for accents.
      This value is a good compromise for different platforms and font sizes. */
   margin-top: -0.1em;
 }
 
 .protections-popup-category-label {
   max-width: 200px;
 }
 
 .protections-popup-category-label,
 .identity-popup-permission-label,
-.protections-popup-settings-label {
+.protections-popup-footer-button-label {
   margin-inline-start: 1em;
 }
 
 .protections-popup-category-state-label,
 .identity-popup-permission-state-label {
   margin-inline-end: 5px;
   text-align: end;
 }
@@ -884,49 +888,59 @@ description#identity-popup-content-verif
 #protections-popup-siteNotWorkingView-body-issue-list {
   padding-inline-start: 1em;
 }
 
 #protections-popup-siteNotWorkingView-footer {
   border-top: 1px solid var(--panel-separator-color);
 }
 
-#protections-popup-settings-section {
-  padding: 4px;
-  -moz-context-properties: fill, fill-opacity;
+/* Protection popup footer categories */
+
+.protections-popup-footer-button {
+  min-height: 24px;
+  padding-inline-start: 2em;
+  margin: 0;
 }
 
-#protections-popup-settings-button {
-  /* We need to align the setting button with the labels in the tp switch
-     section. So we add 6px to offset the margin of labels. */
-  padding-inline-start: calc(1em + 6px);
-  min-height: 37px;
+.protection-popup-footer-icon {
+  width: 16px;
+  height: 16px;
 }
 
-.protection-settings-icon {
-  width: 16px;
-  height: 16px;
+.protections-popup-settings-icon {
   list-style-image: url(chrome://browser/skin/settings.svg);
 }
 
+.protections-popup-show-report-icon {
+  list-style-image: url(chrome://browser/skin/controlcenter/dashboard.svg);
+}
+
 #protections-popup-footer {
-  background-color: var(--arrowpanel-dimmed);
-  border-top: 1px solid var(--panel-separator-color);
-  min-height: 40px;
-  -moz-box-align: center;
-  /* The horizontal padding aligns the content of footer with other sections in
-     the protections panel */
-  padding: 4px calc(1em + 4px);
+  padding: 6px 0;
+}
+
+#protections-popup-trackers-blocked-counter-box {
+  margin-inline-end: calc(4px + 1em);
+  visibility: hidden;
+  opacity: 0;
+  transition: opacity 200ms linear;
+}
+
+#protections-popup-trackers-blocked-counter-box[showing] {
+  visibility: visible;
+  opacity: 1;
 }
 
 #protections-popup-trackers-blocked-counter-description {
-  font-weight: 600;
-  /* This padding is added to align the counter text with the beginning of texts
-     in other sections. */
-  padding-inline-start: 6px;
+  color: #737373;
+}
+
+:root[lwt-popup-brighttext] #protections-popup-trackers-blocked-counter-description {
+  color: #f9f9fa;
 }
 
 #protections-popup-tp-switch[showdotindicator]::after {
   background: #00B3F4;
   border: 1px rgba(0,144,237,0.5) solid;
   content: " ";
   border-radius: 6px;
   position: absolute;
--- a/browser/themes/shared/identity-block/identity-block.inc.css
+++ b/browser/themes/shared/identity-block/identity-block.inc.css
@@ -18,17 +18,18 @@
    will be only the brand icon in the url bar. So, we need to change the padding
    start for proper positing the icon. */
 #urlbar[pageproxystate="valid"] > #identity-box.chromeUI {
   padding-inline-start: 8px;
 }
 
 #urlbar[pageproxystate="invalid"] > #identity-box > #blocked-permissions-container,
 #urlbar[pageproxystate="invalid"] > #identity-box > #notification-popup-box,
-#urlbar[pageproxystate="invalid"] > #identity-box > #identity-icon-labels {
+#urlbar[pageproxystate="invalid"] > #identity-box > #identity-icon-labels,
+#urlbar[pageproxystate="invalid"] > #identity-box > #remote-control-icon {
   display: none;
 }
 
 #urlbar[pageproxystate="invalid"] > #identity-box {
   pointer-events: none;
   /* This would make the spaces around the glass icon balanced. */
   margin-inline-end: 2px;
   -moz-user-focus: ignore;
--- a/browser/tools/mozscreenshots/controlCenter/browser.ini
+++ b/browser/tools/mozscreenshots/controlCenter/browser.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 subsuite = screenshots
 support-files =
   ../head.js
 
 [browser_controlCenter.js]
+skip-if = os == 'mac' # macosx1014 times out, see bug 1554821
--- a/browser/tools/mozscreenshots/devtools/browser.ini
+++ b/browser/tools/mozscreenshots/devtools/browser.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 subsuite = screenshots
 support-files =
   ../head.js
 
 [browser_devtools.js]
+skip-if = os == 'mac' # times out on macosx1014, see 1570100
--- a/browser/tools/mozscreenshots/permissionPrompts/browser.ini
+++ b/browser/tools/mozscreenshots/permissionPrompts/browser.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 subsuite = screenshots
 support-files =
   ../head.js
-
+  
 [browser_permissionPrompts.js]
+skip-if = os == 'mac' # times out on macosx1014, see 1570098
--- a/browser/tools/mozscreenshots/primaryUI/browser.ini
+++ b/browser/tools/mozscreenshots/primaryUI/browser.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 subsuite = screenshots
 support-files =
   ../head.js
 
 [browser_primaryUI.js]
+skip-if = os == 'mac' # macosx1014 times out, see bug 1570102
--- a/build/unix/build-hfsplus/build-hfsplus.sh
+++ b/build/unix/build-hfsplus/build-hfsplus.sh
@@ -5,17 +5,17 @@
 # when that happens: 1
 
 set -e
 set -x
 
 hfplus_version=540.1.linux3
 md5sum=0435afc389b919027b69616ad1b05709
 filename=diskdev_cmds-${hfplus_version}.tar.gz
-make_flags="-j$(getconf _NPROCESSORS_ONLN)"
+make_flags="-j$(nproc)"
 
 root_dir="$1"
 if [ -z "$root_dir" -o ! -d "$root_dir" ]; then
   root_dir=$(mktemp -d)
 fi
 cd $root_dir
 
 if test -z $TMPDIR; then
--- a/devtools/client/debugger/src/components/Editor/ColumnBreakpoints.js
+++ b/devtools/client/debugger/src/components/Editor/ColumnBreakpoints.js
@@ -58,20 +58,18 @@ class ColumnBreakpoints extends Componen
           breakpointActions={breakpointActions}
         />
       ));
     });
     return <div>{breakpoints}</div>;
   }
 }
 
-const mapStateToProps = state => {
-  return {
-    cx: getContext(state),
-    selectedSource: getSelectedSource(state),
-    columnBreakpoints: visibleColumnBreakpoints(state),
-  };
-};
+const mapStateToProps = state => ({
+  cx: getContext(state),
+  selectedSource: getSelectedSource(state),
+  columnBreakpoints: visibleColumnBreakpoints(state),
+});
 
 export default connect(
   mapStateToProps,
   dispatch => ({ breakpointActions: breakpointItemActions(dispatch) })
 )(ColumnBreakpoints);
--- a/devtools/client/debugger/src/components/Editor/ConditionalPanel.js
+++ b/devtools/client/debugger/src/components/Editor/ConditionalPanel.js
@@ -224,22 +224,21 @@ export class ConditionalPanel extends Pu
 
   render() {
     return null;
   }
 }
 
 const mapStateToProps = state => {
   const location = getConditionalPanelLocation(state);
-  const log = getLogPointStatus(state);
   return {
     cx: getContext(state),
     breakpoint: getBreakpointForLocation(state, location),
     location,
-    log,
+    log: getLogPointStatus(state),
   };
 };
 
 const {
   setBreakpointOptions,
   openConditionalPanel,
   closeConditionalPanel,
 } = actions;
--- a/devtools/client/debugger/src/components/Editor/menus/editor.js
+++ b/devtools/client/debugger/src/components/Editor/menus/editor.js
@@ -46,27 +46,25 @@ export const continueToHereItem = (
   label: L10N.getStr("editor.continueToHere.label"),
 });
 
 // menu items
 
 const copyToClipboardItem = (
   selectedContent: SourceContent,
   editorActions: EditorItemActions
-) => {
-  return {
-    id: "node-menu-copy-to-clipboard",
-    label: L10N.getStr("copyToClipboard.label"),
-    accesskey: L10N.getStr("copyToClipboard.accesskey"),
-    disabled: false,
-    click: () =>
-      selectedContent.type === "text" &&
-      copyToTheClipboard(selectedContent.value),
-  };
-};
+) => ({
+  id: "node-menu-copy-to-clipboard",
+  label: L10N.getStr("copyToClipboard.label"),
+  accesskey: L10N.getStr("copyToClipboard.accesskey"),
+  disabled: false,
+  click: () =>
+    selectedContent.type === "text" &&
+    copyToTheClipboard(selectedContent.value),
+});
 
 const copySourceItem = (
   selectedSource: Source,
   selectionText: string,
   editorActions: EditorItemActions
 ) => ({
   id: "node-menu-copy-source",
   label: L10N.getStr("copySource.label"),
@@ -153,24 +151,22 @@ const evaluateInConsoleItem = (
   label: L10N.getStr("evaluateInConsole.label"),
   click: () => editorActions.evaluateInConsole(selectionText),
 });
 
 const downloadFileItem = (
   selectedSource: Source,
   selectedContent: SourceContent,
   editorActions: EditorItemActions
-) => {
-  return {
-    id: "node-menu-download-file",
-    label: L10N.getStr("downloadFile.label"),
-    accesskey: L10N.getStr("downloadFile.accesskey"),
-    click: () => downloadFile(selectedContent, getFilename(selectedSource)),
-  };
-};
+) => ({
+  id: "node-menu-download-file",
+  label: L10N.getStr("downloadFile.label"),
+  accesskey: L10N.getStr("downloadFile.accesskey"),
+  click: () => downloadFile(selectedContent, getFilename(selectedSource)),
+});
 
 export function editorMenuItems({
   cx,
   editorActions,
   selectedSourceWithContent,
   location,
   selectionText,
   hasPrettySource,
--- a/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js
+++ b/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js
@@ -154,23 +154,21 @@ class EventListeners extends Component<P
             );
           })}
         </ul>
       </div>
     );
   }
 }
 
-const mapStateToProps = state => {
-  return {
-    activeEventListeners: getActiveEventListeners(state),
-    categories: getEventListenerBreakpointTypes(state),
-    expandedCategories: getEventListenerExpanded(state),
-  };
-};
+const mapStateToProps = state => ({
+  activeEventListeners: getActiveEventListeners(state),
+  categories: getEventListenerBreakpointTypes(state),
+  expandedCategories: getEventListenerExpanded(state),
+});
 
 export default connect(
   mapStateToProps,
   {
     addEventListeners: actions.addEventListenerBreakpoints,
     removeEventListeners: actions.removeEventListenerBreakpoints,
     addEventListenerExpanded: actions.addEventListenerExpanded,
     removeEventListenerExpanded: actions.removeEventListenerExpanded,
--- a/devtools/client/debugger/src/components/SecondaryPanes/Expressions.js
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Expressions.js
@@ -378,24 +378,22 @@ class Expressions extends Component<Prop
       <ul className="pane expressions-list">
         {expressions.map(this.renderExpression)}
         {(showInput || !expressions.length) && this.renderNewExpressionInput()}
       </ul>
     );
   }
 }
 
-const mapStateToProps = state => {
-  return {
-    cx: getThreadContext(state),
-    autocompleteMatches: getAutocompleteMatchset(state),
-    expressions: getExpressions(state),
-    expressionError: getExpressionError(state),
-  };
-};
+const mapStateToProps = state => ({
+  cx: getThreadContext(state),
+  autocompleteMatches: getAutocompleteMatchset(state),
+  expressions: getExpressions(state),
+  expressionError: getExpressionError(state),
+});
 
 export default connect(
   mapStateToProps,
   {
     autocomplete: actions.autocomplete,
     clearAutocomplete: actions.clearAutocomplete,
     addExpression: actions.addExpression,
     clearExpressionError: actions.clearExpressionError,
--- a/devtools/client/debugger/src/components/SecondaryPanes/Workers.js
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Workers.js
@@ -29,17 +29,17 @@ export class Workers extends Component<P
 
     return (
       <div
         className="worker"
         key={thread.actor}
         onClick={() => openWorkerToolbox(thread)}
       >
         <div className="icon">
-          <AccessibleImage className={"worker"} />
+          <AccessibleImage className="worker" />
         </div>
         <div className="label">{getDisplayName(thread)}</div>
       </div>
     );
   }
 
   render() {
     const { threads } = this.props;
--- a/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js
+++ b/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js
@@ -333,17 +333,17 @@ class XHRBreakpoints extends Component<P
       </option>
     );
   };
 
   renderMethodSelectElement = () => {
     return (
       <select
         value={this.state.inputMethod}
-        className={"xhr-input-method"}
+        className="xhr-input-method"
         onChange={this.handleMethodChange}
         onMouseDown={this.onMouseDown}
         onKeyDown={this.handleTab}
       >
         {xhrMethods.map(this.renderMethodOption)}
       </select>
     );
   };
@@ -353,22 +353,20 @@ class XHRBreakpoints extends Component<P
       <div>
         {this.renderCheckbox()}
         {this.renderBreakpoints()}
       </div>
     );
   }
 }
 
-const mapStateToProps = state => {
-  return {
-    xhrBreakpoints: getXHRBreakpoints(state),
-    shouldPauseOnAny: shouldPauseOnAnyXHR(state),
-  };
-};
+const mapStateToProps = state => ({
+  xhrBreakpoints: getXHRBreakpoints(state),
+  shouldPauseOnAny: shouldPauseOnAnyXHR(state),
+});
 
 export default connect(
   mapStateToProps,
   {
     setXHRBreakpoint: actions.setXHRBreakpoint,
     removeXHRBreakpoint: actions.removeXHRBreakpoint,
     enableXHRBreakpoint: actions.enableXHRBreakpoint,
     disableXHRBreakpoint: actions.disableXHRBreakpoint,
--- a/devtools/client/debugger/src/components/shared/SourceIcon.js
+++ b/devtools/client/debugger/src/components/shared/SourceIcon.js
@@ -38,14 +38,12 @@ class SourceIcon extends PureComponent<P
     if (shouldHide && shouldHide(iconClass)) {
       return null;
     }
 
     return <AccessibleImage className={`source-icon ${iconClass}`} />;
   }
 }
 
-export default connect((state, props) => {
-  return {
-    symbols: getSymbols(state, props.source),
-    framework: getFramework(getTabs(state), props.source.url),
-  };
-})(SourceIcon);
+export default connect((state, props) => ({
+  symbols: getSymbols(state, props.source),
+  framework: getFramework(getTabs(state), props.source.url),
+}))(SourceIcon);
--- a/devtools/client/debugger/src/workers/parser/getScopes/index.js
+++ b/devtools/client/debugger/src/workers/parser/getScopes/index.js
@@ -69,26 +69,24 @@ function findScopes(
         return true;
       }
       return false;
     });
     if (!foundOne) {
       break;
     }
   }
-  return found.map(i => {
-    return {
-      type: i.type,
-      scopeKind: i.scopeKind,
-      displayName: i.displayName,
-      start: i.start,
-      end: i.end,
-      bindings: i.bindings,
-    };
-  });
+  return found.map(i => ({
+    type: i.type,
+    scopeKind: i.scopeKind,
+    displayName: i.displayName,
+    start: i.start,
+    end: i.end,
+    bindings: i.bindings,
+  }));
 }
 
 function compareLocations(a: SourceLocation, b: SourceLocation): number {
   // According to type of Location.column can be undefined, if will not be the
   // case here, ignoring flow error.
   // $FlowIgnore
   return a.line == b.line ? a.column - b.column : a.line - b.line;
 }
--- a/devtools/client/debugger/src/workers/parser/getScopes/visitor.js
+++ b/devtools/client/debugger/src/workers/parser/getScopes/visitor.js
@@ -208,32 +208,30 @@ export function buildScopeList(ast: Node
 
 function toParsedScopes(
   children: TempScope[],
   sourceId: SourceId
 ): ?(ParsedScope[]) {
   if (!children || children.length === 0) {
     return undefined;
   }
-  return children.map(scope => {
+  return children.map(scope => ({
     // Removing unneed information from TempScope such as parent reference.
     // We also need to convert BabelLocation to the Location type.
-    return {
-      start: scope.loc.start,
-      end: scope.loc.end,
-      type:
-        scope.type === "module" || scope.type === "function-body"
-          ? "block"
-          : scope.type,
-      scopeKind: "",
-      displayName: scope.displayName,
-      bindings: scope.bindings,
-      children: toParsedScopes(scope.children, sourceId),
-    };
-  });
+    start: scope.loc.start,
+    end: scope.loc.end,
+    type:
+      scope.type === "module" || scope.type === "function-body"
+        ? "block"
+        : scope.type,
+    scopeKind: "",
+    displayName: scope.displayName,
+    bindings: scope.bindings,
+    children: toParsedScopes(scope.children, sourceId),
+  }));
 }
 
 function createTempScope(
   type: "object" | "function" | "function-body" | "block" | "module",
   displayName: string,
   parent: TempScope | null,
   loc: {
     start: SourceLocation,
@@ -412,20 +410,17 @@ function createGlobalScope(
     end: fromBabelLocation(ast.loc.end, sourceId),
   });
 
   const lexical = createTempScope("block", "Lexical Global", global, {
     start: fromBabelLocation(ast.loc.start, sourceId),
     end: fromBabelLocation(ast.loc.end, sourceId),
   });
 
-  return {
-    global,
-    lexical,
-  };
+  return { global, lexical };
 }
 
 const scopeCollectionVisitor = {
   // eslint-disable-next-line complexity
   enter(
     node: Node,
     ancestors: TraversalAncestors,
     state: ScopeCollectionVisitorState
--- a/devtools/client/debugger/src/workers/parser/utils/helpers.js
+++ b/devtools/client/debugger/src/workers/parser/utils/helpers.js
@@ -117,24 +117,22 @@ export function getVariables(dec: Node) 
       return [];
     }
 
     // NOTE: it's possible that an element is empty or has several variables
     // e.g. const [, a] = arr
     // e.g. const [{a, b }] = 2
     return dec.id.elements
       .filter(element => element)
-      .map(element => {
-        return {
-          name: t.isAssignmentPattern(element)
-            ? element.left.name
-            : element.name || (element.argument && element.argument.name),
-          location: element.loc,
-        };
-      })
+      .map(element => ({
+        name: t.isAssignmentPattern(element)
+          ? element.left.name
+          : element.name || (element.argument && element.argument.name),
+        location: element.loc,
+      }))
       .filter(({ name }) => name);
   }
 
   return [
     {
       name: dec.id.name,
       location: dec.loc,
     },
--- a/devtools/client/webconsole/test/fixtures/stub-generators/browser.ini
+++ b/devtools/client/webconsole/test/fixtures/stub-generators/browser.ini
@@ -10,16 +10,17 @@ support-files =
   test-css-message.html
   test-network-event.html
 
 [browser_webconsole_check_stubs_console_api.js]
 [browser_webconsole_check_stubs_css_message.js]
 [browser_webconsole_check_stubs_evaluation_result.js]
 [browser_webconsole_check_stubs_network_event.js]
 [browser_webconsole_check_stubs_page_error.js]
+skip-if = (os == 'win' && os_version == '10.0' && bits == 64 && ccov) #Bug 1559605
 [browser_webconsole_update_stubs_console_api.js]
 skip-if=true # This is only used to update stubs. It is not an actual test.
 [browser_webconsole_update_stubs_css_message.js]
 skip-if=true # This is only used to update stubs. It is not an actual test.
 [browser_webconsole_update_stubs_evaluation_result.js]
 skip-if=true # This is only used to update stubs. It is not an actual test.
 [browser_webconsole_update_stubs_network_event.js]
 skip-if=true # This is only used to update stubs. It is not an actual test.
--- a/devtools/server/actors/targets/browsing-context.js
+++ b/devtools/server/actors/targets/browsing-context.js
@@ -1259,43 +1259,16 @@ const browsingContextTargetPrototype = {
       // The browsing context is already closed.
       return null;
     }
 
     const windowUtils = this.window.windowUtils;
     return windowUtils.serviceWorkersTestingEnabled;
   },
 
-  /**
-   * Prepare to enter a nested event loop by disabling debuggee events.
-   */
-  preNest() {
-    if (!this.window) {
-      // The browsing context is already closed.
-      return;
-    }
-    const windowUtils = this.window.windowUtils;
-
-    windowUtils.suppressEventHandling(true);
-    windowUtils.suspendTimeouts();
-  },
-
-  /**
-   * Prepare to exit a nested event loop by enabling debuggee events.
-   */
-  postNest(nestData) {
-    if (!this.window) {
-      // The browsing context is already closed.
-      return;
-    }
-    const windowUtils = this.window.windowUtils;
-    windowUtils.resumeTimeouts();
-    windowUtils.suppressEventHandling(false);
-  },
-
   _changeTopLevelDocument(window) {
     // Fake a will-navigate on the previous document
     // to let a chance to unregister it
     this._willNavigate(this.window, window.location.href, null, true);
 
     this._windowDestroyed(this.window, null, true);
 
     // Immediately change the window as this window, if in process of unload
--- a/devtools/server/actors/targets/content-process.js
+++ b/devtools/server/actors/targets/content-process.js
@@ -178,19 +178,11 @@ const ContentProcessTargetActor = ActorC
   destroy: function() {
     Actor.prototype.destroy.call(this);
 
     // Tell the live lists we aren't watching any more.
     if (this._workerList) {
       this._workerList.onListChanged = null;
     }
   },
-
-  preNest: function() {
-    // TODO: freeze windows
-    // window mediator doesn't work in child.
-    // it doesn't throw, but doesn't return any window
-  },
-
-  postNest: function() {},
 });
 
 exports.ContentProcessTargetActor = ContentProcessTargetActor;
--- a/devtools/server/actors/targets/parent-process.js
+++ b/devtools/server/actors/targets/parent-process.js
@@ -152,37 +152,13 @@ parentProcessTargetPrototype._detach = f
       continue;
     }
     this._progressListener.unwatch(docShell);
   }
 
   return BrowsingContextTargetActor.prototype._detach.call(this);
 };
 
-/* ThreadActor hooks. */
-
-/**
- * Prepare to enter a nested event loop by disabling debuggee events.
- */
-parentProcessTargetPrototype.preNest = function() {
-  // Disable events in all open windows.
-  for (const { windowUtils } of Services.wm.getEnumerator(null)) {
-    windowUtils.suppressEventHandling(true);
-    windowUtils.suspendTimeouts();
-  }
-};
-
-/**
- * Prepare to exit a nested event loop by enabling debuggee events.
- */
-parentProcessTargetPrototype.postNest = function(nestData) {
-  // Enable events in all open windows.
-  for (const { windowUtils } of Services.wm.getEnumerator(null)) {
-    windowUtils.resumeTimeouts();
-    windowUtils.suppressEventHandling(false);
-  }
-};
-
 exports.parentProcessTargetPrototype = parentProcessTargetPrototype;
 exports.ParentProcessTargetActor = ActorClassWithSpec(
   parentProcessTargetSpec,
   parentProcessTargetPrototype
 );
--- a/devtools/server/actors/thread.js
+++ b/devtools/server/actors/thread.js
@@ -342,18 +342,16 @@ const ThreadActor = ActorClassWithSpec(t
 
     Object.assign(this._options, options || {});
     this.sources.setOptions(this._options);
     this.sources.on("newSource", this.onNewSourceEvent);
 
     // Initialize an event loop stack. This can't be done in the constructor,
     // because this.conn is not yet initialized by the actor pool at that time.
     this._nestedEventLoops = new EventLoopStack({
-      hooks: this._parent,
-      connection: this.conn,
       thread: this,
     });
 
     if (options.breakpoints) {
       this._setBreakpointsOnAttach(options.breakpoints);
     }
     if (options.eventBreakpoints) {
       this.setActiveEventBreakpoints(options.eventBreakpoints);
@@ -1204,24 +1202,22 @@ const ThreadActor = ActorClassWithSpec(t
       };
     }
 
     // In case of multiple nested event loops (due to multiple debuggers open in
     // different tabs or multiple debugger clients connected to the same tab)
     // only allow resumption in a LIFO order.
     if (
       this._nestedEventLoops.size &&
-      this._nestedEventLoops.lastPausedUrl &&
-      (this._nestedEventLoops.lastPausedUrl !== this._parent.url ||
-        this._nestedEventLoops.lastConnection !== this.conn)
+      this._nestedEventLoops.lastPausedThreadActor &&
+      this._nestedEventLoops.lastPausedThreadActor !== this
     ) {
       return {
         error: "wrongOrder",
         message: "trying to resume in the wrong order.",
-        lastPausedUrl: this._nestedEventLoops.lastPausedUrl,
       };
     }
 
     if (rewind && !this.dbg.replaying) {
       return {
         error: "cantRewind",
         message: "Can't rewind a debuggee that is not replaying.",
       };
@@ -2153,18 +2149,18 @@ exports.ChromeDebuggerActor = ChromeDebu
  *
  * @param Error error
  *        The error object you wish to report.
  * @param String prefix
  *        An optional prefix for the reported error message.
  */
 var oldReportError = reportError;
 this.reportError = function(error, prefix = "") {
-  assert(error instanceof Error, "Must pass Error objects to reportError");
-  const msg = prefix + error.message + ":\n" + error.stack;
+  const message = error.message ? error.message : String(error);
+  const msg = prefix + message + ":\n" + error.stack;
   oldReportError(msg);
   dumpn(msg);
 };
 
 function findPausePointForLocation(pausePoints, location) {
   const { line: line, column: column } = location;
   return pausePoints[line] && pausePoints[line][column];
 }
--- a/devtools/server/actors/utils/event-loop.js
+++ b/devtools/server/actors/utils/event-loop.js
@@ -2,135 +2,97 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const xpcInspector = require("xpcInspector");
-const DevToolsUtils = require("devtools/shared/DevToolsUtils");
-const { dumpn } = DevToolsUtils;
 
 /**
  * Manages pushing event loops and automatically pops and exits them in the
  * correct order as they are resolved.
  *
  * @param ThreadActor thread
  *        The thread actor instance that owns this EventLoopStack.
- * @param DebuggerServerConnection connection
- *        The remote protocol connection associated with this event loop stack.
- * @param Object hooks
- *        An object with the following properties:
- *          - url: The URL string of the debuggee we are spinning an event loop
- *                 for.
- *          - preNest: function called before entering a nested event loop
- *          - postNest: function called after exiting a nested event loop
  */
-function EventLoopStack({ thread, connection, hooks }) {
-  this._hooks = hooks;
+function EventLoopStack({ thread }) {
   this._thread = thread;
-  this._connection = connection;
 }
 
 EventLoopStack.prototype = {
   /**
    * The number of nested event loops on the stack.
    */
   get size() {
     return xpcInspector.eventLoopNestLevel;
   },
 
   /**
-   * The URL of the debuggee who pushed the event loop on top of the stack.
+   * The thread actor of the debuggee who pushed the event loop on top of the stack.
    */
-  get lastPausedUrl() {
-    let url = null;
+  get lastPausedThreadActor() {
     if (this.size > 0) {
-      try {
-        url = xpcInspector.lastNestRequestor.url;
-      } catch (e) {
-        // The tab's URL getter may throw if the tab is destroyed by the time
-        // this code runs, but we don't really care at this point.
-        dumpn(e);
-      }
+      return xpcInspector.lastNestRequestor.thread;
     }
-    return url;
-  },
-
-  /**
-   * The DebuggerServerConnection of the debugger who pushed the event loop on
-   * top of the stack
-   */
-  get lastConnection() {
-    return xpcInspector.lastNestRequestor._connection;
+    return null;
   },
 
   /**
    * Push a new nested event loop onto the stack.
    *
    * @returns EventLoop
    */
   push: function() {
     return new EventLoop({
       thread: this._thread,
-      connection: this._connection,
-      hooks: this._hooks,
     });
   },
 };
 
 /**
  * An object that represents a nested event loop. It is used as the nest
  * requestor with nsIJSInspector instances.
  *
  * @param ThreadActor thread
  *        The thread actor that is creating this nested event loop.
- * @param DebuggerServerConnection connection
- *        The remote protocol connection associated with this event loop.
- * @param Object hooks
- *        The same hooks object passed into EventLoopStack during its
- *        initialization.
  */
-function EventLoop({ thread, connection, hooks }) {
+function EventLoop({ thread }) {
   this._thread = thread;
-  this._hooks = hooks;
-  this._connection = connection;
 
   this.enter = this.enter.bind(this);
   this.resolve = this.resolve.bind(this);
 }
 
 EventLoop.prototype = {
   entered: false,
   resolved: false,
-  get url() {
-    return this._hooks.url;
+  get thread() {
+    return this._thread;
   },
 
   /**
    * Enter this nested event loop.
    */
   enter: function() {
-    const nestData = this._hooks.preNest ? this._hooks.preNest() : null;
+    const preNestData = this.preNest();
 
     this.entered = true;
     xpcInspector.enterNestedEventLoop(this);
 
     // Keep exiting nested event loops while the last requestor is resolved.
     if (xpcInspector.eventLoopNestLevel > 0) {
       const { resolved } = xpcInspector.lastNestRequestor;
       if (resolved) {
         xpcInspector.exitNestedEventLoop();
       }
     }
 
-    if (this._hooks.postNest) {
-      this._hooks.postNest(nestData);
-    }
+    this.postNest(preNestData);
   },
 
   /**
    * Resolve this nested event loop.
    *
    * @returns boolean
    *          True if we exited this nested event loop because it was on top of
    *          the stack, false if there is another nested event loop above this
@@ -147,11 +109,60 @@ EventLoop.prototype = {
     }
     this.resolved = true;
     if (this === xpcInspector.lastNestRequestor) {
       xpcInspector.exitNestedEventLoop();
       return true;
     }
     return false;
   },
+
+  /**
+   * Retrieve the list of all DOM Windows debugged by the current thread actor.
+   */
+  getAllWindowDebuggees() {
+    return (
+      this._thread.dbg
+        .getDebuggees()
+        .filter(debuggee => {
+          // Select only debuggee that relates to windows
+          // e.g. ignore sandboxes, jsm and such
+          return debuggee.class == "Window";
+        })
+        .map(debuggee => {
+          // Retrieve the JS reference for these windows
+          return debuggee.unsafeDereference();
+        })
+        // Ignore iframes as they will be paused automatically when pausing their
+        // owner top level document
+        .filter(window => window.top === window)
+    );
+  },
+
+  /**
+   * Prepare to enter a nested event loop by disabling debuggee events.
+   */
+  preNest() {
+    const windows = [];
+    // Disable events in all open windows.
+    for (const window of this.getAllWindowDebuggees()) {
+      const { windowUtils } = window;
+      windowUtils.suppressEventHandling(true);
+      windowUtils.suspendTimeouts();
+      windows.push(window);
+    }
+    return windows;
+  },
+
+  /**
+   * Prepare to exit a nested event loop by enabling debuggee events.
+   */
+  postNest(pausedWindows) {
+    // Enable events in all open windows.
+    for (const window of pausedWindows) {
+      const { windowUtils } = window;
+      windowUtils.resumeTimeouts();
+      windowUtils.suppressEventHandling(false);
+    }
+  },
 };
 
 exports.EventLoopStack = EventLoopStack;
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/unit/test_breakpoint-25.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow */
+
+"use strict";
+
+/**
+ * Ensure that the debugger resume page execution when the connection drops
+ * and when the target is detached.
+ */
+
+add_task(
+  threadFrontTest(({ threadFront, client, debuggee, targetFront }) => {
+    return new Promise(resolve => {
+      threadFront.once("paused", async function(packet) {
+        ok(true, "The page is paused");
+        ok(!debuggee.foo, "foo is still false after we hit the breakpoint");
+
+        await targetFront.detach();
+
+        // `detach` will force the destruction of the thread actor, which,
+        // will resume the page execution. But all of that seems to be
+        // synchronous and we have to spin the event loop in order to ensure
+        // having the content javascript to execute the resumed code.
+        await new Promise(executeSoon);
+
+        // Closing the connection will force the thread actor to resume page
+        // execution
+        ok(debuggee.foo, "foo is true after target's detach request");
+
+        executeSoon(resolve);
+      });
+
+      /* eslint-disable */
+      Cu.evalInSandbox(
+        "var foo = false;\n" +
+        "debugger;\n" +
+        "foo = true;\n",
+        debuggee
+      );
+      /* eslint-enable */
+      ok(debuggee.foo, "foo is false at startup");
+    });
+  })
+);
+
+add_task(
+  threadFrontTest(({ threadFront, client, debuggee }) => {
+    return new Promise(resolve => {
+      threadFront.once("paused", async function(packet) {
+        ok(true, "The page is paused");
+        ok(!debuggee.foo, "foo is still false after we hit the breakpoint");
+
+        await client.close();
+
+        // `detach` will force the destruction of the thread actor, which,
+        // will resume the page execution. But all of that seems to be
+        // synchronous and we have to spin the event loop in order to ensure
+        // having the content javascript to execute the resumed code.
+        await new Promise(executeSoon);
+
+        // Closing the connection will force the thread actor to resume page
+        // execution
+        ok(debuggee.foo, "foo is true after client close");
+
+        executeSoon(resolve);
+        dump("resolved\n");
+      });
+
+      /* eslint-disable */
+      Cu.evalInSandbox(
+        "var foo = false;\n" +
+        "debugger;\n" +
+        "foo = true;\n",
+        debuggee
+      );
+      /* eslint-enable */
+      ok(debuggee.foo, "foo is false at startup");
+    });
+  })
+);
--- a/devtools/server/tests/unit/testactors.js
+++ b/devtools/server/tests/unit/testactors.js
@@ -165,16 +165,17 @@ TestTargetActor.prototype = {
 
     return { type: "tabAttached", threadActor: this.threadActor.actorID };
   },
 
   onDetach: function(request) {
     if (!this._attached) {
       return { error: "wrongState" };
     }
+    this.threadActor.exit();
     return { type: "detached" };
   },
 
   onReload: function(request) {
     this.sources.reset();
     this.threadActor.clearDebuggees();
     this.threadActor.dbg.addDebuggees();
     return {};
--- a/devtools/server/tests/unit/xpcshell.ini
+++ b/devtools/server/tests/unit/xpcshell.ini
@@ -130,16 +130,17 @@ skip-if = true # tests for breakpoint ac
 skip-if = true
 reason = bug 1104838
 [test_breakpoint-20.js]
 [test_breakpoint-21.js]
 [test_breakpoint-22.js]
 skip-if = true # breakpoint sliding is not supported bug 1525685
 [test_breakpoint-23.js]
 [test_breakpoint-24.js]
+[test_breakpoint-25.js]
 [test_conditional_breakpoint-01.js]
 [test_conditional_breakpoint-02.js]
 [test_conditional_breakpoint-03.js]
 [test_conditional_breakpoint-04.js]
 [test_logpoint-01.js]
 [test_logpoint-02.js]
 [test_logpoint-03.js]
 [test_listsources-01.js]
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -34,16 +34,17 @@
 #include "mozilla/StaticPrefs_dom.h"
 #include "mozilla/StaticPrefs_full_screen_api.h"
 #include "mozilla/StaticPrefs_layout.h"
 #include "mozilla/StaticPrefs_plugins.h"
 #include "mozilla/StaticPrefs_privacy.h"
 #include "mozilla/StaticPrefs_security.h"
 #include "mozilla/StorageAccess.h"
 #include "mozilla/TextEditor.h"
+#include "mozilla/URLDecorationStripper.h"
 #include "mozilla/URLExtraData.h"
 #include "mozilla/Base64.h"
 #include <algorithm>
 
 #include "mozilla/Logging.h"
 #include "plstr.h"
 #include "mozilla/Sprintf.h"
 
@@ -5406,17 +5407,17 @@ void Document::GetReferrer(nsAString& aR
   }
 
   nsCOMPtr<nsIURI> referrer = mReferrerInfo->GetComputedReferrer();
   if (!referrer) {
     return;
   }
 
   nsAutoCString uri;
-  nsresult rv = referrer->GetSpec(uri);
+  nsresult rv = URLDecorationStripper::StripTrackingIdentifiers(referrer, uri);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return;
   }
 
   CopyUTF8toUTF16(uri, aReferrer);
 }
 
 void Document::GetCookie(nsAString& aCookie, ErrorResult& rv) {
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -7990,17 +7990,17 @@ bool nsContentUtils::IsThirdPartyWindowO
 
   if (aChannel) {
     // Note, we must call IsThirdPartyChannel() here and not just try to
     // use nsILoadInfo.isThirdPartyContext.  That nsILoadInfo property only
     // indicates if the parent loading window is third party or not.  We
     // want to check the channel URI against the loading principal as well.
     nsresult rv =
         thirdPartyUtil->IsThirdPartyChannel(aChannel, nullptr, &thirdParty);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
+    if (NS_FAILED(rv)) {
       // Assume third-party in case of failure
       thirdParty = true;
     }
 
     // We check isThirdPartyWindow to expand the list of domains that are
     // considered first party (e.g., if facebook.com includes an iframe from
     // fatratgames.com, all subsources included in that iframe are considered
     // third-party with isThirdPartyChannel, even if they are not third-party
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -110,16 +110,17 @@
 #include "mozilla/dom/Promise.h"
 #include "mozilla/ServoBindings.h"
 #include "mozilla/StyleSheetInlines.h"
 #include "mozilla/gfx/GPUProcessManager.h"
 #include "mozilla/dom/TimeoutManager.h"
 #include "mozilla/PreloadedStyleSheet.h"
 #include "mozilla/layers/WebRenderBridgeChild.h"
 #include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/ResultExtensions.h"
 
 #ifdef XP_WIN
 #  undef GetClassName
 #endif
 
 using namespace mozilla;
 using namespace mozilla::dom;
 using namespace mozilla::ipc;
@@ -4098,24 +4099,70 @@ nsDOMWindowUtils::WrCapture() {
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDOMWindowUtils::SetCompositionRecording(bool aValue) {
   if (CompositorBridgeChild* cbc = GetCompositorBridge()) {
     if (aValue) {
-      cbc->SendBeginRecording(TimeStamp::Now());
+      RefPtr<nsDOMWindowUtils> self = this;
+      cbc->SendBeginRecording(TimeStamp::Now())
+          ->Then(
+              GetCurrentThreadSerialEventTarget(), __func__,
+              [self](const bool& aSuccess) {
+                if (!aSuccess) {
+                  self->ReportErrorMessageForWindow(
+                      NS_LITERAL_STRING(
+                          "The composition recorder is already running."),
+                      "DOM", true);
+                }
+              },
+              [self](const mozilla::ipc::ResponseRejectReason&) {
+                self->ReportErrorMessageForWindow(
+                    NS_LITERAL_STRING(
+                        "Could not start the composition recorder."),
+                    "DOM", true);
+              });
     } else {
-      cbc->SendEndRecording();
+      bool success = false;
+      if (!cbc->SendEndRecording(&success)) {
+        ReportErrorMessageForWindow(
+            NS_LITERAL_STRING("Could not stop the composition recorder."),
+            "DOM", true);
+      } else if (!success) {
+        ReportErrorMessageForWindow(
+            NS_LITERAL_STRING("The composition recorder is not running."),
+            "DOM", true);
+      }
     }
   }
+
   return NS_OK;
 }
 
+void nsDOMWindowUtils::ReportErrorMessageForWindow(
+    const nsAString& aErrorMessage, const char* aClassification,
+    bool aFromChrome) {
+  bool isPrivateWindow = false;
+
+  if (nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryReferent(mWindow)) {
+    if (nsIPrincipal* principal =
+            nsGlobalWindowOuter::Cast(window)->GetPrincipal()) {
+      uint32_t privateBrowsingId = 0;
+
+      if (NS_SUCCEEDED(principal->GetPrivateBrowsingId(&privateBrowsingId))) {
+        isPrivateWindow = !!privateBrowsingId;
+      }
+    }
+  }
+  nsContentUtils::LogSimpleConsoleError(aErrorMessage, aClassification,
+                                        isPrivateWindow, aFromChrome);
+}
+
 NS_IMETHODIMP
 nsDOMWindowUtils::SetSystemFont(const nsACString& aFontName) {
   nsIWidget* widget = GetWidget();
   if (!widget) {
     return NS_OK;
   }
 
   nsAutoCString fontName(aFontName);
--- a/dom/base/nsDOMWindowUtils.h
+++ b/dom/base/nsDOMWindowUtils.h
@@ -96,11 +96,15 @@ class nsDOMWindowUtils final : public ns
   MOZ_CAN_RUN_SCRIPT
   nsresult SendTouchEventCommon(
       const nsAString& aType, const nsTArray<uint32_t>& aIdentifiers,
       const nsTArray<int32_t>& aXs, const nsTArray<int32_t>& aYs,
       const nsTArray<uint32_t>& aRxs, const nsTArray<uint32_t>& aRys,
       const nsTArray<float>& aRotationAngles, const nsTArray<float>& aForces,
       int32_t aModifiers, bool aIgnoreRootScrollFrame, bool aToWindow,
       bool* aPreventDefault);
+
+  void ReportErrorMessageForWindow(const nsAString& aErrorMessage,
+                                   const char* aClassification,
+                                   bool aFromChrome);
 };
 
 #endif
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -2273,33 +2273,28 @@ bool nsGlobalWindowInner::ShouldReportFo
       nsGlobalWindowInner::Cast(topOuter->GetCurrentInnerWindow());
   NS_ENSURE_TRUE(topInner, false);
 
   topInner->ShouldReportForServiceWorkerScopeInternal(
       NS_ConvertUTF16toUTF8(aScope), &result);
   return result;
 }
 
-already_AddRefed<InstallTriggerImpl> nsGlobalWindowInner::GetInstallTrigger() {
+InstallTriggerImpl* nsGlobalWindowInner::GetInstallTrigger() {
   if (!mInstallTrigger) {
-    JS::Rooted<JSObject*> jsImplObj(RootingCx());
     ErrorResult rv;
-    ConstructJSImplementation("@mozilla.org/addons/installtrigger;1", this,
-                              &jsImplObj, rv);
+    mInstallTrigger = ConstructJSImplementation<InstallTriggerImpl>(
+        "@mozilla.org/addons/installtrigger;1", this, rv);
     if (rv.Failed()) {
       rv.SuppressException();
       return nullptr;
     }
-    MOZ_RELEASE_ASSERT(!js::IsWrapper(jsImplObj));
-    JS::Rooted<JSObject*> jsImplGlobal(RootingCx(),
-                                       JS::GetNonCCWObjectGlobal(jsImplObj));
-    mInstallTrigger = new InstallTriggerImpl(jsImplObj, jsImplGlobal, this);
-  }
-
-  return do_AddRef(mInstallTrigger);
+  }
+
+  return mInstallTrigger;
 }
 
 nsIDOMWindowUtils* nsGlobalWindowInner::GetWindowUtils(ErrorResult& aRv) {
   FORWARD_TO_OUTER_OR_THROW(WindowUtils, (), aRv, nullptr);
 }
 
 bool nsGlobalWindowInner::HasOpenerForInitialContentBrowser() {
   FORWARD_TO_OUTER(HasOpenerForInitialContentBrowser, (), false);
@@ -6755,32 +6750,27 @@ already_AddRefed<Console> nsGlobalWindow
   return console.forget();
 }
 
 bool nsGlobalWindowInner::IsSecureContext() const {
   JS::Realm* realm = js::GetNonCCWObjectRealm(GetWrapperPreserveColor());
   return JS::GetIsSecureContext(realm);
 }
 
-already_AddRefed<External> nsGlobalWindowInner::GetExternal(ErrorResult& aRv) {
+External* nsGlobalWindowInner::GetExternal(ErrorResult& aRv) {
 #ifdef HAVE_SIDEBAR
   if (!mExternal) {
-    JS::Rooted<JSObject*> jsImplObj(RootingCx());
-    ConstructJSImplementation("@mozilla.org/sidebar;1", this, &jsImplObj, aRv);
+    mExternal = ConstructJSImplementation<External>("@mozilla.org/sidebar;1",
+                                                    this, aRv);
     if (aRv.Failed()) {
       return nullptr;
     }
-    MOZ_RELEASE_ASSERT(!js::IsWrapper(jsImplObj));
-    JS::Rooted<JSObject*> jsImplGlobal(RootingCx(),
-                                       JS::GetNonCCWObjectGlobal(jsImplObj));
-    mExternal = new External(jsImplObj, jsImplGlobal, this);
-  }
-
-  RefPtr<External> external = static_cast<External*>(mExternal.get());
-  return external.forget();
+  }
+
+  return static_cast<External*>(mExternal.get());
 #else
   aRv.Throw(NS_ERROR_NOT_IMPLEMENTED);
   return nullptr;
 #endif
 }
 
 void nsGlobalWindowInner::GetSidebar(OwningExternalOrWindowProxy& aResult,
                                      ErrorResult& aRv) {
--- a/dom/base/nsGlobalWindowInner.h
+++ b/dom/base/nsGlobalWindowInner.h
@@ -648,18 +648,17 @@ class nsGlobalWindowInner final : public
   already_AddRefed<mozilla::dom::Console> GetConsole(JSContext* aCx,
                                                      mozilla::ErrorResult& aRv);
 
   // https://w3c.github.io/webappsec-secure-contexts/#dom-window-issecurecontext
   bool IsSecureContext() const;
 
   void GetSidebar(mozilla::dom::OwningExternalOrWindowProxy& aResult,
                   mozilla::ErrorResult& aRv);
-  already_AddRefed<mozilla::dom::External> GetExternal(
-      mozilla::ErrorResult& aRv);
+  mozilla::dom::External* GetExternal(mozilla::ErrorResult& aRv);
 
   mozilla::dom::Worklet* GetPaintWorklet(mozilla::ErrorResult& aRv);
 
   void GetRegionalPrefsLocales(nsTArray<nsString>& aLocales);
 
   void GetWebExposedLocales(nsTArray<nsString>& aLocales);
 
   mozilla::dom::IntlUtils* GetIntlUtils(mozilla::ErrorResult& aRv);
@@ -941,17 +940,17 @@ class nsGlobalWindowInner final : public
   void GetInterface(JSContext* aCx, JS::Handle<JS::Value> aIID,
                     JS::MutableHandle<JS::Value> aRetval,
                     mozilla::ErrorResult& aError);
 
   already_AddRefed<nsWindowRoot> GetWindowRoot(mozilla::ErrorResult& aError);
 
   bool ShouldReportForServiceWorkerScope(const nsAString& aScope);
 
-  already_AddRefed<mozilla::dom::InstallTriggerImpl> GetInstallTrigger();
+  mozilla::dom::InstallTriggerImpl* GetInstallTrigger();
 
   nsIDOMWindowUtils* GetWindowUtils(mozilla::ErrorResult& aRv);
 
   bool HasOpenerForInitialContentBrowser();
 
   void UpdateTopInnerWindow();
 
   virtual bool IsInSyncOperation() override {
--- a/dom/bindings/BindingUtils.cpp
+++ b/dom/bindings/BindingUtils.cpp
@@ -2570,34 +2570,16 @@ bool GetContentGlobalForJSImplementedObj
 
   DebugOnly<nsresult> rv =
       CallQueryInterface(global.GetAsSupports(), globalObj);
   MOZ_ASSERT(NS_SUCCEEDED(rv));
   MOZ_ASSERT(*globalObj);
   return true;
 }
 
-already_AddRefed<nsIGlobalObject> ConstructJSImplementation(
-    const char* aContractId, const GlobalObject& aGlobal,
-    JS::MutableHandle<JSObject*> aObject, ErrorResult& aRv) {
-  // Get the global object to use as a parent and for initialization.
-  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
-  if (!global) {
-    aRv.Throw(NS_ERROR_FAILURE);
-    return nullptr;
-  }
-
-  ConstructJSImplementation(aContractId, global, aObject, aRv);
-
-  if (aRv.Failed()) {
-    return nullptr;
-  }
-  return global.forget();
-}
-
 void ConstructJSImplementation(const char* aContractId,
                                nsIGlobalObject* aGlobal,
                                JS::MutableHandle<JSObject*> aObject,
                                ErrorResult& aRv) {
   MOZ_ASSERT(NS_IsMainThread());
 
   // Make sure to divorce ourselves from the calling JS while creating and
   // initializing the object, so exceptions from that will get reported
--- a/dom/bindings/BindingUtils.h
+++ b/dom/bindings/BindingUtils.h
@@ -21,16 +21,17 @@
 #include "mozilla/dom/CallbackObject.h"
 #include "mozilla/dom/DOMJSClass.h"
 #include "mozilla/dom/DOMJSProxyHandler.h"
 #include "mozilla/dom/Exceptions.h"
 #include "mozilla/dom/NonRefcountedDOMObject.h"
 #include "mozilla/dom/Nullable.h"
 #include "mozilla/dom/PrototypeList.h"
 #include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/SegmentedVector.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/Likely.h"
 #include "mozilla/MemoryReporting.h"
 #include "nsAutoPtr.h"
 #include "mozilla/dom/Document.h"
 #include "nsIGlobalObject.h"
 #include "nsIXPConnect.h"
@@ -2463,19 +2464,45 @@ bool GetContentGlobalForJSImplementedObj
                                             JS::Handle<JSObject*> obj,
                                             nsIGlobalObject** global);
 
 void ConstructJSImplementation(const char* aContractId,
                                nsIGlobalObject* aGlobal,
                                JS::MutableHandle<JSObject*> aObject,
                                ErrorResult& aRv);
 
-already_AddRefed<nsIGlobalObject> ConstructJSImplementation(
-    const char* aContractId, const GlobalObject& aGlobal,
-    JS::MutableHandle<JSObject*> aObject, ErrorResult& aRv);
+template <typename T>
+already_AddRefed<T> ConstructJSImplementation(const char* aContractId,
+                                              nsIGlobalObject* aGlobal,
+                                              ErrorResult& aRv) {
+  JS::RootingContext* cx = RootingCx();
+  JS::Rooted<JSObject*> jsImplObj(cx);
+  ConstructJSImplementation(aContractId, aGlobal, &jsImplObj, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  MOZ_RELEASE_ASSERT(!js::IsWrapper(jsImplObj));
+  JS::Rooted<JSObject*> jsImplGlobal(cx, JS::GetNonCCWObjectGlobal(jsImplObj));
+  RefPtr<T> newObj = new T(jsImplObj, jsImplGlobal, aGlobal);
+  return newObj.forget();
+}
+
+template <typename T>
+already_AddRefed<T> ConstructJSImplementation(const char* aContractId,
+                                              const GlobalObject& aGlobal,
+                                              ErrorResult& aRv) {
+  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+  if (!global) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  return ConstructJSImplementation<T>(aContractId, global, aRv);
+}
 
 /**
  * Convert an nsCString to jsval, returning true on success.
  * These functions are intended for ByteString implementations.
  * As such, the string is not UTF-8 encoded.  Any UTF8 strings passed to these
  * methods will be mangled.
  */
 bool NonVoidByteStringToJsval(JSContext* cx, const nsACString& str,
--- a/dom/bindings/Codegen.py
+++ b/dom/bindings/Codegen.py
@@ -15902,17 +15902,17 @@ class CGJSImplMethod(CGJSImplMember):
             assert args[1].argType == 'JSContext*'
             assert args[-1].argType == 'JS::Handle<JSObject*>'
             assert args[-1].name == 'aGivenProto'
             constructorArgs = [arg.name for arg in args[2:-1]]
             constructorArgs.append("js::GetNonCCWObjectRealm(scopeObj)")
             initCall = fill(
                 """
                 // Wrap the object before calling __Init so that __DOM_IMPL__ is available.
-                JS::Rooted<JSObject*> scopeObj(cx, globalHolder->GetGlobalJSObject());
+                JS::Rooted<JSObject*> scopeObj(cx, global.Get());
                 MOZ_ASSERT(js::IsObjectInContextCompartment(scopeObj, cx));
                 JS::Rooted<JS::Value> wrappedVal(cx);
                 if (!GetOrCreateDOMReflector(cx, impl, &wrappedVal, aGivenProto)) {
                   MOZ_ASSERT(JS_IsExceptionPending(cx));
                   aRv.Throw(NS_ERROR_UNEXPECTED);
                   return nullptr;
                 }
                 // Initialize the object with the constructor arguments.
@@ -15925,27 +15925,21 @@ class CGJSImplMethod(CGJSImplMember):
         else:
             initCall = ""
         return genConstructorBody(self.descriptor, initCall)
 
 
 def genConstructorBody(descriptor, initCall=""):
     return fill(
         """
-        JS::Rooted<JSObject*> jsImplObj(cx);
-        nsCOMPtr<nsIGlobalObject> globalHolder =
-          ConstructJSImplementation("${contractId}", global, &jsImplObj, aRv);
+        RefPtr<${implClass}> impl =
+          ConstructJSImplementation<${implClass}>("${contractId}", global, aRv);
         if (aRv.Failed()) {
           return nullptr;
         }
-        // We should be getting the implementation object for the relevant
-        // contract here, which should never be a cross-compartment wrapper.
-        JS::Rooted<JSObject*> jsImplGlobal(cx, JS::GetNonCCWObjectGlobal(jsImplObj));
-        // Build the C++ implementation.
-        RefPtr<${implClass}> impl = new ${implClass}(jsImplObj, jsImplGlobal, globalHolder);
         $*{initCall}
         return impl.forget();
         """,
         contractId=descriptor.interface.getJSImplementation(),
         implClass=descriptor.name,
         initCall=initCall)
 
 
--- a/dom/browser-element/mochitest/mochitest.ini
+++ b/dom/browser-element/mochitest/mochitest.ini
@@ -122,17 +122,16 @@ disabled = won't work as Firefox desktop
 [test_browserElement_inproc_OpenWindowRejected.html]
 [test_browserElement_inproc_Opensearch.html]
 [test_browserElement_inproc_PrivateBrowsing.html]
 [test_browserElement_inproc_PromptCheck.html]
 [test_browserElement_inproc_PromptConfirm.html]
 [test_browserElement_inproc_RemoveBrowserElement.html]
 [test_browserElement_inproc_ScrollEvent.html]
 [test_browserElement_inproc_SecurityChange.html]
-skip-if = android_version == '22' # bug 1358876, bug 1322607
 [test_browserElement_inproc_TargetBlank.html]
 [test_browserElement_inproc_TargetTop.html]
 [test_browserElement_inproc_Titlechange.html]
 [test_browserElement_inproc_TopBarrier.html]
 [test_browserElement_inproc_XFrameOptions.html]
 [test_browserElement_inproc_XFrameOptionsDeny.html]
 [test_browserElement_inproc_XFrameOptionsSameOrigin.html]
 [test_browserElement_inproc_Reload.html]
--- a/dom/events/EventStateManager.cpp
+++ b/dom/events/EventStateManager.cpp
@@ -3943,29 +3943,27 @@ void EventStateManager::ClearCachedWidge
 nsresult EventStateManager::SetCursor(StyleCursorKind aCursor,
                                       imgIContainer* aContainer,
                                       const Maybe<gfx::IntPoint>& aHotspot,
                                       nsIWidget* aWidget, bool aLockCursor) {
   EnsureDocument(mPresContext);
   NS_ENSURE_TRUE(mDocument, NS_ERROR_FAILURE);
   sMouseOverDocument = mDocument.get();
 
-  nsCursor c;
-
   NS_ENSURE_TRUE(aWidget, NS_ERROR_FAILURE);
   if (aLockCursor) {
     if (StyleCursorKind::Auto != aCursor) {
       mLockCursor = aCursor;
     } else {
       // If cursor style is set to auto we unlock the cursor again.
       mLockCursor = kInvalidCursorKind;
     }
   }
+  nsCursor c;
   switch (aCursor) {
-    default:
     case StyleCursorKind::Auto:
     case StyleCursorKind::Default:
       c = eCursor_standard;
       break;
     case StyleCursorKind::Pointer:
       c = eCursor_hyperlink;
       break;
     case StyleCursorKind::Crosshair:
@@ -4062,16 +4060,20 @@ nsresult EventStateManager::SetCursor(St
       c = eCursor_ns_resize;
       break;
     case StyleCursorKind::EwResize:
       c = eCursor_ew_resize;
       break;
     case StyleCursorKind::None:
       c = eCursor_none;
       break;
+    default:
+      MOZ_ASSERT_UNREACHABLE("Unknown cursor kind");
+      c = eCursor_standard;
+      break;
   }
 
   int32_t x = aHotspot ? aHotspot->x : 0;
   int32_t y = aHotspot ? aHotspot->y : 0;
   aWidget->SetCursor(c, aContainer, x, y);
   return NS_OK;
 }
 
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -2359,44 +2359,16 @@ RefPtr<MediaManager::StreamPromise> Medi
   docURI->SchemeIs("https", &isHTTPS);
   nsCString host;
   nsresult rv = docURI->GetHost(host);
   // Test for some other schemes that ServiceWorker recognizes
   bool isFile;
   docURI->SchemeIs("file", &isFile);
   bool isApp;
   docURI->SchemeIs("app", &isApp);
-  // Same localhost check as ServiceWorkers uses
-  // (see IsOriginPotentiallyTrustworthy())
-  bool isLocalhost =
-      NS_SUCCEEDED(rv) && (host.LowerCaseEqualsLiteral("localhost") ||
-                           host.LowerCaseEqualsLiteral("127.0.0.1") ||
-                           host.LowerCaseEqualsLiteral("::1"));
-
-  // Record telemetry about whether the source of the call was secure, i.e.,
-  // privileged or HTTPS.  We may handle other cases
-  if (privileged) {
-    Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_SECURE_ORIGIN,
-                          (uint32_t)GetUserMediaSecurityState::Privileged);
-  } else if (isHTTPS) {
-    Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_SECURE_ORIGIN,
-                          (uint32_t)GetUserMediaSecurityState::HTTPS);
-  } else if (isFile) {
-    Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_SECURE_ORIGIN,
-                          (uint32_t)GetUserMediaSecurityState::File);
-  } else if (isApp) {
-    Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_SECURE_ORIGIN,
-                          (uint32_t)GetUserMediaSecurityState::App);
-  } else if (isLocalhost) {
-    Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_SECURE_ORIGIN,
-                          (uint32_t)GetUserMediaSecurityState::Localhost);
-  } else {
-    Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_SECURE_ORIGIN,
-                          (uint32_t)GetUserMediaSecurityState::Other);
-  }
 
   nsCOMPtr<nsIPrincipal> principal =
       nsGlobalWindowInner::Cast(aWindow)->GetPrincipal();
   if (NS_WARN_IF(!principal)) {
     return StreamPromise::CreateAndReject(
         MakeRefPtr<MediaMgrError>(MediaMgrError::Name::SecurityError),
         __func__);
   }
--- a/dom/media/PeerConnection.jsm
+++ b/dom/media/PeerConnection.jsm
@@ -2112,39 +2112,16 @@ class PeerConnectionObserver {
   //
   //   closed        All of the RTCIceTransports are in the closed state.
 
   handleIceConnectionStateChange(iceConnectionState) {
     let pc = this._dompc;
     if (pc.iceConnectionState === iceConnectionState) {
       return;
     }
-    if (pc.iceConnectionState === "new") {
-      var checking_histogram = Services.telemetry.getHistogramById(
-        "WEBRTC_ICE_CHECKING_RATE"
-      );
-      if (iceConnectionState === "checking") {
-        checking_histogram.add(true);
-      } else if (iceConnectionState === "failed") {
-        checking_histogram.add(false);
-      }
-    } else if (pc.iceConnectionState === "checking") {
-      var success_histogram = Services.telemetry.getHistogramById(
-        "WEBRTC_ICE_SUCCESS_RATE"
-      );
-      if (
-        iceConnectionState === "completed" ||
-        iceConnectionState === "connected"
-      ) {
-        success_histogram.add(true);
-        pc._pcTelemetry.recordConnected();
-      } else if (iceConnectionState === "failed") {
-        success_histogram.add(false);
-      }
-    }
 
     if (iceConnectionState === "failed") {
       if (!pc._hasStunServer) {
         pc.logError(
           "ICE failed, add a STUN server and see about:webrtc for more details"
         );
       } else if (!pc._hasTurnServer) {
         pc.logError(
--- a/dom/media/encoder/TrackEncoder.cpp
+++ b/dom/media/encoder/TrackEncoder.cpp
@@ -750,12 +750,16 @@ size_t VideoTrackEncoder::SizeOfExcludin
     mozilla::MallocSizeOf aMallocSizeOf) {
   MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
   return mIncomingBuffer.SizeOfExcludingThis(aMallocSizeOf) +
          mOutgoingBuffer.SizeOfExcludingThis(aMallocSizeOf);
 }
 
 void VideoTrackEncoder::SetKeyFrameInterval(int32_t aKeyFrameInterval) {
   MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+  if (aKeyFrameInterval == 0) {
+    mKeyFrameInterval = DEFAULT_KEYFRAME_INTERVAL_MS;
+    return;
+  }
   mKeyFrameInterval = std::min(aKeyFrameInterval, DEFAULT_KEYFRAME_INTERVAL_MS);
 }
 
 }  // namespace mozilla
--- a/dom/media/mediasource/test/mochitest.ini
+++ b/dom/media/mediasource/test/mochitest.ini
@@ -55,17 +55,16 @@ support-files =
 [test_AppendPartialInitSegment.html]
 [test_AVC3_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_AudioChange_mp4.html]
 skip-if = toolkit == 'android' || (os == "win" && processor == "aarch64") # Not supported on android, aarch64 due to 1538331
 [test_AutoRevocation.html]
 tags = firstpartyisolation
 [test_BufferedSeek.html]
-skip-if = android_version == '22' # bug 1329532 bug 1066090
 [test_BufferedSeek_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_BufferingWait.html]
 skip-if = toolkit == 'android' #timeout android bug 1199531
 [test_BufferingWait_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_ChangeType.html]
 skip-if = toolkit == 'android' # Not supported on android
@@ -73,53 +72,50 @@ skip-if = toolkit == 'android' # Not sup
 skip-if = toolkit == 'android' # Not supported on android
 [test_DrainOnMissingData_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_DurationChange.html]
 [test_DurationUpdated.html]
 [test_DurationUpdated_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_EndedEvent.html]
-skip-if = android_version == '22' || toolkit == 'android' # bug 1358640, bug 1401090
+skip-if = toolkit == 'android' # bug 1358640, bug 1401090
 [test_EndOfStream.html]
 [test_EndOfStream_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_Eviction_mp4.html]
 [test_ExperimentalAsync.html]
-skip-if = android_version == '22' || toolkit == 'android' || (os == "win" && processor == "aarch64") # bug 1341519, bug 1401090, aarch64 due to 1538391
+skip-if = toolkit == 'android' || (os == "win" && processor == "aarch64") # bug 1341519, bug 1401090, aarch64 due to 1538391
 [test_FrameSelection.html]
-skip-if = android_version == '22' || toolkit == 'android' # bug 1341519, bug 1401090
+skip-if = toolkit == 'android' # bug 1341519, bug 1401090
 [test_FrameSelection_mp4.html]
 skip-if = toolkit == 'android' || os == 'win' || (os == 'mac' && os_version == '10.14') # Not supported on android, # bug 1487973, mac due to bug 1487973
 [test_isTypeSupportedExtensions.html]
 skip-if = android_version >= 28 # bug 1543669 ; cross origins broken on our Android 8.0 emulators?
 [test_HaveMetadataUnbufferedSeek.html]
-skip-if = android_version == '22' || toolkit == 'android' # bug 1342247, bug 1401090
+skip-if = toolkit == 'android' # bug 1342247, bug 1401090
 [test_HaveMetadataUnbufferedSeek_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_LiveSeekable.html]
 [test_LoadedDataFired_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_LoadedMetadataFired.html]
 [test_LoadedMetadataFired_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_MediaSource.html]
-skip-if = android_version == '22' # bug 1341146
 [test_MediaSource_memory_reporting.html]
-skip-if = android_version == '22' # bug 1225758
 [test_MediaSource_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_MediaSource_flac_mp4.html]
 skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1526064
 [test_MediaSource_disabled.html]
 [test_MultipleInitSegments.html]
 [test_MultipleInitSegments_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_OnEvents.html]
-skip-if = android_version == '22' # bug 1359010
 [test_PlayEvents.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_PlayEventsAutoPlaying.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_PlayEventsAutoPlaying2.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_RemoveSourceBuffer.html]
 [test_ResumeAfterClearing_mp4.html]
@@ -139,29 +135,26 @@ skip-if = toolkit == 'android' # Not sup
 [test_SeekToLastFrame_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_SeekTwice_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_Sequence_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_SetModeThrows.html]
 [test_SplitAppendDelay.html]
-skip-if = android_version == '22' # bug 1293896 bug 1342683
 [test_SplitAppendDelay_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_SplitAppend.html]
-skip-if = android_version == '22' # bug 1211999
 [test_SplitAppend_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_Threshold_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_TimestampOffset_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_TruncatedDuration.html]
-skip-if = android_version == '22' # bug 1359012
 [test_TruncatedDuration_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_WaitingOnMissingData.html]
 skip-if = (toolkit == 'android') #timeout android only bug 1101187
 [test_WaitingOnMissingData_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_WaitingOnMissingDataEnded_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
--- a/dom/media/test/mochitest.ini
+++ b/dom/media/test/mochitest.ini
@@ -704,25 +704,24 @@ support-files =
   hls/640x480_seg1.ts
   hls/960x720_seg0.ts
   hls/960x720_seg1.ts
 
 [test_access_control.html]
 fail-if = fission
 skip-if = android_version == '17' # bug 1292836, android(bug 1232305)
 [test_arraybuffer.html]
-skip-if = android_version == '22' # bug 1308388, android(bug 1232305)
 [test_aspectratio_mp4.html]
 [test_audio1.html]
 [test_audio2.html]
 [test_audioDocumentTitle.html]
 skip-if = true # bug 475110 - disabled since we don't play Wave files standalone
 [test_autoplay.html]
 [test_autoplay_contentEditable.html]
-skip-if = android_version == '17' || android_version == '22' # android(bug 1232305, bug 1232318, bug 1372457)
+skip-if = android_version == '17'
 [test_autoplay_policy.html]
 skip-if = android_version >= '23' # bug 1424903
 [test_autoplay_policy_activation.html]
 fail-if = fission
 skip-if =
   android_version >= '23' || # bug 1424903
   fission && debug # Crashes: @ std::_Function_handler<void (mozilla::Tuple<nsresult, mozilla::dom::PBrowserBridgeParent*>&&), mozilla::dom::WindowGlobalParent::ChangeFrameRemoteness(mozilla::dom::BrowsingContext*, nsTSubstring<char16_t> const&, unsigned long, mozilla::ErrorResult&)::$_2>::_M_invoke(std::_Any_data const&, mozilla::Tuple<nsresult, mozilla::dom::PBrowserBridgeParent*>&&)
 [test_autoplay_policy_eventdown_activation.html]
@@ -739,17 +738,16 @@ skip-if = android_version >= '23' # bug 
 skip-if = android_version >= '23' # bug 1424903
 [test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html]
 skip-if = android_version >= '23' # bug 1424903
 [test_autoplay_policy_web_audio_AudioParamStream.html]
 skip-if = android_version >= '23' # bug 1424903
 [test_autoplay_policy_web_audio_createMediaStreamSource.html]
 skip-if = android_version >= '23' # bug 1424903
 [test_buffered.html]
-skip-if = android_version == '22' # bug 1308388, android(bug 1232305)
 [test_bug448534.html]
 [test_bug463162.xhtml]
 [test_bug465498.html]
 skip-if = toolkit == 'android' # android(bug 1232305)
 [test_bug495145.html]
 skip-if = toolkit == 'android' # android(bug 1232305)
 [test_bug495300.html]
 skip-if = toolkit == 'android' # bug 1243801, android(bug 1232305)
@@ -930,17 +928,17 @@ tags=msg
 skip-if = android_version == '17' # android(bug 1232305)
 tags=msg
 [test_mediarecorder_getencodeddata.html]
 skip-if = android_version == '17' # android(bug 1232305)
 tags=msg
 [test_mediarecorder_pause_resume_video.html]
 skip-if = toolkit == 'android' # android(bug 1232305)
 [test_mediarecorder_playback_can_repeat.html]
-skip-if = android_version == '17' || android_version == '22' # android(bug 1232305, bug 1372457)
+skip-if = android_version == '17'
 tags=msg
 [test_mediarecorder_principals.html]
 skip-if = toolkit == 'android' || (os == 'win' && os_version == '10.0' && webrender) # android(bug 1232305), Bug 1453375
 tags=msg
 [test_mediarecorder_record_4ch_audiocontext.html]
 skip-if = android_version == '17' # android(bug 1232305)
 tags=msg
 [test_mediarecorder_record_addtracked_stream.html]
@@ -1113,17 +1111,17 @@ skip-if = toolkit == 'android' # bug 132
 skip-if = toolkit == 'android' # bug 1110922, android(bug 1153860, bug 1232305)
 [test_preload_actions.html]
 skip-if = android_version == '17' # android(bug 1232305)
 [test_preload_attribute.html]
 [test_preload_suspend.html]
 [test_preserve_playbackrate_after_ui_play.html]
 skip-if = android_version == '17' && debug # android(bug 1232305)
 [test_progress.html]
-skip-if = android_version == '17' || android_version == '22' # android(bug 1232305)
+skip-if = android_version == '17'
 [test_reactivate.html]
 skip-if = true # see bug 1319725
 [test_readyState.html]
 [test_referer.html]
 skip-if = android_version == '25' && debug # android(bug 1232305)
 [test_replay_metadata.html]
 skip-if = toolkit == 'android' # bug 1311259, bug 1325994, android(bug 1232305)
 [test_reset_events_async.html]
@@ -1216,60 +1214,52 @@ scheme=https
 tags=msg
 [test_streams_srcObject.html]
 skip-if = toolkit == 'android' # bug 1300443, android(bug 1232305)
 tags=msg capturestream
 [test_streams_tracks.html]
 skip-if = toolkit == 'android' # android(bug 1232305)
 tags=msg capturestream
 [test_texttrack.html]
-skip-if = android_version == '22' # android(bug 1368010)
 tags = webvtt
 [test_texttrack_cors_preload_none.html]
 support-files =
   ../../canvas/test/crossorigin/video.sjs
 tags = webvtt
 [test_testtrack_cors_no_response.html]
 support-files =
   ../../canvas/test/crossorigin/video.sjs
 tags = webvtt
 [test_texttrack_mode_change_during_loading.html]
 skip-if = toolkit == 'android' # android(bug 1562021)
 tags = webvtt
 [test_texttrackcue.html]
-skip-if = android_version == '17' || android_version == '22' # android(bug 1368010, bug 1372457)
+skip-if = android_version == '17'
 tags = webvtt
 [test_texttrackcue_moz.html]
-skip-if = android_version == '22' # bug 1294111, android(bug 1368010)
 tags = webvtt
 [test_texttrackevents_video.html]
-skip-if = android_version == '17' || android_version == '22' # android(bug 1368010, bug 1372457)
+skip-if = android_version == '17'
 tags = webvtt
 [test_texttracklist.html]
-skip-if = android_version == '22' # android(bug 1368010)
 tags = webvtt
 [test_texttracklist_moz.html]
-skip-if = android_version == '22' # android(bug 1368010)
 tags = webvtt
 [test_texttrackregion.html]
-skip-if = android_version == '22' # android(bug 1368010)
 tags = webvtt
 [test_texttrack_moz.html]
-skip-if = android_version == '22' # android(bug 1368010)
 tags = webvtt
 [test_timeupdate_small_files.html]
 skip-if = toolkit == 'android' # bug 1195570, android(bug 1232305)
 [test_trackelementevent.html]
-skip-if = android_version == '22' # bug 1294833, android(bug 1368010)
 tags = webvtt
 [test_trackelementsrc.html]
-skip-if = android_version == '17' || android_version == '22' # android(bug 1368010, bug 1372457)
+skip-if = android_version == '17'
 tags = webvtt
 [test_trackevent.html]
-skip-if = android_version == '22' # android(bug 1368010)
 tags = webvtt
 [test_webvtt_event_same_time.html]
 tags = webvtt
 [test_unseekable.html]
 [test_video_to_canvas.html]
 skip-if = toolkit == 'android' # android(bug 1232305), bugs 1320418,1347953,1347954,1348140,1348386
 [test_video_in_audio_element.html]
 skip-if = toolkit == 'android' # bug 1372457
@@ -1281,29 +1271,25 @@ skip-if = toolkit == 'android' # android
 skip-if = toolkit == 'android' # android(bug 1232305)
 [test_VideoPlaybackQuality_disabled.html]
 skip-if = android_version == '17' # android(bug 1232305)
 [test_volume.html]
 skip-if = toolkit == 'android' # android(bug 1232305)
 [test_vp9_superframes.html]
 skip-if = os == 'mac' && os_version == '10.14' # mac due to bug 1545737
 [test_vttparser.html]
-skip-if = android_version == '22' # android(bug 1368010)
 tags = webvtt
 [test_webvtt_empty_displaystate.html]
-skip-if = android_version == '17' || android_version == '22' # android(bug 1368010, bug 1372457)
+skip-if = android_version == '17'
 tags = webvtt
 [test_webvtt_update_display_after_adding_or_removing_cue.html]
-skip-if = android_version == '22' # android(bug 1368010)
 tags = webvtt
 [test_webvtt_overlapping_time.html]
-skip-if = android_version == '22' # android(bug 1368010)
 tags = webvtt
 [test_webvtt_positionalign.html]
-skip-if = android_version == '22' # android(bug 1368010)
 tags = webvtt
 [test_webvtt_seeking.html]
 skip-if = toolkit == 'android' # bug 1368010, bug 1548446
 tags = webvtt
 # The tests below contain backend-specific tests. Write backend independent
 # tests rather than adding to this list.
 [test_can_play_type_webm.html]
 skip-if = toolkit == 'android' # android(bug 1232305)
--- a/dom/media/tests/mochitest/identity/mochitest.ini
+++ b/dom/media/tests/mochitest/identity/mochitest.ini
@@ -24,33 +24,25 @@ support-files =
   /.well-known/idp-proxy/idp-redirect-https.js
   /.well-known/idp-proxy/idp-redirect-https.js^headers^
   /.well-known/idp-proxy/idp-redirect-https-double.js
   /.well-known/idp-proxy/idp-redirect-https-double.js^headers^
   /.well-known/idp-proxy/idp-redirect-https-odd-path.js
   /.well-known/idp-proxy/idp-redirect-https-odd-path.js^headers^
   /.well-known/idp-min.js
   /.well-known/idp-proxy/idp-bad.js
-skip-if = android_version == '22' # bug 1358876, bug 1361325
 
 [test_fingerprints.html]
-skip-if = android_version == '22' # bug 1329257, bug 1358876, bug 1361325
 scheme=https
 [test_getIdentityAssertion.html]
-skip-if = android_version == '22' # bug 1358876, bug 1361325
 [test_setIdentityProvider.html]
-skip-if = android_version == '22' # bug 1358876, bug 1361325
 scheme=https
 [test_setIdentityProviderWithErrors.html]
-skip-if = android_version == '22' # bug 1358876, bug 1361325
 scheme=https
 [test_peerConnection_peerIdentity.html]
-skip-if = android_version == '22' # bug 1358876, bug 1361325
 scheme=https
 [test_peerConnection_asymmetricIsolation.html]
-skip-if = android_version == '22' # bug 1358876, bug 1361325
 scheme=https
 [test_loginNeeded.html]
 fail-if = fission
 support-files =
   /.well-known/idp-proxy/login.html
   /.well-known/idp-proxy/idp.sjs
-skip-if = android_version == '22' # bug 1358876, bug 1361325
--- a/dom/media/webrtc/MediaTransportParent.h
+++ b/dom/media/webrtc/MediaTransportParent.h
@@ -39,17 +39,16 @@ class MediaTransportParent : public dom:
       const string& localPwd, const int& componentCount,
       const string& remoteUfrag, const string& remotePwd,
       nsTArray<uint8_t>&& keyDer, nsTArray<uint8_t>&& certDer,
       const int& authType, const bool& dtlsClient,
       const DtlsDigestList& digests, const bool& privacyRequested);
   mozilla::ipc::IPCResult RecvRemoveTransportsExcept(
       const StringVector& transportIds);
   mozilla::ipc::IPCResult RecvStartIceChecks(const bool& isControlling,
-                                             const bool& isOfferer,
                                              const StringVector& iceOptions);
   mozilla::ipc::IPCResult RecvSendPacket(const string& transportId,
                                          const MediaPacket& packet);
   mozilla::ipc::IPCResult RecvAddIceCandidate(const string& transportId,
                                               const string& candidate,
                                               const string& ufrag);
   mozilla::ipc::IPCResult RecvUpdateNetworkState(const bool& online);
   mozilla::ipc::IPCResult RecvGetIceStats(
--- a/dom/media/webrtc/PMediaTransport.ipdl
+++ b/dom/media/webrtc/PMediaTransport.ipdl
@@ -71,17 +71,16 @@ parent:
                           int authType,
                           bool dtlsClient,
                           DtlsDigestList digests,
                           bool privacyRequested);
 
   async RemoveTransportsExcept(StringVector transportIds);
 
   async StartIceChecks(bool isControlling,
-                       bool isOfferer,
                        StringVector iceOptions);
 
   async SendPacket(string transportId, MediaPacket packet);
 
   async AddIceCandidate(string transportId,
                         string candidate,
                         string ufrag);
 
new file mode 100644
--- /dev/null
+++ b/dom/security/test/general/file_nosniff_navigation.sjs
@@ -0,0 +1,32 @@
+// Custom *.sjs file specifically for the needs of Bug 1286861
+
+// small red image
+const IMG = atob(
+  "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+  "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==");
+
+function getSniffableContent(selector){
+  switch(selector){
+  case "xml":
+    return `<?xml version="1.0"?><test/>`;
+  case "html":
+    return `<!Doctype html> <html> <head></head> <body> Test test </body></html>`;
+  case "css":
+    return `*{ color: pink !important; }`;
+  case 'json':
+      return `{ 'test':'yes' }`;
+  case 'img':
+      return IMG;
+  }
+  return "Basic UTF-8 Text";
+}
+
+function handleRequest(request, response)
+{
+  // avoid confusing cache behaviors
+  response.setHeader('X-Content-Type-Options', 'nosniff'); // Disable Sniffing
+  response.setHeader("Content-Type","*/*");  // Try Browser to force sniffing. 
+  response.write(getSniffableContent(request.queryString));
+  return;
+}
+
new file mode 100644
--- /dev/null
+++ b/dom/security/test/general/file_nosniff_navigation_garbage.sjs
@@ -0,0 +1,33 @@
+// Custom *.sjs file specifically for the needs of Bug 1286861
+
+// small red image
+const IMG = atob(
+  "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+  "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==");
+
+function getSniffableContent(selector){
+  switch(selector){
+  case "xml":
+    return `<?xml version="1.0"?><test/>`;
+  case "html":
+    return `<!Doctype html> <html> <head></head> <body> Test test </body></html>`;
+  case 'js':
+    return `<script> alert("This shouldt not be executed"); </script>`
+  case "css":
+    return `*{ color: pink !important; }`;
+  case 'json':
+      return `{ 'test':'yes' }`;
+  case 'img':
+      return IMG;
+  }
+  return "Basic UTF-8 Text";
+}
+
+function handleRequest(request, response)
+{
+  // avoid confusing cache behaviors
+  response.setHeader('X-Content-Type-Options', 'nosniff'); // Disable Sniffing
+  response.setHeader("Content-Type","garbage/garbage");  // Try Browser to force sniffing. 
+  response.write(getSniffableContent(request.queryString));
+  return;
+}
new file mode 100644
--- /dev/null
+++ b/dom/security/test/general/file_nosniff_navigation_mismatch.sjs
@@ -0,0 +1,33 @@
+// Custom *.sjs file specifically for the needs of Bug 1286861
+
+// small red image
+const IMG = atob(
+  "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+  "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==");
+
+function getSniffableContent(selector){
+  switch(selector){
+  case "xml":
+    return `<?xml version="1.0"?><test/>`;
+  case "html":
+    return `<!Doctype html> <html> <head></head> <body> Test test </body></html>`;
+  case 'js':
+    return `<script> alert("This shouldt not be executed"); </script>`
+  case "css":
+    return `*{ color: pink !important; }`;
+  case 'json':
+      return `{ 'test':'yes' }`;
+  case 'img':
+      return IMG;
+  }
+  return "Basic UTF-8 Text";
+}
+
+function handleRequest(request, response)
+{
+  // avoid confusing cache behaviors
+  response.setHeader('X-Content-Type-Options', 'nosniff'); // Disable Sniffing
+  response.setHeader("Content-Type","picture/png");  // Try Browser to force sniffing. 
+  response.write(getSniffableContent(request.queryString));
+  return;
+}
--- a/dom/security/test/general/mochitest.ini
+++ b/dom/security/test/general/mochitest.ini
@@ -1,12 +1,15 @@
 [DEFAULT]
 support-files =
   file_contentpolicytype_targeted_link_iframe.sjs
   file_nosniff_testserver.sjs
+  file_nosniff_navigation.sjs
+  file_nosniff_navigation_mismatch.sjs
+  file_nosniff_navigation_garbage.sjs
   file_block_script_wrong_mime_server.sjs
   file_block_toplevel_data_navigation.html
   file_block_toplevel_data_navigation2.html
   file_block_toplevel_data_navigation3.html
   file_block_toplevel_data_redirect.sjs
   file_block_subresource_redir_to_data.sjs
   file_same_site_cookies_subrequest.sjs
   file_same_site_cookies_toplevel_nav.sjs
@@ -19,16 +22,17 @@ support-files =
   file_same_site_cookies_iframe.html
   file_same_site_cookies_iframe.sjs
   file_same_site_cookies_about_navigation.html
   file_same_site_cookies_about_inclusion.html
   file_same_site_cookies_about.sjs
 
 [test_contentpolicytype_targeted_link_iframe.html]
 [test_nosniff.html]
+[test_nosniff_navigation.html]
 [test_block_script_wrong_mime.html]
 [test_block_toplevel_data_navigation.html]
 skip-if = toolkit == 'android' # intermittent failure
 [test_block_toplevel_data_img_navigation.html]
 skip-if = toolkit == 'android' # intermittent failure
 [test_allow_opening_data_pdf.html]
 skip-if = toolkit == 'android'
 [test_allow_opening_data_json.html]
new file mode 100644
--- /dev/null
+++ b/dom/security/test/general/test_nosniff_navigation.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Bug 1428473 Support X-Content-Type-Options: nosniff when navigating</title>
+  <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <style>
+    iframe{
+      border: 1px solid orange;
+    }
+  </style>
+
+  <iframe src="file_nosniff_navigation.sjs?xml"> </iframe>
+  <iframe src="file_nosniff_navigation.sjs?html"></iframe>
+  <iframe src="file_nosniff_navigation.sjs?css" ></iframe>
+  <iframe src="file_nosniff_navigation.sjs?json"></iframe>
+  <iframe src="file_nosniff_navigation.sjs?img"></iframe>
+ 
+  <hr>
+  <iframe src="file_nosniff_navigation_mismatch.sjs?html"></iframe>
+  <iframe src="file_nosniff_navigation_mismatch.sjs?xml"></iframe>
+  <iframe src="file_nosniff_navigation_mismatch.sjs"></iframe>
+
+  <iframe src="file_nosniff_navigation_garbage.sjs?xml"> </iframe>
+  <iframe src="file_nosniff_navigation_garbage.sjs?html"></iframe>
+  <iframe src="file_nosniff_navigation_garbage.sjs?css" ></iframe>
+  <iframe src="file_nosniff_navigation_garbage.sjs?json"></iframe>
+  <iframe src="file_nosniff_navigation_garbage.sjs?img"></iframe>
+ 
+
+</head>
+<body>
+
+<!-- add the two script tests -->
+<script id="scriptCorrectType"></script>
+<script id="scriptWrongType"></script>
+
+<script class="testbody" type="text/javascript">
+/* Description of the test:
+ * We're testing if Firefox respects the nosniff Header for Top-Level 
+ * Navigations.
+ * If Firefox cant Display the Page, it will prompt a download 
+ * and the URL of the Page will be about:blank.
+ * So we will try to open different content send with
+ * no-mime, mismatched-mime and garbage-mime types.
+ * 
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+window.addEventListener("load", ()=>{
+  let iframes = Array.from(document.querySelectorAll("iframe"));
+
+  iframes.forEach( frame => {
+    let result = frame.contentWindow.document.URL == "about:blank";
+    let sniffTarget = (new URL(frame.src)).search;
+    ok(result, `${sniffTarget} - was not Sniffed`);
+  });
+  
+  SimpleTest.finish();
+});
+</script>
+</body>
+</html>
--- a/dom/webidl/DataTransferItem.webidl
+++ b/dom/webidl/DataTransferItem.webidl
@@ -7,17 +7,17 @@
  * https://html.spec.whatwg.org/multipage/interaction.html#the-datatransferitem-interface
  * https://wicg.github.io/entries-api/#idl-index
  */
 
 interface DataTransferItem {
   readonly attribute DOMString kind;
   readonly attribute DOMString type;
   [Throws, NeedsSubjectPrincipal]
-  void getAsString(FunctionStringCallback? _callback);
+  void getAsString(FunctionStringCallback? callback);
   [Throws, NeedsSubjectPrincipal]
   File? getAsFile();
 };
 
 callback FunctionStringCallback = void (DOMString data);
 
 // https://wicg.github.io/entries-api/#idl-index
 partial interface DataTransferItem {
--- a/dom/webidl/HTMLCanvasElement.webidl
+++ b/dom/webidl/HTMLCanvasElement.webidl
@@ -22,17 +22,17 @@ interface HTMLCanvasElement : HTMLElemen
 
   [Throws]
   nsISupports? getContext(DOMString contextId, optional any contextOptions = null);
 
   [Throws, NeedsSubjectPrincipal]
   DOMString toDataURL(optional DOMString type = "",
                       optional any encoderOptions);
   [Throws, NeedsSubjectPrincipal]
-  void toBlob(BlobCallback _callback,
+  void toBlob(BlobCallback callback,
               optional DOMString type = "",
               optional any encoderOptions);
 };
 
 // Mozilla specific bits
 partial interface HTMLCanvasElement {
   [Pure, SetterThrows]
            attribute boolean mozOpaque;
--- a/gfx/ipc/CrossProcessPaint.cpp
+++ b/gfx/ipc/CrossProcessPaint.cpp
@@ -165,18 +165,19 @@ bool CrossProcessPaint::Start(dom::Windo
       "[wgp=%p, "
       "scale=%f, "
       "color=(%u, %u, %u, %u)]\n",
       aRoot, aScale, NS_GET_R(aBackgroundColor), NS_GET_G(aBackgroundColor),
       NS_GET_B(aBackgroundColor), NS_GET_A(aBackgroundColor));
 
   Maybe<IntRect> rect;
   if (aRect) {
-    *rect = IntRect::RoundOut((float)aRect->X(), (float)aRect->Y(),
-                              (float)aRect->Width(), (float)aRect->Height());
+    rect =
+        Some(IntRect::RoundOut((float)aRect->X(), (float)aRect->Y(),
+                               (float)aRect->Width(), (float)aRect->Height()));
   }
 
   RefPtr<CrossProcessPaint> resolver =
       new CrossProcessPaint(aPromise, aScale, aRoot);
 
   if (aRoot->IsInProcess()) {
     RefPtr<dom::WindowGlobalChild> childActor = aRoot->GetChildActor();
     if (!childActor) {
--- a/gfx/layers/CompositionRecorder.h
+++ b/gfx/layers/CompositionRecorder.h
@@ -45,34 +45,30 @@ class RecordedFrame {
  *
  * This object collects frames sent to it by a |LayerManager| and writes them
  * out as a series of images until recording has finished.
  *
  * If GPU-accelerated rendering is used, the frames will not be mapped into
  * memory until |WriteCollectedFrames| is called.
  */
 class CompositionRecorder {
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CompositionRecorder)
-
  public:
   explicit CompositionRecorder(TimeStamp aRecordingStart);
 
   /**
    * Record a composited frame.
    */
-  virtual void RecordFrame(RecordedFrame* aFrame);
+  void RecordFrame(RecordedFrame* aFrame);
 
   /**
    * Write out the collected frames as a series of timestamped images.
    */
-  virtual void WriteCollectedFrames();
+  void WriteCollectedFrames();
 
  protected:
-  virtual ~CompositionRecorder() = default;
-
   void ClearCollectedFrames();
 
  private:
   nsTArray<RefPtr<RecordedFrame>> mCollectedFrames;
   TimeStamp mRecordingStart;
 };
 
 }  // namespace layers
--- a/gfx/layers/composite/LayerManagerComposite.cpp
+++ b/gfx/layers/composite/LayerManagerComposite.cpp
@@ -24,20 +24,20 @@
 #include "Units.h"                           // for ScreenIntRect
 #include "UnitTransforms.h"                  // for ViewAs
 #include "apz/src/AsyncPanZoomController.h"  // for AsyncPanZoomController
 #include "gfxEnv.h"                          // for gfxEnv
 
 #ifdef XP_MACOSX
 #  include "gfxPlatformMac.h"
 #endif
-#include "gfxRect.h"                    // for gfxRect
-#include "gfxUtils.h"                   // for frame color util
-#include "mozilla/Assertions.h"         // for MOZ_ASSERT, etc
-#include "mozilla/RefPtr.h"             // for RefPtr, already_AddRefed
+#include "gfxRect.h"             // for gfxRect
+#include "gfxUtils.h"            // for frame color util
+#include "mozilla/Assertions.h"  // for MOZ_ASSERT, etc
+#include "mozilla/RefPtr.h"      // for RefPtr, already_AddRefed
 #include "mozilla/StaticPrefs_gfx.h"
 #include "mozilla/StaticPrefs_layers.h"
 #include "mozilla/gfx/2D.h"             // for DrawTarget
 #include "mozilla/gfx/Matrix.h"         // for Matrix4x4
 #include "mozilla/gfx/Point.h"          // for IntSize, Point
 #include "mozilla/gfx/Rect.h"           // for Rect
 #include "mozilla/gfx/Types.h"          // for Color, SurfaceFormat
 #include "mozilla/layers/Compositor.h"  // for Compositor
@@ -125,16 +125,23 @@ HostLayerManager::~HostLayerManager() {}
 void HostLayerManager::RecordPaintTimes(const PaintTiming& aTiming) {
   mDiagnostics->RecordPaintTimes(aTiming);
 }
 
 void HostLayerManager::RecordUpdateTime(float aValue) {
   mDiagnostics->RecordUpdateTime(aValue);
 }
 
+void HostLayerManager::WriteCollectedFrames() {
+  if (mCompositionRecorder) {
+    mCompositionRecorder->WriteCollectedFrames();
+    mCompositionRecorder = nullptr;
+  }
+}
+
 /**
  * LayerManagerComposite
  */
 LayerManagerComposite::LayerManagerComposite(Compositor* aCompositor)
     : mUnusedApzTransformWarning(false),
       mDisabledApzWarning(false),
       mCompositor(aCompositor),
       mInTransaction(false),
--- a/gfx/layers/composite/LayerManagerComposite.h
+++ b/gfx/layers/composite/LayerManagerComposite.h
@@ -178,50 +178,58 @@ class HostLayerManager : public LayerMan
   TimeStamp GetCompositionTime() const { return mCompositionTime; }
   void SetCompositionTime(TimeStamp aTimeStamp) {
     mCompositionTime = aTimeStamp;
     if (!mCompositionTime.IsNull() && !mCompositeUntilTime.IsNull() &&
         mCompositionTime >= mCompositeUntilTime) {
       mCompositeUntilTime = TimeStamp();
     }
   }
+
   void CompositeUntil(TimeStamp aTimeStamp) {
     if (mCompositeUntilTime.IsNull() || mCompositeUntilTime < aTimeStamp) {
       mCompositeUntilTime = aTimeStamp;
     }
   }
   TimeStamp GetCompositeUntilTime() const { return mCompositeUntilTime; }
 
   // We maintaining a global mapping from ID to CompositorBridgeParent for
   // async compositables.
   uint64_t GetCompositorBridgeID() const { return mCompositorBridgeID; }
   void SetCompositorBridgeID(uint64_t aID) {
     MOZ_ASSERT(mCompositorBridgeID == 0,
                "The compositor ID must be set only once.");
     mCompositorBridgeID = aID;
   }
 
-  void SetCompositionRecorder(already_AddRefed<CompositionRecorder> aRecorder) {
-    mCompositionRecorder = aRecorder;
+  void SetCompositionRecorder(UniquePtr<CompositionRecorder> aRecorder) {
+    mCompositionRecorder = std::move(aRecorder);
   }
 
+  /**
+   * Write the frames collected by the |CompositionRecorder| to disk.
+   *
+   * If there is not currently a |CompositionRecorder|, this is a no-op.
+   */
+  void WriteCollectedFrames();
+
  protected:
   bool mDebugOverlayWantsNextFrame;
   nsTArray<ImageCompositeNotificationInfo> mImageCompositeNotifications;
   // Testing property. If hardware composer is supported, this will return
   // true if the last frame was deemed 'too complicated' to be rendered.
   float mWarningLevel;
   mozilla::TimeStamp mWarnTime;
   UniquePtr<Diagnostics> mDiagnostics;
   uint64_t mCompositorBridgeID;
 
   bool mWindowOverlayChanged;
   TimeDuration mLastPaintTime;
   TimeStamp mRenderStartTime;
-  RefPtr<CompositionRecorder> mCompositionRecorder = nullptr;
+  UniquePtr<CompositionRecorder> mCompositionRecorder = nullptr;
 
   // Render time for the current composition.
   TimeStamp mCompositionTime;
 
   // When nonnull, during rendering, some compositable indicated that it will
   // change its rendering at this time. In order not to miss it, we composite
   // on every vsync until this time occurs (this is the latest such time).
   TimeStamp mCompositeUntilTime;
--- a/gfx/layers/ipc/CompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CompositorBridgeParent.cpp
@@ -317,16 +317,17 @@ CompositorBridgeParent::CompositorBridge
     const TimeDuration& aVsyncRate, const CompositorOptions& aOptions,
     bool aUseExternalSurfaceSize, const gfx::IntSize& aSurfaceSize)
     : CompositorBridgeParentBase(aManager),
       mWidget(nullptr),
       mScale(aScale),
       mVsyncRate(aVsyncRate),
       mPendingTransaction{0},
       mPaused(false),
+      mHaveCompositionRecorder(false),
       mUseExternalSurfaceSize(aUseExternalSurfaceSize),
       mEGLSurfaceSize(aSurfaceSize),
       mOptions(aOptions),
       mPauseCompositionMonitor("PauseCompositionMonitor"),
       mResumeCompositionMonitor("ResumeCompositionMonitor"),
       mCompositorBridgeID(0),
       mRootLayerTreeID{0},
       mOverrideComposeReadiness(false),
@@ -2607,39 +2608,49 @@ int32_t RecordContentFrameTime(
     }
     return result;
   }
 
   return 0;
 }
 
 mozilla::ipc::IPCResult CompositorBridgeParent::RecvBeginRecording(
-    const TimeStamp& aRecordingStart) {
+    const TimeStamp& aRecordingStart, BeginRecordingResolver&& aResolve) {
+  if (mHaveCompositionRecorder) {
+    aResolve(false);
+    return IPC_OK();
+  }
+
   if (mLayerManager) {
-    mCompositionRecorder = new CompositionRecorder(aRecordingStart);
-    mLayerManager->SetCompositionRecorder(do_AddRef(mCompositionRecorder));
+    mLayerManager->SetCompositionRecorder(
+        MakeUnique<CompositionRecorder>(aRecordingStart));
   } else if (mWrBridge) {
-    RefPtr<WebRenderCompositionRecorder> recorder =
-        new WebRenderCompositionRecorder(aRecordingStart,
-                                         mWrBridge->PipelineId());
-    mCompositionRecorder = recorder;
-    mWrBridge->SetCompositionRecorder(std::move(recorder));
+    mWrBridge->SetCompositionRecorder(MakeUnique<WebRenderCompositionRecorder>(
+        aRecordingStart, mWrBridge->PipelineId()));
   }
 
+  mHaveCompositionRecorder = true;
+  aResolve(true);
+
   return IPC_OK();
 }
 
-mozilla::ipc::IPCResult CompositorBridgeParent::RecvEndRecording() {
-  if (mLayerManager) {
-    mLayerManager->SetCompositionRecorder(nullptr);
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvEndRecording(
+    bool* aOutSuccess) {
+  if (!mHaveCompositionRecorder) {
+    *aOutSuccess = false;
+    return IPC_OK();
   }
 
-  // If we are using WebRender, the |RenderThread| will have a handle to this
-  // |WebRenderCompositionRecorder|, which it will release once the frames have
-  // been written.
+  if (mLayerManager) {
+    mLayerManager->WriteCollectedFrames();
+  } else if (mWrBridge) {
+    mWrBridge->WriteCollectedFrames();
+  }
 
-  mCompositionRecorder->WriteCollectedFrames();
-  mCompositionRecorder = nullptr;
+  mHaveCompositionRecorder = false;
+  *aOutSuccess = true;
+
   return IPC_OK();
 }
 
 }  // namespace layers
 }  // namespace mozilla
--- a/gfx/layers/ipc/CompositorBridgeParent.h
+++ b/gfx/layers/ipc/CompositorBridgeParent.h
@@ -240,18 +240,18 @@ class CompositorBridgeParentBase : publi
   virtual mozilla::ipc::IPCResult RecvAdoptChild(const LayersId& id) = 0;
   virtual mozilla::ipc::IPCResult RecvFlushRenderingAsync() = 0;
   virtual mozilla::ipc::IPCResult RecvForcePresent() = 0;
   virtual mozilla::ipc::IPCResult RecvNotifyRegionInvalidated(
       const nsIntRegion& region) = 0;
   virtual mozilla::ipc::IPCResult RecvRequestNotifyAfterRemotePaint() = 0;
   virtual mozilla::ipc::IPCResult RecvAllPluginsCaptured() = 0;
   virtual mozilla::ipc::IPCResult RecvBeginRecording(
-      const TimeStamp& aRecordingStart) = 0;
-  virtual mozilla::ipc::IPCResult RecvEndRecording() = 0;
+      const TimeStamp& aRecordingStart, BeginRecordingResolver&& aResolve) = 0;
+  virtual mozilla::ipc::IPCResult RecvEndRecording(bool* aOutSuccess) = 0;
   virtual mozilla::ipc::IPCResult RecvInitialize(
       const LayersId& rootLayerTreeId) = 0;
   virtual mozilla::ipc::IPCResult RecvGetFrameUniformity(
       FrameUniformityData* data) = 0;
   virtual mozilla::ipc::IPCResult RecvWillClose() = 0;
   virtual mozilla::ipc::IPCResult RecvPause() = 0;
   virtual mozilla::ipc::IPCResult RecvResume() = 0;
   virtual mozilla::ipc::IPCResult RecvResumeAsync() = 0;
@@ -352,18 +352,19 @@ class CompositorBridgeParent final : pub
   // Unused for chrome <-> compositor communication (which this class does).
   // @see ContentCompositorBridgeParent::RecvRequestNotifyAfterRemotePaint
   mozilla::ipc::IPCResult RecvRequestNotifyAfterRemotePaint() override {
     return IPC_OK();
   };
 
   mozilla::ipc::IPCResult RecvAllPluginsCaptured() override;
   mozilla::ipc::IPCResult RecvBeginRecording(
-      const TimeStamp& aRecordingStart) override;
-  mozilla::ipc::IPCResult RecvEndRecording() override;
+      const TimeStamp& aRecordingStart,
+      BeginRecordingResolver&& aResolve) override;
+  mozilla::ipc::IPCResult RecvEndRecording(bool* aOutSuccess) override;
 
   void NotifyMemoryPressure() override;
   void AccumulateMemoryReport(wr::MemoryReport*) override;
 
   void ActorDestroy(ActorDestroyReason why) override;
 
   void ShadowLayersUpdated(LayerTransactionParent* aLayerTree,
                            const TransactionInfo& aInfo,
@@ -751,16 +752,17 @@ class CompositorBridgeParent final : pub
   TimeDuration mVsyncRate;
 
   TransactionId mPendingTransaction;
   TimeStamp mRefreshStartTime;
   TimeStamp mTxnStartTime;
   TimeStamp mFwdTime;
 
   bool mPaused;
+  bool mHaveCompositionRecorder;
 
   bool mUseExternalSurfaceSize;
   gfx::IntSize mEGLSurfaceSize;
 
   CompositorOptions mOptions;
 
   mozilla::Monitor mPauseCompositionMonitor;
   mozilla::Monitor mResumeCompositionMonitor;
@@ -776,17 +778,16 @@ class CompositorBridgeParent final : pub
   RefPtr<APZUpdater> mApzUpdater;
 
   RefPtr<CompositorVsyncScheduler> mCompositorScheduler;
   // This makes sure the compositorParent is not destroyed before receiving
   // confirmation that the channel is closed.
   // mSelfRef is cleared in DeferredDestroy which is scheduled by ActorDestroy.
   RefPtr<CompositorBridgeParent> mSelfRef;
   RefPtr<CompositorAnimationStorage> mAnimationStorage;
-  RefPtr<CompositionRecorder> mCompositionRecorder;
 
   TimeDuration mPaintTime;
 
 #if defined(XP_WIN) || defined(MOZ_WIDGET_GTK)
   // cached plugin data used to reduce the number of updates we request.
   LayersId mLastPluginUpdateLayerTreeId;
   nsIntPoint mPluginsLayerOffset;
   nsIntRegion mPluginsLayerVisibleRegion;
--- a/gfx/layers/ipc/ContentCompositorBridgeParent.h
+++ b/gfx/layers/ipc/ContentCompositorBridgeParent.h
@@ -82,20 +82,26 @@ class ContentCompositorBridgeParent fina
   }
 
   mozilla::ipc::IPCResult RecvCheckContentOnlyTDR(
       const uint32_t& sequenceNum, bool* isContentOnlyTDR) override;
 
   mozilla::ipc::IPCResult RecvAllPluginsCaptured() override { return IPC_OK(); }
 
   mozilla::ipc::IPCResult RecvBeginRecording(
-      const TimeStamp& aRecordingStart) override {
+      const TimeStamp& aRecordingStart,
+      BeginRecordingResolver&& aResolve) override {
+    aResolve(false);
     return IPC_OK();
   }
-  mozilla::ipc::IPCResult RecvEndRecording() override { return IPC_OK(); }
+
+  mozilla::ipc::IPCResult RecvEndRecording(bool* aOutSuccess) override {
+    *aOutSuccess = false;
+    return IPC_OK();
+  }
 
   mozilla::ipc::IPCResult RecvGetFrameUniformity(
       FrameUniformityData* aOutData) override {
     // Don't support calculating frame uniformity on the child process and
     // this is just a stub for now.
     MOZ_ASSERT(false);
     return IPC_OK();
   }
--- a/gfx/layers/ipc/PCompositorBridge.ipdl
+++ b/gfx/layers/ipc/PCompositorBridge.ipdl
@@ -262,18 +262,21 @@ parent:
   sync SyncWithCompositor();
 
   // The pipelineId is the same as the layersId
   async PWebRenderBridge(PipelineId pipelineId, LayoutDeviceIntSize aSize);
 
   sync CheckContentOnlyTDR(uint32_t sequenceNum)
     returns (bool isContentOnlyTDR);
 
-  async BeginRecording(TimeStamp aRecordingStart);
-  sync EndRecording();
+  async BeginRecording(TimeStamp aRecordingStart)
+    returns (bool success);
+
+  sync EndRecording()
+    returns (bool success);
 
 child:
   // Send back Compositor Frame Metrics from APZCs so tiled layers can
   // update progressively.
   async SharedCompositorFrameMetrics(Handle metrics, CrossProcessMutexHandle mutex, LayersId aLayersId, uint32_t aAPZCId);
   async ReleaseSharedCompositorFrameMetrics(ViewID aId, uint32_t aAPZCId);
 };
 
--- a/gfx/layers/wr/StackingContextHelper.cpp
+++ b/gfx/layers/wr/StackingContextHelper.cpp
@@ -39,19 +39,20 @@ StackingContextHelper::StackingContextHe
   if (aParams.mBoundTransform &&
       aParams.mBoundTransform->CanDraw2D(&transform2d) &&
       aParams.reference_frame_kind != wr::WrReferenceFrameKind::Perspective &&
       !aParentSC.mIsPreserve3D) {
     mInheritedTransform = transform2d * aParentSC.mInheritedTransform;
 
     int32_t apd = aContainerFrame->PresContext()->AppUnitsPerDevPixel();
     nsRect r = LayoutDevicePixel::ToAppUnits(aBounds, apd);
-    mScale = FrameLayerBuilder::ChooseScale(aContainerFrame, aContainerItem, r,
-                                            1.f, 1.f, mInheritedTransform,
-                                            /* aCanDraw2D = */ true);
+    mScale = FrameLayerBuilder::ChooseScale(
+        aContainerFrame, aContainerItem, r, aParentSC.mScale.width,
+        aParentSC.mScale.height, mInheritedTransform,
+        /* aCanDraw2D = */ true);
 
     if (aParams.mAnimated) {
       mSnappingSurfaceTransform =
           gfx::Matrix::Scaling(mScale.width, mScale.height);
     } else {
       mSnappingSurfaceTransform =
           transform2d * aParentSC.mSnappingSurfaceTransform;
     }
--- a/gfx/layers/wr/WebRenderBridgeParent.cpp
+++ b/gfx/layers/wr/WebRenderBridgeParent.cpp
@@ -379,17 +379,16 @@ void WebRenderBridgeParent::RemoveDeferr
             wr::IpcResourceUpdateQueue::ReleaseShmems(self, x.mLargeShmems);
           },
           [=](FocusTarget& x) {});
     }
     entry.Remove();
   }
 }
 
-
 /* static */
 WebRenderBridgeParent* WebRenderBridgeParent::CreateDestroyed(
     const wr::PipelineId& aPipelineId) {
   return new WebRenderBridgeParent(aPipelineId);
 }
 
 void WebRenderBridgeParent::PushDeferredPipelineData(
     RenderRootDeferredData&& aDeferredData) {
@@ -410,21 +409,18 @@ bool WebRenderBridgeParent::HandleDeferr
     bool success = entry.match(
         [&](RenderRootDisplayListData& data) {
           // We ensure this with RenderRootIsValid before calling
           // PushDeferredPipelineData:
           MOZ_ASSERT(data.mRenderRoot == wr::RenderRoot::Default);
           wr::Epoch wrEpoch = GetNextWrEpoch();
           bool validTransaction = data.mIdNamespace == mIdNamespace;
 
-          if (!ProcessRenderRootDisplayListData(data,
-                                                wrEpoch,
-                                                aTxnStartTime,
-                                                validTransaction,
-                                                false)){
+          if (!ProcessRenderRootDisplayListData(data, wrEpoch, aTxnStartTime,
+                                                validTransaction, false)) {
             return false;
           }
 
           wr::IpcResourceUpdateQueue::ReleaseShmems(this, data.mSmallShmems);
           wr::IpcResourceUpdateQueue::ReleaseShmems(this, data.mLargeShmems);
           return true;
         },
         [&](RenderRootUpdates& data) {
@@ -516,23 +512,21 @@ bool WebRenderBridgeParent::MaybeHandleD
   if (!StaticPrefs::gfx_webrender_split_render_roots_AtStartup()) {
     return true;
   }
   for (wr::PipelineId pipelineId : aPipelineIds) {
     if (!MaybeHandleDeferredPipelineDataForPipeline(aRenderRoot, pipelineId,
                                                     aTxnStartTime)) {
       return false;
     }
-
   }
 
   return true;
 }
 
-
 mozilla::ipc::IPCResult WebRenderBridgeParent::RecvEnsureConnected(
     TextureFactoryIdentifier* aTextureFactoryIdentifier,
     MaybeIdNamespace* aMaybeIdNamespace) {
   if (mDestroyed) {
     *aTextureFactoryIdentifier =
         TextureFactoryIdentifier(LayersBackend::LAYERS_NONE);
     *aMaybeIdNamespace = Nothing();
     return IPC_OK();
@@ -986,20 +980,24 @@ void WebRenderBridgeParent::RemoveEpochD
   }
 }
 
 bool WebRenderBridgeParent::IsRootWebRenderBridgeParent() const {
   return !!mWidget;
 }
 
 void WebRenderBridgeParent::SetCompositionRecorder(
-    RefPtr<layers::WebRenderCompositionRecorder>&& aRecorder) {
+    UniquePtr<layers::WebRenderCompositionRecorder> aRecorder) {
   Api(wr::RenderRoot::Default)->SetCompositionRecorder(std::move(aRecorder));
 }
 
+void WebRenderBridgeParent::WriteCollectedFrames() {
+  Api(wr::RenderRoot::Default)->WriteCollectedFrames();
+}
+
 CompositorBridgeParent* WebRenderBridgeParent::GetRootCompositorBridgeParent()
     const {
   if (!mCompositorBridge) {
     return nullptr;
   }
 
   if (IsRootWebRenderBridgeParent()) {
     // This WebRenderBridgeParent is attached to the root
@@ -1160,20 +1158,18 @@ bool WebRenderBridgeParent::SetDisplayLi
     // We will schedule generating a frame after the scene
     // build is done, so we don't need to do it here.
   }
 
   return true;
 }
 
 bool WebRenderBridgeParent::ProcessRenderRootDisplayListData(
-    RenderRootDisplayListData& aDisplayList,
-    wr::Epoch aWrEpoch,
-    const TimeStamp& aTxnStartTime,
-    bool aValidTransaction,
+    RenderRootDisplayListData& aDisplayList, wr::Epoch aWrEpoch,
+    const TimeStamp& aTxnStartTime, bool aValidTransaction,
     bool aObserveLayersUpdate) {
   wr::TransactionBuilder txn;
   Maybe<wr::AutoTransactionSender> sender;
 
   // Note that this needs to happen before the display list transaction is
   // sent to WebRender, so that the UpdateHitTestingTree call is guaranteed to
   // be in the updater queue at the time that the scene swap completes.
   if (aDisplayList.mScrollData) {
@@ -1271,28 +1267,29 @@ mozilla::ipc::IPCResult WebRenderBridgeP
       }
     }
   }
 
   if (!mRenderRoot) {
     // Only non-root WRBPs will ever have an unresolved mRenderRoot
     MOZ_ASSERT(!IsRootWebRenderBridgeParent());
     if (aDisplayLists.Length() != 1) {
-      return IPC_FAIL(this, "Well-behaved content processes must only send a DL for a single renderRoot");
+      return IPC_FAIL(this,
+                      "Well-behaved content processes must only send a DL for "
+                      "a single renderRoot");
     }
     PushDeferredPipelineData(AsVariant(std::move(aDisplayLists[0])));
     aDisplayLists.Clear();
   }
 
   for (auto& displayList : aDisplayLists) {
     if (IsRootWebRenderBridgeParent()) {
-      if (!MaybeHandleDeferredPipelineData(
-              displayList.mRenderRoot,
-              displayList.mRemotePipelineIds,
-              aTxnStartTime)) {
+      if (!MaybeHandleDeferredPipelineData(displayList.mRenderRoot,
+                                           displayList.mRemotePipelineIds,
+                                           aTxnStartTime)) {
         return IPC_FAIL(this, "Failed processing deferred pipeline data");
       }
     } else {
       RefPtr<WebRenderBridgeParent> root = GetRootWebRenderBridgeParent();
       if (!root) {
         return IPC_FAIL(this, "Root WRBP is missing (shutting down?)");
       }
 
@@ -1304,22 +1301,19 @@ mozilla::ipc::IPCResult WebRenderBridgeP
     }
   }
 
   bool validTransaction = aDisplayLists.Length() > 0 &&
                           aDisplayLists[0].mIdNamespace == mIdNamespace;
   bool observeLayersUpdate = ShouldParentObserveEpoch();
 
   for (auto& displayList : aDisplayLists) {
-    if (!ProcessRenderRootDisplayListData(
-            displayList,
-            wrEpoch,
-            aTxnStartTime,
-            validTransaction,
-            observeLayersUpdate)) {
+    if (!ProcessRenderRootDisplayListData(displayList, wrEpoch, aTxnStartTime,
+                                          validTransaction,
+                                          observeLayersUpdate)) {
       return IPC_FAIL(this, "Failed to process RenderRootDisplayListData.");
     }
   }
 
   if (!validTransaction && observeLayersUpdate) {
     mCompositorBridge->ObserveLayersUpdate(GetLayersId(),
                                            mChildLayersObserverEpoch, true);
   }
@@ -1451,17 +1445,19 @@ mozilla::ipc::IPCResult WebRenderBridgeP
   // to early-return without doing so.
   AutoWebRenderBridgeParentAsyncMessageSender autoAsyncMessageSender(
       this, &aToDestroy);
 
   if (!mRenderRoot && aRenderRootUpdates.Length() > 0) {
     // Only non-root WRBPs will ever have an unresolved mRenderRoot
     MOZ_ASSERT(!IsRootWebRenderBridgeParent());
     if (aRenderRootUpdates.Length() != 1) {
-      return IPC_FAIL(this, "Well-behaved content processes must only send a DL for a single renderRoot");
+      return IPC_FAIL(this,
+                      "Well-behaved content processes must only send a DL for "
+                      "a single renderRoot");
     }
     PushDeferredPipelineData(AsVariant(std::move(aRenderRootUpdates[0])));
     aRenderRootUpdates.Clear();
   }
 
   UpdateAPZFocusState(aFocusTarget);
 
   bool scheduleAnyComposite = false;
--- a/gfx/layers/wr/WebRenderBridgeParent.h
+++ b/gfx/layers/wr/WebRenderBridgeParent.h
@@ -277,33 +277,40 @@ class WebRenderBridgeParent final
    * can determine what RenderRoot it belongs to, then we defer that data until
    * we can. This handles processing that deferred data.
    */
   bool MaybeHandleDeferredPipelineData(
       wr::RenderRoot aRenderRoot, const nsTArray<wr::PipelineId>& aPipelineIds,
       const TimeStamp& aTxnStartTime);
 
   /**
-   * See MaybeHandleDeferredPipelineData - this is the implementation of that for
-   * a single pipeline.
+   * See MaybeHandleDeferredPipelineData - this is the implementation of that
+   * for a single pipeline.
    */
   bool MaybeHandleDeferredPipelineDataForPipeline(
       wr::RenderRoot aRenderRoot, wr::PipelineId aPipelineId,
       const TimeStamp& aTxnStartTime);
 
   bool HandleDeferredPipelineData(
       nsTArray<RenderRootDeferredData>& aDeferredData,
       const TimeStamp& aTxnStartTime);
 
   bool IsRootWebRenderBridgeParent() const;
   LayersId GetLayersId() const;
   WRRootId GetWRRootId() const;
 
   void SetCompositionRecorder(
-      RefPtr<layers::WebRenderCompositionRecorder>&& aRecorder);
+      UniquePtr<layers::WebRenderCompositionRecorder> aRecorder);
+
+  /**
+   * Write the frames collected by the |WebRenderCompositionRecorder| to disk.
+   *
+   * If there is not currently a recorder, this is a no-op.
+   */
+  void WriteCollectedFrames();
 
  private:
   class ScheduleSharedSurfaceRelease;
 
   explicit WebRenderBridgeParent(const wr::PipelineId& aPipelineId);
   virtual ~WebRenderBridgeParent();
 
   wr::WebRenderAPI* Api(wr::RenderRoot aRenderRoot) {
--- a/gfx/layers/wr/WebRenderCommandBuilder.cpp
+++ b/gfx/layers/wr/WebRenderCommandBuilder.cpp
@@ -1521,16 +1521,20 @@ void WebRenderCommandBuilder::DoGrouping
   const nsRect& untransformedPaintRect =
       aWrappingItem->GetUntransformedPaintRect();
 
   group.mPaintRect = LayerIntRect::FromUnknownRect(
                          ScaleToOutsidePixelsOffset(
                              untransformedPaintRect, scale.width, scale.height,
                              group.mAppUnitsPerDevPixel, residualOffset))
                          .Intersect(group.mLayerBounds);
+  // XXX: Make the paint rect relative to the layer bounds. After we include
+  // mLayerBounds.TopLeft() in the blob image we want to stop doing this
+  // adjustment.
+  group.mPaintRect = group.mPaintRect - group.mLayerBounds.TopLeft();
 
   g.mTransform = Matrix::Scaling(scale.width, scale.height)
                      .PostTranslate(residualOffset.x, residualOffset.y);
   group.mScale = scale;
   group.mScrollId = scrollId;
   g.ConstructGroups(aDisplayListBuilder, this, aBuilder, aResources, &group,
                     aList, aSc);
   mCurrentClipManager->EndList(aSc);
--- a/gfx/layers/wr/WebRenderCompositionRecorder.cpp
+++ b/gfx/layers/wr/WebRenderCompositionRecorder.cpp
@@ -40,87 +40,34 @@ class RendererRecordedFrame final : publ
 
  private:
   wr::Renderer* mRenderer;
   RefPtr<gfx::DataSourceSurface> mSurface;
   gfx::IntSize mSize;
   wr::RecordedFrameHandle mHandle;
 };
 
-void WebRenderCompositionRecorder::RecordFrame(RecordedFrame* aFrame) {
-  MOZ_CRASH(
-      "WebRenderCompositionRecorder::RecordFrame should not be called; call "
-      "MaybeRecordFrame instead.");
-}
-
-bool WebRenderCompositionRecorder::MaybeRecordFrame(
+void WebRenderCompositionRecorder::MaybeRecordFrame(
     wr::Renderer* aRenderer, wr::WebRenderPipelineInfo* aFrameEpochs) {
   MOZ_ASSERT(wr::RenderThread::IsInRenderThread());
 
-  if (!aRenderer || !aFrameEpochs) {
-    return false;
-  }
-
-  if (!mMutex.TryLock()) {
-    // If we cannot lock the mutex, then either (a) the |CompositorBridgeParent|
-    // is holding the mutex in |WriteCollectedFrames| or (b) the |RenderThread|
-    // is holding the mutex in |ForceFinishRecording|.
-    //
-    // In either case we do not want to wait to acquire the mutex to record a
-    // frame since frames recorded now will not be written to disk.
-
-    return false;
-  }
-
-  auto unlockGuard = MakeScopeExit([&]() { mMutex.Unlock(); });
-
-  if (mFinishedRecording) {
-    return true;
-  }
-
-  if (!DidPaintContent(aFrameEpochs)) {
-    return false;
+  if (!aRenderer || !aFrameEpochs || !DidPaintContent(aFrameEpochs)) {
+    return;
   }
 
   wr::RecordedFrameHandle handle{0};
   gfx::IntSize size(0, 0);
 
   if (wr_renderer_record_frame(aRenderer, wr::ImageFormat::BGRA8, &handle,
                                &size.width, &size.height)) {
     RefPtr<RecordedFrame> frame =
         new RendererRecordedFrame(TimeStamp::Now(), aRenderer, handle, size);
 
-    CompositionRecorder::RecordFrame(frame);
+    RecordFrame(frame);
   }
-
-  return false;
-}
-
-void WebRenderCompositionRecorder::WriteCollectedFrames() {
-  MutexAutoLock guard(mMutex);
-
-  MOZ_RELEASE_ASSERT(
-      !mFinishedRecording,
-      "WebRenderCompositionRecorder: Attempting to write frames from invalid "
-      "state.");
-
-  CompositionRecorder::WriteCollectedFrames();
-
-  mFinishedRecording = true;
-}
-
-bool WebRenderCompositionRecorder::ForceFinishRecording() {
-  MutexAutoLock guard(mMutex);
-
-  bool wasRecording = !mFinishedRecording;
-  mFinishedRecording = true;
-
-  ClearCollectedFrames();
-
-  return wasRecording;
 }
 
 bool WebRenderCompositionRecorder::DidPaintContent(
     wr::WebRenderPipelineInfo* aFrameEpochs) {
   const wr::WrPipelineInfo& info = aFrameEpochs->Raw();
   bool didPaintContent = false;
 
   for (wr::usize i = 0; i < info.epochs.length; i++) {
--- a/gfx/layers/wr/WebRenderCompositionRecorder.h
+++ b/gfx/layers/wr/WebRenderCompositionRecorder.h
@@ -28,93 +28,51 @@ namespace layers {
  * frames are written on the thread holding the |CompositorBridgeParent|.
  *
  */
 class WebRenderCompositionRecorder final : public CompositionRecorder {
  public:
   explicit WebRenderCompositionRecorder(TimeStamp aRecordingStart,
                                         wr::PipelineId aRootPipelineId)
       : CompositionRecorder(aRecordingStart),
-        mMutex("CompositionRecorder"),
-        mFinishedRecording(false),
         mRootPipelineId(aRootPipelineId) {}
 
   WebRenderCompositionRecorder() = delete;
   WebRenderCompositionRecorder(WebRenderCompositionRecorder&) = delete;
   WebRenderCompositionRecorder(WebRenderCompositionRecorder&&) = delete;
 
   WebRenderCompositionRecorder& operator=(WebRenderCompositionRecorder&) =
       delete;
   WebRenderCompositionRecorder& operator=(WebRenderCompositionRecorder&&) =
       delete;
 
   /**
-   * Do not call this method.
-   *
-   * Instead, call |MaybeRecordFrame|, which will only attempt to record a
-   * frame if we have not yet written frames to disk.
-   */
-  void RecordFrame(RecordedFrame* aFrame) override;
-
-  /**
-   * Write the collected frames to disk.
-   *
-   * This method should not be called if frames have already been written or if
-   * |ForceFinishRecording| has been called as the object will be in an invalid
-   * state to write to disk.
-   *
-   * Note: This method will block acquiring a lock.
-   */
-  void WriteCollectedFrames() override;
-
-  /**
    * Attempt to record a frame from the given renderer.
    *
    * This method will only record a frame if the following are true:
    *
    * - this object's lock was acquired immediately (i.e., we are not currently
    *   writing frames to disk);
    * - we have not yet written frames to disk; and
    * - one of the pipelines in |aFrameEpochs| has updated and it is not the
    *   root pipeline.
    *
    * Returns whether or not the recorder has finished recording frames. If
    * true, it is safe to release both this object and Web Render's composition
    * recorder structures.
    */
-  bool MaybeRecordFrame(wr::Renderer* aRenderer,
+  void MaybeRecordFrame(wr::Renderer* aRenderer,
                         wr::WebRenderPipelineInfo* aFrameEpochs);
 
-  /**
-   * Force the composition recorder to finish recording.
-   *
-   * This should only be called if |WriteCollectedFrames| is not to be called,
-   * since the recorder will be in an invalid state to do so.
-   *
-   * This returns whether or not the recorder was recording before this method
-   * was called.
-   *
-   * Note: This method will block acquiring a lock.
-   */
-  bool ForceFinishRecording();
-
- protected:
-  ~WebRenderCompositionRecorder() = default;
-
+ private:
   /**
    * Determine if any content pipelines updated.
    */
   bool DidPaintContent(wr::WebRenderPipelineInfo* aFrameEpochs);
 
- private:
-  Mutex mMutex;
-
-  // Whether or not we have finished recording.
-  bool mFinishedRecording;
-
   // The id of the root WebRender pipeline.
   //
   // All other pipelines are considered content.
   wr::PipelineId mRootPipelineId;
 
   // A mapping of wr::PipelineId to the epochs when last they updated.
   //
   // We need to use uint64_t here since wr::PipelineId is not default
--- a/gfx/webrender_bindings/RenderThread.cpp
+++ b/gfx/webrender_bindings/RenderThread.cpp
@@ -233,43 +233,45 @@ RendererOGL* RenderThread::GetRenderer(w
 
 size_t RenderThread::RendererCount() {
   MOZ_ASSERT(IsInRenderThread());
   return mRenderers.size();
 }
 
 void RenderThread::SetCompositionRecorderForWindow(
     wr::WindowId aWindowId,
-    RefPtr<layers::WebRenderCompositionRecorder>&& aCompositionRecorder) {
+    UniquePtr<layers::WebRenderCompositionRecorder> aCompositionRecorder) {
   MOZ_ASSERT(IsInRenderThread());
   MOZ_ASSERT(GetRenderer(aWindowId));
+  MOZ_ASSERT(mCompositionRecorders.find(aWindowId) ==
+             mCompositionRecorders.end());
+
+  mCompositionRecorders[aWindowId] = std::move(aCompositionRecorder);
+}
+
+void RenderThread::WriteCollectedFramesForWindow(wr::WindowId aWindowId) {
+  MOZ_ASSERT(IsInRenderThread());
+
+  RendererOGL* renderer = GetRenderer(aWindowId);
+  MOZ_ASSERT(renderer);
 
   auto it = mCompositionRecorders.find(aWindowId);
-  if (it != mCompositionRecorders.end() && it->second->ForceFinishRecording()) {
-    // This case should never occur since the |CompositorBridgeParent| will
-    // receive its "EndRecording" IPC message before another "BeginRecording"
-    // IPC message.
-    //
-    // However, if we do hit this case, then we should handle it gracefully.
-    // We free the structures here because any captured frames are not going
-    // to be read back.
-    if (RendererOGL* renderer = GetRenderer(aWindowId)) {
+  MOZ_DIAGNOSTIC_ASSERT(
+      it != mCompositionRecorders.end(),
+      "Attempted to write frames from a window that was not recording.");
+  if (it != mCompositionRecorders.end()) {
+    it->second->WriteCollectedFrames();
+
+    if (renderer) {
       wr_renderer_release_composition_recorder_structures(
           renderer->GetRenderer());
     }
+
+    mCompositionRecorders.erase(it);
   }
-
-  // If we have finished recording, then we have received
-  // |SetCompositionRecorderEvent| after the compositor brige parent finished
-  // writing but before we handled another frame to delete the data structure.
-  //
-  // In this case we do not need to free the |wr::Renderer|'s composition
-  // recorder structures since we can re-use them.
-
-  mCompositionRecorders[aWindowId] = std::move(aCompositionRecorder);
 }
 
 void RenderThread::HandleFrame(wr::WindowId aWindowId, bool aRender) {
   if (mHasShutdown) {
     return;
   }
 
   if (!IsInRenderThread()) {
@@ -425,25 +427,17 @@ void RenderThread::UpdateAndRender(
   layers::CompositorThreadHolder::Loop()->PostTask(
       NewRunnableFunction("NotifyDidRenderRunnable", &NotifyDidRender,
                           renderer->GetCompositorBridge(), info, aStartId,
                           aStartTime, start, end, aRender, stats));
 
   if (rendered) {
     auto recorderIt = mCompositionRecorders.find(aWindowId);
     if (recorderIt != mCompositionRecorders.end()) {
-      bool shouldRelease = recorderIt->second->MaybeRecordFrame(
-          renderer->GetRenderer(), info.get());
-
-      if (shouldRelease) {
-        mCompositionRecorders.erase(recorderIt);
-
-        wr_renderer_release_composition_recorder_structures(
-            renderer->GetRenderer());
-      }
+      recorderIt->second->MaybeRecordFrame(renderer->GetRenderer(), info.get());
     }
   }
 
   if (rendered) {
     // Wait for GPU after posting NotifyDidRender, since the wait is not
     // necessary for the NotifyDidRender.
     // The wait is necessary for Textures recycling of AsyncImagePipelineManager
     // and for avoiding GPU queue is filled with too much tasks.
--- a/gfx/webrender_bindings/RenderThread.h
+++ b/gfx/webrender_bindings/RenderThread.h
@@ -264,17 +264,19 @@ class RenderThread final {
   void HandleWebRenderError(WebRenderError aError);
   /// Can only be called from the render thread.
   bool IsHandlingWebRenderError();
 
   size_t RendererCount();
 
   void SetCompositionRecorderForWindow(
       wr::WindowId aWindowId,
-      RefPtr<layers::WebRenderCompositionRecorder>&& aCompositionRecorder);
+      UniquePtr<layers::WebRenderCompositionRecorder> aCompositionRecorder);
+
+  void WriteCollectedFramesForWindow(wr::WindowId aWindowId);
 
  private:
   explicit RenderThread(base::Thread* aThread);
 
   void DeferredRenderTextureHostDestroy();
   void ShutDownTask(layers::SynchronousTask* aTask);
   void InitDeviceTask();
 
@@ -290,17 +292,17 @@ class RenderThread final {
   UniquePtr<WebRenderProgramCache> mProgramCache;
   UniquePtr<WebRenderShaders> mShaders;
 
   // An optional shared GLContext to be used for all
   // windows.
   RefPtr<gl::GLContext> mSharedGL;
 
   std::map<wr::WindowId, UniquePtr<RendererOGL>> mRenderers;
-  std::map<wr::WindowId, RefPtr<layers::WebRenderCompositionRecorder>>
+  std::map<wr::WindowId, UniquePtr<layers::WebRenderCompositionRecorder>>
       mCompositionRecorders;
 
   struct WindowInfo {
     bool mIsDestroyed = false;
     bool mRender = false;
     int64_t mPendingCount = 0;
     int64_t mRenderingCount = 0;
     uint8_t mDocFramesSeen = 0;
--- a/gfx/webrender_bindings/WebRenderAPI.cpp
+++ b/gfx/webrender_bindings/WebRenderAPI.cpp
@@ -574,43 +574,62 @@ void WebRenderAPI::WaitFlushed() {
 
 void WebRenderAPI::Capture() {
   uint8_t bits = 3;                 // TODO: get from JavaScript
   const char* path = "wr-capture";  // TODO: get from JavaScript
   wr_api_capture(mDocHandle, path, bits);
 }
 
 void WebRenderAPI::SetCompositionRecorder(
-    RefPtr<layers::WebRenderCompositionRecorder>&& aRecorder) {
+    UniquePtr<layers::WebRenderCompositionRecorder> aRecorder) {
   class SetCompositionRecorderEvent final : public RendererEvent {
    public:
     explicit SetCompositionRecorderEvent(
-        RefPtr<layers::WebRenderCompositionRecorder>&& aRecorder)
+        UniquePtr<layers::WebRenderCompositionRecorder> aRecorder)
         : mRecorder(std::move(aRecorder)) {
       MOZ_COUNT_CTOR(SetCompositionRecorderEvent);
     }
 
     ~SetCompositionRecorderEvent() {
       MOZ_COUNT_DTOR(SetCompositionRecorderEvent);
     }
 
     void Run(RenderThread& aRenderThread, WindowId aWindowId) override {
       MOZ_ASSERT(mRecorder);
 
       aRenderThread.SetCompositionRecorderForWindow(aWindowId,
                                                     std::move(mRecorder));
     }
 
    private:
-    RefPtr<layers::WebRenderCompositionRecorder> mRecorder;
+    UniquePtr<layers::WebRenderCompositionRecorder> mRecorder;
   };
 
   auto event = MakeUnique<SetCompositionRecorderEvent>(std::move(aRecorder));
   RunOnRenderThread(std::move(event));
 }
+
+void WebRenderAPI::WriteCollectedFrames() {
+  class WriteCollectedFramesEvent final : public RendererEvent {
+   public:
+    explicit WriteCollectedFramesEvent() {
+      MOZ_COUNT_CTOR(WriteCollectedFramesEvent);
+    }
+
+    ~WriteCollectedFramesEvent() { MOZ_COUNT_DTOR(WriteCollectedFramesEvent); }
+
+    void Run(RenderThread& aRenderThread, WindowId aWindowId) override {
+      aRenderThread.WriteCollectedFramesForWindow(aWindowId);
+    }
+  };
+
+  auto event = MakeUnique<WriteCollectedFramesEvent>();
+  RunOnRenderThread(std::move(event));
+}
+
 void TransactionBuilder::Clear() { wr_resource_updates_clear(mTxn); }
 
 void TransactionBuilder::Notify(wr::Checkpoint aWhen,
                                 UniquePtr<NotificationHandler> aEvent) {
   wr_transaction_notify(mTxn, aWhen,
                         reinterpret_cast<uintptr_t>(aEvent.release()));
 }
 
--- a/gfx/webrender_bindings/WebRenderAPI.h
+++ b/gfx/webrender_bindings/WebRenderAPI.h
@@ -261,17 +261,24 @@ class WebRenderAPI final {
   bool GetUseANGLE() const { return mUseANGLE; }
   bool GetUseDComp() const { return mUseDComp; }
   bool GetUseTripleBuffering() const { return mUseTripleBuffering; }
   layers::SyncHandle GetSyncHandle() const { return mSyncHandle; }
 
   void Capture();
 
   void SetCompositionRecorder(
-      RefPtr<layers::WebRenderCompositionRecorder>&& aRecorder);
+      UniquePtr<layers::WebRenderCompositionRecorder> aRecorder);
+
+  /**
+   * Write the frames collected by the |WebRenderCompositionRecorder| to disk.
+   *
+   * If there is not currently a recorder, this is a no-op.
+   */
+  void WriteCollectedFrames();
 
  protected:
   WebRenderAPI(wr::DocumentHandle* aHandle, wr::WindowId aId,
                uint32_t aMaxTextureSize, bool aUseANGLE, bool aUseDComp,
                bool aUseTripleBuffering, layers::SyncHandle aSyncHandle,
                wr::RenderRoot aRenderRoot);
 
   ~WebRenderAPI();
--- a/gfx/wr/webrender/src/clip.rs
+++ b/gfx/wr/webrender/src/clip.rs
@@ -1325,63 +1325,73 @@ impl ClipItem {
 
     fn get_clip_result_complex(
         &self,
         local_pos: LayoutPoint,
         transform: &LayoutToWorldTransform,
         prim_world_rect: &WorldRect,
         world_rect: &WorldRect,
     ) -> ClipResult {
-        let (clip_rect, inner_rect) = match *self {
-            ClipItem::Rectangle(size, ClipMode::Clip) => {
+        let visible_rect = match prim_world_rect.intersection(world_rect) {
+            Some(rect) => rect,
+            None => return ClipResult::Reject,
+        };
+
+        let (clip_rect, inner_rect, mode) = match *self {
+            ClipItem::Rectangle(size, mode) => {
                 let clip_rect = LayoutRect::new(local_pos, size);
-                (clip_rect, Some(clip_rect))
+                (clip_rect, Some(clip_rect), mode)
             }
-            ClipItem::RoundedRectangle(size, ref radius, ClipMode::Clip) => {
+            ClipItem::RoundedRectangle(size, ref radius, mode) => {
                 let clip_rect = LayoutRect::new(local_pos, size);
                 let inner_clip_rect = extract_inner_rect_safe(&clip_rect, radius);
-                (clip_rect, inner_clip_rect)
+                (clip_rect, inner_clip_rect, mode)
             }
-            ClipItem::Rectangle(_, ClipMode::ClipOut) |
-            ClipItem::RoundedRectangle(_, _, ClipMode::ClipOut) |
-            ClipItem::Image { .. } |
+            ClipItem::Image { size, repeat: false, .. } => {
+                let clip_rect = LayoutRect::new(local_pos, size);
+                (clip_rect, None, ClipMode::Clip)
+            }
+            ClipItem::Image { repeat: true, .. } |
             ClipItem::BoxShadow(..) => {
-                return ClipResult::Partial
+                return ClipResult::Partial;
             }
         };
 
-        let inner_clip_rect = inner_rect.and_then(|ref inner_rect| {
+        if let Some(inner_clip_rect) = inner_rect.and_then(|ref inner_rect| {
             project_inner_rect(transform, inner_rect)
-        });
-
-        if let Some(inner_clip_rect) = inner_clip_rect {
-            match prim_world_rect.intersection(world_rect) {
-                Some(ref rect) if inner_clip_rect.contains_rect(rect) =>
-                    return ClipResult::Accept,
-                Some(_) => (),
-                None => return ClipResult::Reject,
+        }) {
+            if inner_clip_rect.contains_rect(&visible_rect) {
+                return match mode {
+                    ClipMode::Clip => ClipResult::Accept,
+                    ClipMode::ClipOut => ClipResult::Reject,
+                };
             }
         }
 
-        let outer_clip_rect = match project_rect(
-            transform,
-            &clip_rect,
-            world_rect,
-        ) {
-            Some(outer_clip_rect) => outer_clip_rect,
-            None => return ClipResult::Partial,
-        };
+        match mode {
+            ClipMode::Clip => {
+                let outer_clip_rect = match project_rect(
+                    transform,
+                    &clip_rect,
+                    world_rect,
+                ) {
+                    Some(outer_clip_rect) => outer_clip_rect,
+                    None => return ClipResult::Partial,
+                };
 
-        match outer_clip_rect.intersection(prim_world_rect) {
-            Some(..) => {
-                ClipResult::Partial
+                match outer_clip_rect.intersection(prim_world_rect) {
+                    Some(..) => {
+                        ClipResult::Partial
+                    }
+                    None => {
+                        ClipResult::Reject
+                    }
+                }
             }
-            None => {
-                ClipResult::Reject
-            }
+            ClipMode::ClipOut => ClipResult::Partial,
         }
     }
 
     // Check how a given clip source affects a local primitive region.
     fn get_clip_result(
         &self,
         local_pos: LayoutPoint,
         prim_rect: &LayoutRect,
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/blank.yaml
@@ -0,0 +1,2 @@
+---
+root:
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-out-rotation.yaml
@@ -0,0 +1,41 @@
+# Test that transformed content is clipped out properly by clips with a different transform.
+# Also verifies that we aren't trying to render the clip mask at all for this clip-out case.
+#
+# The clip is a 500x500 rounded cornered rectangle rotated by 15 degrees. It fully contains the
+# bounds of the red rect and has a clip-out mode (within the conservative logic of our
+# `project_inner_rect` transformation), so nothing is visible, and no mask is rendered.
+---
+root:
+  items:
+    -
+      bounds: [0, 0, 0, 0]
+      type: "reference-frame"
+      id: 2
+    -
+      bounds: [0, 0, 0, 0]
+      type: "stacking-context"
+      transform: rotate(15) translate(200, 0)
+      items:
+        -
+          bounds: [0, 0, 1000, 1000]
+          type: clip
+          id: 5
+          complex:
+            -
+              rect: [0, 0, 500, 500]
+              radius: [5, 5]
+              clip-mode: clip-out
+        # uncomment this to see the clip area
+        #-
+        #  bounds: [0, 0, 500, 500]
+        #  type: rect
+        #  color: green
+    -
+      bounds: [0, 0, 0, 0]
+      type: "stacking-context"
+      clip-and-scroll: [2, 5]
+      items:
+        -
+          bounds: [225, 150, 300, 300]
+          type: rect
+          color: red
--- a/gfx/wr/wrench/reftests/clip/reftest.list
+++ b/gfx/wr/wrench/reftests/clip/reftest.list
@@ -7,8 +7,9 @@ fuzzy(1,3) == clip-corner-overlap.yaml c
 fuzzy(8,60) == custom-clip-chains.yaml custom-clip-chains-ref.yaml
 == custom-clip-chain-node-ancestors.yaml custom-clip-chain-node-ancestors-ref.yaml
 == fixed-position-clipping.yaml fixed-position-clipping-ref.yaml
 platform(linux,mac) == segmentation-with-other-coordinate-system-clip.yaml segmentation-with-other-coordinate-system-clip.png
 skip_on(android,emulator) == segmentation-across-rotation.yaml segmentation-across-rotation-ref.yaml
 == color_targets(3) alpha_targets(1) stacking-context-clip.yaml stacking-context-clip-ref.yaml
 == snapping.yaml snapping-ref.yaml
 skip_on(android,emulator) fuzzy(70,2400) == clip-and-filter-with-rotation.yaml clip-and-filter-with-rotation-ref.yaml
+color_targets(1) alpha_targets(0) == clip-out-rotation.yaml blank.yaml
--- a/ipc/glue/BackgroundUtils.cpp
+++ b/ipc/glue/BackgroundUtils.cpp
@@ -579,16 +579,17 @@ nsresult LoadInfoToLoadInfoArgs(nsILoadI
       aLoadInfo->GetOriginAttributes(), redirectChainIncludingInternalRedirects,
       redirectChain, ancestorPrincipals, aLoadInfo->AncestorOuterWindowIDs(),
       ipcClientInfo, ipcReservedClientInfo, ipcInitialClientInfo, ipcController,
       aLoadInfo->CorsUnsafeHeaders(), aLoadInfo->GetForcePreflight(),
       aLoadInfo->GetIsPreflight(), aLoadInfo->GetLoadTriggeredFromExternal(),
       aLoadInfo->GetServiceWorkerTaintingSynthesized(),
       aLoadInfo->GetDocumentHasUserInteracted(),
       aLoadInfo->GetDocumentHasLoaded(), cspNonce,
+      aLoadInfo->GetSkipContentSniffing(),
       aLoadInfo->GetIsFromProcessingFrameAttributes(), cookieSettingsArgs,
       aLoadInfo->GetRequestBlockingReason(), maybeCspToInheritInfo));
 
   return NS_OK;
 }
 
 nsresult LoadInfoArgsToLoadInfo(
     const Maybe<LoadInfoArgs>& aOptionalLoadInfoArgs,
@@ -741,31 +742,32 @@ nsresult LoadInfoArgsToLoadInfo(
       loadInfoArgs.originAttributes(), redirectChainIncludingInternalRedirects,
       redirectChain, std::move(ancestorPrincipals),
       loadInfoArgs.ancestorOuterWindowIDs(), loadInfoArgs.corsUnsafeHeaders(),
       loadInfoArgs.forcePreflight(), loadInfoArgs.isPreflight(),
       loadInfoArgs.loadTriggeredFromExternal(),
       loadInfoArgs.serviceWorkerTaintingSynthesized(),
       loadInfoArgs.documentHasUserInteracted(),
       loadInfoArgs.documentHasLoaded(), loadInfoArgs.cspNonce(),
-      loadInfoArgs.requestBlockingReason());
+      loadInfoArgs.skipContentSniffing(), loadInfoArgs.requestBlockingReason());
 
   if (loadInfoArgs.isFromProcessingFrameAttributes()) {
     loadInfo->SetIsFromProcessingFrameAttributes();
   }
 
   loadInfo.forget(outLoadInfo);
   return NS_OK;
 }
 
 void LoadInfoToParentLoadInfoForwarder(
     nsILoadInfo* aLoadInfo, ParentLoadInfoForwarderArgs* aForwarderArgsOut) {
   if (!aLoadInfo) {
     *aForwarderArgsOut = ParentLoadInfoForwarderArgs(
         false, false, Nothing(), nsILoadInfo::TAINTING_BASIC,
+        false,  // SkipContentSniffing
         false,  // serviceWorkerTaintingSynthesized
         false,  // documentHasUserInteracted
         false,  // documentHasLoaded
         Maybe<CookieSettingsArgs>(),
         nsILoadInfo::BLOCKING_REASON_NONE);  // requestBlockingReason
     return;
   }
 
@@ -787,16 +789,17 @@ void LoadInfoToParentLoadInfoForwarder(
     CookieSettingsArgs args;
     cs->Serialize(args);
     cookieSettingsArgs = Some(args);
   }
 
   *aForwarderArgsOut = ParentLoadInfoForwarderArgs(
       aLoadInfo->GetAllowInsecureRedirectToDataURI(),
       aLoadInfo->GetBypassCORSChecks(), ipcController, tainting,
+      aLoadInfo->GetSkipContentSniffing(),
       aLoadInfo->GetServiceWorkerTaintingSynthesized(),
       aLoadInfo->GetDocumentHasUserInteracted(),
       aLoadInfo->GetDocumentHasLoaded(), cookieSettingsArgs,
       aLoadInfo->GetRequestBlockingReason());
 }
 
 nsresult MergeParentLoadInfoForwarder(
     ParentLoadInfoForwarderArgs const& aForwarderArgs, nsILoadInfo* aLoadInfo) {
@@ -821,16 +824,19 @@ nsresult MergeParentLoadInfoForwarder(
 
   if (aForwarderArgs.serviceWorkerTaintingSynthesized()) {
     aLoadInfo->SynthesizeServiceWorkerTainting(
         static_cast<LoadTainting>(aForwarderArgs.tainting()));
   } else {
     aLoadInfo->MaybeIncreaseTainting(aForwarderArgs.tainting());
   }
 
+  rv = aLoadInfo->SetSkipContentSniffing(aForwarderArgs.skipContentSniffing());
+  NS_ENSURE_SUCCESS(rv, rv);
+
   MOZ_ALWAYS_SUCCEEDS(aLoadInfo->SetDocumentHasUserInteracted(
       aForwarderArgs.documentHasUserInteracted()));
   MOZ_ALWAYS_SUCCEEDS(
       aLoadInfo->SetDocumentHasLoaded(aForwarderArgs.documentHasLoaded()));
   MOZ_ALWAYS_SUCCEEDS(aLoadInfo->SetRequestBlockingReason(
       aForwarderArgs.requestBlockingReason()));
 
   const Maybe<CookieSettingsArgs>& cookieSettingsArgs =
--- a/js/src/builtin/ReflectParse.cpp
+++ b/js/src/builtin/ReflectParse.cpp
@@ -3140,17 +3140,17 @@ bool ASTSerializer::property(ParseNode* 
 
   BinaryNode* node = &pn->as<BinaryNode>();
   ParseNode* keyNode = node->left();
   ParseNode* valNode = node->right();
 
   bool isShorthand = node->isKind(ParseNodeKind::Shorthand);
   bool isMethod =
       valNode->is<FunctionNode>() &&
-      valNode->as<FunctionNode>().funbox()->kind() == JSFunction::Method;
+      valNode->as<FunctionNode>().funbox()->kind() == FunctionFlags::Method;
   RootedValue key(cx), val(cx);
   return propertyName(keyNode, &key) && expression(valNode, &val) &&
          builder.propertyInitializer(key, val, kind, isShorthand, isMethod,
                                      &node->pn_pos, dst);
 }
 
 bool ASTSerializer::literal(ParseNode* pn, MutableHandleValue dst) {
   RootedValue val(cx);
--- a/js/src/builtin/Stream.cpp
+++ b/js/src/builtin/Stream.cpp
@@ -3,30 +3,32 @@
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "builtin/Stream.h"
 
 #include "js/Stream.h"
 
+#include <stdint.h>  // int32_t
+
 #include "gc/Heap.h"
 #include "js/ArrayBuffer.h"  // JS::NewArrayBuffer
 #include "js/PropertySpec.h"
 #include "vm/Interpreter.h"
 #include "vm/JSContext.h"
 #include "vm/SelfHosting.h"
 
 #include "vm/Compartment-inl.h"
 #include "vm/List-inl.h"
 #include "vm/NativeObject-inl.h"
 
 using namespace js;
 
-enum ReaderType { ReaderType_Default, ReaderType_BYOB };
+enum class ReaderType : int32_t { Default = 0, BYOB = 1 };
 
 template <class T>
 bool Is(const HandleValue v) {
   return v.isObject() && v.toObject().is<T>();
 }
 
 template <class T>
 bool IsMaybeWrapped(const HandleValue v) {
@@ -244,38 +246,42 @@ class PullIntoDescriptor : public Native
     return getFixedSlot(Slot_BytesFilled).toInt32();
   }
   void setBytesFilled(int32_t bytes) {
     setFixedSlot(Slot_BytesFilled, Int32Value(bytes));
   }
   uint32_t elementSize() const {
     return getFixedSlot(Slot_ElementSize).toInt32();
   }
-  uint32_t readerType() const {
-    return getFixedSlot(Slot_ReaderType).toInt32();
+  ReaderType readerType() const {
+    int32_t n = getFixedSlot(Slot_ReaderType).toInt32();
+    MOZ_ASSERT(n == int32_t(ReaderType::Default) ||
+               n == int32_t(ReaderType::BYOB));
+    return ReaderType(n);
   }
 
   static PullIntoDescriptor* create(JSContext* cx,
                                     HandleArrayBufferObject buffer,
                                     uint32_t byteOffset, uint32_t byteLength,
                                     uint32_t bytesFilled, uint32_t elementSize,
-                                    HandleObject ctor, uint32_t readerType) {
+                                    HandleObject ctor, ReaderType readerType) {
     Rooted<PullIntoDescriptor*> descriptor(
         cx, NewBuiltinClassInstance<PullIntoDescriptor>(cx));
     if (!descriptor) {
       return nullptr;
     }
 
     descriptor->setFixedSlot(Slot_buffer, ObjectValue(*buffer));
     descriptor->setFixedSlot(Slot_Ctor, ObjectOrNullValue(ctor));
     descriptor->setFixedSlot(Slot_ByteOffset, Int32Value(byteOffset));
     descriptor->setFixedSlot(Slot_ByteLength, Int32Value(byteLength));
     descriptor->setFixedSlot(Slot_BytesFilled, Int32Value(bytesFilled));
     descriptor->setFixedSlot(Slot_ElementSize, Int32Value(elementSize));
-    descriptor->setFixedSlot(Slot_ReaderType, Int32Value(readerType));
+    descriptor->setFixedSlot(Slot_ReaderType,
+                             Int32Value(static_cast<int32_t>(readerType)));
     return descriptor;
   }
 };
 
 const Class PullIntoDescriptor::class_ = {
     "PullIntoDescriptor", JSCLASS_HAS_RESERVED_SLOTS(SlotCount)};
 
 class QueueEntry : public NativeObject {
@@ -669,24 +675,29 @@ static MOZ_MUST_USE bool ReadableStream_
       cx, ::ReadableStreamCancel(cx, unwrappedStream, args.get(0)));
   if (!cancelPromise) {
     return false;
   }
   args.rval().setObject(*cancelPromise);
   return true;
 }
 
+// Streams spec, 3.2.5.3.
+//      getIterator({ preventCancel } = {})
+//
+// Not implemented.
+
 static MOZ_MUST_USE ReadableStreamDefaultReader*
 CreateReadableStreamDefaultReader(
     JSContext* cx, Handle<ReadableStream*> unwrappedStream,
     ForAuthorCodeBool forAuthorCode = ForAuthorCodeBool::No,
     HandleObject proto = nullptr);
 
 /**
- * Streams spec, 3.2.5.3. getReader({ mode } = {})
+ * Streams spec, 3.2.5.4. getReader({ mode } = {})
  */
 static bool ReadableStream_getReader(JSContext* cx, unsigned argc, Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   // Implicit in the spec: Argument defaults and destructuring.
   RootedValue optionsVal(cx, args.get(0));
   if (optionsVal.isUndefined()) {
     JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx);
@@ -722,16 +733,17 @@ static bool ReadableStream_getReader(JSC
 
     // Step 4: If mode is "byob",
     //         return ? AcquireReadableStreamBYOBReader(this).
     bool equal;
     if (!EqualStrings(cx, mode, cx->names().byob, &equal)) {
       return false;
     }
     if (equal) {
+      // BYOB readers aren't implemented yet.
       JS_ReportErrorNumberASCII(
           cx, GetErrorMessage, nullptr,
           JSMSG_READABLESTREAM_BYTES_TYPE_NOT_IMPLEMENTED);
       return false;
     }
 
     // Step 5: Throw a RangeError exception.
     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
@@ -742,33 +754,34 @@ static bool ReadableStream_getReader(JSC
   // Reordered second part of steps 2 and 4.
   if (!reader) {
     return false;
   }
   args.rval().setObject(*reader);
   return true;
 }
 
-// Streams spec, 3.2.5.4.
-//      pipeThrough({ writable, readable }, options)
+// Streams spec, 3.2.5.5.
+//      pipeThrough({ writable, readable },
+//                  { preventClose, preventAbort, preventCancel, signal })
 //
 // Not implemented.
 
-// Streams spec, 3.2.5.5.
-//      pipeTo(dest, { preventClose, preventAbort, preventCancel } = {})
+// Streams spec, 3.2.5.6.
+//      pipeTo(dest, { preventClose, preventAbort, preventCancel, signal } = {})
 //
 // Not implemented.
 
 static MOZ_MUST_USE bool ReadableStreamTee(
     JSContext* cx, Handle<ReadableStream*> unwrappedStream,
     bool cloneForBranch2, MutableHandle<ReadableStream*> branch1,
     MutableHandle<ReadableStream*> branch2);
 
 /**
- * Streams spec, 3.2.5.6. tee()
+ * Streams spec, 3.2.5.7. tee()
  */
 static bool ReadableStream_tee(JSContext* cx, unsigned argc, Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
   Rooted<ReadableStream*> unwrappedStream(
       cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "tee"));
   if (!unwrappedStream) {
@@ -790,34 +803,43 @@ static bool ReadableStream_tee(JSContext
   branches->setDenseInitializedLength(2);
   branches->initDenseElement(0, ObjectValue(*branch1));
   branches->initDenseElement(1, ObjectValue(*branch2));
 
   args.rval().setObject(*branches);
   return true;
 }
 
+// Streams spec, 3.2.5.8.
+//      [@@asyncIterator]({ preventCancel } = {})
+//
+// Not implemented.
+
 static const JSFunctionSpec ReadableStream_methods[] = {
     JS_FN("cancel", ReadableStream_cancel, 1, 0),
     JS_FN("getReader", ReadableStream_getReader, 0, 0),
     JS_FN("tee", ReadableStream_tee, 0, 0), JS_FS_END};
 
 static const JSPropertySpec ReadableStream_properties[] = {
     JS_PSG("locked", ReadableStream_locked, 0), JS_PS_END};
 
 CLASS_SPEC(ReadableStream, 0, SlotCount, 0,
            JSCLASS_PRIVATE_IS_NSISUPPORTS | JSCLASS_HAS_PRIVATE,
            JS_NULL_CLASS_OPS);
 
-/*** 3.3. General readable stream abstract operations ***********************/
-
-// Streams spec, 3.3.1. AcquireReadableStreamBYOBReader ( stream )
+/*** 3.3. ReadableStreamAsyncIteratorPrototype ******************************/
+
+// Not implemented.
+
+/*** 3.4. General readable stream abstract operations ***********************/
+
+// Streams spec, 3.4.1. AcquireReadableStreamBYOBReader ( stream )
 // Always inlined.
 
-// Streams spec, 3.3.2. AcquireReadableStreamDefaultReader ( stream )
+// Streams spec, 3.4.2. AcquireReadableStreamDefaultReader ( stream )
 // Always inlined. See CreateReadableStreamDefaultReader.
 
 /**
  * Characterizes the family of algorithms, (startAlgorithm, pullAlgorithm,
  * cancelAlgorithm), associated with a readable stream.
  *
  * See the comment on SetUpReadableStreamDefaultController().
  */
@@ -827,17 +849,17 @@ enum class SourceAlgorithms {
 };
 
 static MOZ_MUST_USE bool SetUpReadableStreamDefaultController(
     JSContext* cx, Handle<ReadableStream*> stream, SourceAlgorithms algorithms,
     HandleValue underlyingSource, HandleValue pullMethod,
     HandleValue cancelMethod, double highWaterMark, HandleValue size);
 
 /**
- * Streams spec, 3.3.3. CreateReadableStream (
+ * Streams spec, 3.4.3. CreateReadableStream (
  *                          startAlgorithm, pullAlgorithm, cancelAlgorithm
  *                          [, highWaterMark [, sizeAlgorithm ] ] )
  *
  * The start/pull/cancelAlgorithm arguments are represented instead as four
  * arguments: sourceAlgorithms, underlyingSource, pullMethod, cancelMethod.
  * See the comment on SetUpReadableStreamDefaultController.
  */
 MOZ_MUST_USE ReadableStream* CreateReadableStream(
@@ -874,23 +896,23 @@ MOZ_MUST_USE ReadableStream* CreateReada
           cancelMethod, highWaterMark, sizeAlgorithm)) {
     return nullptr;
   }
 
   // Step 8: Return stream.
   return stream;
 }
 
-// Streams spec, 3.3.4. CreateReadableByteStream (
+// Streams spec, 3.4.4. CreateReadableByteStream (
 //                          startAlgorithm, pullAlgorithm, cancelAlgorithm
 //                          [, highWaterMark [, autoAllocateChunkSize ] ] )
 // Not implemented.
 
 /**
- * Streams spec, 3.3.5. InitializeReadableStream ( stream )
+ * Streams spec, 3.4.5. InitializeReadableStream ( stream )
  */
 MOZ_MUST_USE /* static */
     ReadableStream*
     ReadableStream::create(
         JSContext* cx, void* nsISupportsObject_alreadyAddreffed /* = nullptr */,
         HandleObject proto /* = nullptr */) {
   // In the spec, InitializeReadableStream is always passed a newly created
   // ReadableStream object. We instead create it here and return it below.
@@ -912,57 +934,68 @@ MOZ_MUST_USE /* static */
   MOZ_ASSERT(stream->storedError().isUndefined());
 
   // Step 3: Set stream.[[disturbed]] to false (done in step 1).
   MOZ_ASSERT(!stream->disturbed());
 
   return stream;
 }
 
-// Streams spec, 3.3.6. IsReadableStream ( x )
+// Streams spec, 3.4.6. IsReadableStream ( x )
 // Using UnwrapAndTypeCheck templates instead.
 
-// Streams spec, 3.3.7. IsReadableStreamDisturbed ( stream )
+// Streams spec, 3.4.7. IsReadableStreamDisturbed ( stream )
 // Using stream->disturbed() instead.
 
 /**
- * Streams spec, 3.3.8. IsReadableStreamLocked ( stream )
+ * Streams spec, 3.4.8. IsReadableStreamLocked ( stream )
  */
 bool ReadableStream::locked() const {
   // Step 1: Assert: ! IsReadableStream(stream) is true (implicit).
   // Step 2: If stream.[[reader]] is undefined, return false.
   // Step 3: Return true.
   // Special-casing for streams with external sources. Those can be locked
   // explicitly via JSAPI, which is indicated by a controller flag.
   // IsReadableStreamLocked is called from the controller's constructor, at
   // which point we can't yet call stream->controller(), but the source also
   // can't be locked yet.
   if (hasController() && controller()->sourceLocked()) {
     return true;
   }
   return hasReader();
 }
 
+// Streams spec, 3.4.9. IsReadableStreamAsyncIterator ( x )
+//
+// Not implemented.
+
 static MOZ_MUST_USE bool ReadableStreamDefaultControllerClose(
     JSContext* cx,
     Handle<ReadableStreamDefaultController*> unwrappedController);
 
 static MOZ_MUST_USE bool ReadableStreamDefaultControllerEnqueue(
     JSContext* cx, Handle<ReadableStreamDefaultController*> unwrappedController,
     HandleValue chunk);
 
 /**
- * Streams spec, 3.3.9. ReadableStreamTee steps 12.a.i-ix.
+ * Streams spec, 3.4.10. ReadableStreamTee steps 12.c.i-ix.
+ *
+ * BEWARE: This algorithm isn't up-to-date with the current specification.
  */
 static bool TeeReaderReadHandler(JSContext* cx, unsigned argc, Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
   Rooted<TeeState*> unwrappedTeeState(cx,
                                       UnwrapCalleeSlot<TeeState>(cx, args, 0));
   HandleValue resultVal = args.get(0);
 
+  // XXX The step numbers and algorithm below are inconsistent with the current
+  //     spec!  (For one example -- there may be others -- the current spec gets
+  //     the "done" property before it gets the "value" property.)  This code
+  //     really needs an audit for spec-correctness.  See bug 1570398.
+
   // Step i: Assert: Type(result) is Object.
   RootedObject result(cx, &resultVal.toObject());
 
   // Step ii: Let value be ? Get(result, "value").
   // (This can fail only if `result` was nuked.)
   RootedValue value(cx);
   if (!GetProperty(cx, result, result, cx->names().value, &value)) {
     return false;
@@ -1049,17 +1082,17 @@ static bool TeeReaderReadHandler(JSConte
   args.rval().setUndefined();
   return true;
 }
 
 static MOZ_MUST_USE JSObject* ReadableStreamDefaultReaderRead(
     JSContext* cx, Handle<ReadableStreamDefaultReader*> unwrappedReader);
 
 /**
- * Streams spec, 3.3.9. ReadableStreamTee step 12, "Let pullAlgorithm be the
+ * Streams spec, 3.4.10. ReadableStreamTee step 12, "Let pullAlgorithm be the
  * following steps:"
  */
 static MOZ_MUST_USE JSObject* ReadableStreamTee_Pull(
     JSContext* cx, Handle<TeeState*> unwrappedTeeState) {
   // Implicit in the spec: Unpack the closed-over variables `stream` and
   // `reader` from the TeeState.
   Rooted<ReadableStream*> unwrappedStream(
       cx, UnwrapInternalSlot<ReadableStream>(cx, unwrappedTeeState,
@@ -1096,17 +1129,17 @@ static MOZ_MUST_USE JSObject* ReadableSt
   }
 
   return JS::CallOriginalPromiseThen(cx, readPromise, onFulfilled, nullptr);
 }
 
 /**
  * Cancel one branch of a tee'd stream with the given |reason_|.
  *
- * Streams spec, 3.3.9. ReadableStreamTee steps 13 and 14: "Let
+ * Streams spec, 3.4.10. ReadableStreamTee steps 13 and 14: "Let
  * cancel1Algorithm/cancel2Algorithm be the following steps, taking a reason
  * argument:"
  */
 static MOZ_MUST_USE JSObject* ReadableStreamTee_Cancel(
     JSContext* cx, Handle<TeeState*> unwrappedTeeState,
     Handle<ReadableStreamDefaultController*> unwrappedBranch,
     HandleValue reason) {
   Rooted<ReadableStream*> unwrappedStream(
@@ -1195,17 +1228,17 @@ static MOZ_MUST_USE JSObject* ReadableSt
   return cancelPromise;
 }
 
 static MOZ_MUST_USE bool ReadableStreamControllerError(
     JSContext* cx, Handle<ReadableStreamController*> unwrappedController,
     HandleValue e);
 
 /**
- * Streams spec, 3.3.9. step 18:
+ * Streams spec, 3.4.10. step 18:
  * Upon rejection of reader.[[closedPromise]] with reason r,
  */
 static bool TeeReaderErroredHandler(JSContext* cx, unsigned argc, Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
   Rooted<TeeState*> teeState(cx, TargetFromHandler<TeeState>(args));
   HandleValue reason = args.get(0);
 
   // Step a: If closedOrErrored is false, then:
@@ -1232,17 +1265,17 @@ static bool TeeReaderErroredHandler(JSCo
     }
   }
 
   args.rval().setUndefined();
   return true;
 }
 
 /**
- * Streams spec, 3.3.9. ReadableStreamTee ( stream, cloneForBranch2 )
+ * Streams spec, 3.4.10. ReadableStreamTee ( stream, cloneForBranch2 )
  */
 static MOZ_MUST_USE bool ReadableStreamTee(
     JSContext* cx, Handle<ReadableStream*> unwrappedStream,
     bool cloneForBranch2, MutableHandle<ReadableStream*> branch1Stream,
     MutableHandle<ReadableStream*> branch2Stream) {
   // Step 1: Assert: ! IsReadableStream(stream) is true (implicit).
   // Step 2: Assert: Type(cloneForBranch2) is Boolean (implicit).
 
@@ -1323,26 +1356,32 @@ static MOZ_MUST_USE bool ReadableStreamT
   if (!JS::AddPromiseReactions(cx, closedPromise, nullptr, onRejected)) {
     return false;
   }
 
   // Step 19: Return « branch1, branch2 ».
   return true;
 }
 
-/*** 3.4. The interface between readable streams and controllers ************/
+// Streams spec, 3.4.11.
+//      ReadableStreamPipeTo ( source, dest, preventClose, preventAbort,
+//                             preventCancel, signal )
+//
+// Not implemented.
+
+/*** 3.5. The interface between readable streams and controllers ************/
 
 inline static MOZ_MUST_USE bool AppendToListAtSlot(
     JSContext* cx, HandleNativeObject unwrappedContainer, uint32_t slot,
     HandleObject obj);
 
 /**
- * Streams spec, 3.4.1.
+ * Streams spec, 3.5.1.
  *      ReadableStreamAddReadIntoRequest ( stream, forAuthorCode )
- * Streams spec, 3.4.2.
+ * Streams spec, 3.5.2.
  *      ReadableStreamAddReadRequest ( stream, forAuthorCode )
  *
  * Our implementation does not pass around forAuthorCode parameters in the same
  * places as the standard, but the effect is the same. See the comment on
  * `ReadableStreamReader::forAuthorCode()`.
  */
 static MOZ_MUST_USE JSObject* ReadableStreamAddReadOrReadIntoRequest(
     JSContext* cx, Handle<ReadableStream*> unwrappedStream) {
@@ -1351,18 +1390,18 @@ static MOZ_MUST_USE JSObject* ReadableSt
   // (Only default readers exist so far.)
   Rooted<ReadableStreamReader*> unwrappedReader(
       cx, UnwrapReaderFromStream(cx, unwrappedStream));
   if (!unwrappedReader) {
     return nullptr;
   }
   MOZ_ASSERT(unwrappedReader->is<ReadableStreamDefaultReader>());
 
-  // Step 2 of 3.4.1: Assert: stream.[[state]] is "readable" or "closed".
-  // Step 2 of 3.4.2: Assert: stream.[[state]] is "readable".
+  // Step 2 of 3.5.1: Assert: stream.[[state]] is "readable" or "closed".
+  // Step 2 of 3.5.2: Assert: stream.[[state]] is "readable".
   MOZ_ASSERT(unwrappedStream->readable() || unwrappedStream->closed());
   MOZ_ASSERT_IF(unwrappedReader->is<ReadableStreamDefaultReader>(),
                 unwrappedStream->readable());
 
   // Step 3: Let promise be a new promise.
   RootedObject promise(cx, PromiseObject::createSkippingExecutor(cx));
   if (!promise) {
     return nullptr;
@@ -1396,17 +1435,17 @@ static bool ReturnUndefined(JSContext* c
   args.rval().setUndefined();
   return true;
 }
 
 MOZ_MUST_USE bool ReadableStreamCloseInternal(
     JSContext* cx, Handle<ReadableStream*> unwrappedStream);
 
 /**
- * Streams spec, 3.4.3. ReadableStreamCancel ( stream, reason )
+ * Streams spec, 3.5.3. ReadableStreamCancel ( stream, reason )
  */
 static MOZ_MUST_USE JSObject* ReadableStreamCancel(
     JSContext* cx, Handle<ReadableStream*> unwrappedStream,
     HandleValue reason) {
   AssertSameCompartment(cx, reason);
 
   // Step 1: Set stream.[[disturbed]] to true.
   unwrappedStream->setDisturbed();
@@ -1455,17 +1494,17 @@ static MOZ_MUST_USE JSObject* ReadableSt
                                      nullptr);
 }
 
 static MOZ_MUST_USE JSObject* ReadableStreamCreateReadResult(
     JSContext* cx, HandleValue value, bool done,
     ForAuthorCodeBool forAuthorCode);
 
 /**
- * Streams spec, 3.4.4. ReadableStreamClose ( stream )
+ * Streams spec, 3.5.4. ReadableStreamClose ( stream )
  */
 MOZ_MUST_USE bool ReadableStreamCloseInternal(
     JSContext* cx, Handle<ReadableStream*> unwrappedStream) {
   // Step 1: Assert: stream.[[state]] is "readable".
   MOZ_ASSERT(unwrappedStream->readable());
 
   // Step 2: Set stream.[[state]] to "closed".
   unwrappedStream->setClosed();
@@ -1533,18 +1572,18 @@ MOZ_MUST_USE bool ReadableStreamCloseInt
         unwrappedStream->controller()->externalSource();
     source->onClosed(cx, unwrappedStream);
   }
 
   return true;
 }
 
 /**
- * Streams spec, 3.4.5. ReadableStreamCreateReadResult ( value, done,
- * forAuthorCode )
+ * Streams spec, 3.5.5. ReadableStreamCreateReadResult ( value, done,
+ *                                                       forAuthorCode )
  */
 static MOZ_MUST_USE JSObject* ReadableStreamCreateReadResult(
     JSContext* cx, HandleValue value, bool done,
     ForAuthorCodeBool forAuthorCode) {
   // Step 1: Let prototype be null.
   // Step 2: If forAuthorCode is true, set prototype to %ObjectPrototype%.
   RootedObject templateObject(
       cx,
@@ -1570,17 +1609,17 @@ static MOZ_MUST_USE JSObject* ReadableSt
   obj->setSlot(Realm::IterResultObjectDoneSlot,
                done ? TrueHandleValue : FalseHandleValue);
 
   // Step 7: Return obj.
   return obj;
 }
 
 /**
- * Streams spec, 3.4.6. ReadableStreamError ( stream, e )
+ * Streams spec, 3.5.6. ReadableStreamError ( stream, e )
  */
 MOZ_MUST_USE bool ReadableStreamErrorInternal(
     JSContext* cx, Handle<ReadableStream*> unwrappedStream, HandleValue e) {
   // Step 1: Assert: ! IsReadableStream(stream) is true (implicit).
 
   // Step 2: Assert: stream.[[state]] is "readable".
   MOZ_ASSERT(unwrappedStream->readable());
 
@@ -1665,19 +1704,19 @@ MOZ_MUST_USE bool ReadableStreamErrorInt
     }
     source->onErrored(cx, unwrappedStream, error);
   }
 
   return true;
 }
 
 /**
- * Streams spec, 3.4.7.
+ * Streams spec, 3.5.7.
  *      ReadableStreamFulfillReadIntoRequest( stream, chunk, done )
- * Streams spec, 3.4.8.
+ * Streams spec, 3.5.8.
  *      ReadableStreamFulfillReadRequest ( stream, chunk, done )
  * These two spec functions are identical in our implementation.
  */
 static MOZ_MUST_USE bool ReadableStreamFulfillReadOrReadIntoRequest(
     JSContext* cx, Handle<ReadableStream*> unwrappedStream, HandleValue chunk,
     bool done) {
   cx->check(chunk);
 
@@ -1711,18 +1750,18 @@ static MOZ_MUST_USE bool ReadableStreamF
   if (!iterResult) {
     return false;
   }
   RootedValue val(cx, ObjectValue(*iterResult));
   return ResolvePromise(cx, readIntoRequest, val);
 }
 
 /**
- * Streams spec, 3.4.9. ReadableStreamGetNumReadIntoRequests ( stream )
- * Streams spec, 3.4.10. ReadableStreamGetNumReadRequests ( stream )
+ * Streams spec, 3.5.9. ReadableStreamGetNumReadIntoRequests ( stream )
+ * Streams spec, 3.5.10. ReadableStreamGetNumReadRequests ( stream )
  * (Identical implementation.)
  */
 static uint32_t ReadableStreamGetNumReadRequests(ReadableStream* stream) {
   // Step 1: Return the number of elements in
   //         stream.[[reader]].[[read{Into}Requests]].
   if (!stream->hasReader()) {
     return 0;
   }
@@ -1733,18 +1772,22 @@ static uint32_t ReadableStreamGetNumRead
   // Reader is a dead wrapper, treat it as non-existent.
   if (!reader) {
     return 0;
   }
 
   return reader->requests()->length();
 }
 
+// Streams spec, 3.5.11. ReadableStreamHasBYOBReader ( stream )
+//
+// Not implemented.
+
 /**
- * Streams spec 3.4.12. ReadableStreamHasDefaultReader ( stream )
+ * Streams spec 3.5.12. ReadableStreamHasDefaultReader ( stream )
  */
 static MOZ_MUST_USE bool ReadableStreamHasDefaultReader(
     JSContext* cx, Handle<ReadableStream*> unwrappedStream, bool* result) {
   // Step 1: Let reader be stream.[[reader]].
   // Step 2: If reader is undefined, return false.
   if (!unwrappedStream->hasReader()) {
     *result = false;
     return true;
@@ -1756,24 +1799,24 @@ static MOZ_MUST_USE bool ReadableStreamH
   }
 
   // Step 3: If ! ReadableStreamDefaultReader(reader) is false, return false.
   // Step 4: Return true.
   *result = unwrappedReader->is<ReadableStreamDefaultReader>();
   return true;
 }
 
-/*** 3.5. Class ReadableStreamDefaultReader *********************************/
+/*** 3.6. Class ReadableStreamDefaultReader *********************************/
 
 static MOZ_MUST_USE bool ReadableStreamReaderGenericInitialize(
     JSContext* cx, Handle<ReadableStreamReader*> reader,
     Handle<ReadableStream*> unwrappedStream, ForAuthorCodeBool forAuthorCode);
 
 /**
- * Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream )
+ * Stream spec, 3.6.3. new ReadableStreamDefaultReader ( stream )
  * Steps 2-4.
  */
 static MOZ_MUST_USE ReadableStreamDefaultReader*
 CreateReadableStreamDefaultReader(JSContext* cx,
                                   Handle<ReadableStream*> unwrappedStream,
                                   ForAuthorCodeBool forAuthorCode,
                                   HandleObject proto /* = nullptr */) {
   Rooted<ReadableStreamDefaultReader*> reader(
@@ -1796,17 +1839,17 @@ CreateReadableStreamDefaultReader(JSCont
                                              forAuthorCode)) {
     return nullptr;
   }
 
   return reader;
 }
 
 /**
- * Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream )
+ * Stream spec, 3.6.3. new ReadableStreamDefaultReader ( stream )
  */
 bool ReadableStreamDefaultReader::constructor(JSContext* cx, unsigned argc,
                                               Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   if (!ThrowIfNotConstructing(cx, args, "ReadableStreamDefaultReader")) {
     return false;
   }
@@ -1833,17 +1876,17 @@ bool ReadableStreamDefaultReader::constr
     return false;
   }
 
   args.rval().setObject(*reader);
   return true;
 }
 
 /**
- * Streams spec, 3.5.4.1 get closed
+ * Streams spec, 3.6.4.1 get closed
  */
 static MOZ_MUST_USE bool ReadableStreamDefaultReader_closed(JSContext* cx,
                                                             unsigned argc,
                                                             Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise
   //         rejected with a TypeError exception.
@@ -1864,17 +1907,17 @@ static MOZ_MUST_USE bool ReadableStreamD
   return true;
 }
 
 static MOZ_MUST_USE JSObject* ReadableStreamReaderGenericCancel(
     JSContext* cx, Handle<ReadableStreamReader*> unwrappedReader,
     HandleValue reason);
 
 /**
- * Streams spec, 3.5.4.2. cancel ( reason )
+ * Streams spec, 3.6.4.2. cancel ( reason )
  */
 static MOZ_MUST_USE bool ReadableStreamDefaultReader_cancel(JSContext* cx,
                                                             unsigned argc,
                                                             Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise
   //         rejected with a TypeError exception.
@@ -1899,17 +1942,17 @@ static MOZ_MUST_USE bool ReadableStreamD
   if (!cancelPromise) {
     return false;
   }
   args.rval().setObject(*cancelPromise);
   return true;
 }
 
 /**
- * Streams spec, 3.5.4.3 read ( )
+ * Streams spec, 3.6.4.3 read ( )
  */
 static MOZ_MUST_USE bool ReadableStreamDefaultReader_read(JSContext* cx,
                                                           unsigned argc,
                                                           Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise
   //         rejected with a TypeError exception.
@@ -1937,17 +1980,17 @@ static MOZ_MUST_USE bool ReadableStreamD
   args.rval().setObject(*readPromise);
   return true;
 }
 
 static MOZ_MUST_USE bool ReadableStreamReaderGenericRelease(
     JSContext* cx, Handle<ReadableStreamReader*> reader);
 
 /**
- * Streams spec, 3.5.4.4. releaseLock ( )
+ * Streams spec, 3.6.4.4. releaseLock ( )
  */
 static bool ReadableStreamDefaultReader_releaseLock(JSContext* cx,
                                                     unsigned argc, Value* vp) {
   // Step 1: If ! IsReadableStreamDefaultReader(this) is false,
   //         throw a TypeError exception.
   CallArgs args = CallArgsFromVp(argc, vp);
   Rooted<ReadableStreamDefaultReader*> reader(
       cx, UnwrapAndTypeCheckThis<ReadableStreamDefaultReader>(cx, args,
@@ -1992,26 +2035,30 @@ static const JSFunctionSpec ReadableStre
 static const JSPropertySpec ReadableStreamDefaultReader_properties[] = {
     JS_PSG("closed", ReadableStreamDefaultReader_closed, 0), JS_PS_END};
 
 const Class ReadableStreamReader::class_ = {"ReadableStreamReader"};
 
 CLASS_SPEC(ReadableStreamDefaultReader, 1, SlotCount,
            ClassSpec::DontDefineConstructor, 0, JS_NULL_CLASS_OPS);
 
-/*** 3.7. Readable stream reader abstract operations ************************/
-
-// Streams spec, 3.7.1. IsReadableStreamDefaultReader ( x )
+/*** 3.7. Class ReadableStreamBYOBReader ************************************/
+
+// Not implemented.
+
+/*** 3.8. Readable stream reader abstract operations ************************/
+
+// Streams spec, 3.8.1. IsReadableStreamDefaultReader ( x )
 // Implemented via is<ReadableStreamDefaultReader>()
 
-// Streams spec, 3.7.2. IsReadableStreamBYOBReader ( x )
+// Streams spec, 3.8.2. IsReadableStreamBYOBReader ( x )
 // Implemented via is<ReadableStreamBYOBReader>()
 
 /**
- * Streams spec, 3.7.3. ReadableStreamReaderGenericCancel ( reader, reason )
+ * Streams spec, 3.8.3. ReadableStreamReaderGenericCancel ( reader, reason )
  */
 static MOZ_MUST_USE JSObject* ReadableStreamReaderGenericCancel(
     JSContext* cx, Handle<ReadableStreamReader*> unwrappedReader,
     HandleValue reason) {
   // Step 1: Let stream be reader.[[ownerReadableStream]].
   // Step 2: Assert: stream is not undefined (implicit).
   Rooted<ReadableStream*> unwrappedStream(
       cx, UnwrapStreamFromReader(cx, unwrappedReader));
@@ -2019,17 +2066,17 @@ static MOZ_MUST_USE JSObject* ReadableSt
     return nullptr;
   }
 
   // Step 3: Return ! ReadableStreamCancel(stream, reason).
   return ::ReadableStreamCancel(cx, unwrappedStream, reason);
 }
 
 /**
- * Streams spec, 3.7.4.
+ * Streams spec, 3.8.4.
  *      ReadableStreamReaderGenericInitialize ( reader, stream )
  */
 static MOZ_MUST_USE bool ReadableStreamReaderGenericInitialize(
     JSContext* cx, Handle<ReadableStreamReader*> reader,
     Handle<ReadableStream*> unwrappedStream, ForAuthorCodeBool forAuthorCode) {
   cx->check(reader);
 
   // Step 1: Set reader.[[ownerReadableStream]] to stream.
@@ -2079,18 +2126,18 @@ static MOZ_MUST_USE bool ReadableStreamR
   }
 
   reader->setClosedPromise(promise);
 
   // Extra step not in the standard. See the comment on
   // `ReadableStreamReader::forAuthorCode()`.
   reader->setForAuthorCode(forAuthorCode);
 
-  // Step 4 of caller 3.5.3. new ReadableStreamDefaultReader(stream):
-  // Step 5 of caller 3.6.3. new ReadableStreamBYOBReader(stream):
+  // Step 4 of caller 3.6.3. new ReadableStreamDefaultReader(stream):
+  // Step 5 of caller 3.7.3. new ReadableStreamBYOBReader(stream):
   //     Set this.[[read{Into}Requests]] to a new empty List.
   if (!SetNewList(cx, reader, ReadableStreamReader::Slot_Requests)) {
     return false;
   }
 
   // Step 2: Set stream.[[reader]] to reader.
   // Doing this last prevents a partially-initialized reader from being
   // attached to the stream (and possibly left there on OOM).
@@ -2102,17 +2149,17 @@ static MOZ_MUST_USE bool ReadableStreamR
     }
     unwrappedStream->setReader(streamCompartmentReader);
   }
 
   return true;
 }
 
 /**
- * Streams spec, 3.7.5. ReadableStreamReaderGenericRelease ( reader )
+ * Streams spec, 3.8.5. ReadableStreamReaderGenericRelease ( reader )
  */
 static MOZ_MUST_USE bool ReadableStreamReaderGenericRelease(
     JSContext* cx, Handle<ReadableStreamReader*> unwrappedReader) {
   // Step 1: Assert: reader.[[ownerReadableStream]] is not undefined.
   Rooted<ReadableStream*> unwrappedStream(
       cx, UnwrapStreamFromReader(cx, unwrappedReader));
   if (!unwrappedStream) {
     return false;
@@ -2182,17 +2229,17 @@ static MOZ_MUST_USE bool ReadableStreamR
 
   return true;
 }
 
 static MOZ_MUST_USE JSObject* ReadableStreamControllerPullSteps(
     JSContext* cx, Handle<ReadableStreamController*> unwrappedController);
 
 /**
- * Streams spec, 3.7.7.
+ * Streams spec, 3.8.7.
  *      ReadableStreamDefaultReaderRead ( reader [, forAuthorCode ] )
  */
 static MOZ_MUST_USE JSObject* ReadableStreamDefaultReaderRead(
     JSContext* cx, Handle<ReadableStreamDefaultReader*> unwrappedReader) {
   // Step 1: If forAuthorCode was not passed, set it to false (implicit).
 
   // Step 2: Let stream be reader.[[ownerReadableStream]].
   // Step 3: Assert: stream is not undefined.
@@ -2232,25 +2279,25 @@ static MOZ_MUST_USE JSObject* ReadableSt
   MOZ_ASSERT(unwrappedStream->readable());
 
   // Step 8: Return ! stream.[[readableStreamController]].[[PullSteps]]().
   Rooted<ReadableStreamController*> unwrappedController(
       cx, unwrappedStream->controller());
   return ReadableStreamControllerPullSteps(cx, unwrappedController);
 }
 
-/*** 3.8. Class ReadableStreamDefaultController *****************************/
+/*** 3.9. Class ReadableStreamDefaultController *****************************/
 
 inline static MOZ_MUST_USE bool ReadableStreamControllerCallPullIfNeeded(
     JSContext* cx, Handle<ReadableStreamController*> unwrappedController);
 
 /**
- * Streams spec, 3.9.11. SetUpReadableStreamDefaultController, step 11
+ * Streams spec, 3.10.11. SetUpReadableStreamDefaultController, step 11
  * and
- * Streams spec, 3.12.26. SetUpReadableByteStreamController, step 16:
+ * Streams spec, 3.13.26. SetUpReadableByteStreamController, step 16:
  *      Upon fulfillment of startPromise, [...]
  */
 static bool ControllerStartHandler(JSContext* cx, unsigned argc, Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
   Rooted<ReadableStreamController*> controller(
       cx, TargetFromHandler<ReadableStreamController>(args));
 
   // Step a: Set controller.[[started]] to true.
@@ -2268,19 +2315,19 @@ static bool ControllerStartHandler(JSCon
   if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) {
     return false;
   }
   args.rval().setUndefined();
   return true;
 }
 
 /**
- * Streams spec, 3.9.11. SetUpReadableStreamDefaultController, step 12
+ * Streams spec, 3.10.11. SetUpReadableStreamDefaultController, step 12
  * and
- * Streams spec, 3.12.26. SetUpReadableByteStreamController, step 17:
+ * Streams spec, 3.13.26. SetUpReadableByteStreamController, step 17:
  *      Upon rejection of startPromise with reason r, [...]
  */
 static bool ControllerStartFailedHandler(JSContext* cx, unsigned argc,
                                          Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
   Rooted<ReadableStreamController*> controller(
       cx, TargetFromHandler<ReadableStreamController>(args));
 
@@ -2291,115 +2338,115 @@ static bool ControllerStartFailedHandler
     return false;
   }
 
   args.rval().setUndefined();
   return true;
 }
 
 /**
- * Streams spec, 3.8.3.
+ * Streams spec, 3.9.3.
  * new ReadableStreamDefaultController( stream, underlyingSource, size,
  *                                      highWaterMark )
  */
 bool ReadableStreamDefaultController::constructor(JSContext* cx, unsigned argc,
                                                   Value* vp) {
   // Step 1: Throw a TypeError.
   JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                             JSMSG_BOGUS_CONSTRUCTOR,
                             "ReadableStreamDefaultController");
   return false;
 }
 
 static MOZ_MUST_USE double ReadableStreamControllerGetDesiredSizeUnchecked(
     ReadableStreamController* controller);
 
 /**
- * Streams spec, 3.8.4.1. get desiredSize
+ * Streams spec, 3.9.4.1. get desiredSize
  */
 static bool ReadableStreamDefaultController_desiredSize(JSContext* cx,
                                                         unsigned argc,
                                                         Value* vp) {
   // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a
   //         TypeError exception.
   CallArgs args = CallArgsFromVp(argc, vp);
   Rooted<ReadableStreamController*> unwrappedController(
       cx, UnwrapAndTypeCheckThis<ReadableStreamDefaultController>(
               cx, args, "get desiredSize"));
   if (!unwrappedController) {
     return false;
   }
 
-  // 3.9.8. ReadableStreamDefaultControllerGetDesiredSize, steps 1-4.
-  // 3.9.8. Step 1: Let stream be controller.[[controlledReadableStream]].
+  // 3.10.8. ReadableStreamDefaultControllerGetDesiredSize, steps 1-4.
+  // 3.10.8. Step 1: Let stream be controller.[[controlledReadableStream]].
   ReadableStream* unwrappedStream = unwrappedController->stream();
 
-  // 3.9.8. Step 2: Let state be stream.[[state]].
-  // 3.9.8. Step 3: If state is "errored", return null.
+  // 3.10.8. Step 2: Let state be stream.[[state]].
+  // 3.10.8. Step 3: If state is "errored", return null.
   if (unwrappedStream->errored()) {
     args.rval().setNull();
     return true;
   }
 
-  // 3.9.8. Step 4: If state is "closed", return 0.
+  // 3.10.8. Step 4: If state is "closed", return 0.
   if (unwrappedStream->closed()) {
     args.rval().setInt32(0);
     return true;
   }
 
   // Step 2: Return ! ReadableStreamDefaultControllerGetDesiredSize(this).
   args.rval().setNumber(
       ReadableStreamControllerGetDesiredSizeUnchecked(unwrappedController));
   return true;
 }
 
 static MOZ_MUST_USE bool ReadableStreamDefaultControllerClose(
     JSContext* cx,
     Handle<ReadableStreamDefaultController*> unwrappedController);
 
 /**
- * Unified implementation of step 2 of 3.8.4.2 and 3.8.4.3,
- * and steps 2-3 of 3.10.4.3.
+ * Unified implementation of step 2 of 3.9.4.2 and 3.9.4.3,
+ * and steps 2-3 of 3.11.4.3.
  */
 static MOZ_MUST_USE bool CheckReadableStreamControllerCanCloseOrEnqueue(
     JSContext* cx, Handle<ReadableStreamController*> unwrappedController,
     const char* action) {
-  // 3.8.4.2. close(), step 2, and
-  // 3.8.4.3. enqueue(chunk), step 2:
+  // 3.9.4.2. close(), step 2, and
+  // 3.9.4.3. enqueue(chunk), step 2:
   //      If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(this) is false,
   //      throw a TypeError exception.
   // RSDCCanCloseOrEnqueue returns false in two cases: (1)
   // controller.[[closeRequested]] is true; (2) the stream is not readable,
   // i.e. already closed or errored. This amounts to exactly the same thing as
-  // 3.10.4.3 steps 2-3 below, and we want different error messages for the two
+  // 3.11.4.3 steps 2-3 below, and we want different error messages for the two
   // cases anyway.
 
-  // 3.10.4.3. Step 2: If this.[[closeRequested]] is true, throw a TypeError
+  // 3.11.4.3. Step 2: If this.[[closeRequested]] is true, throw a TypeError
   //                   exception.
   if (unwrappedController->closeRequested()) {
     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                               JSMSG_READABLESTREAMCONTROLLER_CLOSED, action);
     return false;
   }
 
-  // 3.10.4.3. Step 3: If this.[[controlledReadableByteStream]].[[state]] is
+  // 3.11.4.3. Step 3: If this.[[controlledReadableByteStream]].[[state]] is
   //                   not "readable", throw a TypeError exception.
   ReadableStream* unwrappedStream = unwrappedController->stream();
   if (!unwrappedStream->readable()) {
     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                               JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE,
                               action);
     return false;
   }
 
   return true;
 }
 
 /**
- * Streams spec, 3.8.4.2 close()
+ * Streams spec, 3.9.4.2 close()
  */
 static bool ReadableStreamDefaultController_close(JSContext* cx, unsigned argc,
                                                   Value* vp) {
   // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a
   //         TypeError exception.
   CallArgs args = CallArgsFromVp(argc, vp);
   Rooted<ReadableStreamDefaultController*> unwrappedController(
       cx, UnwrapAndTypeCheckThis<ReadableStreamDefaultController>(cx, args,
@@ -2420,17 +2467,17 @@ static bool ReadableStreamDefaultControl
     return false;
   }
 
   args.rval().setUndefined();
   return true;
 }
 
 /**
- * Streams spec, 3.8.4.3. enqueue ( chunk )
+ * Streams spec, 3.9.4.3. enqueue ( chunk )
  */
 static bool ReadableStreamDefaultController_enqueue(JSContext* cx,
                                                     unsigned argc, Value* vp) {
   // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a
   //         TypeError exception.
   CallArgs args = CallArgsFromVp(argc, vp);
   Rooted<ReadableStreamDefaultController*> unwrappedController(
       cx, UnwrapAndTypeCheckThis<ReadableStreamDefaultController>(cx, args,
@@ -2451,17 +2498,17 @@ static bool ReadableStreamDefaultControl
                                               args.get(0))) {
     return false;
   }
   args.rval().setUndefined();
   return true;
 }
 
 /**
- * Streams spec, 3.8.4.4. error ( e )
+ * Streams spec, 3.9.4.4. error ( e )
  */
 static bool ReadableStreamDefaultController_error(JSContext* cx, unsigned argc,
                                                   Value* vp) {
   // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a
   //         TypeError exception.
   CallArgs args = CallArgsFromVp(argc, vp);
   Rooted<ReadableStreamDefaultController*> unwrappedController(
       cx, UnwrapAndTypeCheckThis<ReadableStreamDefaultController>(cx, args,
@@ -2495,26 +2542,26 @@ static MOZ_MUST_USE JSObject* PromiseCal
                                           HandleValue V, HandleValue arg);
 
 static void ReadableStreamControllerClearAlgorithms(
     Handle<ReadableStreamController*> controller);
 
 /**
  * Unified implementation of ReadableStream controllers' [[CancelSteps]]
  * internal methods.
- * Streams spec, 3.8.5.1. [[CancelSteps]] ( reason )
+ * Streams spec, 3.9.5.1. [[CancelSteps]] ( reason )
  * and
- * Streams spec, 3.10.5.1. [[CancelSteps]] ( reason )
+ * Streams spec, 3.11.5.1. [[CancelSteps]] ( reason )
  */
 static MOZ_MUST_USE JSObject* ReadableStreamControllerCancelSteps(
     JSContext* cx, Handle<ReadableStreamController*> unwrappedController,
     HandleValue reason) {
   AssertSameCompartment(cx, reason);
 
-  // Step 1 of 3.10.5.1: If this.[[pendingPullIntos]] is not empty,
+  // Step 1 of 3.11.5.1: If this.[[pendingPullIntos]] is not empty,
   if (!unwrappedController->is<ReadableStreamDefaultController>()) {
     Rooted<ListObject*> unwrappedPendingPullIntos(
         cx, unwrappedController->as<ReadableByteStreamController>()
                 .pendingPullIntos());
 
     if (unwrappedPendingPullIntos->length() != 0) {
       // Step a: Let firstDescriptor be the first element of
       //         this.[[pendingPullIntos]].
@@ -2528,22 +2575,22 @@ static MOZ_MUST_USE JSObject* ReadableSt
       // Step b: Set firstDescriptor.[[bytesFilled]] to 0.
       unwrappedDescriptor->setBytesFilled(0);
     }
   }
 
   RootedValue unwrappedUnderlyingSource(cx);
   unwrappedUnderlyingSource = unwrappedController->underlyingSource();
 
-  // Step 1 of 3.8.5.1, step 2 of 3.10.5.1: Perform ! ResetQueue(this).
+  // Step 1 of 3.9.5.1, step 2 of 3.11.5.1: Perform ! ResetQueue(this).
   if (!ResetQueue(cx, unwrappedController)) {
     return nullptr;
   }
 
-  // Step 2 of 3.8.5.1, step 3 of 3.10.5.1: Let result be the result of
+  // Step 2 of 3.9.5.1, step 3 of 3.11.5.1: Let result be the result of
   //     performing this.[[cancelAlgorithm]], passing reason.
   //
   // Our representation of cancel algorithms is a bit awkward, for
   // performance, so we must figure out which algorithm is being invoked.
   RootedObject result(cx);
   if (IsMaybeWrapped<TeeState>(unwrappedUnderlyingSource)) {
     // The cancel algorithm given in ReadableStreamTee step 13 or 14.
     MOZ_ASSERT(unwrappedUnderlyingSource.toObject().is<TeeState>(),
@@ -2619,17 +2666,17 @@ static MOZ_MUST_USE JSObject* ReadableSt
   return result;
 }
 
 inline static MOZ_MUST_USE bool DequeueValue(
     JSContext* cx, Handle<ReadableStreamController*> unwrappedContainer,
     MutableHandleValue chunk);
 
 /**
- * Streams spec, 3.8.5.2.
+ * Streams spec, 3.9.5.2.
  *     ReadableStreamDefaultController [[PullSteps]]( forAuthorCode )
  */
 static JSObject* ReadableStreamDefaultControllerPullSteps(
     JSContext* cx,
     Handle<ReadableStreamDefaultController*> unwrappedController) {
   // Step 1: Let stream be this.[[controlledReadableStream]].
   Rooted<ReadableStream*> unwrappedStream(cx, unwrappedController->stream());
 
@@ -2698,23 +2745,23 @@ static JSObject* ReadableStreamDefaultCo
   if (!ReadableStreamControllerCallPullIfNeeded(cx, unwrappedController)) {
     return nullptr;
   }
 
   // Step 5: Return pendingPromise.
   return pendingPromise;
 }
 
-/*** 3.9. Readable stream default controller abstract operations ************/
-
-// Streams spec, 3.9.1. IsReadableStreamDefaultController ( x )
+/*** 3.10. Readable stream default controller abstract operations ***********/
+
+// Streams spec, 3.10.1. IsReadableStreamDefaultController ( x )
 // Implemented via is<ReadableStreamDefaultController>()
 
 /**
- * Streams spec, 3.9.2 and 3.12.3. step 7:
+ * Streams spec, 3.10.2 and 3.13.3. step 7:
  *      Upon fulfillment of pullPromise, [...]
  */
 static bool ControllerPullHandler(JSContext* cx, unsigned argc, Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   Rooted<ReadableStreamController*> unwrappedController(
       cx, UnwrapCalleeSlot<ReadableStreamController>(cx, args, 0));
   if (!unwrappedController) {
@@ -2737,17 +2784,17 @@ static bool ControllerPullHandler(JSCont
     }
   }
 
   args.rval().setUndefined();
   return true;
 }
 
 /**
- * Streams spec, 3.9.2 and 3.12.3. step 8:
+ * Streams spec, 3.10.2 and 3.13.3. step 8:
  * Upon rejection of pullPromise with reason e,
  */
 static bool ControllerPullFailedHandler(JSContext* cx, unsigned argc,
                                         Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
   HandleValue e = args.get(0);
 
   Rooted<ReadableStreamController*> controller(
@@ -2768,26 +2815,26 @@ static bool ControllerPullFailedHandler(
 
 static bool ReadableStreamControllerShouldCallPull(
     ReadableStreamController* unwrappedController);
 
 static MOZ_MUST_USE double ReadableStreamControllerGetDesiredSizeUnchecked(
     ReadableStreamController* unwrappedController);
 
 /**
- * Streams spec, 3.9.2
+ * Streams spec, 3.10.2
  *      ReadableStreamDefaultControllerCallPullIfNeeded ( controller )
- * Streams spec, 3.12.3.
+ * Streams spec, 3.13.3.
  *      ReadableByteStreamControllerCallPullIfNeeded ( controller )
  */
 inline static MOZ_MUST_USE bool ReadableStreamControllerCallPullIfNeeded(
     JSContext* cx, Handle<ReadableStreamController*> unwrappedController) {
   // Step 1: Let shouldPull be
   //         ! ReadableStreamDefaultControllerShouldCallPull(controller).
-  // (ReadableByteStreamDefaultControllerShouldCallPull in 3.12.3.)
+  // (ReadableByteStreamDefaultControllerShouldCallPull in 3.13.3.)
   bool shouldPull = ReadableStreamControllerShouldCallPull(unwrappedController);
 
   // Step 2: If shouldPull is false, return.
   if (!shouldPull) {
     return true;
   }
 
   // Step 3: If controller.[[pulling]] is true,
@@ -2886,103 +2933,103 @@ inline static MOZ_MUST_USE bool Readable
   if (!onPullRejected) {
     return false;
   }
   return JS::AddPromiseReactions(cx, pullPromise, onPullFulfilled,
                                  onPullRejected);
 }
 
 /**
- * Streams spec, 3.9.3.
+ * Streams spec, 3.10.3.
  *      ReadableStreamDefaultControllerShouldCallPull ( controller )
- * Streams spec, 3.12.25.
+ * Streams spec, 3.13.25.
  *      ReadableByteStreamControllerShouldCallPull ( controller )
  */
 static bool ReadableStreamControllerShouldCallPull(
     ReadableStreamController* unwrappedController) {
   // Step 1: Let stream be controller.[[controlledReadableStream]]
   //         (or [[controlledReadableByteStream]]).
   ReadableStream* unwrappedStream = unwrappedController->stream();
 
-  // 3.9.3. Step 2:
+  // 3.10.3. Step 2:
   //      If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(controller)
   //      is false, return false.
-  // This turns out to be the same as 3.12.25 steps 2-3.
-
-  // 3.12.25 Step 2: If stream.[[state]] is not "readable", return false.
+  // This turns out to be the same as 3.13.25 steps 2-3.
+
+  // 3.13.25 Step 2: If stream.[[state]] is not "readable", return false.
   if (!unwrappedStream->readable()) {
     return false;
   }
 
-  // 3.12.25 Step 3: If controller.[[closeRequested]] is true, return false.
+  // 3.13.25 Step 3: If controller.[[closeRequested]] is true, return false.
   if (unwrappedController->closeRequested()) {
     return false;
   }
 
   // Step 3 (or 4):
   //      If controller.[[started]] is false, return false.
   if (!unwrappedController->started()) {
     return false;
   }
 
-  // 3.9.3.
+  // 3.10.3.
   // Step 4: If ! IsReadableStreamLocked(stream) is true and
   //      ! ReadableStreamGetNumReadRequests(stream) > 0, return true.
   //
-  // 3.12.25.
+  // 3.13.25.
   // Step 5: If ! ReadableStreamHasDefaultReader(stream) is true and
   //         ! ReadableStreamGetNumReadRequests(stream) > 0, return true.
   // Step 6: If ! ReadableStreamHasBYOBReader(stream) is true and
   //         ! ReadableStreamGetNumReadIntoRequests(stream) > 0, return true.
   //
   // All of these amount to the same thing in this implementation:
   if (unwrappedStream->locked() &&
       ReadableStreamGetNumReadRequests(unwrappedStream) > 0) {
     return true;
   }
 
   // Step 5 (or 7):
   //      Let desiredSize be
   //      ! ReadableStreamDefaultControllerGetDesiredSize(controller).
-  //      (ReadableByteStreamControllerGetDesiredSize in 3.12.25.)
+  //      (ReadableByteStreamControllerGetDesiredSize in 3.13.25.)
   double desiredSize =
       ReadableStreamControllerGetDesiredSizeUnchecked(unwrappedController);
 
   // Step 6 (or 8): Assert: desiredSize is not null (implicit).
   // Step 7 (or 9): If desiredSize > 0, return true.
   // Step 8 (or 10): Return false.
   return desiredSize > 0;
 }
 
 /**
- * Streams spec, 3.9.4.
+ * Streams spec, 3.10.4.
  *      ReadableStreamDefaultControllerClearAlgorithms ( controller )
- * and 3.12.4.
+ * and 3.13.4.
  *      ReadableByteStreamControllerClearAlgorithms ( controller )
  */
 static void ReadableStreamControllerClearAlgorithms(
     Handle<ReadableStreamController*> controller) {
   // Step 1: Set controller.[[pullAlgorithm]] to undefined.
   // Step 2: Set controller.[[cancelAlgorithm]] to undefined.
   // (In this implementation, the UnderlyingSource slot is part of the
   // representation of these algorithms.)
   controller->setPullMethod(UndefinedHandleValue);
   controller->setCancelMethod(UndefinedHandleValue);
   ReadableStreamController::clearUnderlyingSource(controller);
 
-  // Step 3 (of 3.9.4 only) : Set controller.[[strategySizeAlgorithm]] to
+  // Step 3 (of 3.10.4 only) : Set controller.[[strategySizeAlgorithm]] to
   // undefined.
   if (controller->is<ReadableStreamDefaultController>()) {
     controller->as<ReadableStreamDefaultController>().setStrategySize(
         UndefinedHandleValue);
   }
 }
 
 /**
- * Streams spec, 3.9.5. ReadableStreamDefaultControllerClose ( controller )
+ * Streams spec, 3.10.5. ReadableStreamDefaultControllerClose ( controller )
  */
 static MOZ_MUST_USE bool ReadableStreamDefaultControllerClose(
     JSContext* cx,
     Handle<ReadableStreamDefaultController*> unwrappedController) {
   // Step 1: Let stream be controller.[[controlledReadableStream]].
   Rooted<ReadableStream*> unwrappedStream(cx, unwrappedController->stream());
 
   // Step 2: Assert:
@@ -3008,17 +3055,17 @@ static MOZ_MUST_USE bool ReadableStreamD
   return true;
 }
 
 static MOZ_MUST_USE bool EnqueueValueWithSize(
     JSContext* cx, Handle<ReadableStreamController*> unwrappedContainer,
     HandleValue value, HandleValue sizeVal);
 
 /**
- * Streams spec, 3.9.6.
+ * Streams spec, 3.10.6.
  *      ReadableStreamDefaultControllerEnqueue ( controller, chunk )
  */
 static MOZ_MUST_USE bool ReadableStreamDefaultControllerEnqueue(
     JSContext* cx, Handle<ReadableStreamDefaultController*> unwrappedController,
     HandleValue chunk) {
   AssertSameCompartment(cx, chunk);
 
   // Step 1: Let stream be controller.[[controlledReadableStream]].
@@ -3094,35 +3141,35 @@ static MOZ_MUST_USE bool ReadableStreamD
   //         ! ReadableStreamDefaultControllerCallPullIfNeeded(controller).
   return ReadableStreamControllerCallPullIfNeeded(cx, unwrappedController);
 }
 
 static MOZ_MUST_USE bool ReadableByteStreamControllerClearPendingPullIntos(
     JSContext* cx, Handle<ReadableByteStreamController*> unwrappedController);
 
 /**
- * Streams spec, 3.9.7. ReadableStreamDefaultControllerError ( controller, e )
- * Streams spec, 3.12.11. ReadableByteStreamControllerError ( controller, e )
+ * Streams spec, 3.10.7. ReadableStreamDefaultControllerError ( controller, e )
+ * Streams spec, 3.13.11. ReadableByteStreamControllerError ( controller, e )
  */
 static MOZ_MUST_USE bool ReadableStreamControllerError(
     JSContext* cx, Handle<ReadableStreamController*> unwrappedController,
     HandleValue e) {
   MOZ_ASSERT(!cx->isExceptionPending());
   AssertSameCompartment(cx, e);
 
   // Step 1: Let stream be controller.[[controlledReadableStream]]
   //         (or controller.[[controlledReadableByteStream]]).
   Rooted<ReadableStream*> unwrappedStream(cx, unwrappedController->stream());
 
   // Step 2: If stream.[[state]] is not "readable", return.
   if (!unwrappedStream->readable()) {
     return true;
   }
 
-  // Step 3 of 3.12.10:
+  // Step 3 of 3.13.10:
   // Perform ! ReadableByteStreamControllerClearPendingPullIntos(controller).
   if (unwrappedController->is<ReadableByteStreamController>()) {
     Rooted<ReadableByteStreamController*> unwrappedByteStreamController(
         cx, &unwrappedController->as<ReadableByteStreamController>());
     if (!ReadableByteStreamControllerClearPendingPullIntos(
             cx, unwrappedByteStreamController)) {
       return false;
     }
@@ -3138,35 +3185,35 @@ static MOZ_MUST_USE bool ReadableStreamC
   //      (or ReadableByteStreamControllerClearAlgorithms(controller)).
   ReadableStreamControllerClearAlgorithms(unwrappedController);
 
   // Step 5 (or 6): Perform ! ReadableStreamError(stream, e).
   return ReadableStreamErrorInternal(cx, unwrappedStream, e);
 }
 
 /**
- * Streams spec, 3.9.8.
+ * Streams spec, 3.10.8.
  *      ReadableStreamDefaultControllerGetDesiredSize ( controller )
- * Streams spec 3.12.14.
+ * Streams spec 3.13.14.
  *      ReadableByteStreamControllerGetDesiredSize ( controller )
  */
 static MOZ_MUST_USE double ReadableStreamControllerGetDesiredSizeUnchecked(
     ReadableStreamController* controller) {
   // Steps 1-4 done at callsites, so only assert that they have been done.
 #if DEBUG
   ReadableStream* stream = controller->stream();
   MOZ_ASSERT(!(stream->errored() || stream->closed()));
 #endif  // DEBUG
 
   // Step 5: Return controller.[[strategyHWM]] − controller.[[queueTotalSize]].
   return controller->strategyHWM() - controller->queueTotalSize();
 }
 
 /**
- * Streams spec, 3.9.11.
+ * Streams spec, 3.10.11.
  *      SetUpReadableStreamDefaultController(stream, controller,
  *          startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark,
  *          sizeAlgorithm )
  *
  * The standard algorithm takes a `controller` argument which must be a new,
  * blank object. This implementation creates a new controller instead.
  *
  * In the spec, three algorithms (startAlgorithm, pullAlgorithm,
@@ -3178,24 +3225,24 @@ static MOZ_MUST_USE double ReadableStrea
  * -   SourceAlgorithms::Script - We're creating a stream from a JS source.
  *     The caller is `new ReadableStream(underlyingSource)` or
  *     `JS::NewReadableDefaultStreamObject`. `underlyingSource` is the
  *     source; `pullMethod` and `cancelMethod` are its .pull and
  *     .cancel methods, which the caller has already extracted and
  *     type-checked: each one must be either a callable JS object or undefined.
  *
  *     Script streams use the start/pull/cancel algorithms defined in
- *     3.9.12. SetUpReadableStreamDefaultControllerFromUnderlyingSource, which
+ *     3.10.12. SetUpReadableStreamDefaultControllerFromUnderlyingSource, which
  *     call JS methods of the underlyingSource.
  *
  * -   SourceAlgorithms::Tee - We're creating a tee stream. `underlyingSource`
  *     is a TeeState object. `pullMethod` and `cancelMethod` are undefined.
  *
  *     Tee streams use the start/pull/cancel algorithms given in
- *     3.3.9. ReadableStreamTee.
+ *     3.4.10. ReadableStreamTee.
  *
  * Note: All arguments must be same-compartment with cx. ReadableStream
  * controllers are always created in the same compartment as the stream.
  */
 static MOZ_MUST_USE bool SetUpReadableStreamDefaultController(
     JSContext* cx, Handle<ReadableStream*> stream,
     SourceAlgorithms sourceAlgorithms, HandleValue underlyingSource,
     HandleValue pullMethod, HandleValue cancelMethod, double highWaterMark,
@@ -3288,17 +3335,17 @@ static MOZ_MUST_USE bool SetUpReadableSt
 }
 
 static MOZ_MUST_USE bool CreateAlgorithmFromUnderlyingMethod(
     JSContext* cx, HandleValue underlyingObject,
     const char* methodNameForErrorMessage, HandlePropertyName methodName,
     MutableHandleValue method);
 
 /**
- * Streams spec, 3.9.12.
+ * Streams spec, 3.10.12.
  *      SetUpReadableStreamDefaultControllerFromUnderlyingSource( stream,
  *          underlyingSource, highWaterMark, sizeAlgorithm )
  */
 static MOZ_MUST_USE bool
 SetUpReadableStreamDefaultControllerFromUnderlyingSource(
     JSContext* cx, Handle<ReadableStream*> stream, HandleValue underlyingSource,
     double highWaterMark, HandleValue sizeAlgorithm) {
   // Step 1: Assert: underlyingSource is not undefined.
@@ -3336,17 +3383,17 @@ SetUpReadableStreamDefaultControllerFrom
   // Step 6. Perform ? SetUpReadableStreamDefaultController(stream,
   //             controller, startAlgorithm, pullAlgorithm, cancelAlgorithm,
   //             highWaterMark, sizeAlgorithm).
   return SetUpReadableStreamDefaultController(
       cx, stream, sourceAlgorithms, underlyingSource, pullMethod, cancelMethod,
       highWaterMark, sizeAlgorithm);
 }
 
-/*** 3.10. Class ReadableByteStreamController *******************************/
+/*** 3.11. Class ReadableByteStreamController *******************************/
 
 #if 0  // disable user-defined byte streams
 
 /**
  * Streams spec, 3.10.3
  *      new ReadableByteStreamController ( stream, underlyingSource,
  *                                         highWaterMark )
  * Steps 3 - 16.
@@ -3458,17 +3505,17 @@ CreateReadableByteStreamController(JSCon
     }
 
     return controller;
 }
 
 #endif  // user-defined byte streams
 
 /**
- * Streams spec, 3.10.3.
+ * Streams spec, 3.11.3.
  * new ReadableByteStreamController ( stream, underlyingByteSource,
  *                                    highWaterMark )
  */
 bool ReadableByteStreamController::constructor(JSContext* cx, unsigned argc,
                                                Value* vp) {
   // Step 1: Throw a TypeError exception.
   JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                             JSMSG_BOGUS_CONSTRUCTOR,
@@ -3626,24 +3673,24 @@ static const ClassOps ReadableByteStream
     nullptr, /* construct   */
     nullptr, /* trace   */
 };
 
 CLASS_SPEC(ReadableByteStreamController, 0, SlotCount,
            ClassSpec::DontDefineConstructor, JSCLASS_BACKGROUND_FINALIZE,
            &ReadableByteStreamControllerClassOps);
 
-// Streams spec, 3.10.5.1. [[CancelSteps]] ()
-// Unified with 3.8.5.1 above.
+// Streams spec, 3.11.5.1. [[CancelSteps]] ()
+// Unified with 3.9.5.1 above.
 
 static MOZ_MUST_USE bool ReadableByteStreamControllerHandleQueueDrain(
     JSContext* cx, Handle<ReadableStreamController*> unwrappedController);
 
 /**
- * Streams spec, 3.10.5.2. [[PullSteps]] ( forAuthorCode )
+ * Streams spec, 3.11.5.2. [[PullSteps]] ( forAuthorCode )
  */
 static MOZ_MUST_USE JSObject* ReadableByteStreamControllerPullSteps(
     JSContext* cx, Handle<ReadableByteStreamController*> unwrappedController) {
   // Step 1: Let stream be this.[[controlledReadableByteStream]].
   Rooted<ReadableStream*> unwrappedStream(cx, unwrappedController->stream());
 
   // Step 2: Assert: ! ReadableStreamHasDefaultReader(stream) is true.
 #ifdef DEBUG
@@ -3781,17 +3828,17 @@ static MOZ_MUST_USE JSObject* ReadableBy
     //                   [[byteOffset]]: 0,
     //                   [[byteLength]]: autoAllocateChunkSize,
     //                   [[bytesFilled]]: 0,
     //                   [[elementSize]]: 1,
     //                   [[ctor]]: %Uint8Array%,
     //                   [[readerType]]: `"default"`}.
     RootedObject pullIntoDescriptor(
         cx, PullIntoDescriptor::create(cx, buffer, 0, autoAllocateChunkSize, 0,
-                                       1, nullptr, ReaderType_Default));
+                                       1, nullptr, ReaderType::Default));
     if (!pullIntoDescriptor) {
       return PromiseRejectedWithPendingError(cx);
     }
 
     // Step 5.d: Append pullIntoDescriptor as the last element of
     //           this.[[pendingPullIntos]].
     if (!AppendToListAtSlot(cx, unwrappedController,
                             ReadableByteStreamController::Slot_PendingPullIntos,
@@ -3815,51 +3862,51 @@ static MOZ_MUST_USE JSObject* ReadableBy
 
   // Step 8: Return promise.
   return promise;
 }
 
 /**
  * Unified implementation of ReadableStream controllers' [[PullSteps]] internal
  * methods.
- * Streams spec, 3.8.5.2. [[PullSteps]] ( forAuthorCode )
+ * Streams spec, 3.9.5.2. [[PullSteps]] ( forAuthorCode )
  * and
- * Streams spec, 3.10.5.2. [[PullSteps]] ( forAuthorCode )
+ * Streams spec, 3.11.5.2. [[PullSteps]] ( forAuthorCode )
  */
 static MOZ_MUST_USE JSObject* ReadableStreamControllerPullSteps(
     JSContext* cx, Handle<ReadableStreamController*> unwrappedController) {
   if (unwrappedController->is<ReadableStreamDefaultController>()) {
     Rooted<ReadableStreamDefaultController*> unwrappedDefaultController(
         cx, &unwrappedController->as<ReadableStreamDefaultController>());
     return ReadableStreamDefaultControllerPullSteps(cx,
                                                     unwrappedDefaultController);
   }
 
   Rooted<ReadableByteStreamController*> unwrappedByteController(
       cx, &unwrappedController->as<ReadableByteStreamController>());
   return ReadableByteStreamControllerPullSteps(cx, unwrappedByteController);
 }
 
-/*** 3.12. Readable stream BYOB controller abstract operations **************/
-
-// Streams spec, 3.12.1. IsReadableStreamBYOBRequest ( x )
+/*** 3.13. Readable stream BYOB controller abstract operations **************/
+
+// Streams spec, 3.13.1. IsReadableStreamBYOBRequest ( x )
 // Implemented via is<ReadableStreamBYOBRequest>()
 
-// Streams spec, 3.12.2. IsReadableByteStreamController ( x )
+// Streams spec, 3.13.2. IsReadableByteStreamController ( x )
 // Implemented via is<ReadableByteStreamController>()
 
-// Streams spec, 3.12.3.
+// Streams spec, 3.13.3.
 //      ReadableByteStreamControllerCallPullIfNeeded ( controller )
 // Unified with 3.9.2 above.
 
 static MOZ_MUST_USE bool ReadableByteStreamControllerInvalidateBYOBRequest(
     JSContext* cx, Handle<ReadableByteStreamController*> unwrappedController);
 
 /**
- * Streams spec, 3.12.5.
+ * Streams spec, 3.13.5.
  *      ReadableByteStreamControllerClearPendingPullIntos ( controller )
  */
 static MOZ_MUST_USE bool ReadableByteStreamControllerClearPendingPullIntos(
     JSContext* cx, Handle<ReadableByteStreamController*> unwrappedController) {
   // Step 1: Perform
   //         ! ReadableByteStreamControllerInvalidateBYOBRequest(controller).
   if (!ReadableByteStreamControllerInvalidateBYOBRequest(cx,
                                                          unwrappedController)) {
@@ -3867,17 +3914,17 @@ static MOZ_MUST_USE bool ReadableByteStr
   }
 
   // Step 2: Set controller.[[pendingPullIntos]] to a new empty List.
   return SetNewList(cx, unwrappedController,
                     ReadableByteStreamController::Slot_PendingPullIntos);
 }
 
 /**
- * Streams spec, 3.12.6. ReadableByteStreamControllerClose ( controller )
+ * Streams spec, 3.13.6. ReadableByteStreamControllerClose ( controller )
  */
 static MOZ_MUST_USE bool ReadableByteStreamControllerClose(
     JSContext* cx, Handle<ReadableByteStreamController*> unwrappedController) {
   // Step 1: Let stream be controller.[[controlledReadableByteStream]].
   Rooted<ReadableStream*> unwrappedStream(cx, unwrappedController->stream());
 
   // Step 2: Assert: controller.[[closeRequested]] is false.
   MOZ_ASSERT(!unwrappedController->closeRequested());
@@ -3935,25 +3982,25 @@ static MOZ_MUST_USE bool ReadableByteStr
 
   // Step 6: Perform ! ReadableByteStreamControllerClearAlgorithms(controller).
   ReadableStreamControllerClearAlgorithms(unwrappedController);
 
   // Step 7: Perform ! ReadableStreamClose(stream).
   return ReadableStreamCloseInternal(cx, unwrappedStream);
 }
 
-// Streams spec, 3.12.11. ReadableByteStreamControllerError ( controller, e )
-// Unified with 3.9.7 above.
-
-// Streams spec 3.12.14.
+// Streams spec, 3.13.11. ReadableByteStreamControllerError ( controller, e )
+// Unified with 3.10.7 above.
+
+// Streams spec 3.13.14.
 //      ReadableByteStreamControllerGetDesiredSize ( controller )
-// Unified with 3.9.8 above.
+// Unified with 3.10.8 above.
 
 /**
- * Streams spec, 3.12.15.
+ * Streams spec, 3.13.15.
  *      ReadableByteStreamControllerHandleQueueDrain ( controller )
  */
 static MOZ_MUST_USE bool ReadableByteStreamControllerHandleQueueDrain(
     JSContext* cx, Handle<ReadableStreamController*> unwrappedController) {
   MOZ_ASSERT(unwrappedController->is<ReadableByteStreamController>());
 
   // Step 1: Assert: controller.[[controlledReadableStream]].[[state]]
   //                 is "readable".
@@ -3980,17 +4027,17 @@ static MOZ_MUST_USE bool ReadableByteStr
 
 enum BYOBRequestSlots {
   BYOBRequestSlot_Controller,
   BYOBRequestSlot_View,
   BYOBRequestSlotCount
 };
 
 /**
- * Streams spec 3.12.16.
+ * Streams spec 3.13.16.
  *      ReadableByteStreamControllerInvalidateBYOBRequest ( controller )
  */
 static MOZ_MUST_USE bool ReadableByteStreamControllerInvalidateBYOBRequest(
     JSContext* cx, Handle<ReadableByteStreamController*> unwrappedController) {
   // Step 1: If controller.[[byobRequest]] is undefined, return.
   RootedValue unwrappedBYOBRequestVal(cx, unwrappedController->byobRequest());
   if (unwrappedBYOBRequestVal.isUndefined()) {
     return true;
@@ -4012,19 +4059,19 @@ static MOZ_MUST_USE bool ReadableByteStr
   unwrappedBYOBRequest->setFixedSlot(BYOBRequestSlot_View, UndefinedValue());
 
   // Step 4: Set controller.[[byobRequest]] to undefined.
   unwrappedController->clearBYOBRequest();
 
   return true;
 }
 
-// Streams spec, 3.12.25.
+// Streams spec, 3.13.25.
 //      ReadableByteStreamControllerShouldCallPull ( controller )
-// Unified with 3.9.3 above.
+// Unified with 3.10.3 above.
 
 /*** 6.1. Queuing strategies ************************************************/
 
 /**
  * ECMA-262 7.3.4 CreateDataProperty(O, P, V)
  */
 static MOZ_MUST_USE bool CreateDataProperty(JSContext* cx, HandleObject obj,
                                             HandlePropertyName key,
@@ -4134,17 +4181,17 @@ bool js::CountQueuingStrategy::construct
                           highWaterMark, ignored)) {
     return false;
   }
 
   args.rval().setObject(*strategy);
   return true;
 }
 
-// Streams spec 6.2.3.3.1. size ( chunk )
+// Streams spec 6.1.3.3.1. size ( chunk )
 bool CountQueuingStrategy_size(JSContext* cx, unsigned argc, Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   // Step 1: Return 1.
   args.rval().setInt32(1);
   return true;
 }
 
@@ -4670,18 +4717,18 @@ JS_PUBLIC_API bool JS::ReadableStreamUpd
   CHECK_THREAD(cx);
 
   Rooted<ReadableStream*> unwrappedStream(
       cx, APIUnwrapAndDowncast<ReadableStream>(cx, streamObj));
   if (!unwrappedStream) {
     return false;
   }
 
-  // This is based on Streams spec 3.10.4.4. enqueue(chunk) steps 1-3 and
-  // 3.12.9. ReadableByteStreamControllerEnqueue(controller, chunk) steps
+  // This is based on Streams spec 3.11.4.4. enqueue(chunk) steps 1-3 and
+  // 3.13.9. ReadableByteStreamControllerEnqueue(controller, chunk) steps
   // 8-9.
   //
   // Adapted to handling updates signaled by the embedding for streams with
   // external underlying sources.
   //
   // The remaining steps of those two functions perform checks and asserts
   // that don't apply to streams with external underlying sources.
 
@@ -4708,17 +4755,17 @@ JS_PUBLIC_API bool JS::ReadableStreamUpd
 
 #if DEBUG
   uint32_t oldAvailableData =
       unwrappedController->getFixedSlot(StreamController::Slot_TotalSize)
           .toInt32();
 #endif  // DEBUG
   unwrappedController->setQueueTotalSize(availableData);
 
-  // 3.12.9. ReadableByteStreamControllerEnqueue
+  // 3.139. ReadableByteStreamControllerEnqueue
   // Step 8.a: If ! ReadableStreamGetNumReadRequests(stream) is 0,
   // Reordered because for externally-sourced streams it applies regardless
   // of reader type.
   if (ReadableStreamGetNumReadRequests(unwrappedStream) == 0) {
     return true;
   }
 
   // Step 8: If ! ReadableStreamHasDefaultReader(stream) is true
--- a/js/src/frontend/BinASTParserPerTokenizer.cpp
+++ b/js/src/frontend/BinASTParserPerTokenizer.cpp
@@ -161,17 +161,17 @@ JS::Result<FunctionNode*> BinASTParserPe
 
   MOZ_TRY(tokenizer_->initFromScriptSource(scriptSource));
 
   tokenizer_->seek(firstOffset);
 
   // For now, only function declarations and function expression are supported.
   RootedFunction func(cx_, lazyScript_->functionNonDelazifying());
   bool isExpr = func->isLambda();
-  MOZ_ASSERT(func->kind() == JSFunction::FunctionKind::NormalFunction);
+  MOZ_ASSERT(func->kind() == FunctionFlags::FunctionKind::NormalFunction);
 
   // Poison the tokenizer when we leave to ensure that it's not used again by
   // accident.
   auto onExit = mozilla::MakeScopeExit([&]() { poison(); });
 
   // TODO: This should be actually shared with the auto-generated version.
 
   auto syntaxKind =
@@ -258,17 +258,17 @@ JS::Result<FunctionBox*> BinASTParserPer
       RedeclareVar(ptr, DeclarationKind::BodyLevelFunction);
     }
   }
 
   // Allocate the function before walking down the tree.
   RootedFunction fun(cx_);
   BINJS_TRY_VAR(fun, !pc_ ? lazyScript_->functionNonDelazifying()
                           : AllocNewFunction(cx_, atom, syntax, generatorKind,
-                                             functionAsyncKind, nullptr));
+                                             functionAsyncKind));
   MOZ_ASSERT_IF(pc_, fun->explicitName() == atom);
 
   mozilla::Maybe<Directives> directives;
   if (pc_) {
     directives.emplace(pc_);
   } else {
     directives.emplace(lazyScript_->strict());
   }
@@ -384,21 +384,22 @@ JS::Result<Ok> BinASTParserPerTokenizer<
 template <typename Tok>
 JS::Result<Ok> BinASTParserPerTokenizer<Tok>::finishLazyFunction(
     FunctionBox* funbox, uint32_t nargs, size_t start, size_t end) {
   RootedFunction fun(cx_, funbox->function());
 
   funbox->setArgCount(nargs);
   funbox->synchronizeArgCount();
 
-  BINJS_TRY_DECL(
-      lazy, LazyScript::Create(cx_, fun, sourceObject_,
-                               pc_->closedOverBindingsForLazy(),
-                               pc_->innerFunctionsForLazy, start, end, start, 0,
-                               start, ParseGoal::Script));
+  BINJS_TRY_DECL(lazy, LazyScript::Create(cx_, fun, sourceObject_,
+                                          pc_->closedOverBindingsForLazy(),
+                                          pc_->innerFunctionBoxesForLazy,
+                                          start, end, start, end,
+                                          /* lineno = */ 0, start,
+                                          ParseGoal::Script));
 
   if (funbox->strict()) {
     lazy->setStrict();
   }
   MOZ_ASSERT(lazy->isBinAST());
   funbox->initLazyScript(lazy);
 
   return Ok();
--- a/js/src/frontend/BytecodeCompiler.cpp
+++ b/js/src/frontend/BytecodeCompiler.cpp
@@ -958,17 +958,17 @@ static bool CompileLazyFunctionImpl(JSCo
   if (lazy->isLikelyConstructorWrapper()) {
     script->setLikelyConstructorWrapper();
   }
   if (lazy->hasBeenCloned()) {
     script->setHasBeenCloned();
   }
 
   FieldInitializers fieldInitializers = FieldInitializers::Invalid();
-  if (fun->kind() == JSFunction::FunctionKind::ClassConstructor) {
+  if (fun->kind() == FunctionFlags::FunctionKind::ClassConstructor) {
     fieldInitializers = lazy->getFieldInitializers();
   }
 
   BytecodeEmitter bce(/* parent = */ nullptr, &parser, pn->funbox(), script,
                       lazy, pn->pn_pos, BytecodeEmitter::LazyFunction,
                       fieldInitializers);
   if (!bce.init()) {
     return false;
--- a/js/src/frontend/BytecodeEmitter.cpp
+++ b/js/src/frontend/BytecodeEmitter.cpp
@@ -2525,17 +2525,17 @@ bool BytecodeEmitter::emitFunctionScript
   MOZ_ASSERT(inPrologue());
   ListNode* paramsBody = &funNode->body()->as<ListNode>();
   MOZ_ASSERT(paramsBody->isKind(ParseNodeKind::ParamsBody));
   FunctionBox* funbox = sc->asFunctionBox();
   AutoFrontendTraceLog traceLog(cx, TraceLogger_BytecodeEmission,
                                 parser->errorReporter(), funbox);
 
   MOZ_ASSERT((fieldInitializers_.valid) ==
-             (funbox->kind() == JSFunction::FunctionKind::ClassConstructor));
+             (funbox->kind() == FunctionFlags::FunctionKind::ClassConstructor));
 
   setScriptStartOffsetIfUnset(paramsBody->pn_pos.begin);
 
   //                [stack]
 
   FunctionScriptEmitter fse(this, funbox, Some(paramsBody->pn_pos.begin),
                             Some(paramsBody->pn_pos.end));
   if (!fse.prepareForParameters()) {
@@ -5691,17 +5691,17 @@ bool BytecodeEmitter::emitFor(ForNode* f
 
 MOZ_NEVER_INLINE bool BytecodeEmitter::emitFunction(
     FunctionNode* funNode, bool needsProto /* = false */,
     ListNode* classContentsIfConstructor /* = nullptr */) {
   FunctionBox* funbox = funNode->funbox();
   RootedFunction fun(cx, funbox->function());
 
   MOZ_ASSERT((classContentsIfConstructor != nullptr) ==
-             (funbox->kind() == JSFunction::FunctionKind::ClassConstructor));
+             (funbox->kind() == FunctionFlags::FunctionKind::ClassConstructor));
 
   //                [stack]
 
   FunctionEmitter fe(this, funbox, funNode->syntaxKind(),
                      funNode->functionIsHoisted()
                          ? FunctionEmitter::IsHoisted::Yes
                          : FunctionEmitter::IsHoisted::No);
 
@@ -8125,29 +8125,29 @@ bool BytecodeEmitter::emitCreateFieldIni
 
   return true;
 }
 
 const FieldInitializers& BytecodeEmitter::findFieldInitializersForCall() {
   for (BytecodeEmitter* current = this; current; current = current->parent) {
     if (current->sc->isFunctionBox()) {
       FunctionBox* box = current->sc->asFunctionBox();
-      if (box->kind() == JSFunction::FunctionKind::ClassConstructor) {
+      if (box->kind() == FunctionFlags::FunctionKind::ClassConstructor) {
         const FieldInitializers& fieldInitializers =
             current->getFieldInitializers();
         MOZ_ASSERT(fieldInitializers.valid);
         return fieldInitializers;
       }
     }
   }
 
   for (ScopeIter si(innermostScope()); si; si++) {
     if (si.scope()->is<FunctionScope>()) {
       JSFunction* fun = si.scope()->as<FunctionScope>().canonicalFunction();
-      if (fun->kind() == JSFunction::FunctionKind::ClassConstructor) {
+      if (fun->kind() == FunctionFlags::FunctionKind::ClassConstructor) {
         const FieldInitializers& fieldInitializers =
             fun->isInterpretedLazy()
                 ? fun->lazyScript()->getFieldInitializers()
                 : fun->nonLazyScript()->getFieldInitializers();
         MOZ_ASSERT(fieldInitializers.valid);
         return fieldInitializers;
       }
     }
--- a/js/src/frontend/FunctionEmitter.cpp
+++ b/js/src/frontend/FunctionEmitter.cpp
@@ -479,17 +479,17 @@ bool FunctionScriptEmitter::prepareForBo
   }
 
   if (funbox_->needsPromiseResult()) {
     if (!emitAsyncFunctionRejectPrologue()) {
       return false;
     }
   }
 
-  if (funbox_->kind() == JSFunction::FunctionKind::ClassConstructor) {
+  if (funbox_->kind() == FunctionFlags::FunctionKind::ClassConstructor) {
     if (!funbox_->isDerivedClassConstructor()) {
       if (!bce_->emitInitializeInstanceFields()) {
         //          [stack]
         return false;
       }
     }
   }
 
--- a/js/src/frontend/NameAnalysisTypes.h
+++ b/js/src/frontend/NameAnalysisTypes.h
@@ -322,19 +322,24 @@ class NameLocation {
   bool isConst() const { return bindingKind() == BindingKind::Const; }
 
   bool hasKnownSlot() const {
     return kind_ == Kind::ArgumentSlot || kind_ == Kind::FrameSlot ||
            kind_ == Kind::EnvironmentCoordinate;
   }
 };
 
-// This type is declared here for LazyScript::Create.
+// These types are declared here for LazyScript::Create.
 using AtomVector = Vector<JSAtom*, 24, SystemAllocPolicy>;
 
+class FunctionBox;
+// FunctionBoxes stored in this type are required to be rooted
+// by the parser
+using FunctionBoxVector = Vector<const FunctionBox*, 8>;
+
 }  // namespace frontend
 }  // namespace js
 
 namespace mozilla {
 
 template <>
 struct IsPod<js::frontend::DeclaredNameInfo> : TrueType {};
 
--- a/js/src/frontend/NameCollections.h
+++ b/js/src/frontend/NameCollections.h
@@ -161,18 +161,16 @@ class InlineTablePool
         Table::SizeOfInlineEntries == RepresentativeTable::SizeOfInlineEntries,
         "Only tables with the same size for inline entries are usable in the "
         "pool.");
     static_assert(mozilla::IsPod<typename Table::Table::Entry>::value,
                   "Only tables with POD values are usable in the pool.");
   }
 };
 
-using FunctionBoxVector = Vector<FunctionBox*, 24, SystemAllocPolicy>;
-
 template <typename RepresentativeVector>
 class VectorPool : public CollectionPool<RepresentativeVector,
                                          VectorPool<RepresentativeVector>> {
  public:
   template <typename Vector>
   static void assertInvariants() {
     static_assert(
         Vector::sMaxInlineStorage == RepresentativeVector::sMaxInlineStorage,
--- a/js/src/frontend/ParseContext.cpp
+++ b/js/src/frontend/ParseContext.cpp
@@ -228,17 +228,17 @@ ParseContext::ParseContext(JSContext* cx
                 errorReporter),
       sc_(sc),
       errorReporter_(errorReporter),
       innermostStatement_(nullptr),
       innermostScope_(nullptr),
       varScope_(nullptr),
       positionalFormalParameterNames_(cx->frontendCollectionPool()),
       closedOverBindingsForLazy_(cx->frontendCollectionPool()),
-      innerFunctionsForLazy(cx, GCVector<JSFunction*, 8>(cx)),
+      innerFunctionBoxesForLazy(cx),
       newDirectives(newDirectives),
       lastYieldOffset(NoYieldOffset),
       lastAwaitOffset(NoAwaitOffset),
       scriptId_(usedNames.nextScriptId()),
       isStandaloneFunctionBody_(false),
       superScopeNeedsHomeObject_(false) {
   if (isFunctionBox()) {
     if (functionBox()->isNamedLambda()) {
@@ -501,17 +501,17 @@ bool ParseContext::declareFunctionThis(c
   HandlePropertyName dotThis = sc()->cx_->names().dotThis;
 
   bool declareThis;
   if (canSkipLazyClosedOverBindings) {
     declareThis = funbox->function()->lazyScript()->hasThisBinding();
   } else {
     declareThis = hasUsedFunctionSpecialName(usedNames, dotThis) ||
                   funbox->function()->kind() ==
-                      JSFunction::FunctionKind::ClassConstructor;
+                      FunctionFlags::FunctionKind::ClassConstructor;
   }
 
   if (declareThis) {
     ParseContext::Scope& funScope = functionScope();
     AddDeclaredNamePtr p = funScope.lookupDeclaredNameForAdd(dotThis);
     MOZ_ASSERT(!p);
     if (!funScope.addDeclaredName(this, p, dotThis, DeclarationKind::Var,
                                   DeclaredNameInfo::npos)) {
--- a/js/src/frontend/ParseContext.h
+++ b/js/src/frontend/ParseContext.h
@@ -243,16 +243,17 @@ class ParseContext : public Nestable<Par
     //
     // A lexically declared name is a member only of the declared name set of
     // the scope in which it is declared.
     PooledMapPtr<DeclaredNameMap> declared_;
 
     // FunctionBoxes in this scope that need to be considered for Annex
     // B.3.3 semantics. This is checked on Scope exit, as by then we have
     // all the declared names and would know if Annex B.3.3 is applicable.
+    using FunctionBoxVector = Vector<FunctionBox*, 24, SystemAllocPolicy>;
     PooledVectorPtr<FunctionBoxVector> possibleAnnexBFunctionBoxes_;
 
     // Monotonically increasing id.
     uint32_t id_;
 
     bool maybeReportOOM(ParseContext* pc, bool result) {
       if (!result) {
         ReportOutOfMemory(pc->sc()->cx_);
@@ -438,18 +439,20 @@ class ParseContext : public Nestable<Par
   // isFunctionBox().
   PooledVectorPtr<AtomVector> positionalFormalParameterNames_;
 
   // Closed over binding names, in order of appearance. Null-delimited
   // between scopes. Only used when syntax parsing.
   PooledVectorPtr<AtomVector> closedOverBindingsForLazy_;
 
  public:
-  // All inner functions in this context. Only used when syntax parsing.
-  Rooted<GCVector<JSFunction*, 8>> innerFunctionsForLazy;
+  // All inner FunctionBoxes in this context. Only used when syntax parsing.
+  // The FunctionBoxes are traced as part of the TraceList on the parser,
+  // (see TraceListNode::TraceList)
+  FunctionBoxVector innerFunctionBoxesForLazy;
 
   // In a function context, points to a Directive struct that can be updated
   // to reflect new directives encountered in the Directive Prologue that
   // require reparsing the function. In global/module/generator-tail contexts,
   // we don't need to reparse when encountering a DirectivePrologue so this
   // pointer may be nullptr.
   Directives* newDirectives;
 
--- a/js/src/frontend/Parser.cpp
+++ b/js/src/frontend/Parser.cpp
@@ -1685,30 +1685,30 @@ bool PerHandlerParser<SyntaxParseHandler
   if (!finishFunctionScopes(isStandaloneFunction)) {
     return false;
   }
 
   // There are too many bindings or inner functions to be saved into the
   // LazyScript. Do a full parse.
   if (pc_->closedOverBindingsForLazy().length() >=
           LazyScript::NumClosedOverBindingsLimit ||
-      pc_->innerFunctionsForLazy.length() >=
+      pc_->innerFunctionBoxesForLazy.length() >=
           LazyScript::NumInnerFunctionsLimit) {
     MOZ_ALWAYS_FALSE(abortIfSyntaxParser());
     return false;
   }
 
   FunctionBox* funbox = pc_->functionBox();
   funbox->synchronizeArgCount();
   RootedFunction fun(cx_, funbox->function());
   LazyScript* lazy = LazyScript::Create(
       cx_, fun, sourceObject_, pc_->closedOverBindingsForLazy(),
-      pc_->innerFunctionsForLazy, funbox->bufStart, funbox->bufEnd,
-      funbox->toStringStart, funbox->startLine, funbox->startColumn,
-      parseGoal());
+      pc_->innerFunctionBoxesForLazy, funbox->bufStart, funbox->bufEnd,
+      funbox->toStringStart, funbox->toStringEnd, funbox->startLine,
+      funbox->startColumn, parseGoal());
   if (!lazy) {
     return false;
   }
 
   // Flags that need to be copied into the JSScript when we do the full
   // parse.
   if (pc_->sc()->strict()) {
     lazy->setStrict();
@@ -1964,85 +1964,89 @@ GeneralParser<ParseHandler, Unit>::funct
   }
 
   return finishLexicalScope(pc_->varScope(), body, ScopeKind::FunctionLexical);
 }
 
 JSFunction* AllocNewFunction(JSContext* cx, HandleAtom atom,
                              FunctionSyntaxKind kind,
                              GeneratorKind generatorKind,
-                             FunctionAsyncKind asyncKind, HandleObject proto,
+                             FunctionAsyncKind asyncKind,
                              bool isSelfHosting /* = false */,
                              bool inFunctionBox /* = false */) {
   MOZ_ASSERT_IF(kind == FunctionSyntaxKind::Statement, atom != nullptr);
 
+  RootedObject proto(cx);
+  if (!GetFunctionPrototype(cx, generatorKind, asyncKind, &proto)) {
+    return nullptr;
+  }
+
   RootedFunction fun(cx);
 
   gc::AllocKind allocKind = gc::AllocKind::FUNCTION;
-  JSFunction::Flags flags;
+  FunctionFlags flags;
   bool isExtendedUnclonedSelfHostedFunctionName =
       isSelfHosting && atom && IsExtendedUnclonedSelfHostedFunctionName(atom);
   MOZ_ASSERT_IF(isExtendedUnclonedSelfHostedFunctionName, !inFunctionBox);
 
   switch (kind) {
     case FunctionSyntaxKind::Expression:
       flags = (generatorKind == GeneratorKind::NotGenerator &&
                        asyncKind == FunctionAsyncKind::SyncFunction
-                   ? JSFunction::INTERPRETED_LAMBDA
-                   : JSFunction::INTERPRETED_LAMBDA_GENERATOR_OR_ASYNC);
+                   ? FunctionFlags::INTERPRETED_LAMBDA
+                   : FunctionFlags::INTERPRETED_LAMBDA_GENERATOR_OR_ASYNC);
       break;
     case FunctionSyntaxKind::Arrow:
-      flags = JSFunction::INTERPRETED_LAMBDA_ARROW;
+      flags = FunctionFlags::INTERPRETED_LAMBDA_ARROW;
       allocKind = gc::AllocKind::FUNCTION_EXTENDED;
       break;
     case FunctionSyntaxKind::Method:
-      flags = JSFunction::INTERPRETED_METHOD;
+      flags = FunctionFlags::INTERPRETED_METHOD;
       allocKind = gc::AllocKind::FUNCTION_EXTENDED;
       break;
     case FunctionSyntaxKind::ClassConstructor:
     case FunctionSyntaxKind::DerivedClassConstructor:
-      flags = JSFunction::INTERPRETED_CLASS_CONSTRUCTOR;
+      flags = FunctionFlags::INTERPRETED_CLASS_CONSTRUCTOR;
       allocKind = gc::AllocKind::FUNCTION_EXTENDED;
       break;
     case FunctionSyntaxKind::Getter:
-      flags = JSFunction::INTERPRETED_GETTER;
+      flags = FunctionFlags::INTERPRETED_GETTER;
       allocKind = gc::AllocKind::FUNCTION_EXTENDED;
       break;
     case FunctionSyntaxKind::Setter:
-      flags = JSFunction::INTERPRETED_SETTER;
+      flags = FunctionFlags::INTERPRETED_SETTER;
       allocKind = gc::AllocKind::FUNCTION_EXTENDED;
       break;
     default:
       MOZ_ASSERT(kind == FunctionSyntaxKind::Statement);
       if (isExtendedUnclonedSelfHostedFunctionName) {
         allocKind = gc::AllocKind::FUNCTION_EXTENDED;
       }
       flags = (generatorKind == GeneratorKind::NotGenerator &&
                        asyncKind == FunctionAsyncKind::SyncFunction
-                   ? JSFunction::INTERPRETED_NORMAL
-                   : JSFunction::INTERPRETED_GENERATOR_OR_ASYNC);
+                   ? FunctionFlags::INTERPRETED_NORMAL
+                   : FunctionFlags::INTERPRETED_GENERATOR_OR_ASYNC);
   }
 
   fun = NewFunctionWithProto(cx, nullptr, 0, flags, nullptr, atom, proto,
                              allocKind, TenuredObject);
   if (!fun) {
     return nullptr;
   }
   if (isSelfHosting) {
     fun->setIsSelfHostedBuiltin();
     MOZ_ASSERT(!fun->isInterpretedLazy());
   }
   return fun;
 }
 
 JSFunction* ParserBase::newFunction(HandleAtom atom, FunctionSyntaxKind kind,
                                     GeneratorKind generatorKind,
-                                    FunctionAsyncKind asyncKind,
-                                    HandleObject proto /* = nullptr */) {
-  return AllocNewFunction(cx_, atom, kind, generatorKind, asyncKind, proto,
+                                    FunctionAsyncKind asyncKind) {
+  return AllocNewFunction(cx_, atom, kind, generatorKind, asyncKind,
                           options().selfHostingMode, pc_->isFunctionBox());
 }
 
 template <class ParseHandler, typename Unit>
 bool GeneralParser<ParseHandler, Unit>::matchOrInsertSemicolon(
     Modifier modifier /* = TokenStream::SlashIsRegExp */) {
   TokenKind tt = TokenKind::Eof;
   if (!tokenStream.peekTokenSameLine(&tt, modifier)) {
@@ -2094,20 +2098,20 @@ bool ParserBase::leaveInnerFunction(Pars
       outerpc->setSuperScopeNeedsHomeObject();
     }
   }
 
   // Lazy functions inner to another lazy function need to be remembered by
   // the inner function so that if the outer function is eventually parsed
   // we do not need any further parsing or processing of the inner function.
   //
-  // Append the inner function here unconditionally; the vector is only used
+  // Append the inner functionbox here unconditionally; the vector is only used
   // if the Parser using outerpc is a syntax parsing. See
   // GeneralParser<SyntaxParseHandler>::finishFunction.
-  if (!outerpc->innerFunctionsForLazy.append(pc_->functionBox()->function())) {
+  if (!outerpc->innerFunctionBoxesForLazy.append(pc_->functionBox())) {
     return false;
   }
 
   PropagateTransitiveParseFlags(pc_->functionBox(), outerpc->sc());
 
   return true;
 }
 
@@ -2596,22 +2600,18 @@ GeneralParser<ParseHandler, Unit>::funct
   if (handler_.canSkipLazyInnerFunctions()) {
     if (!skipLazyInnerFunction(funNode, toStringStart, kind, tryAnnexB)) {
       return null();
     }
 
     return funNode;
   }
 
-  RootedObject proto(cx_);
-  if (!GetFunctionPrototype(cx_, generatorKind, asyncKind, &proto)) {
-    return null();
-  }
   RootedFunction fun(
-      cx_, newFunction(funName, kind, generatorKind, asyncKind, proto));
+      cx_, newFunction(funName, kind, generatorKind, asyncKind));
   if (!fun) {
     return null();
   }
 
   // Speculatively parse using the directives of the parent parsing context.
   // If a directive is encountered (e.g., "use strict") that changes how the
   // function should have been parsed, we backup and reparse with the new set
   // of directives.
--- a/js/src/frontend/Parser.h
+++ b/js/src/frontend/Parser.h
@@ -396,18 +396,17 @@ class MOZ_STACK_CLASS ParserBase : publi
   bool hasValidSimpleStrictParameterNames();
 
   /*
    * Create a new function object given a name (which is optional if this is
    * a function expression).
    */
   JSFunction* newFunction(HandleAtom atom, FunctionSyntaxKind kind,
                           GeneratorKind generatorKind,
-                          FunctionAsyncKind asyncKind,
-                          HandleObject proto = nullptr);
+                          FunctionAsyncKind asyncKind);
 
   // A Parser::Mark is the extension of the LifoAlloc::Mark to the entire
   // Parser's state. Note: clients must still take care that any ParseContext
   // that points into released ParseNodes is destroyed.
   class Mark {
     friend class ParserBase;
     LifoAlloc::Mark mark;
     TraceListNode* traceListHead;
@@ -1894,16 +1893,16 @@ mozilla::Maybe<VarScope::Data*> NewVarSc
                                                 ParseContext* pc);
 mozilla::Maybe<LexicalScope::Data*> NewLexicalScopeData(
     JSContext* context, ParseContext::Scope& scope, LifoAlloc& alloc,
     ParseContext* pc);
 
 JSFunction* AllocNewFunction(JSContext* cx, HandleAtom atom,
                              FunctionSyntaxKind kind,
                              GeneratorKind generatorKind,
-                             FunctionAsyncKind asyncKind, HandleObject proto,
+                             FunctionAsyncKind asyncKind,
                              bool isSelfHosting = false,
                              bool inFunctionBox = false);
 
 } /* namespace frontend */
 } /* namespace js */
 
 #endif /* frontend_Parser_h */
--- a/js/src/frontend/SharedContext.h
+++ b/js/src/frontend/SharedContext.h
@@ -392,17 +392,17 @@ class FunctionBox : public ObjectBox, pu
   bool isNamedLambda_ : 1;
   bool isGetter_ : 1;
   bool isSetter_ : 1;
   bool isMethod_ : 1;
 
   bool isInterpreted_ : 1;
   bool isInterpretedLazy_ : 1;
 
-  JSFunction::FunctionKind kind_;
+  FunctionFlags::FunctionKind kind_;
   JSAtom* explicitName_;
 
   uint16_t nargs_;
 
   FunctionBox(JSContext* cx, TraceListNode* traceListHead, JSFunction* fun,
               uint32_t toStringStart, Directives directives, bool extraWarnings,
               GeneratorKind generatorKind, FunctionAsyncKind asyncKind);
 
@@ -540,17 +540,17 @@ class FunctionBox : public ObjectBox, pu
   bool isInterpretedLazy() const { return isInterpretedLazy_; }
   void setIsInterpretedLazy(bool interpretedLazy) { isInterpretedLazy_ = interpretedLazy; }
 
   void initLazyScript(LazyScript* script) { 
     function()->initLazyScript(script);
     setIsInterpretedLazy(function()->isInterpretedLazy());
   }
 
-  JSFunction::FunctionKind kind() { return kind_; }
+  FunctionFlags::FunctionKind kind() { return kind_; }
 
   JSAtom* explicitName() const { return explicitName_; }
 
   void setHasExtensibleScope() { hasExtensibleScope_ = true; }
   void setHasThisBinding() { hasThisBinding_ = true; }
   void setArgumentsHasLocalBinding() { argumentsHasLocalBinding_ = true; }
   void setDefinitelyNeedsArgsObj() {
     MOZ_ASSERT(argumentsHasLocalBinding_);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/class/bug1567579.js
@@ -0,0 +1,4 @@
+var res = "class { constructor() {} }";
+var test = eval("(" + res + ").toString()");
+
+assertEq(test, res);
\ No newline at end of file
--- a/js/src/jit/BaselineCodeGen.cpp
+++ b/js/src/jit/BaselineCodeGen.cpp
@@ -4455,18 +4455,19 @@ bool BaselineInterpreterCodeGen::emit_JS
     frame.pushEvalNewTarget();
     masm.jump(&done);
   }
 
   masm.bind(&isFunction);
 
   Label notArrow;
   masm.andPtr(Imm32(uint32_t(CalleeTokenMask)), scratch1);
-  masm.branchFunctionKind(Assembler::NotEqual, JSFunction::FunctionKind::Arrow,
-                          scratch1, scratch2, &notArrow);
+  masm.branchFunctionKind(Assembler::NotEqual,
+                          FunctionFlags::FunctionKind::Arrow, scratch1,
+                          scratch2, &notArrow);
   {
     // Case 2: arrow function.
     masm.pushValue(
         Address(scratch1, FunctionExtended::offsetOfArrowNewTargetSlot()));
     masm.jump(&done);
   }
 
   masm.bind(&notArrow);
@@ -5736,17 +5737,17 @@ bool BaselineCodeGen<Handler>::emit_JSOP
   masm.branchPtr(Assembler::BelowOrEqual, proto, ImmWord(1), &needVMCall);
 
   // Use VMCall for non-JSFunction objects (eg. Proxy)
   masm.branchTestObjClass(Assembler::NotEqual, proto, &JSFunction::class_,
                           scratch, proto, &needVMCall);
 
   // Use VMCall if not constructor
   masm.load16ZeroExtend(Address(proto, JSFunction::offsetOfFlags()), scratch);
-  masm.branchTest32(Assembler::Zero, scratch, Imm32(JSFunction::CONSTRUCTOR),
+  masm.branchTest32(Assembler::Zero, scratch, Imm32(FunctionFlags::CONSTRUCTOR),
                     &needVMCall);
 
   // Valid constructor
   Label hasSuperFun;
   masm.jump(&hasSuperFun);
 
   // Slow path VM Call
   masm.bind(&needVMCall);
--- a/js/src/jit/CacheIRCompiler.cpp
+++ b/js/src/jit/CacheIRCompiler.cpp
@@ -2673,25 +2673,25 @@ bool CacheIRCompiler::emitLoadFunctionLe
 
   // Get the JSFunction flags.
   masm.load16ZeroExtend(Address(obj, JSFunction::offsetOfFlags()), scratch);
 
   // Functions with lazy scripts don't store their length.
   // If the length was resolved before the length property might be shadowed.
   masm.branchTest32(
       Assembler::NonZero, scratch,
-      Imm32(JSFunction::INTERPRETED_LAZY | JSFunction::RESOLVED_LENGTH),
+      Imm32(FunctionFlags::INTERPRETED_LAZY | FunctionFlags::RESOLVED_LENGTH),
       failure->label());
 
   Label boundFunction;
-  masm.branchTest32(Assembler::NonZero, scratch, Imm32(JSFunction::BOUND_FUN),
-                    &boundFunction);
+  masm.branchTest32(Assembler::NonZero, scratch,
+                    Imm32(FunctionFlags::BOUND_FUN), &boundFunction);
   Label interpreted;
-  masm.branchTest32(Assembler::NonZero, scratch, Imm32(JSFunction::INTERPRETED),
-                    &interpreted);
+  masm.branchTest32(Assembler::NonZero, scratch,
+                    Imm32(FunctionFlags::INTERPRETED), &interpreted);
 
   // Load the length of the native function.
   masm.load16ZeroExtend(Address(obj, JSFunction::offsetOfNargs()), scratch);
   Label done;
   masm.jump(&done);
 
   masm.bind(&boundFunction);
   // Bound functions might have a non-int32 length.
@@ -3088,32 +3088,32 @@ bool CacheIRCompiler::emitGuardFunctionI
   AutoScratchRegister scratch(allocator, masm);
 
   FailurePath* failure;
   if (!addFailurePath(&failure)) {
     return false;
   }
 
   // Ensure obj is a constructor
-  masm.branchTestFunctionFlags(funcReg, JSFunction::CONSTRUCTOR,
+  masm.branchTestFunctionFlags(funcReg, FunctionFlags::CONSTRUCTOR,
                                Assembler::Zero, failure->label());
   return true;
 }
 
 bool CacheIRCompiler::emitGuardNotClassConstructor() {
   Register fun = allocator.useRegister(masm, reader.objOperandId());
   AutoScratchRegister scratch(allocator, masm);
 
   FailurePath* failure;
   if (!addFailurePath(&failure)) {
     return false;
   }
 
-  masm.branchFunctionKind(Assembler::Equal, JSFunction::ClassConstructor, fun,
-                          scratch, failure->label());
+  masm.branchFunctionKind(Assembler::Equal, FunctionFlags::ClassConstructor,
+                          fun, scratch, failure->label());
   return true;
 }
 
 bool CacheIRCompiler::emitLoadDenseElementHoleResult() {
   JitSpew(JitSpew_Codegen, __FUNCTION__);
   AutoOutputRegister output(*this);
   Register obj = allocator.useRegister(masm, reader.objOperandId());
   Register index = allocator.useRegister(masm, reader.int32OperandId());
--- a/js/src/jit/CodeGenerator.cpp
+++ b/js/src/jit/CodeGenerator.cpp
@@ -3402,17 +3402,17 @@ void CodeGenerator::visitLambda(LLambda*
   MOZ_ASSERT(!info.singletonType);
 
   TemplateObject templateObject(info.funUnsafe());
   masm.createGCObject(output, tempReg, templateObject, gc::DefaultHeap,
                       ool->entry());
 
   emitLambdaInit(output, envChain, info);
 
-  if (info.flags & JSFunction::EXTENDED) {
+  if (info.flags & FunctionFlags::EXTENDED) {
     static_assert(FunctionExtended::NUM_EXTENDED_SLOTS == 2,
                   "All slots must be initialized");
     masm.storeValue(UndefinedValue(),
                     Address(output, FunctionExtended::offsetOfExtendedSlot(0)));
     masm.storeValue(UndefinedValue(),
                     Address(output, FunctionExtended::offsetOfExtendedSlot(1)));
   }
 
@@ -3490,17 +3490,17 @@ void CodeGenerator::visitLambdaArrow(LLa
   masm.createGCObject(output, tempReg, templateObject, gc::DefaultHeap,
                       ool->entry());
 
   masm.pop(newTarget.scratchReg());
 
   emitLambdaInit(output, envChain, info);
 
   // Initialize extended slots. Lexical |this| is stored in the first one.
-  MOZ_ASSERT(info.flags & JSFunction::EXTENDED);
+  MOZ_ASSERT(info.flags & FunctionFlags::EXTENDED);
   static_assert(FunctionExtended::NUM_EXTENDED_SLOTS == 2,
                 "All slots must be initialized");
   static_assert(FunctionExtended::ARROW_NEWTARGET_SLOT == 0,
                 "|new.target| must be stored in first slot");
   masm.storeValue(newTarget,
                   Address(output, FunctionExtended::offsetOfExtendedSlot(0)));
   masm.storeValue(UndefinedValue(),
                   Address(output, FunctionExtended::offsetOfExtendedSlot(1)));
@@ -4973,17 +4973,17 @@ void CodeGenerator::visitCallGeneric(LCa
   } else {
     // See visitCallKnown.
     if (call->mir()->needsArgCheck()) {
       masm.branchIfFunctionHasNoJitEntry(calleereg, /* isConstructing */ false,
                                          &invoke);
     } else {
       masm.branchIfFunctionHasNoScript(calleereg, &invoke);
     }
-    masm.branchFunctionKind(Assembler::Equal, JSFunction::ClassConstructor,
+    masm.branchFunctionKind(Assembler::Equal, FunctionFlags::ClassConstructor,
                             calleereg, objreg, &invoke);
   }
 
   if (call->mir()->maybeCrossRealm()) {
     masm.switchToObjectRealm(calleereg, objreg);
   }
 
   if (call->mir()->needsArgCheck()) {
@@ -5417,17 +5417,17 @@ void CodeGenerator::emitApplyGeneric(T* 
 
   Label end, invoke;
 
   // Guard that calleereg is an interpreted function with a JSScript.
   masm.branchIfFunctionHasNoJitEntry(calleereg, /* constructing */ false,
                                      &invoke);
 
   // Guard that calleereg is not a class constrcuctor
-  masm.branchFunctionKind(Assembler::Equal, JSFunction::ClassConstructor,
+  masm.branchFunctionKind(Assembler::Equal, FunctionFlags::ClassConstructor,
                           calleereg, objreg, &invoke);
 
   // Call with an Ion frame or a rectifier frame.
   {
     if (apply->mir()->maybeCrossRealm()) {
       masm.switchToObjectRealm(calleereg, objreg);
     }
 
@@ -12719,21 +12719,22 @@ void CodeGenerator::emitIsCallableOrCons
   // An object is constructor iff:
   //  ((is<JSFunction>() && as<JSFunction>().isConstructor) ||
   //   (getClass()->cOps && getClass()->cOps->construct)).
   masm.branchPtr(Assembler::NotEqual, output, ImmPtr(&JSFunction::class_),
                  &notFunction);
   if (mode == Callable) {
     masm.move32(Imm32(1), output);
   } else {
-    static_assert(mozilla::IsPowerOfTwo(unsigned(JSFunction::CONSTRUCTOR)),
-                  "JSFunction::CONSTRUCTOR has only one bit set");
+    static_assert(mozilla::IsPowerOfTwo(unsigned(FunctionFlags::CONSTRUCTOR)),
+                  "FunctionFlags::CONSTRUCTOR has only one bit set");
 
     masm.load16ZeroExtend(Address(object, JSFunction::offsetOfFlags()), output);
-    masm.rshift32(Imm32(mozilla::FloorLog2(JSFunction::CONSTRUCTOR)), output);
+    masm.rshift32(Imm32(mozilla::FloorLog2(FunctionFlags::CONSTRUCTOR)),
+                  output);
     masm.and32(Imm32(1), output);
   }
   masm.jump(&done);
 
   masm.bind(&notFunction);
 
   // Just skim proxies off. Their notion of isCallable()/isConstructor() is
   // more complicated.
@@ -13663,88 +13664,91 @@ void CodeGenerator::visitFinishBoundFunc
 
   // Get the function flags.
   masm.load16ZeroExtend(Address(target, JSFunction::offsetOfFlags()), temp1);
 
   // Functions with lazy scripts don't store their length.
   // If the length or name property is resolved, it might be shadowed.
   masm.branchTest32(
       Assembler::NonZero, temp1,
-      Imm32(JSFunction::INTERPRETED_LAZY | JSFunction::RESOLVED_NAME |
-            JSFunction::RESOLVED_LENGTH),
+      Imm32(FunctionFlags::INTERPRETED_LAZY | FunctionFlags::RESOLVED_NAME |
+            FunctionFlags::RESOLVED_LENGTH),
       slowPath);
 
   Label notBoundTarget, loadName;
-  masm.branchTest32(Assembler::Zero, temp1, Imm32(JSFunction::BOUND_FUN),
+  masm.branchTest32(Assembler::Zero, temp1, Imm32(FunctionFlags::BOUND_FUN),
                     &notBoundTarget);
   {
     // Call into the VM if the target's name atom contains the bound
     // function prefix.
     masm.branchTest32(Assembler::NonZero, temp1,
-                      Imm32(JSFunction::HAS_BOUND_FUNCTION_NAME_PREFIX),
+                      Imm32(FunctionFlags::HAS_BOUND_FUNCTION_NAME_PREFIX),
                       slowPath);
 
     // We also take the slow path when target's length isn't an int32.
     masm.branchTestInt32(Assembler::NotEqual,
                          Address(target, boundLengthOffset), slowPath);
 
     // Bound functions reuse HAS_GUESSED_ATOM for
     // HAS_BOUND_FUNCTION_NAME_PREFIX, so skip the guessed atom check below.
     static_assert(
-        JSFunction::HAS_BOUND_FUNCTION_NAME_PREFIX ==
-            JSFunction::HAS_GUESSED_ATOM,
+        FunctionFlags::HAS_BOUND_FUNCTION_NAME_PREFIX ==
+            FunctionFlags::HAS_GUESSED_ATOM,
         "HAS_BOUND_FUNCTION_NAME_PREFIX is shared with HAS_GUESSED_ATOM");
     masm.jump(&loadName);
   }
   masm.bind(&notBoundTarget);
 
   Label guessed, hasName;
   masm.branchTest32(Assembler::NonZero, temp1,
-                    Imm32(JSFunction::HAS_GUESSED_ATOM), &guessed);
+                    Imm32(FunctionFlags::HAS_GUESSED_ATOM), &guessed);
   masm.bind(&loadName);
   masm.loadPtr(Address(target, JSFunction::offsetOfAtom()), temp2);
   masm.branchTestPtr(Assembler::NonZero, temp2, temp2, &hasName);
   {
     masm.bind(&guessed);
 
     // Unnamed class expression don't have a name property. To avoid
     // looking it up from the prototype chain, we take the slow path here.
-    masm.branchFunctionKind(Assembler::Equal, JSFunction::ClassConstructor,
+    masm.branchFunctionKind(Assembler::Equal, FunctionFlags::ClassConstructor,
                             target, temp2, slowPath);
 
     // An absent name property defaults to the empty string.
     const JSAtomState& names = gen->runtime->names();
     masm.movePtr(ImmGCPtr(names.empty), temp2);
   }
   masm.bind(&hasName);
 
   // Store the target's name atom in the bound function as is.
   masm.storePtr(temp2, Address(bound, JSFunction::offsetOfAtom()));
 
   // Set the BOUND_FN flag and, if the target is a constructor, the
   // CONSTRUCTOR flag.
   Label isConstructor, boundFlagsComputed;
   masm.load16ZeroExtend(Address(bound, JSFunction::offsetOfFlags()), temp2);
-  masm.branchTest32(Assembler::NonZero, temp1, Imm32(JSFunction::CONSTRUCTOR),
-                    &isConstructor);
+  masm.branchTest32(Assembler::NonZero, temp1,
+                    Imm32(FunctionFlags::CONSTRUCTOR), &isConstructor);
   {
-    masm.or32(Imm32(JSFunction::BOUND_FUN), temp2);
+    masm.or32(Imm32(FunctionFlags::BOUND_FUN), temp2);
     masm.jump(&boundFlagsComputed);
   }
   masm.bind(&isConstructor);
-  { masm.or32(Imm32(JSFunction::BOUND_FUN | JSFunction::CONSTRUCTOR), temp2); }
+  {
+    masm.or32(Imm32(FunctionFlags::BOUND_FUN | FunctionFlags::CONSTRUCTOR),
+              temp2);
+  }
   masm.bind(&boundFlagsComputed);
   masm.store16(temp2, Address(bound, JSFunction::offsetOfFlags()));
 
   // Load the target function's length.
   Label isInterpreted, isBound, lengthLoaded;
-  masm.branchTest32(Assembler::NonZero, temp1, Imm32(JSFunction::BOUND_FUN),
+  masm.branchTest32(Assembler::NonZero, temp1, Imm32(FunctionFlags::BOUND_FUN),
                     &isBound);
-  masm.branchTest32(Assembler::NonZero, temp1, Imm32(JSFunction::INTERPRETED),
-                    &isInterpreted);
+  masm.branchTest32(Assembler::NonZero, temp1,
+                    Imm32(FunctionFlags::INTERPRETED), &isInterpreted);
   {
     // Load the length property of a native function.
     masm.load16ZeroExtend(Address(target, JSFunction::offsetOfNargs()), temp1);
     masm.jump(&lengthLoaded);
   }
   masm.bind(&isBound);
   {
     // Load the length property of a bound function.
--- a/js/src/jit/MIR.h
+++ b/js/src/jit/MIR.h
@@ -6693,29 +6693,29 @@ struct LambdaFunctionInfo {
   uint16_t flags;
   uint16_t nargs;
   gc::Cell* scriptOrLazyScript;
   bool singletonType;
   bool useSingletonForClone;
 
   explicit LambdaFunctionInfo(JSFunction* fun)
       : fun_(fun),
-        flags(fun->flags()),
+        flags(fun->flags().toRaw()),
         nargs(fun->nargs()),
         scriptOrLazyScript(fun->hasScript() ? (gc::Cell*)fun->nonLazyScript()
                                             : (gc::Cell*)fun->lazyScript()),
         singletonType(fun->isSingleton()),
         useSingletonForClone(ObjectGroup::useSingletonForClone(fun)) {
     // If this assert fails, make sure CodeGenerator::visitLambda does the
     // right thing. We can't assert this off-thread in CodeGenerator,
     // because fun->isAsync() accesses the script/lazyScript and can race
     // with delazification on the main thread.
-    MOZ_ASSERT_IF(flags & JSFunction::EXTENDED, fun->isArrow() ||
-                                                    fun->allowSuperProperty() ||
-                                                    fun->isSelfHostedBuiltin());
+    MOZ_ASSERT_IF(flags & FunctionFlags::EXTENDED,
+                  fun->isArrow() || fun->allowSuperProperty() ||
+                      fun->isSelfHostedBuiltin());
   }
 
   // Be careful when calling this off-thread. Don't call any JSFunction*
   // methods that depend on script/lazyScript - this can race with
   // delazification on the main thread.
   JSFunction* funUnsafe() const { return fun_; }
 
   bool appendRoots(MRootList& roots) const {
--- a/js/src/jit/MacroAssembler-inl.h
+++ b/js/src/jit/MacroAssembler-inl.h
@@ -364,33 +364,33 @@ void MacroAssembler::branchTestFunctionF
   int32_t bit = IMM32_16ADJ(flags);
   Address address(fun, JSFunction::offsetOfNargs());
   branchTest32(cond, address, Imm32(bit), label);
 }
 
 void MacroAssembler::branchIfFunctionHasNoJitEntry(Register fun,
                                                    bool isConstructing,
                                                    Label* label) {
-  int32_t flags = JSFunction::INTERPRETED | JSFunction::INTERPRETED_LAZY;
+  int32_t flags = FunctionFlags::INTERPRETED | FunctionFlags::INTERPRETED_LAZY;
   if (!isConstructing) {
-    flags |= JSFunction::WASM_JIT_ENTRY;
+    flags |= FunctionFlags::WASM_JIT_ENTRY;
   }
   branchTestFunctionFlags(fun, flags, Assembler::Zero, label);
 }
 
 void MacroAssembler::branchIfFunctionHasNoScript(Register fun, Label* label) {
-  int32_t flags = JSFunction::INTERPRETED;
+  int32_t flags = FunctionFlags::INTERPRETED;
   branchTestFunctionFlags(fun, flags, Assembler::Zero, label);
 }
 
 void MacroAssembler::branchIfInterpreted(Register fun, bool isConstructing,
                                          Label* label) {
-  int32_t flags = JSFunction::INTERPRETED | JSFunction::INTERPRETED_LAZY;
+  int32_t flags = FunctionFlags::INTERPRETED | FunctionFlags::INTERPRETED_LAZY;
   if (!isConstructing) {
-    flags |= JSFunction::WASM_JIT_ENTRY;
+    flags |= FunctionFlags::WASM_JIT_ENTRY;
   }
   branchTestFunctionFlags(fun, flags, Assembler::NonZero, label);
 }
 
 void MacroAssembler::branchIfObjectEmulatesUndefined(Register objReg,
                                                      Register scratch,
                                                      Label* slowCheck,
                                                      Label* label) {
@@ -401,26 +401,26 @@ void MacroAssembler::branchIfObjectEmula
   branchTestClassIsProxy(true, scratch, slowCheck);
 
   Address flags(scratch, Class::offsetOfFlags());
   branchTest32(Assembler::NonZero, flags, Imm32(JSCLASS_EMULATES_UNDEFINED),
                label);
 }
 
 void MacroAssembler::branchFunctionKind(Condition cond,
-                                        JSFunction::FunctionKind kind,
+                                        FunctionFlags::FunctionKind kind,
                                         Register fun, Register scratch,
                                         Label* label) {
   // 16-bit loads are slow and unaligned 32-bit loads may be too so
   // perform an aligned 32-bit load and adjust the bitmask accordingly.
   MOZ_ASSERT(JSFunction::offsetOfNargs() % sizeof(uint32_t) == 0);
   MOZ_ASSERT(JSFunction::offsetOfFlags() == JSFunction::offsetOfNargs() + 2);
   Address address(fun, JSFunction::offsetOfNargs());
-  int32_t mask = IMM32_16ADJ(JSFunction::FUNCTION_KIND_MASK);
-  int32_t bit = IMM32_16ADJ(kind << JSFunction::FUNCTION_KIND_SHIFT);
+  int32_t mask = IMM32_16ADJ(FunctionFlags::FUNCTION_KIND_MASK);
+  int32_t bit = IMM32_16ADJ(kind << FunctionFlags::FUNCTION_KIND_SHIFT);
   load32(address, scratch);
   and32(Imm32(mask), scratch);
   branch32(cond, scratch, Imm32(bit), label);
 }
 
 void MacroAssembler::branchTestObjClass(Condition cond, Register obj,
                                         const js::Class* clasp,
                                         Register scratch,
--- a/js/src/jit/MacroAssembler.cpp
+++ b/js/src/jit/MacroAssembler.cpp
@@ -2865,21 +2865,22 @@ void MacroAssembler::moveRegPair(Registe
 // ===============================================================
 // Branch functions
 
 void MacroAssembler::branchIfNotInterpretedConstructor(Register fun,
                                                        Register scratch,
                                                        Label* label) {
   // First, ensure it's a scripted function. It is fine if it is still lazy.
   branchTestFunctionFlags(
-      fun, JSFunction::INTERPRETED | JSFunction::INTERPRETED_LAZY,
+      fun, FunctionFlags::INTERPRETED | FunctionFlags::INTERPRETED_LAZY,
       Assembler::Zero, label);
 
   // Check if the CONSTRUCTOR bit is set.
-  branchTestFunctionFlags(fun, JSFunction::CONSTRUCTOR, Assembler::Zero, label);
+  branchTestFunctionFlags(fun, FunctionFlags::CONSTRUCTOR, Assembler::Zero,
+                          label);
 }
 
 void MacroAssembler::branchTestObjGroupNoSpectreMitigations(
     Condition cond, Register obj, const Address& group, Register scratch,
     Label* label) {
   // Note: obj and scratch registers may alias.
   MOZ_ASSERT(group.base != scratch);
   MOZ_ASSERT(group.base != obj);
--- a/js/src/jit/MacroAssembler.h
+++ b/js/src/jit/MacroAssembler.h
@@ -1302,18 +1302,19 @@ class MacroAssembler : public MacroAssem
                                       Condition cond, Label* label);
 
   inline void branchIfFunctionHasNoJitEntry(Register fun, bool isConstructing,
                                             Label* label);
   inline void branchIfFunctionHasNoScript(Register fun, Label* label);
   inline void branchIfInterpreted(Register fun, bool isConstructing,
                                   Label* label);
 
-  inline void branchFunctionKind(Condition cond, JSFunction::FunctionKind kind,
-                                 Register fun, Register scratch, Label* label);
+  inline void branchFunctionKind(Condition cond,
+                                 FunctionFlags::FunctionKind kind, Register fun,
+                                 Register scratch, Label* label);
 
   void branchIfNotInterpretedConstructor(Register fun, Register scratch,
                                          Label* label);
 
   inline void branchIfObjectEmulatesUndefined(Register objReg, Register scratch,
                                               Label* slowCheck, Label* label);
 
   // For all methods below: spectreRegToZero is a register that will be zeroed
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -3308,17 +3308,17 @@ static JSObject* CloneFunctionObject(JSC
     RootedValue v(cx, ObjectValue(*funobj));
     ReportIsNotFunction(cx, v);
     return nullptr;
   }
 
   // Only allow cloning normal, interpreted functions.
   RootedFunction fun(cx, &funobj->as<JSFunction>());
   if (fun->isNative() || fun->isBoundFunction() ||
-      fun->kind() != JSFunction::NormalFunction || fun->isExtended() ||
+      fun->kind() != FunctionFlags::NormalFunction || fun->isExtended() ||
       fun->isSelfHostedBuiltin()) {
     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                               JSMSG_CANT_CLONE_OBJECT);
     return nullptr;
   }
 
   if (fun->isInterpretedLazy()) {
     AutoRealm ar(cx, fun);
--- a/js/src/jsexn.cpp
+++ b/js/src/jsexn.cpp
@@ -577,17 +577,17 @@ JSObject* ErrorObject::createConstructor
   } else {
     RootedFunction proto(
         cx, GlobalObject::getOrCreateErrorConstructor(cx, cx->global()));
     if (!proto) {
       return nullptr;
     }
 
     ctor = NewFunctionWithProto(
-        cx, Error, 1, JSFunction::NATIVE_CTOR, nullptr, ClassName(key, cx),
+        cx, Error, 1, FunctionFlags::NATIVE_CTOR, nullptr, ClassName(key, cx),
         proto, gc::AllocKind::FUNCTION_EXTENDED, SingletonObject);
   }
 
   if (!ctor) {
     return nullptr;
   }
 
   ctor->as<JSFunction>().setExtendedSlot(0, Int32Value(type));
--- a/js/src/vm/AsyncFunction.cpp
+++ b/js/src/vm/AsyncFunction.cpp
@@ -40,18 +40,19 @@ bool GlobalObject::initAsyncFunction(JSC
 
   RootedObject proto(
       cx, GlobalObject::getOrCreateFunctionConstructor(cx, cx->global()));
   if (!proto) {
     return false;
   }
   HandlePropertyName name = cx->names().AsyncFunction;
   RootedObject asyncFunction(
-      cx, NewFunctionWithProto(cx, AsyncFunctionConstructor, 1,
-                               JSFunction::NATIVE_CTOR, nullptr, name, proto));
+      cx,
+      NewFunctionWithProto(cx, AsyncFunctionConstructor, 1,
+                           FunctionFlags::NATIVE_CTOR, nullptr, name, proto));
   if (!asyncFunction) {
     return false;
   }
   if (!LinkConstructorAndPrototype(cx, asyncFunction, asyncFunctionProto,
                                    JSPROP_PERMANENT | JSPROP_READONLY,
                                    JSPROP_READONLY)) {
     return false;
   }
--- a/js/src/vm/AsyncIteration.cpp
+++ b/js/src/vm/AsyncIteration.cpp
@@ -467,17 +467,17 @@ static const JSFunctionSpec async_genera
   if (!proto) {
     return false;
   }
   HandlePropertyName name = cx->names().AsyncGeneratorFunction;
 
   // 25.3.1 The AsyncGeneratorFunction Constructor
   RootedObject asyncGenFunction(
       cx, NewFunctionWithProto(cx, AsyncGeneratorConstructor, 1,
-                               JSFunction::NATIVE_CTOR, nullptr, name, proto,
+                               FunctionFlags::NATIVE_CTOR, nullptr, name, proto,
                                gc::AllocKind::FUNCTION, SingletonObject));
   if (!asyncGenFunction) {
     return false;
   }
   if (!LinkConstructorAndPrototype(cx, asyncGenFunction, asyncGenerator,
                                    JSPROP_PERMANENT | JSPROP_READONLY,
                                    JSPROP_READONLY)) {
     return false;
--- a/js/src/vm/CompilationAndEvaluation.cpp
+++ b/js/src/vm/CompilationAndEvaluation.cpp
@@ -342,17 +342,17 @@ class FunctionCompiler {
                                             &enclosingScope)) {
       return nullptr;
     }
 
     cx_->check(enclosingEnv);
 
     RootedFunction fun(
         cx_,
-        NewScriptedFunction(cx_, 0, JSFunction::INTERPRETED_NORMAL,
+        NewScriptedFunction(cx_, 0, FunctionFlags::INTERPRETED_NORMAL,
                             nameIsIdentifier_ ? HandleAtom(nameAtom_) : nullptr,
                             /* proto = */ nullptr, gc::AllocKind::FUNCTION,
                             TenuredObject, enclosingEnv));
     if (!fun) {
       return nullptr;
     }
 
     // Make sure the static scope chain matches up when we have a
--- a/js/src/vm/GeneratorObject.cpp
+++ b/js/src/vm/GeneratorObject.cpp
@@ -276,17 +276,17 @@ bool GlobalObject::initGenerators(JSCont
 
   RootedObject proto(
       cx, GlobalObject::getOrCreateFunctionConstructor(cx, cx->global()));
   if (!proto) {
     return false;
   }
   HandlePropertyName name = cx->names().GeneratorFunction;
   RootedObject genFunction(
-      cx, NewFunctionWithProto(cx, Generator, 1, JSFunction::NATIVE_CTOR,
+      cx, NewFunctionWithProto(cx, Generator, 1, FunctionFlags::NATIVE_CTOR,
                                nullptr, name, proto, gc::AllocKind::FUNCTION,
                                SingletonObject));
   if (!genFunction) {
     return false;
   }
   if (!LinkConstructorAndPrototype(cx, genFunction, genFunctionProto,
                                    JSPROP_PERMANENT | JSPROP_READONLY,
                                    JSPROP_READONLY)) {
--- a/js/src/vm/JSFunction-inl.h
+++ b/js/src/vm/JSFunction-inl.h
@@ -134,17 +134,17 @@ inline JSFunction* CloneFunctionObjectIf
   // This must be overwritten by some ultimate caller: there's no default
   // value to which we could sensibly initialize this.
   MOZ_MAKE_MEM_UNDEFINED(&fun->u, sizeof(u));