Merge autoland to mozilla-central. a=merge
authorDorel Luca <dluca@mozilla.com>
Sat, 31 Aug 2019 12:42:33 +0300
changeset 551539 56db66978b427e18362c3d1d733f7fcb61d2912e
parent 551527 cae93ef1993e02a136ef64d974856071b905997f (current diff)
parent 551524 4bed9794caf24942c017d05a0d24c2c7086d3c98 (diff)
child 551540 b3cc8963e8718dbd40761f14664f45320c258bbd
push id11865
push userbtara@mozilla.com
push dateMon, 02 Sep 2019 08:54:37 +0000
treeherdermozilla-beta@37f59c4671b3 [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 autoland to mozilla-central. a=merge
layout/reftests/forms/input/text/line-height-1.5.html
--- a/browser/actors/ContextMenuChild.jsm
+++ b/browser/actors/ContextMenuChild.jsm
@@ -618,21 +618,29 @@ class ContextMenuChild extends JSWindowA
     let spellInfo = null;
     let editFlags = null;
     let principal = null;
     let customMenuItems = null;
 
     let referrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
       Ci.nsIReferrerInfo
     );
-    referrerInfo.initWithNode(
-      context.onLink ? context.link : aEvent.composedTarget
-    );
+    referrerInfo.initWithNode(aEvent.composedTarget);
     referrerInfo = E10SUtils.serializeReferrerInfo(referrerInfo);
 
+    // In the case "onLink" we may have to send link referrerInfo to use in
+    // _openLinkInParameters
+    let linkReferrerInfo = null;
+    if (context.onLink) {
+      linkReferrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
+        Ci.nsIReferrerInfo
+      );
+      linkReferrerInfo.initWithNode(context.link);
+    }
+
     let target = context.target;
     if (target) {
       this._cleanContext();
     }
 
     editFlags = SpellCheckHelper.isEditable(
       aEvent.composedTarget,
       this.contentWindow
@@ -676,27 +684,18 @@ class ContextMenuChild extends JSWindowA
     };
 
     if (context.inFrame && !context.inSrcdocFrame) {
       data.frameReferrerInfo = E10SUtils.serializeReferrerInfo(
         doc.referrerInfo
       );
     }
 
-    // In the case "onLink" we may have to send target referrerInfo. This object
-    // may be used to in saveMedia function.
-    if (context.onLink) {
-      let targetReferrerInfo = Cc[
-        "@mozilla.org/referrer-info;1"
-      ].createInstance(Ci.nsIReferrerInfo);
-
-      targetReferrerInfo.initWithNode(aEvent.composedTarget);
-      data.targetReferrerInfo = E10SUtils.serializeReferrerInfo(
-        targetReferrerInfo
-      );
+    if (linkReferrerInfo) {
+      data.linkReferrerInfo = E10SUtils.serializeReferrerInfo(linkReferrerInfo);
     }
 
     if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
       data.customMenuItems = PageMenuChild.build(aEvent.composedTarget);
     }
 
     Services.obs.notifyObservers(
       { wrappedJSObject: data },
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -38,17 +38,17 @@ XPCOMUtils.defineLazyGetter(this, "Refer
 var gContextMenuContentData = null;
 
 function openContextMenu(aMessage, aBrowser, aActor) {
   let data = aMessage.data;
   let browser = aBrowser;
   let actor = aActor;
   let spellInfo = data.spellInfo;
   let frameReferrerInfo = data.frameReferrerInfo;
-  let targetReferrerInfo = data.targetReferrerInfo;
+  let linkReferrerInfo = data.linkReferrerInfo;
   let principal = data.principal;
   let storagePrincipal = data.storagePrincipal;
 
   if (spellInfo) {
     spellInfo.target = browser.messageManager;
   }
 
   let documentURIObject = makeURI(
@@ -56,18 +56,18 @@ function openContextMenu(aMessage, aBrow
     data.charSet,
     makeURI(data.baseURI)
   );
 
   if (frameReferrerInfo) {
     frameReferrerInfo = E10SUtils.deserializeReferrerInfo(frameReferrerInfo);
   }
 
-  if (targetReferrerInfo) {
-    targetReferrerInfo = E10SUtils.deserializeReferrerInfo(targetReferrerInfo);
+  if (linkReferrerInfo) {
+    linkReferrerInfo = E10SUtils.deserializeReferrerInfo(linkReferrerInfo);
   }
 
   // For now, JS Window Actors don't deserialize Principals automatically, so we
   // have to do it ourselves. See bug 1557852.
   if (principal) {
     principal = E10SUtils.deserializePrincipal(principal);
   }
   if (storagePrincipal) {
@@ -95,17 +95,17 @@ function openContextMenu(aMessage, aBrow
     principal,
     storagePrincipal,
     customMenuItems: data.customMenuItems,
     documentURIObject,
     docLocation: data.docLocation,
     charSet: data.charSet,
     referrerInfo: E10SUtils.deserializeReferrerInfo(data.referrerInfo),
     frameReferrerInfo,
-    targetReferrerInfo,
+    linkReferrerInfo,
     contentType: data.contentType,
     contentDisposition: data.contentDisposition,
     frameOuterWindowID: data.frameOuterWindowID,
     selectionInfo: data.selectionInfo,
     disableSetDesktopBackground: data.disableSetDesktopBackground,
     loginFillInfo: data.loginFillInfo,
     parentAllowsMixedContent: data.parentAllowsMixedContent,
     userContextId: data.userContextId,
@@ -1090,17 +1090,19 @@ nsContextMenu.prototype = {
       triggeringPrincipal: this.principal,
       csp: this.csp,
       frameOuterWindowID: gContextMenuContentData.frameOuterWindowID,
     };
     for (let p in extra) {
       params[p] = extra[p];
     }
 
-    let referrerInfo = gContextMenuContentData.referrerInfo;
+    let referrerInfo = this.onLink
+      ? gContextMenuContentData.linkReferrerInfo
+      : gContextMenuContentData.referrerInfo;
     // If we want to change userContextId, we must be sure that we don't
     // propagate the referrer.
     if (
       ("userContextId" in params &&
         params.userContextId != gContextMenuContentData.userContextId) ||
       this.onPlainTextLink
     ) {
       referrerInfo = new ReferrerInfo(
@@ -1337,27 +1339,27 @@ nsContextMenu.prototype = {
         }
       } catch (e) {}
     }
     if (!name) {
       name = "snapshot.jpg";
     }
 
     // Cache this because we fetch the data async
-    let { targetReferrerInfo } = gContextMenuContentData;
+    let referrerInfo = gContextMenuContentData.referrerInfo;
 
     this.actor.saveVideoFrameAsImage(this.targetIdentifier).then(dataURL => {
       // FIXME can we switch this to a blob URL?
       saveImageURL(
         dataURL,
         name,
         "SaveImageTitle",
         true, // bypass cache
         false, // don't skip prompt for where to save
-        targetReferrerInfo, // referrer info
+        referrerInfo, // referrer info
         null, // document
         null, // content type
         null, // content disposition
         isPrivate,
         this.principal
       );
     });
   },
@@ -1615,24 +1617,28 @@ nsContextMenu.prototype = {
     );
 
     // kick off the channel with our proxy object as the listener
     channel.asyncOpen(new saveAsListener(this.principal));
   },
 
   // Save URL of clicked-on link.
   saveLink() {
+    let referrerInfo = this.onLink
+      ? gContextMenuContentData.linkReferrerInfo
+      : gContextMenuContentData.referrerInfo;
+
     let isContentWindowPrivate = this.ownerDoc.isPrivate;
     this.saveHelper(
       this.linkURL,
       this.linkTextStr,
       null,
       true,
       this.ownerDoc,
-      gContextMenuContentData.referrerInfo,
+      referrerInfo,
       this.frameOuterWindowID,
       this.linkDownload,
       isContentWindowPrivate
     );
   },
 
   // Backwards-compatibility wrapper
   saveImage() {
@@ -1640,17 +1646,17 @@ nsContextMenu.prototype = {
       this.saveMedia();
     }
   },
 
   // Save URL of the clicked upon image, video, or audio.
   saveMedia() {
     let doc = this.ownerDoc;
     let isContentWindowPrivate = this.ownerDoc.isPrivate;
-    let referrerInfo = gContextMenuContentData.targetReferrerInfo;
+    let referrerInfo = gContextMenuContentData.referrerInfo;
     let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
     if (this.onCanvas) {
       // Bypass cache, since it's a data: URL.
       this._canvasToBlobURL(this.targetIdentifier).then(function(blobURL) {
         saveImageURL(
           blobURL,
           "canvas.png",
           "SaveImageTitle",
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -274,16 +274,21 @@ if (!isDevtools) {
     "prefs.js",
     "tabs.js",
     "extension-storage.js",
   ]) {
     whitelist.add("resource://services-sync/engines/" + module);
   }
 }
 
+if (!AppConstants.NIGHTLY_BUILD) {
+  // Bug 1532703 - only used in HTML-based about:config
+  whitelist.add("chrome://browser/skin/toggle.svg");
+}
+
 if (AppConstants.MOZ_CODE_COVERAGE) {
   whitelist.add("chrome://marionette/content/PerTestCoverageUtils.jsm");
 }
 
 const gInterestingCategories = new Set([
   "agent-style-sheets",
   "addon-provider-module",
   "webextension-modules",
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -2030,16 +2030,21 @@ BrowserGlue.prototype = {
     Services.tm.idleDispatchToMainThread(() => {
       try {
         Services.logins;
       } catch (ex) {
         Cu.reportError(ex);
       }
     }, 3000);
 
+    // Add breach alerts pref observer reasonably early so the pref flip works
+    Services.tm.idleDispatchToMainThread(() => {
+      this._addBreachAlertsPrefObserver();
+    });
+
     // It's important that SafeBrowsing is initialized reasonably
     // early, so we use a maximum timeout for it.
     Services.tm.idleDispatchToMainThread(() => {
       SafeBrowsing.init();
     }, 5000);
 
     if (AppConstants.MOZ_CRASHREPORTER) {
       UnsubmittedCrashHandler.scheduleCheckForUnsubmittedCrashReports();
@@ -2195,16 +2200,30 @@ BrowserGlue.prototype = {
         "sync",
         async event => {
           await LoginBreaches.update(event.data.current);
         }
       );
     }
   },
 
+  _addBreachAlertsPrefObserver() {
+    const BREACH_ALERTS_PREF = "signon.management.page.breach-alerts.enabled";
+    const clearVulnerablePasswordsIfBreachAlertsDisabled = async function() {
+      if (!Services.prefs.getBoolPref(BREACH_ALERTS_PREF)) {
+        await LoginBreaches.clearAllPotentiallyVulnerablePasswords();
+      }
+    };
+    clearVulnerablePasswordsIfBreachAlertsDisabled();
+    Services.prefs.addObserver(
+      BREACH_ALERTS_PREF,
+      clearVulnerablePasswordsIfBreachAlertsDisabled
+    );
+  },
+
   _onQuitRequest: function BG__onQuitRequest(aCancelQuit, aQuitType) {
     // If user has already dismissed quit request, then do nothing
     if (aCancelQuit instanceof Ci.nsISupportsPRBool && aCancelQuit.data) {
       return;
     }
 
     // There are several cases where we won't show a dialog here:
     // 1. There is only 1 tab open in 1 window
--- a/browser/components/aboutlogins/LoginBreaches.jsm
+++ b/browser/components/aboutlogins/LoginBreaches.jsm
@@ -139,16 +139,23 @@ this.LoginBreaches = {
     for (const login of logins) {
       if (storageJSON.isPotentiallyVulnerablePassword(login)) {
         vulnerablePasswordsByLoginGUID.set(login.guid, true);
       }
     }
     return vulnerablePasswordsByLoginGUID;
   },
 
+  async clearAllPotentiallyVulnerablePasswords() {
+    await Services.logins.initializationPromise;
+    const storageJSON =
+      Services.logins.wrappedJSObject._storage.wrappedJSObject;
+    storageJSON.clearAllPotentiallyVulnerablePasswords();
+  },
+
   _breachAlertIsDismissed(login, breach, dismissedBreachAlerts) {
     const breachAddedDate = new Date(breach.AddedDate).getTime();
     const breachAlertIsDismissed =
       dismissedBreachAlerts[login.guid] &&
       dismissedBreachAlerts[login.guid].timeBreachAlertDismissed >
         breachAddedDate;
     return breachAlertIsDismissed;
   },
--- a/browser/components/aboutlogins/tests/browser/browser.ini
+++ b/browser/components/aboutlogins/tests/browser/browser.ini
@@ -13,16 +13,17 @@ skip-if = asan || debug || verify # bug 
 [browser_confirmDeleteDialog.js]
 [browser_contextmenuFillLogins.js]
 [browser_copyToClipboardButton.js]
 [browser_createLogin.js]
 [browser_deleteLogin.js]
 [browser_dismissFooter.js]
 [browser_fxAccounts.js]
 [browser_loginItemErrors.js]
+skip-if = debug # Bug 1577710
 [browser_loginListChanges.js]
 [browser_masterPassword.js]
 skip-if = (os == 'linux') # bug 1569789
 [browser_noLoginsView.js]
 [browser_openFiltered.js]
 [browser_openImport.js]
 skip-if = (os != "win") # import is only available on Windows
 [browser_openPreferences.js]
--- a/browser/components/downloads/DownloadsViewUI.jsm
+++ b/browser/components/downloads/DownloadsViewUI.jsm
@@ -322,18 +322,24 @@ this.DownloadsViewUI.DownloadElementShel
    *        example "Failed - example.com - 1:45 PM".
    * @param hoverStatus
    *        Label to show in the Downloads Panel when the mouse pointer is over
    *        the main area of the item. If not specified, this will be the
    *        state label combined with the host and date. This is ignored in the
    *        Downloads View.
    */
   showStatusWithDetails(stateLabel, hoverStatus) {
+    let referrer =
+      this.download.source.referrerInfo &&
+      this.download.source.referrerInfo.originalReferrer
+        ? this.download.source.referrerInfo.originalReferrer.spec
+        : null;
+
     let [displayHost] = DownloadUtils.getURIHost(
-      this.download.source.referrer || this.download.source.url
+      referrer || this.download.source.url
     );
     let [displayDate] = DownloadUtils.getReadableDates(
       new Date(this.download.endTime)
     );
 
     let firstPart = DownloadsCommon.strings.statusSeparator(
       stateLabel,
       displayHost
--- a/browser/components/downloads/test/browser/browser.ini
+++ b/browser/components/downloads/test/browser/browser.ini
@@ -11,8 +11,9 @@ skip-if = os == "linux" # Bug 952422
 [browser_indicatorDrop.js]
 [browser_libraryDrop.js]
 skip-if = (os == 'win' && os_version == '10.0' && ccov) # Bug 1306510
 [browser_library_clearall.js]
 [browser_downloads_panel_block.js]
 skip-if = true # Bug 1352792
 [browser_downloads_panel_height.js]
 [browser_downloads_autohide.js]
+[browser_go_to_download_page.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_go_to_download_page.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ReferrerInfo = Components.Constructor(
+  "@mozilla.org/referrer-info;1",
+  "nsIReferrerInfo",
+  "init"
+);
+
+const TEST_REFERRER = "https://example.com/";
+
+registerCleanupFunction(async function() {
+  await task_resetState();
+  await PlacesUtils.history.clear();
+});
+
+async function addDownload(referrerInfo) {
+  let startTimeMs = Date.now();
+
+  let publicList = await Downloads.getList(Downloads.PUBLIC);
+  let downloadData = {
+    source: {
+      url: "http://www.example.com/test-download.txt",
+      referrerInfo,
+    },
+    target: {
+      path: gTestTargetFile.path,
+    },
+    startTime: new Date(startTimeMs++),
+  };
+  let download = await Downloads.createDownload(downloadData);
+  await publicList.add(download);
+  await download.start();
+}
+
+/**
+ * Make sure "Go To Download Page" is enabled and works as expected.
+ */
+add_task(async function test_go_to_download_page() {
+  let referrerInfo = new ReferrerInfo(
+    Ci.nsIReferrerInfo.NO_REFERRER,
+    true,
+    NetUtil.newURI(TEST_REFERRER)
+  );
+
+  let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, TEST_REFERRER);
+
+  // Wait for focus first
+  await promiseFocus();
+
+  // Ensure that state is reset in case previous tests didn't finish.
+  await task_resetState();
+
+  // Populate the downloads database with the data required by this test.
+  await addDownload(referrerInfo);
+
+  // Open the user interface and wait for data to be fully loaded.
+  await task_openPanel();
+
+  let win = await openLibrary("Downloads");
+  registerCleanupFunction(function() {
+    win.close();
+  });
+
+  let listbox = win.document.getElementById("downloadsRichListBox");
+  ok(listbox, "download list box present");
+
+  // Select one of the downloads.
+  listbox.itemChildren[0].click();
+
+  let contextMenu = win.document.getElementById("downloadsContextMenu");
+
+  let popupShownPromise = BrowserTestUtils.waitForEvent(
+    contextMenu,
+    "popupshown"
+  );
+  EventUtils.synthesizeMouseAtCenter(
+    listbox.itemChildren[0],
+    { type: "contextmenu", button: 2 },
+    win
+  );
+  await popupShownPromise;
+
+  // Find and click "Go To Download Page"
+  let goToDownloadButton = [...contextMenu.children].find(
+    child => child.command == "downloadsCmd_openReferrer"
+  );
+  goToDownloadButton.click();
+
+  let newTab = await tabPromise;
+  ok(newTab, "Go To Download Page opened a new tab");
+  gBrowser.removeTab(newTab);
+});
--- a/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/OnboardingMessage.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/OnboardingMessage.jsx
@@ -24,17 +24,17 @@ export class OnboardingCard extends Reac
   render() {
     const { content } = this.props;
     const className = this.props.className || "onboardingMessage";
     return (
       <div className={className}>
         <div className={`onboardingMessageImage ${content.icon}`} />
         <div className="onboardingContent">
           <span>
-            <h3
+            <h2
               className="onboardingTitle"
               data-l10n-id={content.title.string_id}
             />
             <p
               className="onboardingText"
               data-l10n-id={content.text.string_id}
             />
           </span>
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
@@ -81,17 +81,18 @@
       padding: 0;
       text-align: inherit;
     }
 
     @media (min-width: $break-point-medium) {
       @include full-width-styles;
     }
 
-    @media (max-width: 1120px) {
+    // There is an off-by-one gap between breakpoints; this is to prevent weirdness at exactly 1121px.
+    @media (max-width: $break-point-widest + 1px) {
       margin: 0 60px;
     }
 
     @media (max-width: 865px) {
       margin-inline-start: 0;
     }
 
     // There is an off-by-one gap between breakpoints; this is to prevent weirdness at exactly 610px.
@@ -170,17 +171,18 @@
       margin: auto;
       top: unset;
 
       &:focus {
         opacity: 1;
         box-shadow: none;
       }
 
-      @media (max-width: 1120px) {
+      // There is an off-by-one gap between breakpoints; this is to prevent weirdness at exactly 1121px.
+      @media (max-width: $break-point-widest + 1px) {
         inset-inline-end: 2%;
       }
 
       .ds-outer-wrapper-breakpoint-override & {
         inset-inline-end: -10%;
         margin: auto;
 
         @media (max-width: 865px) {
--- a/browser/components/newtab/content-src/asrouter/templates/Trailhead/Trailhead.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/Trailhead/Trailhead.jsx
@@ -143,17 +143,17 @@ export class Trailhead extends React.Pur
           <div className="trailheadContent">
             <h1 data-l10n-id={content.title.string_id} id="trailheadHeader" />
             {content.subtitle && (
               <p data-l10n-id={content.subtitle.string_id} />
             )}
             <ul className="trailheadBenefits">
               {content.benefits.map(item => (
                 <li key={item.id} className={item.id}>
-                  <h3 data-l10n-id={item.title.string_id} />
+                  <h2 data-l10n-id={item.title.string_id} />
                   <p data-l10n-id={item.text.string_id} />
                 </li>
               ))}
             </ul>
             <a
               className="trailheadLearn"
               data-l10n-id={content.learn.text.string_id}
               href={addUtmParams(content.learn.url, UTMTerm)}
--- a/browser/components/newtab/content-src/asrouter/templates/Trailhead/_Trailhead.scss
+++ b/browser/components/newtab/content-src/asrouter/templates/Trailhead/_Trailhead.scss
@@ -108,17 +108,19 @@
         background-image: url('#{$image-path}trailhead/benefit-privacy.png');
       }
 
       &.products {
         background-image: url('#{$image-path}trailhead/benefit-products.png');
       }
     }
 
-    h3 {
+    h2 {
+      text-align: start;
+      line-height: inherit;
       color: $violet-20;
       font-size: 22px;
       font-weight: 400;
       margin: 0 0 4px;
       padding-inline-start: $benefit-icon-spacing-small;
 
       @media (min-width: $responsive-breakpoint) {
         padding-inline-start: 0;
--- a/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx
@@ -138,16 +138,17 @@ export class _DiscoveryStreamBase extend
           !component.data.spocs[0]
         ) {
           return null;
         }
         // Grab the first item in the array as we only have 1 spoc position.
         const [spoc] = component.data.spocs;
         const {
           image_src,
+          raw_image_src,
           alt_text,
           title,
           url,
           context,
           cta,
           campaign_id,
           id,
           shim,
@@ -161,16 +162,17 @@ export class _DiscoveryStreamBase extend
               shim: spoc.shim,
             }}
             dispatch={this.props.dispatch}
             shouldSendImpressionStats={true}
           >
             <DSTextPromo
               dispatch={this.props.dispatch}
               image={image_src}
+              raw_image_src={raw_image_src}
               alt_text={alt_text || title}
               header={title}
               cta_text={cta}
               cta_url={url}
               subtitle={context}
               campaignId={campaign_id}
               id={id}
               pos={0}
--- a/browser/components/newtab/content-src/components/DiscoveryStreamBase/_DiscoveryStreamBase.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamBase/_DiscoveryStreamBase.scss
@@ -46,18 +46,16 @@
   }
 }
 
 .collapsible-section.ds-layout {
   margin: auto;
   width: $ds-width + 2 * $section-horizontal-padding;
 
   .section-top-bar {
-    margin-bottom: 0;
-
     .learn-more-link a {
       color: var(--newtab-link-primary-color);
       font-weight: normal;
 
       &:-moz-any(:focus, :hover) {
         text-decoration: underline;
       }
     }
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
@@ -71,17 +71,19 @@ export class CardGrid extends React.Pure
       return null;
     }
 
     // Handle the case where a user has dismissed all recommendations
     const isEmpty = data.recommendations.length === 0;
 
     return (
       <div>
-        <div className="ds-header">{this.props.title}</div>
+        {this.props.title && (
+          <div className="ds-header">{this.props.title}</div>
+        )}
         {isEmpty ? (
           <div className="ds-card-grid empty">
             <DSEmptyState
               status={data.status}
               dispatch={this.props.dispatch}
               feed={this.props.feed}
             />
           </div>
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/_CardGrid.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/_CardGrid.scss
@@ -1,15 +1,14 @@
 $col4-header-line-height: 20;
 $col4-header-font-size: 14;
 
 .ds-card-grid {
   display: grid;
   grid-gap: 24px;
-  margin: 16px 0;
 
   .ds-card {
     @include dark-theme-only {
       background: none;
     }
 
     background: $white;
     border-radius: 4px;
@@ -63,17 +62,17 @@
       grid-template-columns: repeat(3, 1fr);
 
       .title {
         font-size: 17px;
         line-height: 24px;
       }
 
       .excerpt {
-        @include limit-visibile-lines(4, 24, 15);
+        @include limit-visibile-lines(3, 24, 15);
       }
     }
 
     &.ds-card-grid-divisible-by-4 .title {
       @include limit-visibile-lines(3, 20, 15);
     }
   }
 
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
@@ -124,17 +124,31 @@ export class DSCard extends React.PureCo
         // Stop observing since element has been seen
         this.setState({
           isSeen: true,
         });
       }
     }
   }
 
+  onIdleCallback() {
+    if (!this.state.isSeen) {
+      if (this.observer && this.placholderElement) {
+        this.observer.unobserve(this.placholderElement);
+      }
+      this.setState({
+        isSeen: true,
+      });
+    }
+  }
+
   componentDidMount() {
+    this.idleCallbackId = window.requestIdleCallback(
+      this.onIdleCallback.bind(this)
+    );
     if (this.placholderElement) {
       this.observer = new IntersectionObserver(this.onSeen.bind(this));
       this.observer.observe(this.placholderElement);
     }
   }
 
   componentWillUnmount() {
     // Remove observer on unmount
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
@@ -92,17 +92,17 @@
       // show only 3 lines of copy
       @include limit-visibile-lines(3, $header-line-height, $header-font-size);
       font-weight: 600;
     }
 
     .excerpt {
       // show only 3 lines of copy
       @include limit-visibile-lines(
-        4,
+        3,
         $excerpt-line-height,
         $excerpt-font-size
       );
     }
 
     .source {
       @include dark-theme-only {
         color: $grey-40;
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx
@@ -10,66 +10,86 @@ export class DSImage extends React.PureC
     super(props);
 
     this.onOptimizedImageError = this.onOptimizedImageError.bind(this);
     this.onNonOptimizedImageError = this.onNonOptimizedImageError.bind(this);
 
     this.state = {
       isSeen: false,
       optimizedImageFailed: false,
+      useTransition: false,
     };
   }
 
   onSeen(entries) {
     if (this.state) {
       const entry = entries.find(e => e.isIntersecting);
 
       if (entry) {
         if (this.props.optimize) {
           this.setState({
-            containerWidth: entry.boundingClientRect.width,
-            containerHeight: entry.boundingClientRect.height,
+            // Thumbor doesn't handle subpixels and just errors out, so rounding...
+            containerWidth: Math.round(entry.boundingClientRect.width),
+            containerHeight: Math.round(entry.boundingClientRect.height),
           });
         }
 
         this.setState({
           isSeen: true,
         });
 
         // Stop observing since element has been seen
         this.observer.unobserve(ReactDOM.findDOMNode(this));
       }
     }
   }
 
+  onIdleCallback() {
+    if (!this.state.isSeen) {
+      this.setState({
+        useTransition: true,
+      });
+    }
+  }
+
   reformatImageURL(url, width, height) {
     // Change the image URL to request a size tailored for the parent container width
     // Also: force JPEG, quality 60, no upscaling, no EXIF data
     // Uses Thumbor: https://thumbor.readthedocs.io/en/latest/usage.html
     return `https://img-getpocket.cdn.mozilla.net/${width}x${height}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/${encodeURIComponent(
       url
     )}`;
   }
 
   componentDidMount() {
-    this.observer = new IntersectionObserver(this.onSeen.bind(this));
+    this.idleCallbackId = window.requestIdleCallback(
+      this.onIdleCallback.bind(this)
+    );
+    this.observer = new IntersectionObserver(this.onSeen.bind(this), {
+      // Assume an image will be eventually seen if it is within
+      // half the average Desktop vertical screen size:
+      // http://gs.statcounter.com/screen-resolution-stats/desktop/north-america
+      rootMargin: `540px`,
+    });
     this.observer.observe(ReactDOM.findDOMNode(this));
   }
 
   componentWillUnmount() {
     // Remove observer on unmount
     if (this.observer) {
       this.observer.unobserve(ReactDOM.findDOMNode(this));
     }
   }
 
   render() {
-    const classNames = `ds-image${
-      this.props.extraClassNames ? ` ${this.props.extraClassNames}` : ``
-    }`;
+    let classNames = `ds-image
+      ${this.props.extraClassNames ? ` ${this.props.extraClassNames}` : ``}
+      ${this.state && this.state.useTransition ? ` use-transition` : ``}
+      ${this.state && this.state.isSeen ? ` loaded` : ``}
+    `;
 
     let img;
 
     if (this.state && this.state.isSeen) {
       if (
         this.props.optimize &&
         this.props.rawSource &&
         !this.state.optimizedImageFailed
@@ -89,28 +109,28 @@ export class DSImage extends React.PureC
           source2x = this.reformatImageURL(
             baseSource,
             this.state.containerWidth * 2,
             this.state.containerHeight * 2
           );
 
           img = (
             <img
-              alt=""
+              alt={this.props.alt_text}
               crossOrigin="anonymous"
               onError={this.onOptimizedImageError}
               src={source}
               srcSet={`${source2x} 2x`}
             />
           );
         }
       } else if (!this.state.nonOptimizedImageFailed) {
         img = (
           <img
-            alt=""
+            alt={this.props.alt_text}
             crossOrigin="anonymous"
             onError={this.onNonOptimizedImageError}
             src={this.props.source}
           />
         );
       } else {
         // Remove the img element if both sources fail. Render a placeholder instead.
         img = <div className="broken-image" />;
@@ -134,9 +154,10 @@ export class DSImage extends React.PureC
   }
 }
 
 DSImage.defaultProps = {
   source: null, // The current source style from Pocket API (always 450px)
   rawSource: null, // Unadulterated image URL to filter through Thumbor
   extraClassNames: null, // Additional classnames to append to component
   optimize: true, // Measure parent container to request exact sizes
+  alt_text: null,
 };
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/_DSImage.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/_DSImage.scss
@@ -1,11 +1,20 @@
 .ds-image {
   display: block;
   position: relative;
+  opacity: 0;
+
+  &.use-transition {
+    transition: opacity 0.8s;
+  }
+
+  &.loaded {
+    opacity: 1;
+  }
 
   img,
   .broken-image {
     background-color: var(--newtab-card-placeholder-color);
     position: absolute;
     top: 0;
     width: 100%;
     height: 100%;
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx
@@ -1,13 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 import { actionCreators as ac } from "common/Actions.jsm";
+import { DSImage } from "../DSImage/DSImage.jsx";
 import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats";
 import React from "react";
 import { SafeAnchor } from "../SafeAnchor/SafeAnchor";
 
 export class DSTextPromo extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onLinkClick = this.onLinkClick.bind(this);
@@ -39,17 +40,21 @@ export class DSTextPromo extends React.P
         })
       );
     }
   }
 
   render() {
     return (
       <div className="ds-text-promo">
-        <img src={this.props.image} alt={this.props.alt_text} />
+        <DSImage
+          alt_text={this.props.alt_text}
+          source={this.props.image}
+          rawSource={this.props.raw_image_src}
+        />
         <div className="text">
           <h3>
             {`${this.props.header}\u2003`}
             <SafeAnchor
               className="ds-chevron-link"
               dispatch={this.props.dispatch}
               onLinkClick={this.onLinkClick}
               url={this.props.cta_url}
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/_DSTextPromo.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/_DSTextPromo.scss
@@ -1,18 +1,19 @@
 .ds-text-promo {
   display: flex;
   max-width: 744px;
   margin: 16px auto;
 
-  img {
+  picture {
     width: 40px;
     height: 40px;
     margin: 0 12px 0 0;
     border-radius: 4px;
+    flex-shrink: 0;
   }
 
   .text {
     line-height: 24px;
     margin: -4.5px 0 0;
   }
 
   h3 {
--- a/browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx
@@ -18,18 +18,19 @@ export const INTERSECTION_RATIO = 0.5;
 /**
  * Impression wrapper for Discovery Stream related React components.
  *
  * It makses use of the Intersection Observer API to detect the visibility,
  * and relies on page visibility to ensure the impression is reported
  * only when the component is visible on the page.
  *
  * Note:
- *   * This wrapper could be used either at the individual card level,
- *     or by the card container components
+ *   * This wrapper used to be used either at the individual card level,
+ *     or by the card container components.
+ *     It is now only used for individual card level.
  *   * Each impression will be sent only once as soon as the desired
  *     visibility is detected
  *   * Batching is not yet implemented, hence it might send multiple
  *     impression pings separately
  */
 export class ImpressionStats extends React.PureComponent {
   // This checks if the given cards are the same as those in the last impression ping.
   // If so, it should not send the same impression ping again.
@@ -188,22 +189,16 @@ export class ImpressionStats extends Rea
   }
 
   componentDidMount() {
     if (this.props.rows.length) {
       this.setImpressionObserverOrAddListener();
     }
   }
 
-  componentDidUpdate(prevProps) {
-    if (this.props.rows.length && this.props.rows !== prevProps.rows) {
-      this.setImpressionObserverOrAddListener();
-    }
-  }
-
   componentWillUnmount() {
     if (this._handleIntersect && this.impressionObserver) {
       this.impressionObserver.unobserve(this.refs.impression);
     }
     if (this._onVisibilityChange) {
       this.props.document.removeEventListener(
         VISIBILITY_CHANGE_EVENT,
         this._onVisibilityChange
--- a/browser/components/newtab/css/activity-stream-linux.css
+++ b/browser/components/newtab/css/activity-stream-linux.css
@@ -1887,28 +1887,25 @@ main {
     color: #D7D7DB; }
   .ds-header .icon,
   .ds-layout .section-title span .icon {
     fill: var(--newtab-text-secondary-color); }
 
 .collapsible-section.ds-layout {
   margin: auto;
   width: 986px; }
-  .collapsible-section.ds-layout .section-top-bar {
-    margin-bottom: 0; }
-    .collapsible-section.ds-layout .section-top-bar .learn-more-link a {
-      color: var(--newtab-link-primary-color);
-      font-weight: normal; }
-      .collapsible-section.ds-layout .section-top-bar .learn-more-link a:-moz-any(:focus, :hover) {
-        text-decoration: underline; }
+  .collapsible-section.ds-layout .section-top-bar .learn-more-link a {
+    color: var(--newtab-link-primary-color);
+    font-weight: normal; }
+    .collapsible-section.ds-layout .section-top-bar .learn-more-link a:-moz-any(:focus, :hover) {
+      text-decoration: underline; }
 
 .ds-card-grid {
   display: grid;
-  grid-gap: 24px;
-  margin: 16px 0; }
+  grid-gap: 24px; }
   .ds-card-grid .ds-card {
     background: #FFF;
     border-radius: 4px; }
     [lwt-newtab-brighttext] .ds-card-grid .ds-card {
       background: none; }
   .ds-card-grid .ds-card-link:focus {
     box-shadow: 0 0 0 5px rgba(10, 132, 255, 0.3);
     transition: box-shadow 150ms;
@@ -1946,17 +1943,17 @@ main {
       .ds-column-12 .ds-card-grid.ds-card-grid-divisible-by-3 .title {
         font-size: 17px;
         line-height: 24px; }
       .ds-column-9 .ds-card-grid.ds-card-grid-divisible-by-3 .excerpt,
       .ds-column-10 .ds-card-grid.ds-card-grid-divisible-by-3 .excerpt,
       .ds-column-11 .ds-card-grid.ds-card-grid-divisible-by-3 .excerpt,
       .ds-column-12 .ds-card-grid.ds-card-grid-divisible-by-3 .excerpt {
         font-size: 15px;
-        -webkit-line-clamp: 4;
+        -webkit-line-clamp: 3;
         line-height: 24px; }
     .ds-column-9 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
     .ds-column-10 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
     .ds-column-11 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
     .ds-column-12 .ds-card-grid.ds-card-grid-divisible-by-4 .title {
       font-size: 15px;
       -webkit-line-clamp: 3;
       line-height: 20px; }
@@ -2707,17 +2704,17 @@ main {
       flex-grow: 1; }
     .ds-card .meta .title {
       font-size: 17px;
       -webkit-line-clamp: 3;
       line-height: 24px;
       font-weight: 600; }
     .ds-card .meta .excerpt {
       font-size: 14px;
-      -webkit-line-clamp: 4;
+      -webkit-line-clamp: 3;
       line-height: 20px; }
     .ds-card .meta .source {
       -webkit-line-clamp: 1;
       margin-bottom: 2px;
       font-size: 13px;
       color: #737373; }
       [lwt-newtab-brighttext] .ds-card .meta .source {
         color: #B1B1B3; }
@@ -2865,17 +2862,22 @@ main {
   opacity: 1; }
 
 .story-animate-exit-active {
   opacity: 0;
   transition: opacity 250ms ease-in; }
 
 .ds-image {
   display: block;
-  position: relative; }
+  position: relative;
+  opacity: 0; }
+  .ds-image.use-transition {
+    transition: opacity 0.8s; }
+  .ds-image.loaded {
+    opacity: 1; }
   .ds-image img,
   .ds-image .broken-image {
     background-color: var(--newtab-card-placeholder-color);
     position: absolute;
     top: 0;
     width: 100%;
     height: 100%;
     object-fit: cover; }
@@ -3021,21 +3023,22 @@ main {
 @keyframes spinner {
   to {
     transform: rotate(360deg); } }
 
 .ds-text-promo {
   display: flex;
   max-width: 744px;
   margin: 16px auto; }
-  .ds-text-promo img {
+  .ds-text-promo picture {
     width: 40px;
     height: 40px;
     margin: 0 12px 0 0;
-    border-radius: 4px; }
+    border-radius: 4px;
+    flex-shrink: 0; }
   .ds-text-promo .text {
     line-height: 24px;
     margin: -4.5px 0 0; }
   .ds-text-promo h3 {
     margin: 0;
     font-weight: 600;
     font-size: 15px; }
     [lwt-newtab-brighttext] .ds-text-promo h3 {
@@ -3451,17 +3454,17 @@ body[lwt-newtab-brighttext] .scene2Icon 
       .SimpleBelowSearchSnippet .innerWrapper {
         align-items: flex-start;
         background-color: transparent;
         border-radius: 4px;
         box-shadow: none;
         flex-direction: row;
         padding: 0;
         text-align: inherit; } }
-    @media (max-width: 1120px) {
+    @media (max-width: 1123px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         margin: 0 60px; } }
     @media (max-width: 865px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         margin-inline-start: 0; } }
     @media (max-width: 609px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         margin: auto; } }
@@ -3514,17 +3517,17 @@ body[lwt-newtab-brighttext] .scene2Icon 
       display: block;
       inset-inline-end: -15%;
       opacity: 0;
       margin: auto;
       top: unset; }
       .SimpleBelowSearchSnippet.withButton .blockButton:focus {
         opacity: 1;
         box-shadow: none; }
-      @media (max-width: 1120px) {
+      @media (max-width: 1123px) {
         .SimpleBelowSearchSnippet.withButton .blockButton {
           inset-inline-end: 2%; } }
       .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
         inset-inline-end: -10%;
         margin: auto; }
         @media (max-width: 865px) {
           .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
             inset-inline-end: 2%; } }
@@ -4222,24 +4225,26 @@ a.firstrun-link {
       .trailhead .trailheadBenefits li:dir(rtl) {
         background-position-x: right; }
       .trailhead .trailheadBenefits li.knowledge {
         background-image: url("../data/content/assets/trailhead/benefit-knowledge.png"); }
       .trailhead .trailheadBenefits li.privacy {
         background-image: url("../data/content/assets/trailhead/benefit-privacy.png"); }
       .trailhead .trailheadBenefits li.products {
         background-image: url("../data/content/assets/trailhead/benefit-products.png"); }
-    .trailhead .trailheadBenefits h3 {
+    .trailhead .trailheadBenefits h2 {
+      text-align: start;
+      line-height: inherit;
       color: #CB9EFF;
       font-size: 22px;
       font-weight: 400;
       margin: 0 0 4px;
       padding-inline-start: 52px; }
       @media (min-width: 850px) {
-        .trailhead .trailheadBenefits h3 {
+        .trailhead .trailheadBenefits h2 {
           padding-inline-start: 0; } }
     .trailhead .trailheadBenefits p {
       color: #FFF;
       font-size: 15px;
       line-height: 22px;
       margin: 4px 0 15px; }
   .trailhead .trailheadForm {
     background: url("../data/content/assets/trailhead/firefox-logo.png") top center/100px no-repeat;
--- a/browser/components/newtab/css/activity-stream-mac.css
+++ b/browser/components/newtab/css/activity-stream-mac.css
@@ -1890,28 +1890,25 @@ main {
     color: #D7D7DB; }
   .ds-header .icon,
   .ds-layout .section-title span .icon {
     fill: var(--newtab-text-secondary-color); }
 
 .collapsible-section.ds-layout {
   margin: auto;
   width: 986px; }
-  .collapsible-section.ds-layout .section-top-bar {
-    margin-bottom: 0; }
-    .collapsible-section.ds-layout .section-top-bar .learn-more-link a {
-      color: var(--newtab-link-primary-color);
-      font-weight: normal; }
-      .collapsible-section.ds-layout .section-top-bar .learn-more-link a:-moz-any(:focus, :hover) {
-        text-decoration: underline; }
+  .collapsible-section.ds-layout .section-top-bar .learn-more-link a {
+    color: var(--newtab-link-primary-color);
+    font-weight: normal; }
+    .collapsible-section.ds-layout .section-top-bar .learn-more-link a:-moz-any(:focus, :hover) {
+      text-decoration: underline; }
 
 .ds-card-grid {
   display: grid;
-  grid-gap: 24px;
-  margin: 16px 0; }
+  grid-gap: 24px; }
   .ds-card-grid .ds-card {
     background: #FFF;
     border-radius: 4px; }
     [lwt-newtab-brighttext] .ds-card-grid .ds-card {
       background: none; }
   .ds-card-grid .ds-card-link:focus {
     box-shadow: 0 0 0 5px rgba(10, 132, 255, 0.3);
     transition: box-shadow 150ms;
@@ -1949,17 +1946,17 @@ main {
       .ds-column-12 .ds-card-grid.ds-card-grid-divisible-by-3 .title {
         font-size: 17px;
         line-height: 24px; }
       .ds-column-9 .ds-card-grid.ds-card-grid-divisible-by-3 .excerpt,
       .ds-column-10 .ds-card-grid.ds-card-grid-divisible-by-3 .excerpt,
       .ds-column-11 .ds-card-grid.ds-card-grid-divisible-by-3 .excerpt,
       .ds-column-12 .ds-card-grid.ds-card-grid-divisible-by-3 .excerpt {
         font-size: 15px;
-        -webkit-line-clamp: 4;
+        -webkit-line-clamp: 3;
         line-height: 24px; }
     .ds-column-9 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
     .ds-column-10 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
     .ds-column-11 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
     .ds-column-12 .ds-card-grid.ds-card-grid-divisible-by-4 .title {
       font-size: 15px;
       -webkit-line-clamp: 3;
       line-height: 20px; }
@@ -2710,17 +2707,17 @@ main {
       flex-grow: 1; }
     .ds-card .meta .title {
       font-size: 17px;
       -webkit-line-clamp: 3;
       line-height: 24px;
       font-weight: 600; }
     .ds-card .meta .excerpt {
       font-size: 14px;
-      -webkit-line-clamp: 4;
+      -webkit-line-clamp: 3;
       line-height: 20px; }
     .ds-card .meta .source {
       -webkit-line-clamp: 1;
       margin-bottom: 2px;
       font-size: 13px;
       color: #737373; }
       [lwt-newtab-brighttext] .ds-card .meta .source {
         color: #B1B1B3; }
@@ -2868,17 +2865,22 @@ main {
   opacity: 1; }
 
 .story-animate-exit-active {
   opacity: 0;
   transition: opacity 250ms ease-in; }
 
 .ds-image {
   display: block;
-  position: relative; }
+  position: relative;
+  opacity: 0; }
+  .ds-image.use-transition {
+    transition: opacity 0.8s; }
+  .ds-image.loaded {
+    opacity: 1; }
   .ds-image img,
   .ds-image .broken-image {
     background-color: var(--newtab-card-placeholder-color);
     position: absolute;
     top: 0;
     width: 100%;
     height: 100%;
     object-fit: cover; }
@@ -3024,21 +3026,22 @@ main {
 @keyframes spinner {
   to {
     transform: rotate(360deg); } }
 
 .ds-text-promo {
   display: flex;
   max-width: 744px;
   margin: 16px auto; }
-  .ds-text-promo img {
+  .ds-text-promo picture {
     width: 40px;
     height: 40px;
     margin: 0 12px 0 0;
-    border-radius: 4px; }
+    border-radius: 4px;
+    flex-shrink: 0; }
   .ds-text-promo .text {
     line-height: 24px;
     margin: -4.5px 0 0; }
   .ds-text-promo h3 {
     margin: 0;
     font-weight: 600;
     font-size: 15px; }
     [lwt-newtab-brighttext] .ds-text-promo h3 {
@@ -3454,17 +3457,17 @@ body[lwt-newtab-brighttext] .scene2Icon 
       .SimpleBelowSearchSnippet .innerWrapper {
         align-items: flex-start;
         background-color: transparent;
         border-radius: 4px;
         box-shadow: none;
         flex-direction: row;
         padding: 0;
         text-align: inherit; } }
-    @media (max-width: 1120px) {
+    @media (max-width: 1123px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         margin: 0 60px; } }
     @media (max-width: 865px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         margin-inline-start: 0; } }
     @media (max-width: 609px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         margin: auto; } }
@@ -3517,17 +3520,17 @@ body[lwt-newtab-brighttext] .scene2Icon 
       display: block;
       inset-inline-end: -15%;
       opacity: 0;
       margin: auto;
       top: unset; }
       .SimpleBelowSearchSnippet.withButton .blockButton:focus {
         opacity: 1;
         box-shadow: none; }
-      @media (max-width: 1120px) {
+      @media (max-width: 1123px) {
         .SimpleBelowSearchSnippet.withButton .blockButton {
           inset-inline-end: 2%; } }
       .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
         inset-inline-end: -10%;
         margin: auto; }
         @media (max-width: 865px) {
           .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
             inset-inline-end: 2%; } }
@@ -4225,24 +4228,26 @@ a.firstrun-link {
       .trailhead .trailheadBenefits li:dir(rtl) {
         background-position-x: right; }
       .trailhead .trailheadBenefits li.knowledge {
         background-image: url("../data/content/assets/trailhead/benefit-knowledge.png"); }
       .trailhead .trailheadBenefits li.privacy {
         background-image: url("../data/content/assets/trailhead/benefit-privacy.png"); }
       .trailhead .trailheadBenefits li.products {
         background-image: url("../data/content/assets/trailhead/benefit-products.png"); }
-    .trailhead .trailheadBenefits h3 {
+    .trailhead .trailheadBenefits h2 {
+      text-align: start;
+      line-height: inherit;
       color: #CB9EFF;
       font-size: 22px;
       font-weight: 400;
       margin: 0 0 4px;
       padding-inline-start: 52px; }
       @media (min-width: 850px) {
-        .trailhead .trailheadBenefits h3 {
+        .trailhead .trailheadBenefits h2 {
           padding-inline-start: 0; } }
     .trailhead .trailheadBenefits p {
       color: #FFF;
       font-size: 15px;
       line-height: 22px;
       margin: 4px 0 15px; }
   .trailhead .trailheadForm {
     background: url("../data/content/assets/trailhead/firefox-logo.png") top center/100px no-repeat;
--- a/browser/components/newtab/css/activity-stream-windows.css
+++ b/browser/components/newtab/css/activity-stream-windows.css
@@ -1887,28 +1887,25 @@ main {
     color: #D7D7DB; }
   .ds-header .icon,
   .ds-layout .section-title span .icon {
     fill: var(--newtab-text-secondary-color); }
 
 .collapsible-section.ds-layout {
   margin: auto;
   width: 986px; }
-  .collapsible-section.ds-layout .section-top-bar {
-    margin-bottom: 0; }
-    .collapsible-section.ds-layout .section-top-bar .learn-more-link a {
-      color: var(--newtab-link-primary-color);
-      font-weight: normal; }
-      .collapsible-section.ds-layout .section-top-bar .learn-more-link a:-moz-any(:focus, :hover) {
-        text-decoration: underline; }
+  .collapsible-section.ds-layout .section-top-bar .learn-more-link a {
+    color: var(--newtab-link-primary-color);
+    font-weight: normal; }
+    .collapsible-section.ds-layout .section-top-bar .learn-more-link a:-moz-any(:focus, :hover) {
+      text-decoration: underline; }
 
 .ds-card-grid {
   display: grid;
-  grid-gap: 24px;
-  margin: 16px 0; }
+  grid-gap: 24px; }
   .ds-card-grid .ds-card {
     background: #FFF;
     border-radius: 4px; }
     [lwt-newtab-brighttext] .ds-card-grid .ds-card {
       background: none; }
   .ds-card-grid .ds-card-link:focus {
     box-shadow: 0 0 0 5px rgba(10, 132, 255, 0.3);
     transition: box-shadow 150ms;
@@ -1946,17 +1943,17 @@ main {
       .ds-column-12 .ds-card-grid.ds-card-grid-divisible-by-3 .title {
         font-size: 17px;
         line-height: 24px; }
       .ds-column-9 .ds-card-grid.ds-card-grid-divisible-by-3 .excerpt,
       .ds-column-10 .ds-card-grid.ds-card-grid-divisible-by-3 .excerpt,
       .ds-column-11 .ds-card-grid.ds-card-grid-divisible-by-3 .excerpt,
       .ds-column-12 .ds-card-grid.ds-card-grid-divisible-by-3 .excerpt {
         font-size: 15px;
-        -webkit-line-clamp: 4;
+        -webkit-line-clamp: 3;
         line-height: 24px; }
     .ds-column-9 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
     .ds-column-10 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
     .ds-column-11 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
     .ds-column-12 .ds-card-grid.ds-card-grid-divisible-by-4 .title {
       font-size: 15px;
       -webkit-line-clamp: 3;
       line-height: 20px; }
@@ -2707,17 +2704,17 @@ main {
       flex-grow: 1; }
     .ds-card .meta .title {
       font-size: 17px;
       -webkit-line-clamp: 3;
       line-height: 24px;
       font-weight: 600; }
     .ds-card .meta .excerpt {
       font-size: 14px;
-      -webkit-line-clamp: 4;
+      -webkit-line-clamp: 3;
       line-height: 20px; }
     .ds-card .meta .source {
       -webkit-line-clamp: 1;
       margin-bottom: 2px;
       font-size: 13px;
       color: #737373; }
       [lwt-newtab-brighttext] .ds-card .meta .source {
         color: #B1B1B3; }
@@ -2865,17 +2862,22 @@ main {
   opacity: 1; }
 
 .story-animate-exit-active {
   opacity: 0;
   transition: opacity 250ms ease-in; }
 
 .ds-image {
   display: block;
-  position: relative; }
+  position: relative;
+  opacity: 0; }
+  .ds-image.use-transition {
+    transition: opacity 0.8s; }
+  .ds-image.loaded {
+    opacity: 1; }
   .ds-image img,
   .ds-image .broken-image {
     background-color: var(--newtab-card-placeholder-color);
     position: absolute;
     top: 0;
     width: 100%;
     height: 100%;
     object-fit: cover; }
@@ -3021,21 +3023,22 @@ main {
 @keyframes spinner {
   to {
     transform: rotate(360deg); } }
 
 .ds-text-promo {
   display: flex;
   max-width: 744px;
   margin: 16px auto; }
-  .ds-text-promo img {
+  .ds-text-promo picture {
     width: 40px;
     height: 40px;
     margin: 0 12px 0 0;
-    border-radius: 4px; }
+    border-radius: 4px;
+    flex-shrink: 0; }
   .ds-text-promo .text {
     line-height: 24px;
     margin: -4.5px 0 0; }
   .ds-text-promo h3 {
     margin: 0;
     font-weight: 600;
     font-size: 15px; }
     [lwt-newtab-brighttext] .ds-text-promo h3 {
@@ -3451,17 +3454,17 @@ body[lwt-newtab-brighttext] .scene2Icon 
       .SimpleBelowSearchSnippet .innerWrapper {
         align-items: flex-start;
         background-color: transparent;
         border-radius: 4px;
         box-shadow: none;
         flex-direction: row;
         padding: 0;
         text-align: inherit; } }
-    @media (max-width: 1120px) {
+    @media (max-width: 1123px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         margin: 0 60px; } }
     @media (max-width: 865px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         margin-inline-start: 0; } }
     @media (max-width: 609px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         margin: auto; } }
@@ -3514,17 +3517,17 @@ body[lwt-newtab-brighttext] .scene2Icon 
       display: block;
       inset-inline-end: -15%;
       opacity: 0;
       margin: auto;
       top: unset; }
       .SimpleBelowSearchSnippet.withButton .blockButton:focus {
         opacity: 1;
         box-shadow: none; }
-      @media (max-width: 1120px) {
+      @media (max-width: 1123px) {
         .SimpleBelowSearchSnippet.withButton .blockButton {
           inset-inline-end: 2%; } }
       .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
         inset-inline-end: -10%;
         margin: auto; }
         @media (max-width: 865px) {
           .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
             inset-inline-end: 2%; } }
@@ -4222,24 +4225,26 @@ a.firstrun-link {
       .trailhead .trailheadBenefits li:dir(rtl) {
         background-position-x: right; }
       .trailhead .trailheadBenefits li.knowledge {
         background-image: url("../data/content/assets/trailhead/benefit-knowledge.png"); }
       .trailhead .trailheadBenefits li.privacy {
         background-image: url("../data/content/assets/trailhead/benefit-privacy.png"); }
       .trailhead .trailheadBenefits li.products {
         background-image: url("../data/content/assets/trailhead/benefit-products.png"); }
-    .trailhead .trailheadBenefits h3 {
+    .trailhead .trailheadBenefits h2 {
+      text-align: start;
+      line-height: inherit;
       color: #CB9EFF;
       font-size: 22px;
       font-weight: 400;
       margin: 0 0 4px;
       padding-inline-start: 52px; }
       @media (min-width: 850px) {
-        .trailhead .trailheadBenefits h3 {
+        .trailhead .trailheadBenefits h2 {
           padding-inline-start: 0; } }
     .trailhead .trailheadBenefits p {
       color: #FFF;
       font-size: 15px;
       line-height: 22px;
       margin: 4px 0 15px; }
   .trailhead .trailheadForm {
     background: url("../data/content/assets/trailhead/firefox-logo.png") top center/100px no-repeat;
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -2861,17 +2861,17 @@ class Trailhead extends react__WEBPACK_I
       id: "trailheadHeader"
     }), content.subtitle && react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
       "data-l10n-id": content.subtitle.string_id
     }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("ul", {
       className: "trailheadBenefits"
     }, content.benefits.map(item => react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("li", {
       key: item.id,
       className: item.id
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("h3", {
+    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("h2", {
       "data-l10n-id": item.title.string_id
     }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
       "data-l10n-id": item.text.string_id
     })))), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("a", {
       className: "trailheadLearn",
       "data-l10n-id": content.learn.text.string_id,
       href: Object(_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_2__["addUtmParams"])(content.learn.url, UTMTerm),
       target: "_blank",
@@ -3595,17 +3595,17 @@ class OnboardingCard extends react__WEBP
     } = this.props;
     const className = this.props.className || "onboardingMessage";
     return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
       className: className
     }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
       className: `onboardingMessageImage ${content.icon}`
     }), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
       className: "onboardingContent"
-    }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("span", null, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h3", {
+    }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("span", null, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h2", {
       className: "onboardingTitle",
       "data-l10n-id": content.title.string_id
     }), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("p", {
       className: "onboardingText",
       "data-l10n-id": content.text.string_id
     })), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("span", {
       className: "onboardingButtonContainer"
     }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
@@ -4460,18 +4460,19 @@ const INTERSECTION_RATIO = 0.5;
 /**
  * Impression wrapper for Discovery Stream related React components.
  *
  * It makses use of the Intersection Observer API to detect the visibility,
  * and relies on page visibility to ensure the impression is reported
  * only when the component is visible on the page.
  *
  * Note:
- *   * This wrapper could be used either at the individual card level,
- *     or by the card container components
+ *   * This wrapper used to be used either at the individual card level,
+ *     or by the card container components.
+ *     It is now only used for individual card level.
  *   * Each impression will be sent only once as soon as the desired
  *     visibility is detected
  *   * Batching is not yet implemented, hence it might send multiple
  *     impression pings separately
  */
 
 class ImpressionStats extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
   // This checks if the given cards are the same as those in the last impression ping.
@@ -4623,22 +4624,16 @@ class ImpressionStats extends react__WEB
   }
 
   componentDidMount() {
     if (this.props.rows.length) {
       this.setImpressionObserverOrAddListener();
     }
   }
 
-  componentDidUpdate(prevProps) {
-    if (this.props.rows.length && this.props.rows !== prevProps.rows) {
-      this.setImpressionObserverOrAddListener();
-    }
-  }
-
   componentWillUnmount() {
     if (this._handleIntersect && this.impressionObserver) {
       this.impressionObserver.unobserve(this.refs.impression);
     }
 
     if (this._onVisibilityChange) {
       this.props.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
     }
@@ -8020,84 +8015,104 @@ var external_ReactDOM_default = /*#__PUR
 
 class DSImage_DSImage extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onOptimizedImageError = this.onOptimizedImageError.bind(this);
     this.onNonOptimizedImageError = this.onNonOptimizedImageError.bind(this);
     this.state = {
       isSeen: false,
-      optimizedImageFailed: false
+      optimizedImageFailed: false,
+      useTransition: false
     };
   }
 
   onSeen(entries) {
     if (this.state) {
       const entry = entries.find(e => e.isIntersecting);
 
       if (entry) {
         if (this.props.optimize) {
           this.setState({
-            containerWidth: entry.boundingClientRect.width,
-            containerHeight: entry.boundingClientRect.height
+            // Thumbor doesn't handle subpixels and just errors out, so rounding...
+            containerWidth: Math.round(entry.boundingClientRect.width),
+            containerHeight: Math.round(entry.boundingClientRect.height)
           });
         }
 
         this.setState({
           isSeen: true
         }); // Stop observing since element has been seen
 
         this.observer.unobserve(external_ReactDOM_default.a.findDOMNode(this));
       }
     }
   }
 
+  onIdleCallback() {
+    if (!this.state.isSeen) {
+      this.setState({
+        useTransition: true
+      });
+    }
+  }
+
   reformatImageURL(url, width, height) {
     // Change the image URL to request a size tailored for the parent container width
     // Also: force JPEG, quality 60, no upscaling, no EXIF data
     // Uses Thumbor: https://thumbor.readthedocs.io/en/latest/usage.html
     return `https://img-getpocket.cdn.mozilla.net/${width}x${height}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/${encodeURIComponent(url)}`;
   }
 
   componentDidMount() {
-    this.observer = new IntersectionObserver(this.onSeen.bind(this));
+    this.idleCallbackId = window.requestIdleCallback(this.onIdleCallback.bind(this));
+    this.observer = new IntersectionObserver(this.onSeen.bind(this), {
+      // Assume an image will be eventually seen if it is within
+      // half the average Desktop vertical screen size:
+      // http://gs.statcounter.com/screen-resolution-stats/desktop/north-america
+      rootMargin: `540px`
+    });
     this.observer.observe(external_ReactDOM_default.a.findDOMNode(this));
   }
 
   componentWillUnmount() {
     // Remove observer on unmount
     if (this.observer) {
       this.observer.unobserve(external_ReactDOM_default.a.findDOMNode(this));
     }
   }
 
   render() {
-    const classNames = `ds-image${this.props.extraClassNames ? ` ${this.props.extraClassNames}` : ``}`;
+    let classNames = `ds-image
+      ${this.props.extraClassNames ? ` ${this.props.extraClassNames}` : ``}
+      ${this.state && this.state.useTransition ? ` use-transition` : ``}
+      ${this.state && this.state.isSeen ? ` loaded` : ``}
+    `;
     let img;
 
     if (this.state && this.state.isSeen) {
       if (this.props.optimize && this.props.rawSource && !this.state.optimizedImageFailed) {
         let source;
         let source2x;
 
         if (this.state && this.state.containerWidth) {
           let baseSource = this.props.rawSource;
           source = this.reformatImageURL(baseSource, this.state.containerWidth, this.state.containerHeight);
           source2x = this.reformatImageURL(baseSource, this.state.containerWidth * 2, this.state.containerHeight * 2);
           img = external_React_default.a.createElement("img", {
-            alt: "",
+            alt: this.props.alt_text,
             crossOrigin: "anonymous",
             onError: this.onOptimizedImageError,
             src: source,
             srcSet: `${source2x} 2x`
           });
         }
       } else if (!this.state.nonOptimizedImageFailed) {
         img = external_React_default.a.createElement("img", {
-          alt: "",
+          alt: this.props.alt_text,
           crossOrigin: "anonymous",
           onError: this.onNonOptimizedImageError,
           src: this.props.source
         });
       } else {
         // Remove the img element if both sources fail. Render a placeholder instead.
         img = external_React_default.a.createElement("div", {
           className: "broken-image"
@@ -8126,18 +8141,19 @@ class DSImage_DSImage extends external_R
 }
 DSImage_DSImage.defaultProps = {
   source: null,
   // The current source style from Pocket API (always 450px)
   rawSource: null,
   // Unadulterated image URL to filter through Thumbor
   extraClassNames: null,
   // Additional classnames to append to component
-  optimize: true // Measure parent container to request exact sizes
-
+  optimize: true,
+  // Measure parent container to request exact sizes
+  alt_text: null
 };
 // EXTERNAL MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx
 var LinkMenu = __webpack_require__(30);
 
 // EXTERNAL MODULE: ./content-src/components/ContextMenu/ContextMenuButton.jsx
 var ContextMenuButton = __webpack_require__(33);
 
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx
@@ -8468,17 +8484,31 @@ class DSCard_DSCard extends external_Rea
 
         this.setState({
           isSeen: true
         });
       }
     }
   }
 
+  onIdleCallback() {
+    if (!this.state.isSeen) {
+      if (this.observer && this.placholderElement) {
+        this.observer.unobserve(this.placholderElement);
+      }
+
+      this.setState({
+        isSeen: true
+      });
+    }
+  }
+
   componentDidMount() {
+    this.idleCallbackId = window.requestIdleCallback(this.onIdleCallback.bind(this));
+
     if (this.placholderElement) {
       this.observer = new IntersectionObserver(this.onSeen.bind(this));
       this.observer.observe(this.placholderElement);
     }
   }
 
   componentWillUnmount() {
     // Remove observer on unmount
@@ -8710,17 +8740,17 @@ class CardGrid_CardGrid extends external
     } = this.props; // Handle a render before feed has been fetched by displaying nothing
 
     if (!data) {
       return null;
     } // Handle the case where a user has dismissed all recommendations
 
 
     const isEmpty = data.recommendations.length === 0;
-    return external_React_default.a.createElement("div", null, external_React_default.a.createElement("div", {
+    return external_React_default.a.createElement("div", null, this.props.title && external_React_default.a.createElement("div", {
       className: "ds-header"
     }, this.props.title), isEmpty ? external_React_default.a.createElement("div", {
       className: "ds-card-grid empty"
     }, external_React_default.a.createElement(DSEmptyState_DSEmptyState, {
       status: data.status,
       dispatch: this.props.dispatch,
       feed: this.props.feed
     })) : this.renderCards());
@@ -8843,16 +8873,17 @@ class DSMessage_DSMessage extends extern
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx
 /* 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/. */
 
 
 
 
+
 class DSTextPromo_DSTextPromo extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onLinkClick = this.onLinkClick.bind(this);
   }
 
   onLinkClick() {
     if (this.props.dispatch) {
@@ -8873,19 +8904,20 @@ class DSTextPromo_DSTextPromo extends ex
         }]
       }));
     }
   }
 
   render() {
     return external_React_default.a.createElement("div", {
       className: "ds-text-promo"
-    }, external_React_default.a.createElement("img", {
-      src: this.props.image,
-      alt: this.props.alt_text
+    }, external_React_default.a.createElement(DSImage_DSImage, {
+      alt_text: this.props.alt_text,
+      source: this.props.image,
+      rawSource: this.props.raw_image_src
     }), external_React_default.a.createElement("div", {
       className: "text"
     }, external_React_default.a.createElement("h3", null, `${this.props.header}\u2003`, external_React_default.a.createElement(SafeAnchor_SafeAnchor, {
       className: "ds-chevron-link",
       dispatch: this.props.dispatch,
       onLinkClick: this.onLinkClick,
       url: this.props.cta_url
     }, this.props.cta_text)), external_React_default.a.createElement("p", {
@@ -9862,16 +9894,17 @@ class DiscoveryStreamBase_DiscoveryStrea
         if (!component.data || !component.data.spocs || !component.data.spocs[0]) {
           return null;
         } // Grab the first item in the array as we only have 1 spoc position.
 
 
         const [spoc] = component.data.spocs;
         const {
           image_src,
+          raw_image_src,
           alt_text,
           title,
           url,
           context,
           cta,
           campaign_id,
           id,
           shim
@@ -9882,16 +9915,17 @@ class DiscoveryStreamBase_DiscoveryStrea
             guid: spoc.id,
             shim: spoc.shim
           },
           dispatch: this.props.dispatch,
           shouldSendImpressionStats: true
         }, external_React_default.a.createElement(DSTextPromo_DSTextPromo, {
           dispatch: this.props.dispatch,
           image: image_src,
+          raw_image_src: raw_image_src,
           alt_text: alt_text || title,
           header: title,
           cta_text: cta,
           cta_url: url,
           subtitle: context,
           campaignId: campaign_id,
           id: id,
           pos: 0,
--- a/browser/components/newtab/lib/ASRouter.jsm
+++ b/browser/components/newtab/lib/ASRouter.jsm
@@ -1341,17 +1341,17 @@ class _ASRouter {
   routeMessageToTarget(message, target, trigger, force = false) {
     switch (message.template) {
       case "cfr_doorhanger":
         if (force) {
           CFRPageActions.forceRecommendation(target, message, this.dispatch);
         } else {
           CFRPageActions.addRecommendation(
             target,
-            trigger.param.host,
+            trigger.param && trigger.param.host,
             message,
             this.dispatch
           );
         }
         break;
       case "fxa_bookmark_panel":
         if (force) {
           BookmarkPanelHub._forceShowMessage(target, message);
--- a/browser/components/newtab/lib/CFRMessageProvider.jsm
+++ b/browser/components/newtab/lib/CFRMessageProvider.jsm
@@ -121,16 +121,17 @@ const PINNED_TABS_TARGET_LOCALES = [
   "de",
 ];
 
 const CFR_MESSAGES = [
   {
     id: "FACEBOOK_CONTAINER_3",
     template: "cfr_doorhanger",
     content: {
+      layout: "addon_recommendation",
       category: "cfrAddons",
       bucket_id: "CFR_M1",
       notification_text: { string_id: "cfr-doorhanger-extension-notification" },
       heading_text: { string_id: "cfr-doorhanger-extension-heading" },
       info_icon: {
         label: { string_id: "cfr-doorhanger-extension-sumo-link" },
         sumo_path: FACEBOOK_CONTAINER_PARAMS.sumo_path,
       },
@@ -189,16 +190,17 @@ const CFR_MESSAGES = [
       FACEBOOK_CONTAINER_PARAMS.min_frecency
     }]|mapToProperty('host'))|length > 0`,
     trigger: { id: "openURL", params: FACEBOOK_CONTAINER_PARAMS.open_urls },
   },
   {
     id: "GOOGLE_TRANSLATE_3",
     template: "cfr_doorhanger",
     content: {
+      layout: "addon_recommendation",
       category: "cfrAddons",
       bucket_id: "CFR_M1",
       notification_text: { string_id: "cfr-doorhanger-extension-notification" },
       heading_text: { string_id: "cfr-doorhanger-extension-heading" },
       info_icon: {
         label: { string_id: "cfr-doorhanger-extension-sumo-link" },
         sumo_path: GOOGLE_TRANSLATE_PARAMS.sumo_path,
       },
@@ -258,16 +260,17 @@ const CFR_MESSAGES = [
       GOOGLE_TRANSLATE_PARAMS.min_frecency
     }]|mapToProperty('host'))|length > 0`,
     trigger: { id: "openURL", params: GOOGLE_TRANSLATE_PARAMS.open_urls },
   },
   {
     id: "YOUTUBE_ENHANCE_3",
     template: "cfr_doorhanger",
     content: {
+      layout: "addon_recommendation",
       category: "cfrAddons",
       bucket_id: "CFR_M1",
       notification_text: { string_id: "cfr-doorhanger-extension-notification" },
       heading_text: { string_id: "cfr-doorhanger-extension-heading" },
       info_icon: {
         label: { string_id: "cfr-doorhanger-extension-sumo-link" },
         sumo_path: YOUTUBE_ENHANCE_PARAMS.sumo_path,
       },
@@ -328,16 +331,17 @@ const CFR_MESSAGES = [
     }]|mapToProperty('host'))|length > 0`,
     trigger: { id: "openURL", params: YOUTUBE_ENHANCE_PARAMS.open_urls },
   },
   {
     id: "WIKIPEDIA_CONTEXT_MENU_SEARCH_3",
     template: "cfr_doorhanger",
     exclude: true,
     content: {
+      layout: "addon_recommendation",
       category: "cfrAddons",
       bucket_id: "CFR_M1",
       notification_text: { string_id: "cfr-doorhanger-extension-notification" },
       heading_text: { string_id: "cfr-doorhanger-extension-heading" },
       info_icon: {
         label: { string_id: "cfr-doorhanger-extension-sumo-link" },
         sumo_path: WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.sumo_path,
       },
@@ -401,16 +405,17 @@ const CFR_MESSAGES = [
       params: WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.open_urls,
     },
   },
   {
     id: "REDDIT_ENHANCEMENT_3",
     template: "cfr_doorhanger",
     exclude: true,
     content: {
+      layout: "addon_recommendation",
       category: "cfrAddons",
       bucket_id: "CFR_M1",
       notification_text: { string_id: "cfr-doorhanger-extension-notification" },
       heading_text: { string_id: "cfr-doorhanger-extension-heading" },
       info_icon: {
         label: { string_id: "cfr-doorhanger-extension-sumo-link" },
         sumo_path: REDDIT_ENHANCEMENT_PARAMS.sumo_path,
       },
@@ -470,16 +475,17 @@ const CFR_MESSAGES = [
       REDDIT_ENHANCEMENT_PARAMS.min_frecency
     }]|mapToProperty('host'))|length > 0`,
     trigger: { id: "openURL", params: REDDIT_ENHANCEMENT_PARAMS.open_urls },
   },
   {
     id: "PIN_TAB",
     template: "cfr_doorhanger",
     content: {
+      layout: "message_and_animation",
       category: "cfrFeatures",
       bucket_id: "CFR_PIN_TAB",
       notification_text: { string_id: "cfr-doorhanger-extension-notification" },
       heading_text: { string_id: "cfr-doorhanger-pintab-heading" },
       info_icon: {
         label: { string_id: "cfr-doorhanger-extension-sumo-link" },
         sumo_path: REDDIT_ENHANCEMENT_PARAMS.sumo_path,
       },
@@ -521,16 +527,88 @@ const CFR_MESSAGES = [
       },
     },
     targeting: `locale in ${JSON.stringify(
       PINNED_TABS_TARGET_LOCALES
     )} && !hasPinnedTabs && recentVisits[.timestamp > (currentDate|date - 3600 * 1000 * 1)]|length >= 3`,
     frequency: { lifetime: 3 },
     trigger: { id: "frequentVisits", params: PINNED_TABS_TARGET_SITES },
   },
+  {
+    id: "SAVE_LOGIN",
+    frequency: {
+      lifetime: 3,
+    },
+    targeting: "usesFirefoxSync == false",
+    template: "cfr_doorhanger",
+    last_modified: 1565907636313,
+    content: {
+      layout: "icon_and_message",
+      text: {
+        string_id: "cfr-doorhanger-sync-logins-body",
+      },
+      icon: "chrome://browser/content/aboutlogins/icons/intro-illustration.svg",
+      buttons: {
+        secondary: [
+          {
+            label: {
+              string_id: "cfr-doorhanger-extension-cancel-button",
+            },
+            action: {
+              type: "CANCEL",
+            },
+          },
+          {
+            label: {
+              string_id: "cfr-doorhanger-extension-never-show-recommendation",
+            },
+          },
+          {
+            label: {
+              string_id: "cfr-doorhanger-extension-manage-settings-button",
+            },
+            action: {
+              type: "OPEN_PREFERENCES_PAGE",
+              data: {
+                category: "general-cfrfeatures",
+              },
+            },
+          },
+        ],
+        primary: {
+          label: {
+            string_id: "cfr-doorhanger-sync-logins-ok-button",
+          },
+          action: {
+            type: "OPEN_PREFERENCES_PAGE",
+            data: {
+              category: "sync",
+            },
+          },
+        },
+      },
+      bucket_id: "CFR_SAVE_LOGIN",
+      heading_text: {
+        string_id: "cfr-doorhanger-sync-logins-header",
+      },
+      info_icon: {
+        label: {
+          string_id: "cfr-doorhanger-extension-sumo-link",
+        },
+        sumo_path: "extensionrecommendations",
+      },
+      notification_text: {
+        string_id: "cfr-doorhanger-extension-notification",
+      },
+      category: "cfrFeatures",
+    },
+    trigger: {
+      id: "newSavedLogin",
+    },
+  },
 ];
 
 const CFRMessageProvider = {
   getMessages() {
     return CFR_MESSAGES.filter(msg => !msg.exclude);
   },
 };
 this.CFRMessageProvider = CFRMessageProvider;
--- a/browser/components/newtab/lib/CFRPageActions.jsm
+++ b/browser/components/newtab/lib/CFRPageActions.jsm
@@ -423,16 +423,17 @@ class PageAction {
             string_id: "cfr-doorhanger-pintab-animation-pause",
           });
         }
       };
       animationButton.addEventListener("click", this.onAnimationButtonClick);
     }
   }
 
+  // eslint-disable-next-line max-statements
   async _renderPopup(message, browser) {
     const { id, content } = message;
 
     const headerLabel = this.window.document.getElementById(
       "cfr-notification-header-label"
     );
     const headerLink = this.window.document.getElementById(
       "cfr-notification-header-link"
@@ -446,108 +447,134 @@ class PageAction {
     const footerLink = this.window.document.getElementById(
       "cfr-notification-footer-learn-more-link"
     );
     const { primary, secondary } = content.buttons;
     let primaryActionCallback;
     let options = {};
     let panelTitle;
 
-    // Use the message category as a CSS selector to hide different parts of the
-    // notification template markup
-    this.window.document
-      .getElementById("contextual-feature-recommendation-notification")
-      .setAttribute("data-notification-category", message.content.category);
-
     headerLabel.value = await this.getStrings(content.heading_text);
     headerLink.setAttribute(
       "href",
       SUMO_BASE_URL + content.info_icon.sumo_path
     );
     headerLink.setAttribute(this.window.RTL_UI ? "left" : "right", 0);
     headerImage.setAttribute(
       "tooltiptext",
       await this.getStrings(content.info_icon.label, "tooltiptext")
     );
     headerLink.onclick = () =>
       this._sendTelemetry({
         message_id: id,
         bucket_id: content.bucket_id,
         event: "RATIONALE",
       });
-
-    footerText.textContent = await this.getStrings(content.text);
-
-    if (content.addon) {
-      await this._setAddonAuthorAndRating(this.window.document, content);
-      panelTitle = await this.getStrings(content.addon.title);
-      options = { popupIconURL: content.addon.icon };
+    // Use the message layout as a CSS selector to hide different parts of the
+    // notification template markup
+    this.window.document
+      .getElementById("contextual-feature-recommendation-notification")
+      .setAttribute("data-notification-category", content.layout);
 
-      footerLink.value = await this.getStrings({
-        string_id: "cfr-doorhanger-extension-learn-more-link",
-      });
-      footerLink.setAttribute("href", content.addon.amo_url);
-      footerLink.onclick = () =>
-        this._sendTelemetry({
-          message_id: id,
-          bucket_id: content.bucket_id,
-          event: "LEARN_MORE",
-        });
+    switch (content.layout) {
+      case "icon_and_message":
+        const author = this.window.document.getElementById(
+          "cfr-notification-author"
+        );
+        author.textContent = await this.getStrings(content.text);
+        primaryActionCallback = () => {
+          this._blockMessage(id);
+          this.dispatchUserAction(primary.action);
+          this.hideAddressBarNotifier();
+          this._sendTelemetry({
+            message_id: id,
+            bucket_id: content.bucket_id,
+            event: "ENABLE",
+          });
+          RecommendationMap.delete(browser);
+        };
+        panelTitle = await this.getStrings(content.heading_text);
+        options = {
+          popupIconURL: content.icon,
+          popupIconClass: "cfr-doorhanger-large-icon",
+        };
+        break;
+      case "message_and_animation":
+        footerText.textContent = await this.getStrings(content.text);
+        const stepsContainerId = "cfr-notification-feature-steps";
+        let stepsContainer = this.window.document.getElementById(
+          stepsContainerId
+        );
+        primaryActionCallback = () => {
+          this._blockMessage(id);
+          this.dispatchUserAction(primary.action);
+          this.hideAddressBarNotifier();
+          this._sendTelemetry({
+            message_id: id,
+            bucket_id: content.bucket_id,
+            event: "PIN",
+          });
+          RecommendationMap.delete(browser);
+        };
+        panelTitle = await this.getStrings(content.heading_text);
 
-      primaryActionCallback = async () => {
-        // eslint-disable-next-line no-use-before-define
-        primary.action.data.url = await CFRPageActions._fetchLatestAddonVersion(
-          content.addon.id
-        );
-        this._blockMessage(id);
-        this.dispatchUserAction(primary.action);
-        this.hideAddressBarNotifier();
-        this._sendTelemetry({
-          message_id: id,
-          bucket_id: content.bucket_id,
-          event: "INSTALL",
+        if (content.descriptionDetails) {
+          if (stepsContainer) {
+            // If it exists we need to empty it
+            stepsContainer.remove();
+            stepsContainer = stepsContainer.cloneNode(false);
+          } else {
+            stepsContainer = this.window.document.createXULElement("vbox");
+            stepsContainer.setAttribute("id", stepsContainerId);
+          }
+          footerText.parentNode.appendChild(stepsContainer);
+          for (let step of content.descriptionDetails.steps) {
+            // This li is a generic xul element with custom styling
+            const li = this.window.document.createXULElement("li");
+            this._l10n.setAttributes(li, step.string_id);
+            stepsContainer.appendChild(li);
+          }
+          await this._l10n.translateElements([...stepsContainer.children]);
+        }
+
+        await this._renderPinTabAnimation();
+        break;
+      default:
+        panelTitle = await this.getStrings(content.addon.title);
+        await this._setAddonAuthorAndRating(this.window.document, content);
+        // Main body content of the dropdown
+        footerText.textContent = await this.getStrings(content.text);
+        options = { popupIconURL: content.addon.icon };
+
+        footerLink.value = await this.getStrings({
+          string_id: "cfr-doorhanger-extension-learn-more-link",
         });
-        RecommendationMap.delete(browser);
-      };
-    } else {
-      const stepsContainerId = "cfr-notification-feature-steps";
-      let stepsContainer = this.window.document.getElementById(
-        stepsContainerId
-      );
-      primaryActionCallback = () => {
-        this._blockMessage(id);
-        this.dispatchUserAction(primary.action);
-        this.hideAddressBarNotifier();
-        this._sendTelemetry({
-          message_id: id,
-          bucket_id: content.bucket_id,
-          event: "PIN",
-        });
-        RecommendationMap.delete(browser);
-      };
-      panelTitle = await this.getStrings(content.heading_text);
+        footerLink.setAttribute("href", content.addon.amo_url);
+        footerLink.onclick = () =>
+          this._sendTelemetry({
+            message_id: id,
+            bucket_id: content.bucket_id,
+            event: "LEARN_MORE",
+          });
 
-      if (stepsContainer) {
-        // If it exists we need to empty it
-        stepsContainer.remove();
-        stepsContainer = stepsContainer.cloneNode(false);
-      } else {
-        stepsContainer = this.window.document.createXULElement("vbox");
-        stepsContainer.setAttribute("id", stepsContainerId);
-      }
-      footerText.parentNode.appendChild(stepsContainer);
-      for (let step of content.descriptionDetails.steps) {
-        // This li is a generic xul element with custom styling
-        const li = this.window.document.createXULElement("li");
-        this._l10n.setAttributes(li, step.string_id);
-        stepsContainer.appendChild(li);
-      }
-      await this._l10n.translateElements([...stepsContainer.children]);
-
-      await this._renderPinTabAnimation();
+        primaryActionCallback = async () => {
+          // eslint-disable-next-line no-use-before-define
+          primary.action.data.url = await CFRPageActions._fetchLatestAddonVersion(
+            content.addon.id
+          );
+          this._blockMessage(id);
+          this.dispatchUserAction(primary.action);
+          this.hideAddressBarNotifier();
+          this._sendTelemetry({
+            message_id: id,
+            bucket_id: content.bucket_id,
+            event: "INSTALL",
+          });
+          RecommendationMap.delete(browser);
+        };
     }
 
     const primaryBtnStrings = await this.getStrings(primary.label);
     const mainAction = {
       label: primaryBtnStrings,
       accessKey: primaryBtnStrings.attributes.accesskey,
       callback: primaryActionCallback,
     };
@@ -744,17 +771,18 @@ const CFRPageActions = {
    */
   async addRecommendation(browser, host, recommendation, dispatchToASRouter) {
     const win = browser.ownerGlobal;
     if (PrivateBrowsingUtils.isWindowPrivate(win)) {
       return false;
     }
     if (
       browser !== win.gBrowser.selectedBrowser ||
-      !isHostMatch(browser, host)
+      // We can have recommendations without URL restrictions
+      (host && !isHostMatch(browser, host))
     ) {
       return false;
     }
     if (RecommendationMap.has(browser)) {
       // Don't replace an existing message
       return false;
     }
     const { id, content } = recommendation;
--- a/browser/components/newtab/lib/OnboardingMessageProvider.jsm
+++ b/browser/components/newtab/lib/OnboardingMessageProvider.jsm
@@ -9,16 +9,18 @@ ChromeUtils.defineModuleGetter(
   "resource:///modules/AttributionCode.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "AddonRepository",
   "resource://gre/modules/addons/AddonRepository.jsm"
 );
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const FIREFOX_VERSION = parseInt(Services.appinfo.version.match(/\d+/), 10);
+const ONE_MINUTE = 60 * 1000;
 
 const L10N = new Localization([
   "branding/brand.ftl",
   "browser/branding/brandings.ftl",
   "browser/branding/sync-brand.ftl",
   "browser/newtab/onboarding.ftl",
 ]);
 
@@ -404,16 +406,37 @@ const ONBOARDING_MESSAGES = () => [
       link_text: { string_id: "cfr-protections-panel-link-text" },
       cta_url: `${Services.urlFormatter.formatURLPref(
         "app.support.baseURL"
       )}etp-promotions?as=u&utm_source=inproduct`,
       cta_type: "OPEN_URL",
     },
     trigger: { id: "protectionsPanelOpen" },
   },
+  {
+    id: `WHATS_NEW_BADGE_${FIREFOX_VERSION}`,
+    template: "toolbar_badge",
+    content: {
+      delay: 5 * ONE_MINUTE,
+      target: "whats-new-menu-button",
+      action: { id: "show-whatsnew-button" },
+    },
+    priority: 1,
+    trigger: { id: "toolbarBadgeUpdate" },
+    frequency: {
+      // Makes it so that we track impressions for this message while at the
+      // same time it can have unlimited impressions
+      lifetime: Infinity,
+    },
+    // Never saw this message or saw it in the past 4 days or more recent
+    targeting: `isWhatsNewPanelEnabled &&
+      (!messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}'] ||
+        (messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}']|length >= 1 &&
+          currentDate|date - messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}'][0] <= 4 * 24 * 3600 * 1000))`,
+  },
 ];
 
 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
@@ -1,16 +1,13 @@
 /* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-const FIREFOX_VERSION = parseInt(Services.appinfo.version.match(/\d+/), 10);
 const TWO_DAYS = 2 * 24 * 3600 * 1000;
 
 const MESSAGES = () => [
   {
     id: "SIMPLE_FXA_BOOKMARK_TEST_FLUENT",
     template: "fxa_bookmark_panel",
     content: {
       title: { string_id: "cfr-doorhanger-bookmark-fxa-header" },
@@ -62,39 +59,16 @@ const MESSAGES = () => [
             "https://www.mozilla.org/%LOCALE%/etc/firefox/retention/thank-you-a/",
           expireDelta: TWO_DAYS,
         },
       },
     },
     trigger: { id: "momentsUpdate" },
   },
   {
-    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" },
-    },
-    priority: 1,
-    trigger: { id: "toolbarBadgeUpdate" },
-    frequency: {
-      // Makes it so that we track impressions for this message while at the
-      // same time it can have unlimited impressions
-      lifetime: Infinity,
-    },
-    // Never saw this message or saw it in the past 4 days or more recent
-    targeting: `isWhatsNewPanelEnabled &&
-      (earliestFirefoxVersion && firefoxVersion > earliestFirefoxVersion) &&
-        (!messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}'] ||
-      (messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}']|length >= 1 &&
-        currentDate|date - messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}'][0] <= 4 * 24 * 3600 * 1000))`,
-  },
-  {
     id: "WHATS_NEW_70_1",
     template: "whatsnew_panel_message",
     order: 3,
     content: {
       published_date: 1560969794394,
       title: "Protection Is Our Focus",
       icon_url:
         "resource://activity-stream/data/content/assets/whatsnew-send-icon.png",
--- a/browser/components/newtab/test/browser/browser.ini
+++ b/browser/components/newtab/test/browser/browser.ini
@@ -25,8 +25,9 @@ prefs =
 [browser_onboarding_rtamo.js]
 skip-if = (os == "linux") # Test setup only implemented for OSX and Windows
 [browser_topsites_contextMenu_options.js]
 [browser_topsites_section.js]
 [browser_asrouter_cfr.js]
 skip-if = fission
 [browser_asrouter_bookmarkpanel.js]
 [browser_asrouter_toolbarbadge.js]
+[browser_asrouter_whatsnewpanel.js]
--- a/browser/components/newtab/test/browser/browser_asrouter_cfr.js
+++ b/browser/components/newtab/test/browser/browser_asrouter_cfr.js
@@ -3,18 +3,24 @@ const { CFRPageActions } = ChromeUtils.i
 );
 const { ASRouterTriggerListeners } = ChromeUtils.import(
   "resource://activity-stream/lib/ASRouterTriggerListeners.jsm"
 );
 const { ASRouter } = ChromeUtils.import(
   "resource://activity-stream/lib/ASRouter.jsm"
 );
 
-const createDummyRecommendation = ({ action, category, heading_text }) => ({
+const createDummyRecommendation = ({
+  action,
+  category,
+  heading_text,
+  layout,
+}) => ({
   content: {
+    layout: layout || "addon_recommendation",
     category,
     notification_text: "Mochitest",
     heading_text: heading_text || "Mochitest",
     info_icon: {
       label: { attributes: { tooltiptext: "Why am I seeing this" } },
       sumo_path: "extensionrecommendations",
     },
     addon: {
@@ -58,37 +64,39 @@ const createDummyRecommendation = ({ act
         },
       ],
     },
   },
 });
 
 function checkCFRFeaturesElements(notification) {
   Assert.ok(notification.hidden === false, "Panel should be visible");
-  Assert.ok(
-    notification.getAttribute("data-notification-category") === "cfrFeatures",
-    "Panel have corret data attribute"
+  Assert.equal(
+    notification.getAttribute("data-notification-category"),
+    "message_and_animation",
+    "Panel have correct data attribute"
   );
   Assert.ok(
     notification.querySelector(
       "#cfr-notification-footer-pintab-animation-container"
     ),
     "Pin tab animation exists"
   );
   Assert.ok(
     notification.querySelector("#cfr-notification-feature-steps"),
     "Pin tab steps"
   );
 }
 
 function checkCFRAddonsElements(notification) {
   Assert.ok(notification.hidden === false, "Panel should be visible");
-  Assert.ok(
-    notification.getAttribute("data-notification-category") === "cfrAddons",
-    "Panel have corret data attribute"
+  Assert.equal(
+    notification.getAttribute("data-notification-category"),
+    "addon_recommendation",
+    "Panel have correct data attribute"
   );
   Assert.ok(
     notification.querySelector("#cfr-notification-footer-text-and-addon-info"),
     "Panel should have addon info container"
   );
   Assert.ok(
     notification.querySelector("#cfr-notification-footer-filled-stars"),
     "Panel should have addon rating info"
@@ -110,23 +118,29 @@ function clearNotifications() {
     0,
     "Should have removed the notification"
   );
 }
 
 function trigger_cfr_panel(
   browser,
   trigger,
-  { action = { type: "FOO" }, heading_text, category = "cfrAddons" } = {}
+  {
+    action = { type: "FOO" },
+    heading_text,
+    category = "cfrAddons",
+    layout,
+  } = {}
 ) {
   // a fake action type will result in the action being ignored
   const recommendation = createDummyRecommendation({
     action,
     category,
     heading_text,
+    layout,
   });
   if (category !== "cfrAddons") {
     delete recommendation.content.addon;
   }
 
   clearNotifications();
 
   return CFRPageActions.addRecommendation(
@@ -335,16 +349,17 @@ add_task(async function test_cfr_pin_tab
   // addRecommendation checks that scheme starts with http and host matches
   let browser = gBrowser.selectedBrowser;
   await BrowserTestUtils.loadURI(browser, "http://example.com/");
   await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/");
 
   const response = await trigger_cfr_panel(browser, "example.com", {
     action: { type: "PIN_CURRENT_TAB" },
     category: "cfrFeatures",
+    layout: "message_and_animation",
   });
   Assert.ok(
     response,
     "Should return true if addRecommendation checks were successful"
   );
 
   const showPanel = BrowserTestUtils.waitForEvent(
     PopupNotifications.panel,
@@ -390,16 +405,17 @@ add_task(async function test_cfr_feature
   let browser = gBrowser.selectedBrowser;
   await BrowserTestUtils.loadURI(browser, "http://example.com/");
   await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/");
 
   // Trigger Feature CFR
   let response = await trigger_cfr_panel(browser, "example.com", {
     action: { type: "PIN_CURRENT_TAB" },
     category: "cfrFeatures",
+    layout: "message_and_animation",
   });
   Assert.ok(
     response,
     "Should return true if addRecommendation checks were successful"
   );
 
   let showPanel = BrowserTestUtils.waitForEvent(
     PopupNotifications.panel,
@@ -517,17 +533,17 @@ add_task(async function test_cfr_addon_a
     PopupNotifications._currentNotifications.length,
     0,
     "Should have removed the notification"
   );
 
   // Trigger Addon CFR
   response = await trigger_cfr_panel(browser, "example.com", {
     action: { type: "PIN_CURRENT_TAB" },
-    category: "cfrFeatures",
+    category: "cfrAddons",
   });
   Assert.ok(
     response,
     "Should return true if addRecommendation checks were successful"
   );
 
   showPanel = BrowserTestUtils.waitForEvent(
     PopupNotifications.panel,
@@ -537,17 +553,17 @@ add_task(async function test_cfr_addon_a
   document.getElementById("contextual-feature-recommendation").click();
   await showPanel;
 
   Assert.ok(
     document.getElementById("contextual-feature-recommendation-notification")
       .hidden === false,
     "Panel should be visible"
   );
-  checkCFRFeaturesElements(
+  checkCFRAddonsElements(
     document.getElementById("contextual-feature-recommendation-notification")
   );
 
   // Check there is a primary button and click it. It will trigger the callback.
   Assert.ok(notification.button);
   hidePanel = BrowserTestUtils.waitForEvent(
     PopupNotifications.panel,
     "popuphidden"
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/browser/browser_asrouter_whatsnewpanel.js
@@ -0,0 +1,48 @@
+const { PanelTestProvider } = ChromeUtils.import(
+  "resource://activity-stream/lib/PanelTestProvider.jsm"
+);
+const { ToolbarPanelHub } = ChromeUtils.import(
+  "resource://activity-stream/lib/ToolbarPanelHub.jsm"
+);
+
+add_task(async function test_messages_rendering() {
+  const msgs = (await PanelTestProvider.getMessages()).filter(
+    ({ template }) => template === "whatsnew_panel_message"
+  );
+
+  Assert.ok(msgs.length, "FxA test message exists");
+
+  Object.defineProperty(ToolbarPanelHub, "messages", {
+    get: () => Promise.resolve(msgs),
+    configurable: true,
+  });
+
+  await ToolbarPanelHub.enableAppmenuButton();
+
+  const mainView = document.getElementById("appMenu-mainView");
+  UITour.showMenu(window, "appMenu");
+  await BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+
+  Assert.equal(mainView.hidden, false, "Panel is visible");
+
+  const whatsNewBtn = document.getElementById("appMenu-whatsnew-button");
+  Assert.equal(whatsNewBtn.hidden, false, "What's New is present");
+
+  // Show the What's New Messages
+  whatsNewBtn.click();
+
+  const shownMessages = await BrowserTestUtils.waitForCondition(
+    () =>
+      document.getElementById("PanelUI-whatsNew-message-container") &&
+      document.querySelectorAll(
+        "#PanelUI-whatsNew-message-container .whatsNew-message"
+      ).length
+  );
+  Assert.equal(
+    shownMessages,
+    msgs.length,
+    "Expected number of What's New messages rendered."
+  );
+
+  UITour.hideMenu(window, "appMenu");
+});
--- a/browser/components/newtab/test/unit/asrouter/PanelTestProvider.test.js
+++ b/browser/components/newtab/test/unit/asrouter/PanelTestProvider.test.js
@@ -3,17 +3,17 @@ import schema from "content-src/asrouter
 import update_schema from "content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json";
 import whats_new_schema from "content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json";
 const messages = PanelTestProvider.getMessages();
 
 describe("PanelTestProvider", () => {
   it("should have a message", () => {
     // Careful: when changing this number make sure that new messages also go
     // through schema verifications.
-    assert.lengthOf(messages, 8);
+    assert.lengthOf(messages, 7);
   });
   it("should be a valid message", () => {
     const fxaMessages = messages.filter(
       ({ template }) => template === "fxa_bookmark_panel"
     );
     for (let message of fxaMessages) {
       assert.jsonSchema(message.content, schema);
     }
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx
@@ -148,76 +148,46 @@ describe("<ImpressionStats>", () => {
     const [action] = dispatch.secondCall.args;
     assert.equal(action.type, at.DISCOVERY_STREAM_SPOC_IMPRESSION);
     assert.deepEqual(action.data, { campaignId });
   });
   it("should send an impression when the wrapped item transiting from invisible to visible", () => {
     const dispatch = sinon.spy();
     const props = {
       dispatch,
-      IntersectionObserver: buildIntersectionObserver(
-        ZeroIntersectEntries,
-        false
-      ),
+      IntersectionObserver: buildIntersectionObserver(ZeroIntersectEntries),
     };
     const wrapper = renderImpressionStats(props);
 
     // For the loaded content
     assert.calledOnce(dispatch);
 
     let [action] = dispatch.firstCall.args;
     assert.equal(action.type, at.DISCOVERY_STREAM_LOADED_CONTENT);
     assert.equal(action.data.source, SOURCE);
     assert.deepEqual(action.data.tiles, [
       { id: 1, pos: 0 },
       { id: 2, pos: 1 },
       { id: 3, pos: 2 },
     ]);
 
     dispatch.resetHistory();
-
-    // Simulating the full intersection change with a row change
-    wrapper.setProps({
-      ...props,
-      ...{ rows: [{ id: 1, pos: 0 }, { id: 2, pos: 1 }, { id: 3, pos: 2 }] },
-      ...{
-        IntersectionObserver: buildIntersectionObserver(FullIntersectEntries),
-      },
-    });
+    wrapper.instance().impressionObserver.callback(FullIntersectEntries);
 
     // For the impression
     assert.calledOnce(dispatch);
 
     [action] = dispatch.firstCall.args;
     assert.equal(action.type, at.DISCOVERY_STREAM_IMPRESSION_STATS);
     assert.deepEqual(action.data.tiles, [
       { id: 1, pos: 0 },
       { id: 2, pos: 1 },
       { id: 3, pos: 2 },
     ]);
   });
-  it("should send a loaded content and an impression if props are updated and props.rows are different", () => {
-    const props = { dispatch: sinon.spy() };
-    const wrapper = renderImpressionStats(props);
-    props.dispatch.resetHistory();
-
-    // New rows
-    wrapper.setProps({ ...DEFAULT_PROPS, ...{ rows: [{ id: 4, pos: 3 }] } });
-
-    assert.calledTwice(props.dispatch);
-  });
-  it("should not send any ping if props are updated but IDs are the same", () => {
-    const props = { dispatch: sinon.spy() };
-    const wrapper = renderImpressionStats(props);
-    props.dispatch.resetHistory();
-
-    wrapper.setProps(DEFAULT_PROPS);
-
-    assert.notCalled(props.dispatch);
-  });
   it("should remove visibility change listener when the wrapper is removed", () => {
     const props = {
       dispatch: sinon.spy(),
       document: {
         visibilityState: "hidden",
         addEventListener: sinon.spy(),
         removeEventListener: sinon.spy(),
       },
--- a/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js
+++ b/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js
@@ -1,11 +1,10 @@
 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;
@@ -26,20 +25,19 @@ describe("ToolbarBadgeHub", () => {
   let requestIdleCallbackStub;
   beforeEach(async () => {
     globals = new GlobalOverrider();
     sandbox = sinon.createSandbox();
     instance = new _ToolbarBadgeHub();
     fakeAddImpression = sandbox.stub();
     fakeDispatch = sandbox.stub();
     isBrowserPrivateStub = sandbox.stub();
-    const panelTestMsgs = await PanelTestProvider.getMessages();
     const onboardingMsgs = await OnboardingMessageProvider.getUntranslatedMessages();
     fxaMessage = onboardingMsgs.find(({ id }) => id === "FXA_ACCOUNTS_BADGE");
-    whatsnewMessage = panelTestMsgs.find(({ id }) =>
+    whatsnewMessage = onboardingMsgs.find(({ id }) =>
       id.includes("WHATS_NEW_BADGE_")
     );
     fakeElement = {
       classList: {
         add: sandbox.stub(),
         remove: sandbox.stub(),
       },
       setAttribute: sandbox.stub(),
--- a/browser/components/preferences/in-content/privacy.inc.xul
+++ b/browser/components/preferences/in-content/privacy.inc.xul
@@ -447,16 +447,25 @@
         </hbox>
         <hbox class="indent" id="generatePasswordsBox" flex="1">
           <checkbox id="generatePasswords"
                     data-l10n-id="forms-generate-passwords"
                     search-l10n-ids="forms-generate-passwords.label"
                     preference="signon.generation.enabled"
                     flex="1" />
         </hbox>
+        <hbox class="indent" id="breachAlertsBox" flex="1" align="center">
+          <checkbox id="breachAlerts"
+                    class="tail-with-learn-more"
+                    data-l10n-id="forms-breach-alerts"
+                    search-l10n-ids="breach-alerts.label"
+                    preference="signon.management.page.breach-alerts.enabled"/>
+          <label id="breachAlertsLearnMoreLink" class="learnMore" is="text-link"
+              data-l10n-id="forms-breach-alerts-learn-more-link"/>
+        </hbox>
       </vbox>
       <vbox>
         <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
         <hbox>
           <button id="passwordExceptions"
                   is="highlightable-button"
                   class="accessory-button"
                   data-l10n-id="forms-exceptions"
--- a/browser/components/preferences/in-content/privacy.js
+++ b/browser/components/preferences/in-content/privacy.js
@@ -121,16 +121,17 @@ Preferences.addAll([
   { id: "media.autoplay.default", type: "int" },
 
   // Popups
   { id: "dom.disable_open_during_load", type: "bool" },
   // Passwords
   { id: "signon.rememberSignons", type: "bool" },
   { id: "signon.generation.enabled", type: "bool" },
   { id: "signon.autofillForms", type: "bool" },
+  { id: "signon.management.page.breach-alerts.enabled", type: "bool" },
 
   // Buttons
   { id: "pref.privacy.disable_button.view_passwords", type: "bool" },
   { id: "pref.privacy.disable_button.view_passwords_exceptions", type: "bool" },
 
   /* Certificates tab
    * security.default_personal_cert
    *   - a string:
@@ -496,16 +497,24 @@ var gPrivacyPane = {
       "command",
       gPrivacyPane.showSecurityDevices
     );
 
     this._pane = document.getElementById("panePrivacy");
 
     this._initPasswordGenerationUI();
     this._initMasterPasswordUI();
+    // set up the breach alerts Learn More link with the correct URL
+    const breachAlertsLearnMoreLink = document.getElementById(
+      "breachAlertsLearnMoreLink"
+    );
+    const breachAlertsLearnMoreUrl =
+      Services.urlFormatter.formatURLPref("app.support.baseURL") +
+      "lockwise-alerts";
+    breachAlertsLearnMoreLink.setAttribute("href", breachAlertsLearnMoreUrl);
 
     this._initSafeBrowsing();
 
     setEventListener(
       "autoplaySettingsButton",
       "command",
       gPrivacyPane.showAutoplayMediaExceptions
     );
--- a/browser/locales/en-US/browser/preferences/preferences.ftl
+++ b/browser/locales/en-US/browser/preferences/preferences.ftl
@@ -749,16 +749,20 @@ forms-ask-to-save-logins =
     .label = Ask to save logins and passwords for websites
     .accesskey = r
 forms-exceptions =
     .label = Exceptions…
     .accesskey = x
 forms-generate-passwords =
     .label = Suggest and generate strong passwords
     .accesskey = u
+forms-breach-alerts =
+    .label = Show alerts about passwords for breached websites
+    .accesskey = b
+forms-breach-alerts-learn-more-link = Learn more
 forms-fill-logins-and-passwords =
     .label = Autofill logins and passwords
     .accesskey = i
 forms-saved-logins =
     .label = Saved Logins…
     .accesskey = L
 forms-master-pw-use =
     .label = Use a master password
--- a/browser/themes/shared/browser.inc.css
+++ b/browser/themes/shared/browser.inc.css
@@ -233,16 +233,21 @@
 #contextual-feature-recommendation-notification {
   width: 343px;
 }
 
 #contextual-feature-recommendation-notification .popup-notification-icon {
   margin-inline-end: 4px;
 }
 
+#contextual-feature-recommendation-notification .cfr-doorhanger-large-icon {
+  width: 64px;
+  height: 64px;
+}
+
 #contextual-feature-recommendation-notification .popup-notification-body-container {
   padding-bottom: 0;
 }
 
 #contextual-feature-recommendation-notification popupnotificationcontent {
   margin-top: 0;
 }
 
@@ -252,22 +257,45 @@
 }
 
 #cfr-notification-footer-text-and-addon-info {
   display: block;
   padding: 10px var(--arrowpanel-padding);
   font-size: 13px;
 }
 
-#contextual-feature-recommendation-notification[data-notification-category="cfrFeatures"] .popup-notification-body-container,
-#contextual-feature-recommendation-notification[data-notification-category="cfrFeatures"] #cfr-notification-footer-addon-info,
-#contextual-feature-recommendation-notification[data-notification-category="cfrAddons"] #cfr-notification-feature-steps {
+#contextual-feature-recommendation-notification[data-notification-category="message_and_animation"] .popup-notification-body-container,
+#contextual-feature-recommendation-notification[data-notification-category="message_and_animation"] #cfr-notification-footer-addon-info,
+#contextual-feature-recommendation-notification[data-notification-category="addon_recommendation"] #cfr-notification-feature-steps,
+#contextual-feature-recommendation-notification[data-notification-category="icon_and_message"] .popup-notification-footer-container {
+  display: none;
+}
+
+/*
+ * `icon_and_message` CFR doorhanger with: icon, title and subtitle.
+ * No panel header is shown
+ */
+#contextual-feature-recommendation-notification[data-notification-category="icon_and_message"] #cfr-notification-header {
   display: none;
 }
 
+#contextual-feature-recommendation-notification[data-notification-category="icon_and_message"] .popup-notification-description {
+  font-size: 16px;
+  font-weight: 600;
+  margin-bottom: 4px;
+}
+
+#contextual-feature-recommendation-notification[data-notification-category="icon_and_message"] popupnotificationcontent {
+  display: block; /* This forces the subtitle content to wrap */
+}
+
+#contextual-feature-recommendation-notification[data-notification-category="icon_and_message"] .popup-notification-body-container {
+  padding-bottom: 20px;
+}
+
 #cfr-notification-feature-steps {
   display: flex;
   flex-direction: column;
   margin-top: 10px;
 }
 
 #cfr-notification-feature-steps li {
   padding-inline-start: 9px;
@@ -279,17 +307,17 @@
 }
 
 #cfr-notification-feature-steps li:before {
   content: "\2022";
   position: absolute;
   inset-inline-start: 0;
 }
 
-#contextual-feature-recommendation-notification[data-notification-category="cfrFeatures"] #cfr-notification-footer-text {
+#contextual-feature-recommendation-notification[data-notification-category="message_and_animation"] #cfr-notification-footer-text {
   font-size: 14px;
   font-weight: 600;
 }
 
 #cfr-notification-footer-text,
 #cfr-notification-footer-users,
 #cfr-notification-footer-learn-more-link {
   margin: 0;
@@ -365,17 +393,17 @@
 }
 
 @media (min-resolution: 2dppx) {
   #cfr-notification-footer-pintab-animation-container:before {
     background-image: url("resource://activity-stream/data/content/assets/cfr_pinnedtab_animated@2x.png");
   }
 }
 
-#contextual-feature-recommendation-notification[data-notification-category="cfrAddons"] #cfr-notification-footer-pintab-animation-container {
+#contextual-feature-recommendation-notification[data-notification-category="addon_recommendation"] #cfr-notification-footer-pintab-animation-container {
   display: none;
 }
 
 #cfr-notification-footer-pintab-animation-container:not([animate]):before,
 #cfr-notification-footer-pintab-animation-container[paused]:before,
 :root[lwt-popup-brighttext] #cfr-notification-footer-pintab-animation-container:not([animate]):before,
 :root[lwt-popup-brighttext] #cfr-notification-footer-pintab-animation-container[paused]:before {
   background-image: url("resource://activity-stream/data/content/assets/cfr_pinnedtab_static.png");
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -20,17 +20,17 @@
   --vertical-section-padding: 0.9em;
   --height-offset: 0px;
 }
 
 #protections-popup[toast] {
   --popup-width: 32rem;
 }
 
-#protections-popoup[infoMessageShowing] {
+#protections-popup[infoMessageShowing] {
   --height-offset: 260px;
 }
 
 /* This is used by screenshots tests to hide intermittently different
  * identity popup shadows (see bug 1425253). */
 #identity-popup.no-shadow {
   -moz-window-shadow: none;
 }
--- a/browser/themes/shared/privatebrowsing/aboutPrivateBrowsing.css
+++ b/browser/themes/shared/privatebrowsing/aboutPrivateBrowsing.css
@@ -29,31 +29,31 @@ p {
   align-items: center;
   display: flex;
   justify-content: center;
   margin-bottom: 50px;
 }
 
 .logo {
   background: url("chrome://branding/content/icon128.png") no-repeat center center;
-  background-size: 97px;
+  background-size: 96px;
   display: inline-block;
-  height: 97px;
-  width: 97px;
+  height: 96px;
+  width: 96px;
 }
 
 .wordmark {
   background: url("resource://activity-stream/data/content/assets/firefox-wordmark.svg") no-repeat center center;
-  background-size: 175px;
+  background-size: 172px;
   -moz-context-properties: fill;
   display: inline-block;
   fill: #fff;
-  height: 97px;
+  height: 96px;
   margin-inline-start: 15px;
-  width: 175px;
+  width: 172px;
 }
 
 .search-inner-wrapper {
   display: flex;
   height: 48px;
   margin-bottom: 64px;
   padding: 0 22px;
 }
--- a/devtools/client/debugger/src/components/Editor/menus/editor.js
+++ b/devtools/client/debugger/src/components/Editor/menus/editor.js
@@ -158,18 +158,18 @@ const downloadFileItem = (
   label: L10N.getStr("downloadFile.label"),
   accesskey: L10N.getStr("downloadFile.accesskey"),
   click: () => downloadFile(selectedContent, getFilename(selectedSource)),
 });
 
 const inlinePreviewItem = (editorActions: EditorItemActions) => ({
   id: "node-menu-inline-preview",
   label: features.inlinePreview
-    ? L10N.getStr("inlinePreview.disable.label")
-    : L10N.getStr("inlinePreview.enable.label"),
+    ? L10N.getStr("inlinePreview.hide.label")
+    : L10N.getStr("inlinePreview.show.label"),
   click: () => editorActions.toggleInlinePreview(!features.inlinePreview),
 });
 
 export function editorMenuItems({
   cx,
   editorActions,
   selectedSource,
   location,
--- a/devtools/client/debugger/test/mochitest/browser_dbg-scroll-run-to-completion.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg-scroll-run-to-completion.js
@@ -3,17 +3,16 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 add_task(async function() {
   const dbg = await initDebugger("doc-scroll-run-to-completion.html");
   invokeInTab("pauseOnce", "doc-scroll-run-to-completion.html");
   await waitForPaused(dbg);
   assertPausedLocation(dbg);
 
-  const target = dbg.toolbox.target;
-  await checkEvaluateInTopFrame(target, 'window.scrollBy(0, 10);', undefined);
+  await checkEvaluateInTopFrame(dbg, 'window.scrollBy(0, 10);', undefined);
 
   // checkEvaluateInTopFrame does an implicit resume for some reason.
   await waitForPaused(dbg);
 
   resume(dbg);
   await once(Services.ppmm, "test passed");
 });
--- a/devtools/client/debugger/test/mochitest/helpers.js
+++ b/devtools/client/debugger/test/mochitest/helpers.js
@@ -1734,30 +1734,30 @@ async function getDebuggerSplitConsole(d
   }
 
   await toolbox.openSplitConsole();
   return toolbox.getPanel("webconsole");
 }
 
 // Return a promise that resolves with the result of a thread evaluating a
 // string in the topmost frame.
-async function evaluateInTopFrame(target, text) {
-  const threadFront = target.threadFront;
-  const consoleFront = await target.getFront("console");
+async function evaluateInTopFrame(dbg, text) {
+  const threadFront = dbg.toolbox.target.threadFront;
+  const consoleFront = await dbg.toolbox.target.getFront("console");
   const { frames } = await threadFront.getFrames(0, 1);
   ok(frames.length == 1, "Got one frame");
   const options = { thread: threadFront.actor, frameActor: frames[0].actor };
   const response = await consoleFront.evaluateJS(text, options);
   return response.result.type == "undefined" ? undefined : response.result;
 }
 
 // Return a promise that resolves when a thread evaluates a string in the
 // topmost frame, ensuring the result matches the expected value.
-async function checkEvaluateInTopFrame(target, text, expected) {
-  const rval = await evaluateInTopFrame(target, text);
+async function checkEvaluateInTopFrame(dbg, text, expected) {
+  const rval = await evaluateInTopFrame(dbg, text);
   ok(rval == expected, `Eval returned ${expected}`);
 }
 
 async function findConsoleMessage({toolbox}, query) {
   const [message] = await findConsoleMessages(toolbox, query);
   const value = message.querySelector(".message-body").innerText;
   const link = message.querySelector(".frame-link-source-inner").innerText;
   return { value, link };
--- a/devtools/client/locales/en-US/debugger.properties
+++ b/devtools/client/locales/en-US/debugger.properties
@@ -425,23 +425,23 @@ editor.conditionalPanel.logPoint.placeho
 editor.jumpToMappedLocation1=Jump to %S location
 editor.jumpToMappedLocation1.accesskey=m
 
 # LOCALIZATION NOTE (downloadFile.label): Context menu item
 # for downloading a source's content
 downloadFile.label=Download file
 downloadFile.accesskey=d
 
-# LOCALIZATION NOTE (inlinePreview.enable.label): Context menu item
-# for enabling the inline preview feature
-inlinePreview.enable.label=Enable inline preview
+# LOCALIZATION NOTE (inlinePreview.show.label): Context menu item
+# for showing the inline preview blocks
+inlinePreview.show.label=Show inline preview
 
-# LOCALIZATION NOTE (inlinePreview.disable.label): Context menu item
-# for disabling the inline preview feature
-inlinePreview.disable.label=Disable inline preview
+# LOCALIZATION NOTE (inlinePreview.hide.label): Context menu item
+# for hiding the inline preview block
+inlinePreview.hide.label=Hide inline preview
 
 # LOCALIZATION NOTE (framework.disableGrouping): This is the text that appears in the
 # context menu to disable framework grouping.
 framework.disableGrouping=Disable framework grouping
 framework.disableGrouping.accesskey=u
 
 # LOCALIZATION NOTE (framework.enableGrouping): This is the text that appears in the
 # context menu to enable framework grouping.
--- a/devtools/client/responsive/actions/index.js
+++ b/devtools/client/responsive/actions/index.js
@@ -96,11 +96,14 @@ createEnum(
     // Toggles the user agent input displayed in the toolbar.
     "TOGGLE_USER_AGENT_INPUT",
 
     // Update the device display state in the device selector.
     "UPDATE_DEVICE_DISPLAYED",
 
     // Update the device modal state.
     "UPDATE_DEVICE_MODAL",
+
+    // Zoom the viewport.
+    "ZOOM_VIEWPORT",
   ],
   module.exports
 );
--- a/devtools/client/responsive/actions/viewports.js
+++ b/devtools/client/responsive/actions/viewports.js
@@ -11,16 +11,17 @@ const asyncStorage = require("devtools/s
 const {
   ADD_VIEWPORT,
   CHANGE_DEVICE,
   CHANGE_PIXEL_RATIO,
   CHANGE_VIEWPORT_ANGLE,
   REMOVE_DEVICE_ASSOCIATION,
   RESIZE_VIEWPORT,
   ROTATE_VIEWPORT,
+  ZOOM_VIEWPORT,
 } = require("./index");
 
 const { post } = require("../utils/message");
 
 module.exports = {
   /**
    * Add an additional viewport to display the document.
    */
@@ -106,9 +107,20 @@ module.exports = {
    * Rotate the viewport.
    */
   rotateViewport(id) {
     return {
       type: ROTATE_VIEWPORT,
       id,
     };
   },
+
+  /**
+   * Zoom the viewport.
+   */
+  zoomViewport(id, zoom) {
+    return {
+      type: ZOOM_VIEWPORT,
+      id,
+      zoom,
+    };
+  },
 };
--- a/devtools/client/responsive/components/ResizableViewport.js
+++ b/devtools/client/responsive/components/ResizableViewport.js
@@ -61,44 +61,44 @@ class ResizableViewport extends PureComp
   }
 
   onResizeDrag({ clientX, clientY }) {
     if (!this.state.isResizing) {
       return;
     }
 
     let { lastClientX, lastClientY, ignoreX, ignoreY } = this.state;
-    let deltaX = clientX - lastClientX;
-    let deltaY = clientY - lastClientY;
+    let deltaX = (clientX - lastClientX) / this.props.viewport.zoom;
+    let deltaY = (clientY - lastClientY) / this.props.viewport.zoom;
 
     if (!this.props.leftAlignmentEnabled) {
       // The viewport is centered horizontally, so horizontal resize resizes
       // by twice the distance the mouse was dragged - on left and right side.
       deltaX = deltaX * 2;
     }
 
     if (ignoreX) {
       deltaX = 0;
     }
     if (ignoreY) {
       deltaY = 0;
     }
 
-    let width = this.props.viewport.width + deltaX;
-    let height = this.props.viewport.height + deltaY;
+    let width = Math.round(this.props.viewport.width + deltaX);
+    let height = Math.round(this.props.viewport.height + deltaY);
 
     if (width < VIEWPORT_MIN_WIDTH) {
       width = VIEWPORT_MIN_WIDTH;
-    } else {
+    } else if (width != this.props.viewport.width) {
       lastClientX = clientX;
     }
 
     if (height < VIEWPORT_MIN_HEIGHT) {
       height = VIEWPORT_MIN_HEIGHT;
-    } else {
+    } else if (height != this.props.viewport.height) {
       lastClientY = clientY;
     }
 
     // Update the viewport store with the new width and height.
     const { doResizeViewport, viewport } = this.props;
     doResizeViewport(viewport.id, width, height);
     // Change the device selector back to an unselected device
     // TODO: Bug 1332754: Logic like this probably belongs in the action creator.
@@ -166,18 +166,18 @@ class ResizableViewport extends PureComp
     return dom.div(
       { className: "viewport" },
       dom.div(
         { className: "resizable-viewport" },
         dom.div(
           {
             className: contentClass,
             style: {
-              width: viewport.width + "px",
-              height: viewport.height + "px",
+              width: Math.round(viewport.width * viewport.zoom) + "px",
+              height: Math.round(viewport.height * viewport.zoom) + "px",
             },
           },
           Browser({
             swapAfterMount,
             userContextId: viewport.userContextId,
             viewport,
             onBrowserMounted,
             onChangeViewportOrientation,
--- a/devtools/client/responsive/index.js
+++ b/devtools/client/responsive/index.js
@@ -21,17 +21,21 @@ const {
 } = require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
 const message = require("./utils/message");
 const App = createFactory(require("./components/App"));
 const Store = require("./store");
 const { loadDevices, restoreDeviceState } = require("./actions/devices");
-const { addViewport, resizeViewport } = require("./actions/viewports");
+const {
+  addViewport,
+  resizeViewport,
+  zoomViewport,
+} = require("./actions/viewports");
 const { changeDisplayPixelRatio } = require("./actions/ui");
 
 // Exposed for use by tests
 window.require = require;
 
 const bootstrap = {
   telemetry: new Telemetry(),
 
@@ -171,8 +175,19 @@ window.getViewportBrowser = () => {
         return this.frameLoader.messageManager;
       },
       configurable: true,
       enumerable: true,
     });
   }
   return browser;
 };
+
+/**
+ * Called by manager.js to zoom the viewport.
+ */
+window.setViewportZoom = zoom => {
+  try {
+    bootstrap.dispatch(zoomViewport(0, zoom));
+  } catch (e) {
+    console.error(e);
+  }
+};
--- a/devtools/client/responsive/manager.js
+++ b/devtools/client/responsive/manager.js
@@ -418,21 +418,28 @@ ResponsiveUI.prototype = {
     // re-apply them to re-sync the Zoom UI.
 
     // Cache the values now and we'll re-apply them near the end of this function.
     // This is important since other steps here can also cause the Zoom UI update
     // event to be sent for other browsers, and this means that the changes from
     // our Zoom UI update event would be overwritten. After this function, future
     // changes to zoom levels will send Zoom UI update events in an order that
     // keeps the Zoom UI synchronized with the RDM content zoom levels.
-    const fullZoom = this.tab.linkedBrowser.fullZoom;
-    const textZoom = this.tab.linkedBrowser.textZoom;
+    const rdmContent = this.tab.linkedBrowser;
+    const rdmViewport = ui.toolWindow;
+
+    const fullZoom = rdmContent.fullZoom;
+    const textZoom = rdmContent.textZoom;
 
-    ui.toolWindow.docShell.contentViewer.fullZoom = 1;
-    ui.toolWindow.docShell.contentViewer.textZoom = 1;
+    rdmViewport.docShell.contentViewer.fullZoom = 1;
+    rdmViewport.docShell.contentViewer.textZoom = 1;
+
+    // Listen to FullZoomChange events coming from the linkedBrowser,
+    // so that we can zoom the size of the viewport by the same amount.
+    rdmContent.addEventListener("FullZoomChange", this);
 
     this.tab.addEventListener("BeforeTabRemotenessChange", this);
 
     // Notify the inner browser to start the frame script
     debug("Wait until start frame script");
     await message.request(this.toolWindow, "start-frame-script");
 
     // Get the protocol ready to speak with emulation actor
@@ -440,24 +447,24 @@ ResponsiveUI.prototype = {
     await this.connectToServer();
 
     // Restore the previous state of RDM.
     await this.restoreState();
 
     // Show the settings onboarding tooltip
     if (Services.prefs.getBoolPref(SHOW_SETTING_TOOLTIP_PREF)) {
       this.settingOnboardingTooltip = new SettingOnboardingTooltip(
-        ui.toolWindow.document
+        rdmViewport.document
       );
     }
 
     // Re-apply our cached zoom levels. Other Zoom UI update events have finished
     // by now.
-    this.tab.linkedBrowser.fullZoom = fullZoom;
-    this.tab.linkedBrowser.textZoom = textZoom;
+    rdmContent.fullZoom = fullZoom;
+    rdmContent.textZoom = textZoom;
 
     // Non-blocking message to tool UI to start any delayed init activities
     message.post(this.toolWindow, "post-init");
 
     debug("Init done");
   },
 
   /**
@@ -487,16 +494,17 @@ ResponsiveUI.prototype = {
         (options.reason === "TabClose" ||
           options.reason === "BeforeTabRemotenessChange"));
 
     // Ensure init has finished before starting destroy
     if (!isTabContentDestroying) {
       await this.inited;
     }
 
+    this.tab.linkedBrowser.removeEventListener("FullZoomChange", this);
     this.tab.removeEventListener("TabClose", this);
     this.tab.removeEventListener("BeforeTabRemotenessChange", this);
     this.browserWindow.removeEventListener("unload", this);
     this.toolWindow.removeEventListener("message", this);
 
     if (!isTabContentDestroying) {
       // Notify the inner browser to stop the frame script
       await message.request(this.toolWindow, "stop-frame-script");
@@ -584,22 +592,26 @@ ResponsiveUI.prototype = {
 
   reloadOnChange(id) {
     this.showReloadNotification();
     const pref = RELOAD_CONDITION_PREF_PREFIX + id;
     return Services.prefs.getBoolPref(pref, false);
   },
 
   handleEvent(event) {
-    const { browserWindow, tab } = this;
+    const { browserWindow, tab, toolWindow } = this;
 
     switch (event.type) {
       case "message":
         this.handleMessage(event);
         break;
+      case "FullZoomChange":
+        const zoom = tab.linkedBrowser.fullZoom;
+        toolWindow.setViewportZoom(zoom);
+        break;
       case "BeforeTabRemotenessChange":
       case "TabClose":
       case "unload":
         ResponsiveUIManager.closeIfNeeded(browserWindow, tab, {
           reason: event.type,
         });
         break;
     }
--- a/devtools/client/responsive/reducers/viewports.js
+++ b/devtools/client/responsive/reducers/viewports.js
@@ -10,16 +10,17 @@ const {
   ADD_VIEWPORT,
   CHANGE_DEVICE,
   CHANGE_PIXEL_RATIO,
   CHANGE_VIEWPORT_ANGLE,
   EDIT_DEVICE,
   REMOVE_DEVICE_ASSOCIATION,
   RESIZE_VIEWPORT,
   ROTATE_VIEWPORT,
+  ZOOM_VIEWPORT,
 } = require("../actions/index");
 
 const VIEWPORT_WIDTH_PREF = "devtools.responsive.viewport.width";
 const VIEWPORT_HEIGHT_PREF = "devtools.responsive.viewport.height";
 const VIEWPORT_PIXEL_RATIO_PREF = "devtools.responsive.viewport.pixelRatio";
 const VIEWPORT_ANGLE_PREF = "devtools.responsive.viewport.angle";
 
 let nextViewportId = 0;
@@ -29,16 +30,17 @@ const INITIAL_VIEWPORT = {
   id: nextViewportId++,
   angle: Services.prefs.getIntPref(VIEWPORT_ANGLE_PREF, 0),
   device: "",
   deviceType: "",
   height: Services.prefs.getIntPref(VIEWPORT_HEIGHT_PREF, 480),
   width: Services.prefs.getIntPref(VIEWPORT_WIDTH_PREF, 320),
   pixelRatio: Services.prefs.getIntPref(VIEWPORT_PIXEL_RATIO_PREF, 0),
   userContextId: 0,
+  zoom: 1,
 };
 
 const reducers = {
   [ADD_VIEWPORT](viewports, { userContextId }) {
     // For the moment, there can be at most one viewport.
     if (viewports.length === 1) {
       return viewports;
     }
@@ -179,16 +181,33 @@ const reducers = {
 
       return {
         ...viewport,
         height,
         width,
       };
     });
   },
+
+  [ZOOM_VIEWPORT](viewports, { id, zoom }) {
+    return viewports.map(viewport => {
+      if (viewport.id !== id) {
+        return viewport;
+      }
+
+      if (!zoom) {
+        zoom = viewport.zoom;
+      }
+
+      return {
+        ...viewport,
+        zoom,
+      };
+    });
+  },
 };
 
 module.exports = function(viewports = INITIAL_VIEWPORTS, action) {
   const reducer = reducers[action.type];
   if (!reducer) {
     return viewports;
   }
   return reducer(viewports, action);
--- a/devtools/client/responsive/test/browser/browser_window_sizing.js
+++ b/devtools/client/responsive/test/browser/browser_window_sizing.js
@@ -7,79 +7,106 @@
 // particular, we want to ensure that the values for the window's outer and screen
 // sizing values reflect the size of the viewport.
 
 const TEST_URL = "data:text/html;charset=utf-8,";
 const WIDTH = 375;
 const HEIGHT = 450;
 const ZOOM_LEVELS = [0.3, 0.5, 0.9, 1, 1.5, 2, 2.4];
 
+function promiseContentReflow(ui) {
+  return ContentTask.spawn(ui.getViewportBrowser(), {}, async function() {
+    return new Promise(resolve => {
+      content.window.requestAnimationFrame(resolve);
+    });
+  });
+}
+
 add_task(async function() {
   const tab = await addTab(TEST_URL);
   const browser = tab.linkedBrowser;
 
   const { ui, manager } = await openRDM(tab);
   await setViewportSize(ui, manager, WIDTH, HEIGHT);
 
   info("Ensure outer size values are unchanged at different zoom levels.");
   for (let i = 0; i < ZOOM_LEVELS.length; i++) {
     info(`Setting zoom level to ${ZOOM_LEVELS[i]}`);
     ZoomManager.setZoomForBrowser(browser, ZOOM_LEVELS[i]);
 
-    await checkWindowOuterSize(ui);
-    await checkWindowScreenSize(ui);
+    // We need to ensure that the RDM pane has had time to both change size and
+    // change the zoom level. This is currently not an atomic operation. The event
+    // timing is this:
+    // 1) Pane changes size, content reflows.
+    // 2) Pane changes zoom, content reflows.
+    // So to wait for the post-zoom reflow to be complete, we have two wait on TWO
+    // reflows.
+    await promiseContentReflow(ui);
+    await promiseContentReflow(ui);
+
+    await checkWindowOuterSize(ui, ZOOM_LEVELS[i]);
+    await checkWindowScreenSize(ui, ZOOM_LEVELS[i]);
   }
 });
 
-async function checkWindowOuterSize(ui) {
+async function checkWindowOuterSize(ui, zoom_level) {
   return ContentTask.spawn(
     ui.getViewportBrowser(),
-    { width: WIDTH, height: HEIGHT },
-    async function({ width, height }) {
+    { width: WIDTH, height: HEIGHT, zoom: zoom_level },
+    async function({ width, height, zoom }) {
       // Approximate the outer size value returned on the window content with the expected
-      // value. We should expect, at the very most, a 1px difference between the two due
+      // value. We should expect, at the very most, a 2px difference between the two due
       // to floating point rounding errors that occur when scaling from inner size CSS
       // integer values to outer size CSS integer values. See Part 1 of Bug 1107456.
+      // Some of the drift is also due to full zoom scaling effects; see Bug 1577775.
       ok(
-        Math.abs(content.outerWidth - width) <= 1,
-        `window.outerWidth should be ${width} and we got ${content.outerWidth}.`
+        Math.abs(content.outerWidth - width) <= 2,
+        `window.outerWidth zoom ${zoom} should be ${width} and we got ${
+          content.outerWidth
+        }.`
       );
       ok(
-        Math.abs(content.outerHeight - height) <= 1,
-        `window.outerHeight should be ${height} and we got ${
+        Math.abs(content.outerHeight - height) <= 2,
+        `window.outerHeight zoom ${zoom} should be ${height} and we got ${
           content.outerHeight
         }.`
       );
     }
   );
 }
 
-async function checkWindowScreenSize(ui) {
+async function checkWindowScreenSize(ui, zoom_level) {
   return ContentTask.spawn(
     ui.getViewportBrowser(),
-    { width: WIDTH, height: HEIGHT },
-    async function({ width, height }) {
+    { width: WIDTH, height: HEIGHT, zoom: zoom_level },
+    async function({ width, height, zoom }) {
       const { screen } = content;
 
       ok(
-        Math.abs(screen.availWidth - width) <= 1,
-        `screen.availWidth should be ${width} and we got ${screen.availWidth}.`
+        Math.abs(screen.availWidth - width) <= 2,
+        `screen.availWidth zoom ${zoom} should be ${width} and we got ${
+          screen.availWidth
+        }.`
       );
 
       ok(
-        Math.abs(screen.availHeight - height) <= 1,
-        `screen.availHeight should be ${height} and we got ${
+        Math.abs(screen.availHeight - height) <= 2,
+        `screen.availHeight zoom ${zoom} should be ${height} and we got ${
           screen.availHeight
         }.`
       );
 
       ok(
-        Math.abs(screen.width - width) <= 1,
-        `screen.width should be ${width} and we got ${screen.width}.`
+        Math.abs(screen.width - width) <= 2,
+        `screen.width zoom " ${zoom} should be ${width} and we got ${
+          screen.width
+        }.`
       );
 
       ok(
-        Math.abs(screen.height - height) <= 1,
-        `screen.height should be ${height} and we got ${screen.height}.`
+        Math.abs(screen.height - height) <= 2,
+        `screen.height zoom " ${zoom} should be ${height} and we got ${
+          screen.height
+        }.`
       );
     }
   );
 }
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-01.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-01.js
@@ -4,36 +4,34 @@
 
 "use strict";
 
 // Test basic breakpoint functionality in web replay.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_rr_basic.html", {
     waitForRecording: true,
   });
-  const { threadFront, target } = dbg;
 
-  const bp = await setBreakpoint(threadFront, "doc_rr_basic.html", 21);
+  await addBreakpoint(dbg, "doc_rr_basic.html", 21);
 
   // Visit a lot of breakpoints so that we are sure we have crossed major
   // checkpoint boundaries.
-  await rewindToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 10);
-  await rewindToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 9);
-  await rewindToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 8);
-  await rewindToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 7);
-  await rewindToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 6);
-  await resumeToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 7);
-  await resumeToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 8);
-  await resumeToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 9);
-  await resumeToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 10);
+  await rewindToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 10);
+  await rewindToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 9);
+  await rewindToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 8);
+  await rewindToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 7);
+  await rewindToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 6);
+  await resumeToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 7);
+  await resumeToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 8);
+  await resumeToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 9);
+  await resumeToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 10);
 
-  await threadFront.removeBreakpoint(bp);
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-02.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-02.js
@@ -4,22 +4,20 @@
 
 "use strict";
 
 // Test unhandled divergence while evaluating at a breakpoint with Web Replay.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_rr_basic.html", {
     waitForRecording: true,
   });
-  const { threadFront, target } = dbg;
 
-  const bp = await setBreakpoint(threadFront, "doc_rr_basic.html", 21);
-  await rewindToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 10);
-  await checkEvaluateInTopFrameThrows(target, "window.alert(3)");
-  await checkEvaluateInTopFrame(target, "number", 10);
-  await checkEvaluateInTopFrameThrows(target, "window.alert(3)");
-  await checkEvaluateInTopFrame(target, "number", 10);
-  await checkEvaluateInTopFrame(target, "testStepping2()", undefined);
+  await addBreakpoint(dbg, "doc_rr_basic.html", 21);
+  await rewindToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 10);
+  await checkEvaluateInTopFrameThrows(dbg, "window.alert(3)");
+  await checkEvaluateInTopFrame(dbg, "number", 10);
+  await checkEvaluateInTopFrameThrows(dbg, "window.alert(3)");
+  await checkEvaluateInTopFrame(dbg, "number", 10);
+  await checkEvaluateInTopFrame(dbg, "testStepping2()", undefined);
 
-  await threadFront.removeBreakpoint(bp);
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-03.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-03.js
@@ -2,25 +2,20 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 /* eslint-disable no-undef */
 
 "use strict";
 
 // Test some issues when stepping around after hitting a breakpoint while recording.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_rr_continuous.html");
-  const { threadFront } = dbg;
 
-  await threadFront.interrupt();
-  const bp1 = await setBreakpoint(threadFront, "doc_rr_continuous.html", 19);
-  await resumeToLine(threadFront, 19);
-  await reverseStepOverToLine(threadFront, 18);
-  await stepInToLine(threadFront, 22);
-  const bp2 = await setBreakpoint(threadFront, "doc_rr_continuous.html", 24);
-  await resumeToLine(threadFront, 24);
-  const bp3 = await setBreakpoint(threadFront, "doc_rr_continuous.html", 22);
-  await rewindToLine(threadFront, 22);
+  await addBreakpoint(dbg, "doc_rr_continuous.html", 19);
+  await resumeToLine(dbg, 19);
+  await reverseStepOverToLine(dbg, 18);
+  await stepInToLine(dbg, 22);
+  await addBreakpoint(dbg, "doc_rr_continuous.html", 24);
+  await resumeToLine(dbg, 24);
+  await addBreakpoint(dbg, "doc_rr_continuous.html", 22);
+  await rewindToLine(dbg, 22);
 
-  await threadFront.removeBreakpoint(bp1);
-  await threadFront.removeBreakpoint(bp2);
-  await threadFront.removeBreakpoint(bp3);
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-04.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-04.js
@@ -3,29 +3,27 @@
 /* eslint-disable no-undef */
 
 "use strict";
 
 // Test navigating back to earlier breakpoints while recording, then resuming
 // recording.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_rr_continuous.html");
-  const { threadFront, target } = dbg;
 
-  const bp = await setBreakpoint(threadFront, "doc_rr_continuous.html", 14);
-  await resumeToLine(threadFront, 14);
-  const value = await evaluateInTopFrame(target, "number");
-  await resumeToLine(threadFront, 14);
-  await checkEvaluateInTopFrame(target, "number", value + 1);
-  await rewindToLine(threadFront, 14);
-  await checkEvaluateInTopFrame(target, "number", value);
-  await resumeToLine(threadFront, 14);
-  await checkEvaluateInTopFrame(target, "number", value + 1);
-  await resumeToLine(threadFront, 14);
-  await checkEvaluateInTopFrame(target, "number", value + 2);
-  await resumeToLine(threadFront, 14);
-  await checkEvaluateInTopFrame(target, "number", value + 3);
-  await rewindToLine(threadFront, 14);
-  await checkEvaluateInTopFrame(target, "number", value + 2);
+  await addBreakpoint(dbg, "doc_rr_continuous.html", 14);
+  await resumeToLine(dbg, 14);
+  const value = await evaluateInTopFrame(dbg, "number");
+  await resumeToLine(dbg, 14);
+  await checkEvaluateInTopFrame(dbg, "number", value + 1);
+  await rewindToLine(dbg, 14);
+  await checkEvaluateInTopFrame(dbg, "number", value);
+  await resumeToLine(dbg, 14);
+  await checkEvaluateInTopFrame(dbg, "number", value + 1);
+  await resumeToLine(dbg, 14);
+  await checkEvaluateInTopFrame(dbg, "number", value + 2);
+  await resumeToLine(dbg, 14);
+  await checkEvaluateInTopFrame(dbg, "number", value + 3);
+  await rewindToLine(dbg, 14);
+  await checkEvaluateInTopFrame(dbg, "number", value + 2);
 
-  await threadFront.removeBreakpoint(bp);
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-05.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-05.js
@@ -6,24 +6,19 @@
 
 // Test hitting breakpoints when rewinding past the point where the breakpoint
 // script was created.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_rr_basic.html", {
     waitForRecording: true,
   });
 
-  const { threadFront, target } = dbg;
-
-  await threadFront.interrupt();
-
   // Rewind to the beginning of the recording.
-  await rewindToLine(threadFront, undefined);
+  await rewindToLine(dbg, undefined);
 
-  const bp = await setBreakpoint(threadFront, "doc_rr_basic.html", 21);
-  await resumeToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 1);
-  await resumeToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 2);
+  await addBreakpoint(dbg, "doc_rr_basic.html", 21);
+  await resumeToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 1);
+  await resumeToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 2);
 
-  await threadFront.removeBreakpoint(bp);
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-06.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-06.js
@@ -5,41 +5,34 @@
 "use strict";
 
 // Test hitting breakpoints when using tricky control flow constructs:
 // catch, finally, generators, and async/await.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_control_flow.html", {
     waitForRecording: true,
   });
-  const { threadFront } = dbg;
-  const breakpoints = [];
 
   await rewindToBreakpoint(10);
   await resumeToBreakpoint(12);
   await resumeToBreakpoint(18);
   await resumeToBreakpoint(20);
   await resumeToBreakpoint(32);
   await resumeToBreakpoint(27);
-  await resumeToLine(threadFront, 32);
-  await resumeToLine(threadFront, 27);
+  await resumeToLine(dbg, 32);
+  await resumeToLine(dbg, 27);
   await resumeToBreakpoint(42);
   await resumeToBreakpoint(44);
   await resumeToBreakpoint(50);
   await resumeToBreakpoint(54);
 
-  for (const bp of breakpoints) {
-    await threadFront.removeBreakpoint(bp);
-  }
   await shutdownDebugger(dbg);
 
   async function rewindToBreakpoint(line) {
-    const bp = await setBreakpoint(threadFront, "doc_control_flow.html", line);
-    breakpoints.push(bp);
-    await rewindToLine(threadFront, line);
+    await addBreakpoint(dbg, "doc_control_flow.html", line);
+    await rewindToLine(dbg, line);
   }
 
   async function resumeToBreakpoint(line) {
-    const bp = await setBreakpoint(threadFront, "doc_control_flow.html", line);
-    breakpoints.push(bp);
-    await resumeToLine(threadFront, line);
+    await addBreakpoint(dbg, "doc_control_flow.html", line);
+    await resumeToLine(dbg, line);
   }
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-07.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-07.js
@@ -6,28 +6,25 @@
 
 "use strict";
 
 // Pausing at a debugger statement on startup confuses the debugger.
 PromiseTestUtils.whitelistRejectionsGlobally(/Unknown source actor/);
 
 // Test interaction of breakpoints with debugger statements.
 add_task(async function() {
-  const dbg = await attachRecordingDebugger("doc_debugger_statements.html");
-  const { threadFront } = dbg;
+  const dbg = await attachRecordingDebugger("doc_debugger_statements.html", {
+    skipInterrupt: true,
+  });
 
   await waitForPaused(dbg);
-  const { frames } = await threadFront.getFrames(0, 1);
-  ok(frames[0].where.line == 6, "Paused at first debugger statement");
+  const pauseLine = getVisibleSelectedFrameLine(dbg);
+  ok(pauseLine == 6, "Paused at first debugger statement");
 
-  const bp = await setBreakpoint(
-    threadFront,
-    "doc_debugger_statements.html",
-    7
-  );
-  await resumeToLine(threadFront, 7);
-  await resumeToLine(threadFront, 8);
-  await threadFront.removeBreakpoint(bp);
-  await rewindToLine(threadFront, 6);
-  await resumeToLine(threadFront, 8);
+  await addBreakpoint(dbg, "doc_debugger_statements.html", 7);
+  await resumeToLine(dbg, 7);
+  await resumeToLine(dbg, 8);
+  await dbg.actions.removeAllBreakpoints(getContext(dbg));
+  await rewindToLine(dbg, 6);
+  await resumeToLine(dbg, 8);
 
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_console_warp-01.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_console_warp-01.js
@@ -5,30 +5,27 @@
 "use strict";
 
 // Test basic console time warping functionality in web replay.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_rr_error.html", {
     waitForRecording: true,
   });
 
-  const { threadFront, target } = dbg;
   const console = await getDebuggerSplitConsole(dbg);
   const hud = console.hud;
 
   await warpToMessage(hud, dbg, "Number 5");
-  await threadFront.interrupt();
 
-  await checkEvaluateInTopFrame(target, "number", 5);
+  await checkEvaluateInTopFrame(dbg, "number", 5);
 
   // Initially we are paused inside the 'new Error()' call on line 19. The
   // first reverse step takes us to the start of that line.
-  await reverseStepOverToLine(threadFront, 19);
-  await reverseStepOverToLine(threadFront, 18);
-  const bp = await setBreakpoint(threadFront, "doc_rr_error.html", 12);
-  await rewindToLine(threadFront, 12);
-  await checkEvaluateInTopFrame(target, "number", 4);
-  await resumeToLine(threadFront, 12);
-  await checkEvaluateInTopFrame(target, "number", 5);
+  await reverseStepOverToLine(dbg, 19);
+  await reverseStepOverToLine(dbg, 18);
+  await addBreakpoint(dbg, "doc_rr_error.html", 12);
+  await rewindToLine(dbg, 12);
+  await checkEvaluateInTopFrame(dbg, "number", 4);
+  await resumeToLine(dbg, 12);
+  await checkEvaluateInTopFrame(dbg, "number", 5);
 
-  await threadFront.removeBreakpoint(bp);
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_console_warp-02.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_console_warp-02.js
@@ -5,23 +5,22 @@
 "use strict";
 
 // Test basic console time warping functionality in web replay.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_rr_logs.html", {
     waitForRecording: true,
   });
 
-  const { threadFront } = dbg;
   const console = await getDebuggerSplitConsole(dbg);
   const hud = console.hud;
 
   let message = await warpToMessage(hud, dbg, "number: 1");
   // ok(message.classList.contains("paused-before"), "paused before message is shown");
 
-  await stepOverToLine(threadFront, 18);
-  await reverseStepOverToLine(threadFront, 17);
+  await stepOverToLine(dbg, 18);
+  await reverseStepOverToLine(dbg, 17);
 
   message = findMessage(hud, "number: 1");
   // ok(message.classList.contains("paused-before"), "paused before message is shown");
 
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_logpoint-01.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_logpoint-01.js
@@ -6,40 +6,42 @@
 
 // Test basic logpoint functionality in web replay. When logpoints are added,
 // new messages should appear in the correct order and allow time warping.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_rr_basic.html", {
     waitForRecording: true,
   });
 
-  const { threadFront, target } = dbg;
+  await selectSource(dbg, "doc_rr_basic.html");
+  await addBreakpoint(dbg, "doc_rr_basic.html", 21, undefined, {
+    logValue: `"Logpoint Number " + number`,
+  });
+  await addBreakpoint(dbg, "doc_rr_basic.html", 6, undefined, {
+    logValue: `"Logpoint Beginning"`,
+  });
+  await addBreakpoint(dbg, "doc_rr_basic.html", 8, undefined, {
+    logValue: `"Logpoint Ending"`,
+  });
+
   const console = await getDebuggerSplitConsole(dbg);
   const hud = console.hud;
 
-  const bp1 = await setBreakpoint(threadFront, "doc_rr_basic.html", 21, {
-    logValue: `"Logpoint Number " + number`,
-  });
-  const bp2 = await setBreakpoint(threadFront, "doc_rr_basic.html", 6, {
-    logValue: `"Logpoint Beginning"`,
-  });
-  const bp3 = await setBreakpoint(threadFront, "doc_rr_basic.html", 8, {
-    logValue: `"Logpoint Ending"`,
-  });
+  const messages = await waitForMessageCount(hud, "Logpoint", 12);
 
-  const messages = await waitForMessageCount(hud, "Logpoint", 12);
+  ok(
+    !findMessages(hud, "Loading"),
+    "Loading messages should have been removed"
+  );
+
   ok(messages[0].textContent.includes("Beginning"));
   for (let i = 1; i <= 10; i++) {
     ok(messages[i].textContent.includes("Number " + i));
   }
   ok(messages[11].textContent.includes("Ending"));
 
   await warpToMessage(hud, dbg, "Number 5");
-  await threadFront.interrupt();
 
-  await checkEvaluateInTopFrame(target, "number", 5);
-  await reverseStepOverToLine(threadFront, 20);
+  await checkEvaluateInTopFrame(dbg, "number", 5);
+  await reverseStepOverToLine(dbg, 20);
 
-  await threadFront.removeBreakpoint(bp1);
-  await threadFront.removeBreakpoint(bp2);
-  await threadFront.removeBreakpoint(bp3);
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_logpoint-02.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_logpoint-02.js
@@ -31,12 +31,10 @@ add_task(async function() {
   await waitForMessageCount(hud, "updateNumber Logpoint", 10);
 
   await setBreakpointOptions(dbg, "doc_rr_basic.html", 21, undefined, {
     logValue: `"Logpoint Number " + number`,
     condition: `number % 2 == 0`,
   });
   await waitForMessageCount(hud, "Logpoint", 6);
 
-  await dbg.actions.removeAllBreakpoints(getContext(dbg));
-
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_replay-01.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_replay-01.js
@@ -21,23 +21,20 @@ add_task(async function() {
 
   const replayingTab = BrowserTestUtils.addTab(gBrowser, null, {
     replayExecution: recordingFile,
   });
   gBrowser.selectedTab = replayingTab;
   await once(Services.ppmm, "HitRecordingEndpoint");
 
   const dbg = await attachDebugger(replayingTab);
-  const { threadFront } = dbg.toolbox;
-  const { target } = dbg;
-  await threadFront.interrupt();
-  const bp = await setBreakpoint(threadFront, "doc_rr_basic.html", 21);
-  await rewindToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 10);
-  await rewindToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 9);
-  await resumeToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 10);
 
-  await threadFront.removeBreakpoint(bp);
+  await addBreakpoint(dbg, "doc_rr_basic.html", 21);
+  await rewindToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 10);
+  await rewindToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 9);
+  await resumeToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 10);
+
   await gBrowser.removeTab(recordingTab);
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_replay-02.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_replay-02.js
@@ -10,55 +10,45 @@ add_task(async function() {
 
   const recordingFile = newRecordingFile();
   const recordingTab = BrowserTestUtils.addTab(gBrowser, null, {
     recordExecution: "*",
   });
   gBrowser.selectedTab = recordingTab;
   openTrustedLinkIn(EXAMPLE_URL + "doc_rr_continuous.html", "current");
 
-  const firstTab = await attachDebugger(recordingTab);
-  let toolbox = firstTab.toolbox;
-  let target = firstTab.target;
-  let threadFront = toolbox.threadFront;
-  await threadFront.interrupt();
-  let bp = await setBreakpoint(threadFront, "doc_rr_continuous.html", 14);
-  await resumeToLine(threadFront, 14);
-  await resumeToLine(threadFront, 14);
-  await reverseStepOverToLine(threadFront, 13);
-  const lastNumberValue = await evaluateInTopFrame(target, "number");
+  let dbg = await attachDebugger(recordingTab);
+
+  await addBreakpoint(dbg, "doc_rr_continuous.html", 14);
+  await resumeToLine(dbg, 14);
+  await resumeToLine(dbg, 14);
+  await reverseStepOverToLine(dbg, 13);
+  const lastNumberValue = await evaluateInTopFrame(dbg, "number");
 
   const remoteTab = recordingTab.linkedBrowser.frameLoader.remoteTab;
   ok(remoteTab, "Found recording remote tab");
   ok(remoteTab.saveRecording(recordingFile), "Saved recording");
   await once(Services.ppmm, "SaveRecordingFinished");
 
-  await threadFront.removeBreakpoint(bp);
-  await toolbox.destroy();
-  await gBrowser.removeTab(recordingTab);
+  await shutdownDebugger(dbg);
 
   const replayingTab = BrowserTestUtils.addTab(gBrowser, null, {
     replayExecution: recordingFile,
   });
   gBrowser.selectedTab = replayingTab;
   await once(Services.ppmm, "HitRecordingEndpoint");
 
-  const dbg = await attachDebugger(replayingTab);
-  toolbox = dbg.toolbox;
-  target = dbg.target;
-  threadFront = toolbox.threadFront;
-  await threadFront.interrupt();
+  dbg = await attachDebugger(replayingTab);
 
   // The recording does not actually end at the point where we saved it, but
   // will do at the next checkpoint. Rewind to the point we are interested in.
-  bp = await setBreakpoint(threadFront, "doc_rr_continuous.html", 14);
-  await rewindToLine(threadFront, 14);
+  await addBreakpoint(dbg, "doc_rr_continuous.html", 14);
+  await rewindToLine(dbg, 14);
 
-  await checkEvaluateInTopFrame(target, "number", lastNumberValue);
-  await reverseStepOverToLine(threadFront, 13);
-  await rewindToLine(threadFront, 14);
-  await checkEvaluateInTopFrame(target, "number", lastNumberValue - 1);
-  await resumeToLine(threadFront, 14);
-  await checkEvaluateInTopFrame(target, "number", lastNumberValue);
+  await checkEvaluateInTopFrame(dbg, "number", lastNumberValue);
+  await reverseStepOverToLine(dbg, 13);
+  await rewindToLine(dbg, 14);
+  await checkEvaluateInTopFrame(dbg, "number", lastNumberValue - 1);
+  await resumeToLine(dbg, 14);
+  await checkEvaluateInTopFrame(dbg, "number", lastNumberValue);
 
-  await threadFront.removeBreakpoint(bp);
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_stepping-01.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_stepping-01.js
@@ -4,23 +4,20 @@
 
 "use strict";
 
 // Test basic step-over/back functionality in web replay.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_rr_basic.html", {
     waitForRecording: true,
   });
-  const { threadFront, target } = dbg;
 
-  await threadFront.interrupt();
-  const bp = await setBreakpoint(threadFront, "doc_rr_basic.html", 21);
-  await rewindToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 10);
-  await reverseStepOverToLine(threadFront, 20);
-  await checkEvaluateInTopFrame(target, "number", 9);
-  await checkEvaluateInTopFrameThrows(target, "window.alert(3)");
-  await stepOverToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 10);
+  await addBreakpoint(dbg, "doc_rr_basic.html", 21);
+  await rewindToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 10);
+  await reverseStepOverToLine(dbg, 20);
+  await checkEvaluateInTopFrame(dbg, "number", 9);
+  await checkEvaluateInTopFrameThrows(dbg, "window.alert(3)");
+  await stepOverToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 10);
 
-  await threadFront.removeBreakpoint(bp);
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_stepping-02.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_stepping-02.js
@@ -4,39 +4,35 @@
 
 "use strict";
 
 // Test fixes for some simple stepping bugs.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_rr_basic.html", {
     waitForRecording: true,
   });
-  const { threadFront } = dbg;
 
-  await threadFront.interrupt();
-  const bp = await setBreakpoint(threadFront, "doc_rr_basic.html", 22);
-  await rewindToLine(threadFront, 22);
-  await stepInToLine(threadFront, 25);
-  await stepOverToLine(threadFront, 26);
-  await stepOverToLine(threadFront, 27);
-  await reverseStepOverToLine(threadFront, 26);
-  await stepInToLine(threadFront, 30);
-  await stepOverToLine(threadFront, 31);
-  await stepOverToLine(threadFront, 32);
+  await addBreakpoint(dbg, "doc_rr_basic.html", 22);
+  await rewindToLine(dbg, 22);
+  await stepInToLine(dbg, 25);
+  await stepOverToLine(dbg, 26);
+  await stepOverToLine(dbg, 27);
+  await reverseStepOverToLine(dbg, 26);
+  await stepInToLine(dbg, 30);
+  await stepOverToLine(dbg, 31);
+  await stepOverToLine(dbg, 32);
 
   // Check that the scopes pane shows the value of the local variable.
-  await waitForPaused(dbg);
   for (let i = 1; ; i++) {
     if (getScopeLabel(dbg, i) == "c") {
       is("NaN", getScopeValue(dbg, i));
       break;
     }
   }
 
-  await stepOverToLine(threadFront, 33);
-  await reverseStepOverToLine(threadFront, 32);
-  await stepOutToLine(threadFront, 27);
-  await reverseStepOverToLine(threadFront, 26);
-  await reverseStepOverToLine(threadFront, 25);
+  await stepOverToLine(dbg, 33);
+  await reverseStepOverToLine(dbg, 32);
+  await stepOutToLine(dbg, 27);
+  await reverseStepOverToLine(dbg, 26);
+  await reverseStepOverToLine(dbg, 25);
 
-  await threadFront.removeBreakpoint(bp);
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_stepping-03.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_stepping-03.js
@@ -2,23 +2,20 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 /* eslint-disable no-undef */
 
 "use strict";
 
 // Test stepping back while recording, then resuming recording.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_rr_continuous.html");
-  const { threadFront, target } = dbg;
 
-  await threadFront.interrupt();
-  const bp = await setBreakpoint(threadFront, "doc_rr_continuous.html", 13);
-  await resumeToLine(threadFront, 13);
-  const value = await evaluateInTopFrame(target, "number");
-  await reverseStepOverToLine(threadFront, 12);
-  await checkEvaluateInTopFrame(target, "number", value - 1);
-  await resumeToLine(threadFront, 13);
-  await resumeToLine(threadFront, 13);
-  await checkEvaluateInTopFrame(target, "number", value + 1);
+  await addBreakpoint(dbg, "doc_rr_continuous.html", 13);
+  await resumeToLine(dbg, 13);
+  const value = await evaluateInTopFrame(dbg, "number");
+  await reverseStepOverToLine(dbg, 12);
+  await checkEvaluateInTopFrame(dbg, "number", value - 1);
+  await resumeToLine(dbg, 13);
+  await resumeToLine(dbg, 13);
+  await checkEvaluateInTopFrame(dbg, "number", value + 1);
 
-  await threadFront.removeBreakpoint(bp);
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_stepping-04.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_stepping-04.js
@@ -4,37 +4,34 @@
 
 "use strict";
 
 // Stepping past the beginning or end of a frame should act like a step-out.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_rr_basic.html", {
     waitForRecording: true,
   });
-  const { threadFront, target } = dbg;
 
-  await threadFront.interrupt();
-  const bp = await setBreakpoint(threadFront, "doc_rr_basic.html", 21);
-  await rewindToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 10);
+  await addBreakpoint(dbg, "doc_rr_basic.html", 21);
+  await rewindToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 10);
   await waitForSelectedLocation(dbg, 21);
-  await reverseStepOverToLine(threadFront, 20);
-  await reverseStepOverToLine(threadFront, 12);
+  await reverseStepOverToLine(dbg, 20);
+  await reverseStepOverToLine(dbg, 12);
 
   // After reverse-stepping out of the topmost frame we should rewind to the
   // last breakpoint hit.
-  await reverseStepOverToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 9);
+  await reverseStepOverToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 9);
 
-  await stepOverToLine(threadFront, 22);
-  await stepOverToLine(threadFront, 23);
-  await stepOverToLine(threadFront, 13);
-  await stepOverToLine(threadFront, 17);
-  await stepOverToLine(threadFront, 18);
+  await stepOverToLine(dbg, 22);
+  await stepOverToLine(dbg, 23);
+  await stepOverToLine(dbg, 13);
+  await stepOverToLine(dbg, 17);
+  await stepOverToLine(dbg, 18);
 
   // After forward-stepping out of the topmost frame we should run forward to
   // the next breakpoint hit.
-  await stepOverToLine(threadFront, 21);
-  await checkEvaluateInTopFrame(target, "number", 10);
+  await stepOverToLine(dbg, 21);
+  await checkEvaluateInTopFrame(dbg, "number", 10);
 
-  await threadFront.removeBreakpoint(bp);
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_rr_inspector-01.js
+++ b/devtools/client/webreplay/mochitest/browser_rr_inspector-01.js
@@ -14,49 +14,44 @@ function getContainerForNodeFront(nodeFr
 }
 
 // Test basic inspector functionality in web replay: the inspector is able to
 // show contents when paused according to the child's current position.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_inspector_basic.html", {
     waitForRecording: true,
     disableLogging: true,
+    skipInterrupt: true,
   });
-  const { threadFront } = dbg;
-
-  await threadFront.interrupt();
-  await threadFront.resume();
 
   const { inspector } = await openInspector();
 
   let nodeFront = await getNodeFront("#maindiv", inspector);
   let container = getContainerForNodeFront(nodeFront, inspector);
   ok(!container, "No node container while unpaused");
 
-  await threadFront.interrupt();
+  await interrupt(dbg);
 
   nodeFront = await getNodeFront("#maindiv", inspector);
   await waitFor(
     () => inspector.markup && getContainerForNodeFront(nodeFront, inspector)
   );
   container = getContainerForNodeFront(nodeFront, inspector);
   ok(
     container.editor.textEditor.textNode.state.value == "GOODBYE",
     "Correct late element text"
   );
 
-  const bp = await setBreakpoint(threadFront, "doc_inspector_basic.html", 9);
-
-  await rewindToLine(threadFront, 9);
+  await addBreakpoint(dbg, "doc_inspector_basic.html", 9);
+  await rewindToLine(dbg, 9);
 
   nodeFront = await getNodeFront("#maindiv", inspector);
   await waitFor(
     () => inspector.markup && getContainerForNodeFront(nodeFront, inspector)
   );
   container = getContainerForNodeFront(nodeFront, inspector);
   ok(
     container.editor.textEditor.textNode.state.value == "HELLO",
     "Correct early element text"
   );
 
-  await threadFront.removeBreakpoint(bp);
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/browser_rr_inspector-02.js
+++ b/devtools/client/webreplay/mochitest/browser_rr_inspector-02.js
@@ -10,40 +10,35 @@ Services.scriptloader.loadSubScript(
 );
 
 // Test that the element highlighter works when paused and replaying.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_inspector_basic.html", {
     waitForRecording: true,
     disableLogging: true,
   });
-  const { threadFront, toolbox } = dbg;
+  const { toolbox } = dbg;
 
-  await threadFront.interrupt();
-  await threadFront.resume();
-
-  await threadFront.interrupt();
-  const bp = await setBreakpoint(threadFront, "doc_inspector_basic.html", 9);
-  await rewindToLine(threadFront, 9);
+  await addBreakpoint(dbg, "doc_inspector_basic.html", 9);
+  await rewindToLine(dbg, 9);
 
   const { testActor } = await openInspector();
 
   info("Waiting for element picker to become active.");
   toolbox.win.focus();
   await toolbox.nodePicker.start();
 
   info("Moving mouse over div.");
   await moveMouseOver("#maindiv", 1, 1);
 
   // Checks in isNodeCorrectlyHighlighted are off for an unknown reason, even
   // though the highlighting appears correctly in the UI.
   info("Performing checks");
   await testActor.isNodeCorrectlyHighlighted("#maindiv", is);
 
-  await threadFront.removeBreakpoint(bp);
   await shutdownDebugger(dbg);
 
   function moveMouseOver(selector, x, y) {
     info("Waiting for element " + selector + " to be highlighted");
     testActor.synthesizeMouse({
       selector,
       x,
       y,
--- a/devtools/client/webreplay/mochitest/browser_rr_inspector-03.js
+++ b/devtools/client/webreplay/mochitest/browser_rr_inspector-03.js
@@ -26,33 +26,25 @@ function getComputedViewProperty(view, n
 }
 
 // Test that styles for elements can be viewed when using web replay.
 add_task(async function() {
   const dbg = await attachRecordingDebugger("doc_inspector_styles.html", {
     waitForRecording: true,
     disableLogging: true,
   });
-  const { threadFront } = dbg;
-
-  await threadFront.interrupt();
-  await threadFront.resume();
-
-  await threadFront.interrupt();
 
   const { inspector, view } = await openComputedView();
   await checkBackgroundColor("body", "rgb(0, 128, 0)");
   await checkBackgroundColor("#maindiv", "rgb(0, 0, 255)");
 
-  const bp = await setBreakpoint(threadFront, "doc_inspector_styles.html", 11);
-
-  await rewindToLine(threadFront, 11);
+  await addBreakpoint(dbg, "doc_inspector_styles.html", 11);
+  await rewindToLine(dbg, 11);
   await checkBackgroundColor("#maindiv", "rgb(255, 0, 0)");
 
-  await threadFront.removeBreakpoint(bp);
   await shutdownDebugger(dbg);
 
   async function checkBackgroundColor(node, color) {
     await selectNode(node, inspector);
 
     const value = getComputedViewProperty(view, "background-color").valueSpan;
     const nodeInfo = view.getNodeInfo(value);
     is(nodeInfo.value.property, "background-color");
--- a/devtools/client/webreplay/mochitest/head.js
+++ b/devtools/client/webreplay/mochitest/head.js
@@ -17,88 +17,90 @@ Services.scriptloader.loadSubScript(
 const EXAMPLE_URL =
   "http://example.com/browser/devtools/client/webreplay/mochitest/examples/";
 
 // Attach a debugger to a tab, returning a promise that resolves with the
 // debugger's toolbox.
 async function attachDebugger(tab) {
   const target = await TargetFactory.forTab(tab);
   const toolbox = await gDevTools.showToolbox(target, "jsdebugger");
-  return { toolbox, tab, target };
+  const dbg = createDebuggerContext(toolbox);
+  const threadFront = dbg.toolbox.threadFront;
+  return { ...dbg, tab, threadFront };
 }
 
 async function attachRecordingDebugger(
   url,
-  { waitForRecording, disableLogging } = {}
+  { waitForRecording, disableLogging, skipInterrupt } = {}
 ) {
   if (!disableLogging) {
     await pushPref("devtools.recordreplay.logging", true);
   }
 
   const tab = BrowserTestUtils.addTab(gBrowser, null, { recordExecution: "*" });
   gBrowser.selectedTab = tab;
   openTrustedLinkIn(EXAMPLE_URL + url, "current");
 
   if (waitForRecording) {
     await once(Services.ppmm, "RecordingFinished");
   }
-  const { target, toolbox } = await attachDebugger(tab);
-  const dbg = createDebuggerContext(toolbox);
-  const threadFront = dbg.toolbox.threadFront;
+  const dbg = await attachDebugger(tab);
+
+  if (!skipInterrupt) {
+    await interrupt(dbg);
+  }
 
-  await threadFront.interrupt();
-  return { ...dbg, tab, threadFront, target };
+  return dbg;
+}
+
+async function waitForPausedNoSource(dbg) {
+  await waitForState(dbg, state => isPaused(dbg), "paused");
 }
 
 async function shutdownDebugger(dbg) {
+  await dbg.actions.removeAllBreakpoints(getContext(dbg));
   await waitForRequestsToSettle(dbg);
   await dbg.toolbox.destroy();
   await gBrowser.removeTab(dbg.tab);
 }
 
-// Return a promise that resolves when a breakpoint has been set.
-async function setBreakpoint(threadFront, expectedFile, lineno, options = {}) {
-  const { sources } = await threadFront.getSources();
-  ok(sources.length == 1, "Got one source");
-  ok(RegExp(expectedFile).test(sources[0].url), "Source is " + expectedFile);
-  const location = { sourceUrl: sources[0].url, line: lineno };
-  await threadFront.setBreakpoint(location, options);
-  return location;
+async function interrupt(dbg) {
+  await dbg.actions.breakOnNext(getThreadContext(dbg));
+  await waitForPausedNoSource(dbg);
 }
 
 function resumeThenPauseAtLineFunctionFactory(method) {
-  return async function(threadFront, lineno) {
-    threadFront[method]();
-    await threadFront.once("paused");
-
-    const { frames } = await threadFront.getFrames(0, 1);
-    const frameLine = frames[0] ? frames[0].where.line : undefined;
-    ok(
-      frameLine == lineno,
-      "Paused at line " + frameLine + " expected " + lineno
-    );
+  return async function(dbg, lineno) {
+    await dbg.actions[method](getThreadContext(dbg));
+    if (lineno !== undefined) {
+      await waitForPaused(dbg);
+    } else {
+      await waitForPausedNoSource(dbg);
+    }
+    const pauseLine = getVisibleSelectedFrameLine(dbg);
+    ok(pauseLine == lineno, `Paused at line ${pauseLine} expected ${lineno}`);
   };
 }
 
 // Define various methods that resume a thread in a specific way and ensure it
 // pauses at a specified line.
 var rewindToLine = resumeThenPauseAtLineFunctionFactory("rewind");
 var resumeToLine = resumeThenPauseAtLineFunctionFactory("resume");
 var reverseStepOverToLine = resumeThenPauseAtLineFunctionFactory(
   "reverseStepOver"
 );
 var stepOverToLine = resumeThenPauseAtLineFunctionFactory("stepOver");
 var stepInToLine = resumeThenPauseAtLineFunctionFactory("stepIn");
 var stepOutToLine = resumeThenPauseAtLineFunctionFactory("stepOut");
 
 // Return a promise that resolves when a thread evaluates a string in the
 // topmost frame, with the result throwing an exception.
-async function checkEvaluateInTopFrameThrows(target, text) {
-  const threadFront = target.threadFront;
-  const consoleFront = await target.getFront("console");
+async function checkEvaluateInTopFrameThrows(dbg, text) {
+  const threadFront = dbg.toolbox.target.threadFront;
+  const consoleFront = await dbg.toolbox.target.getFront("console");
   const { frames } = await threadFront.getFrames(0, 1);
   ok(frames.length == 1, "Got one frame");
   const options = { thread: threadFront.actor, frameActor: frames[0].actor };
   const response = await consoleFront.evaluateJS(text, options);
   ok(response.exception, "Eval threw an exception");
 }
 
 // Return a pathname that can be used for a new recording file.
--- a/devtools/server/tests/browser/browser_dbg_promises-fulfillment-stack.js
+++ b/devtools/server/tests/browser/browser_dbg_promises-fulfillment-stack.js
@@ -20,17 +20,17 @@ const TEST_DATA = [
   {
     functionDisplayName: "returnPromise",
     line: 21,
     column: 14,
   },
   {
     functionDisplayName: "makePromise",
     line: 16,
-    column: 30,
+    column: 17,
   },
 ];
 
 add_task(async () => {
   const browser = await addTab(TAB_URL);
   const tab = gBrowser.getTabForBrowser(browser);
   const target = await TargetFactory.forTab(tab);
   await target.attach();
--- a/devtools/server/tests/browser/browser_dbg_promises-rejection-stack.js
+++ b/devtools/server/tests/browser/browser_dbg_promises-rejection-stack.js
@@ -29,17 +29,17 @@ const TEST_DATA = [
   {
     functionDisplayName: "returnPromise",
     line: 21,
     column: 14,
   },
   {
     functionDisplayName: "makePromise",
     line: 16,
-    column: 30,
+    column: 17,
   },
 ];
 
 add_task(async () => {
   const browser = await addTab(TAB_URL);
   const tab = gBrowser.getTabForBrowser(browser);
   const target = await TargetFactory.forTab(tab);
   await target.attach();
--- a/devtools/server/tests/unit/test_source-02.js
+++ b/devtools/server/tests/unit/test_source-02.js
@@ -65,20 +65,16 @@ function test_source() {
       response = await sourceFront.getBreakpointPositions();
       Assert.ok(!!response);
       Assert.deepEqual(response, [
         {
           line: 2,
           column: 2,
         },
         {
-          line: 2,
-          column: 8,
-        },
-        {
           line: 3,
           column: 14,
         },
         {
           line: 3,
           column: 17,
         },
         {
@@ -94,17 +90,17 @@ function test_source() {
           column: 0,
         },
       ]);
 
       response = await sourceFront.getBreakpointPositionsCompressed();
       Assert.ok(!!response);
 
       Assert.deepEqual(response, {
-        2: [2, 8],
+        2: [2],
         3: [14, 17, 24],
         4: [4],
         6: [0],
       });
 
       await gThreadFront.resume();
       finishClient(gClient);
     });
--- a/dom/base/ChromeUtils.cpp
+++ b/dom/base/ChromeUtils.cpp
@@ -662,16 +662,52 @@ void ChromeUtils::GetRecentJSDevError(Gl
 void ChromeUtils::ClearRecentJSDevError(GlobalObject&) {
   auto runtime = CycleCollectedJSRuntime::Get();
   MOZ_ASSERT(runtime);
 
   runtime->ClearRecentDevError();
 }
 #endif  // NIGHTLY_BUILD
 
+#define PROCTYPE_TO_WEBIDL_CASE(_procType, _webidl) \
+  case mozilla::ProcType::_procType:                \
+    return WebIDLProcType::_webidl
+
+static WebIDLProcType ProcTypeToWebIDL(mozilla::ProcType aType) {
+  // |strings| contains an extra non-enum value, so subtract one.
+  // Max is the value of the last enum, not the length, so add one.
+  static_assert(ArrayLength(WebIDLProcTypeValues::strings) - 1 ==
+                    static_cast<size_t>(ProcType::Max) + 1,
+                "In order for this static cast to be okay, "
+                "WebIDLProcType must match ProcType exactly");
+
+  switch (aType) {
+    PROCTYPE_TO_WEBIDL_CASE(Web, Web);
+    PROCTYPE_TO_WEBIDL_CASE(File, File);
+    PROCTYPE_TO_WEBIDL_CASE(Extension, Extension);
+    PROCTYPE_TO_WEBIDL_CASE(PrivilegedAbout, Privilegedabout);
+    PROCTYPE_TO_WEBIDL_CASE(WebLargeAllocation, WebLargeAllocation);
+    PROCTYPE_TO_WEBIDL_CASE(Browser, Browser);
+    PROCTYPE_TO_WEBIDL_CASE(Plugin, Plugin);
+    PROCTYPE_TO_WEBIDL_CASE(IPDLUnitTest, IpdlUnitTest);
+    PROCTYPE_TO_WEBIDL_CASE(GMPlugin, GmpPlugin);
+    PROCTYPE_TO_WEBIDL_CASE(GPU, Gpu);
+    PROCTYPE_TO_WEBIDL_CASE(VR, Vr);
+    PROCTYPE_TO_WEBIDL_CASE(RDD, Rdd);
+    PROCTYPE_TO_WEBIDL_CASE(Socket, Socket);
+    PROCTYPE_TO_WEBIDL_CASE(RemoteSandboxBroker, RemoteSandboxBroker);
+    PROCTYPE_TO_WEBIDL_CASE(Unknown, Unknown);
+  }
+
+  MOZ_ASSERT(false, "Unhandled case in ProcTypeToWebIDL");
+  return WebIDLProcType::Unknown;
+}
+
+#undef PROCTYPE_TO_WEBIDL_CASE
+
 /* static */
 already_AddRefed<Promise> ChromeUtils::RequestProcInfo(GlobalObject& aGlobal,
                                                        ErrorResult& aRv) {
   // This function will use IPDL to enable threads info on macOS
   // see https://bugzilla.mozilla.org/show_bug.cgi?id=1529023
   if (!XRE_IsParentProcess()) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
@@ -703,17 +739,17 @@ already_AddRefed<Promise> ChromeUtils::R
                     mozilla::ipc::GeckoChildProcessHost* aGeckoProcess) {
                   if (!aGeckoProcess->GetChildProcessHandle()) {
                     return;
                   }
 
                   base::ProcessId childPid =
                       base::GetProcId(aGeckoProcess->GetChildProcessHandle());
                   int32_t childId = 0;
-                  mozilla::ProcType type;
+                  mozilla::ProcType type = mozilla::ProcType::Unknown;
                   switch (aGeckoProcess->GetProcessType()) {
                     case GeckoProcessType::GeckoProcessType_Content: {
                       ContentParent* contentParent = nullptr;
                       // This loop can become slow as we get more processes in
                       // Fission, so might need some refactoring in the future.
                       for (ContentParent* parent : contentParents) {
                         // find the match
                         if (parent->Process() == aGeckoProcess) {
@@ -764,17 +800,18 @@ already_AddRefed<Promise> ChromeUtils::R
                       break;
                     case GeckoProcessType::GeckoProcessType_Socket:
                       type = mozilla::ProcType::Socket;
                       break;
                     case GeckoProcessType::GeckoProcessType_RemoteSandboxBroker:
                       type = mozilla::ProcType::RemoteSandboxBroker;
                       break;
                     default:
-                      type = mozilla::ProcType::Unknown;
+                      // Leave the default Unknown value in |type|.
+                      break;
                   }
 
                   promises.AppendElement(
 #ifdef XP_MACOSX
                       mozilla::GetProcInfo(childPid, childId, type,
                                            aGeckoProcess->GetChildTask())
 #else
                       mozilla::GetProcInfo(childPid, childId, type)
@@ -784,17 +821,17 @@ already_AddRefed<Promise> ChromeUtils::R
 
             auto ProcInfoResolver =
                 [domPromise, parentPid, parentInfo = aParentInfo](
                     const nsTArray<ProcInfo>& aChildrenInfo) {
                   mozilla::dom::ParentProcInfoDictionary procInfo;
                   // parent, basic info.
                   procInfo.mPid = parentPid;
                   procInfo.mFilename.Assign(parentInfo.filename);
-                  procInfo.mType = mozilla::dom::ProcType::Browser;
+                  procInfo.mType = mozilla::dom::WebIDLProcType::Browser;
                   procInfo.mVirtualMemorySize = parentInfo.virtualMemorySize;
                   procInfo.mResidentSetSize = parentInfo.residentSetSize;
                   procInfo.mCpuUser = parentInfo.cpuUser;
                   procInfo.mCpuKernel = parentInfo.cpuKernel;
 
                   // parent, threads info.
                   mozilla::dom::Sequence<mozilla::dom::ThreadInfoDictionary>
                       threads;
@@ -817,17 +854,17 @@ already_AddRefed<Promise> ChromeUtils::R
                     ChildProcInfoDictionary* childProcInfo =
                         children.AppendElement(fallible);
                     if (NS_WARN_IF(!childProcInfo)) {
                       domPromise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
                       return;
                     }
                     // Basic info.
                     childProcInfo->mChildID = info.childId;
-                    childProcInfo->mType = static_cast<ProcType>(info.type);
+                    childProcInfo->mType = ProcTypeToWebIDL(info.type);
                     childProcInfo->mPid = info.pid;
                     childProcInfo->mFilename.Assign(info.filename);
                     childProcInfo->mVirtualMemorySize = info.virtualMemorySize;
                     childProcInfo->mResidentSetSize = info.residentSetSize;
                     childProcInfo->mCpuUser = info.cpuUser;
                     childProcInfo->mCpuKernel = info.cpuKernel;
 
                     // Threads info.
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -3721,35 +3721,45 @@ nsRect nsGlobalWindowOuter::GetInnerScre
 
   return rootFrame->GetScreenRectInAppUnits();
 }
 
 Maybe<CSSIntSize> nsGlobalWindowOuter::GetRDMDeviceSize(
     const Document& aDocument) {
   // RDM device size should reflect the simulated device resolution, and
   // be independent of any full zoom or resolution zoom applied to the
-  // content. To get this value, we get the unscaled browser child size.
+  // content. To get this value, we get the "unscaled" browser child size,
+  // and divide by the full zoom. "Unscaled" in this case means unscaled
+  // from device to screen but it has been affected (multipled) by the
+  // full zoom and we need to compensate for that.
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
 
-  Maybe<CSSIntSize> deviceSize;
-
   // Bug 1576256: This does not work for cross-process subframes.
   const Document* topInProcessContentDoc =
       aDocument.GetTopLevelContentDocument();
   if (topInProcessContentDoc && topInProcessContentDoc->InRDMPane()) {
     nsIDocShell* docShell = topInProcessContentDoc->GetDocShell();
     if (docShell) {
-      nsCOMPtr<nsIBrowserChild> child = docShell->GetBrowserChild();
-      if (child) {
-        BrowserChild* bc = static_cast<BrowserChild*>(child.get());
-        deviceSize = Some(bc->GetUnscaledInnerSize());
+      nsPresContext* presContext = docShell->GetPresContext();
+      if (presContext) {
+        nsCOMPtr<nsIBrowserChild> child = docShell->GetBrowserChild();
+        if (child) {
+          // We intentionally use GetFullZoom here instead of
+          // GetDeviceFullZoom, because the unscaledInnerSize is based
+          // on the full zoom and not the device full zoom (which is
+          // rounded to result in integer device pixels).
+          float zoom = presContext->GetFullZoom();
+          BrowserChild* bc = static_cast<BrowserChild*>(child.get());
+          CSSSize unscaledSize = bc->GetUnscaledInnerSize();
+          return Some(CSSIntSize(gfx::RoundedToInt(unscaledSize / zoom)));
+        }
       }
     }
   }
-  return deviceSize;
+  return Nothing();
 }
 
 float nsGlobalWindowOuter::GetMozInnerScreenXOuter(CallerType aCallerType) {
   // When resisting fingerprinting, always return 0.
   if (nsContentUtils::ResistFingerprinting(aCallerType)) {
     return 0.0;
   }
 
--- a/dom/chrome-webidl/ChromeUtils.webidl
+++ b/dom/chrome-webidl/ChromeUtils.webidl
@@ -432,34 +432,43 @@ partial namespace ChromeUtils {
    * If leak detection is enabled, print a note to the leak log that this
    * process will intentionally crash. This should be called only on child
    * processes for testing purpose.
    */
   [ChromeOnly, Throws]
   void privateNoteIntentionalCrash();
 };
 
-/**
- * Holds information about Firefox running processes & threads.
- *
- * See widget/ProcInfo.h for fields documentation.
+/*
+ * This type is a WebIDL representation of mozilla::ProcType.
  */
-enum ProcType {
+enum WebIDLProcType {
  "web",
  "file",
  "extension",
  "privilegedabout",
  "webLargeAllocation",
+ "browser",
+ "plugin",
+ "ipdlUnitTest",
+ "gmpPlugin",
  "gpu",
+ "vr",
  "rdd",
  "socket",
- "browser",
- "unknown"
+ "remoteSandboxBroker",
+ "unknown",
 };
 
+/**
+ * These dictionaries hold information about Firefox running processes and
+ * threads.
+ *
+ * See widget/ProcInfo.h for fields documentation.
+ */
 dictionary ThreadInfoDictionary {
   long long tid = 0;
   DOMString name = "";
   unsigned long long cpuUser = 0;
   unsigned long long cpuKernel = 0;
 };
 
 dictionary ChildProcInfoDictionary {
@@ -468,31 +477,31 @@ dictionary ChildProcInfoDictionary {
   DOMString filename = "";
   unsigned long long virtualMemorySize = 0;
   long long residentSetSize = 0;
   unsigned long long cpuUser = 0;
   unsigned long long cpuKernel = 0;
   sequence<ThreadInfoDictionary> threads = [];
   // Firefox info
   unsigned long long ChildID = 0;
-  ProcType type = "web";
+  WebIDLProcType type = "web";
 };
 
 dictionary ParentProcInfoDictionary {
   // System info
   long long pid = 0;
   DOMString filename = "";
   unsigned long long virtualMemorySize = 0;
   long long residentSetSize = 0;
   unsigned long long cpuUser = 0;
   unsigned long long cpuKernel = 0;
   sequence<ThreadInfoDictionary> threads = [];
   sequence<ChildProcInfoDictionary> children = [];
   // Firefox info
-  ProcType type = "browser";
+  WebIDLProcType type = "browser";
 };
 
 /**
  * Dictionaries duplicating IPDL types in dom/ipc/DOMTypes.ipdlh
  * Used by requestPerformanceMetrics
  */
 dictionary MediaMemoryInfoDictionary {
   unsigned long long audioSize = 0;
--- a/dom/events/IMEStateManager.cpp
+++ b/dom/events/IMEStateManager.cpp
@@ -13,16 +13,17 @@
 #include "mozilla/EventListenerManager.h"
 #include "mozilla/EventStates.h"
 #include "mozilla/MouseEvents.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/PresShell.h"
 #include "mozilla/StaticPrefs_dom.h"
 #include "mozilla/TextComposition.h"
 #include "mozilla/TextEvents.h"
+#include "mozilla/ToString.h"
 #include "mozilla/Unused.h"
 #include "mozilla/dom/BrowserBridgeChild.h"
 #include "mozilla/dom/HTMLFormElement.h"
 #include "mozilla/dom/HTMLTextAreaElement.h"
 #include "mozilla/dom/MouseEventBinding.h"
 #include "mozilla/dom/BrowserParent.h"
 
 #include "HTMLInputElement.h"
@@ -58,81 +59,16 @@ using namespace widget;
  * When a method does something only in some situations and it may be important
  * for debug, log the information with LogLevel::Debug.  In this case, the log
  * should start with "  <method name>(),".
  */
 LazyLogModule sISMLog("IMEStateManager");
 
 static const char* GetBoolName(bool aBool) { return aBool ? "true" : "false"; }
 
-static const char* GetActionCauseName(InputContextAction::Cause aCause) {
-  switch (aCause) {
-    case InputContextAction::CAUSE_UNKNOWN:
-      return "CAUSE_UNKNOWN";
-    case InputContextAction::CAUSE_UNKNOWN_CHROME:
-      return "CAUSE_UNKNOWN_CHROME";
-    case InputContextAction::CAUSE_KEY:
-      return "CAUSE_KEY";
-    case InputContextAction::CAUSE_MOUSE:
-      return "CAUSE_MOUSE";
-    case InputContextAction::CAUSE_TOUCH:
-      return "CAUSE_TOUCH";
-    case InputContextAction::CAUSE_LONGPRESS:
-      return "CAUSE_LONGPRESS";
-    default:
-      return "illegal value";
-  }
-}
-
-static const char* GetActionFocusChangeName(
-    InputContextAction::FocusChange aFocusChange) {
-  switch (aFocusChange) {
-    case InputContextAction::FOCUS_NOT_CHANGED:
-      return "FOCUS_NOT_CHANGED";
-    case InputContextAction::GOT_FOCUS:
-      return "GOT_FOCUS";
-    case InputContextAction::LOST_FOCUS:
-      return "LOST_FOCUS";
-    case InputContextAction::MENU_GOT_PSEUDO_FOCUS:
-      return "MENU_GOT_PSEUDO_FOCUS";
-    case InputContextAction::MENU_LOST_PSEUDO_FOCUS:
-      return "MENU_LOST_PSEUDO_FOCUS";
-    default:
-      return "illegal value";
-  }
-}
-
-static const char* GetIMEStateEnabledName(IMEState::Enabled aEnabled) {
-  switch (aEnabled) {
-    case IMEState::DISABLED:
-      return "DISABLED";
-    case IMEState::ENABLED:
-      return "ENABLED";
-    case IMEState::PASSWORD:
-      return "PASSWORD";
-    case IMEState::PLUGIN:
-      return "PLUGIN";
-    default:
-      return "illegal value";
-  }
-}
-
-static const char* GetIMEStateSetOpenName(IMEState::Open aOpen) {
-  switch (aOpen) {
-    case IMEState::DONT_CHANGE_OPEN_STATE:
-      return "DONT_CHANGE_OPEN_STATE";
-    case IMEState::OPEN:
-      return "OPEN";
-    case IMEState::CLOSED:
-      return "CLOSED";
-    default:
-      return "illegal value";
-  }
-}
-
 StaticRefPtr<nsIContent> IMEStateManager::sContent;
 StaticRefPtr<nsPresContext> IMEStateManager::sPresContext;
 nsIWidget* IMEStateManager::sWidget = nullptr;
 nsIWidget* IMEStateManager::sFocusedIMEWidget = nullptr;
 StaticRefPtr<BrowserParent> IMEStateManager::sFocusedIMEBrowserParent;
 nsIWidget* IMEStateManager::sActiveInputContextWidget = nullptr;
 StaticRefPtr<IMEContentObserver> IMEStateManager::sActiveIMEContentObserver;
 TextCompositionArray* IMEStateManager::sTextCompositions = nullptr;
@@ -427,17 +363,17 @@ bool IMEStateManager::CanHandleWith(nsPr
 }
 
 // static
 nsresult IMEStateManager::OnChangeFocus(nsPresContext* aPresContext,
                                         nsIContent* aContent,
                                         InputContextAction::Cause aCause) {
   MOZ_LOG(sISMLog, LogLevel::Info,
           ("OnChangeFocus(aPresContext=0x%p, aContent=0x%p, aCause=%s)",
-           aPresContext, aContent, GetActionCauseName(aCause)));
+           aPresContext, aContent, ToString(aCause).c_str()));
 
   InputContextAction action(aCause);
   return OnChangeFocusInternal(aPresContext, aContent, action);
 }
 
 // static
 nsresult IMEStateManager::OnChangeFocusInternal(nsPresContext* aPresContext,
                                                 nsIContent* aContent,
@@ -448,18 +384,18 @@ nsresult IMEStateManager::OnChangeFocusI
   MOZ_LOG(sISMLog, LogLevel::Info,
           ("OnChangeFocusInternal(aPresContext=0x%p (available: %s), "
            "aContent=0x%p (remote: %s), aAction={ mCause=%s, "
            "mFocusChange=%s }), "
            "sPresContext=0x%p (available: %s), sContent=0x%p, "
            "sWidget=0x%p (available: %s), BrowserParent::GetFocused()=0x%p, "
            "sActiveIMEContentObserver=0x%p, sInstalledMenuKeyboardListener=%s",
            aPresContext, GetBoolName(CanHandleWith(aPresContext)), aContent,
-           GetBoolName(remoteHasFocus), GetActionCauseName(aAction.mCause),
-           GetActionFocusChangeName(aAction.mFocusChange), sPresContext.get(),
+           GetBoolName(remoteHasFocus), ToString(aAction.mCause).c_str(),
+           ToString(aAction.mFocusChange).c_str(), sPresContext.get(),
            GetBoolName(CanHandleWith(sPresContext)), sContent.get(), sWidget,
            GetBoolName(sWidget && !sWidget->Destroyed()),
            BrowserParent::GetFocused(), sActiveIMEContentObserver.get(),
            GetBoolName(sInstalledMenuKeyboardListener)));
 
   // If new aPresShell has been destroyed, this should handle the focus change
   // as nobody is getting focus.
   if (NS_WARN_IF(aPresContext && !CanHandleWith(aPresContext))) {
@@ -668,18 +604,18 @@ void IMEStateManager::OnInstalledMenuKey
       sISMLog, LogLevel::Info,
       ("OnInstalledMenuKeyboardListener(aInstalling=%s), "
        "sInstalledMenuKeyboardListener=%s, BrowserParent::GetFocused()=0x%p, "
        "sActiveChildInputContext={ mIMEState={ mEnabled=%s, mOpen=%s }, "
        "mHTMLInputType=\"%s\", mHTMLInputInputmode=\"%s\", mActionHint=\"%s\", "
        "mInPrivateBrowsing=%s }",
        GetBoolName(aInstalling), GetBoolName(sInstalledMenuKeyboardListener),
        BrowserParent::GetFocused(),
-       GetIMEStateEnabledName(sActiveChildInputContext.mIMEState.mEnabled),
-       GetIMEStateSetOpenName(sActiveChildInputContext.mIMEState.mOpen),
+       ToString(sActiveChildInputContext.mIMEState.mEnabled).c_str(),
+       ToString(sActiveChildInputContext.mIMEState.mOpen).c_str(),
        NS_ConvertUTF16toUTF8(sActiveChildInputContext.mHTMLInputType).get(),
        NS_ConvertUTF16toUTF8(sActiveChildInputContext.mHTMLInputInputmode)
            .get(),
        NS_ConvertUTF16toUTF8(sActiveChildInputContext.mActionHint).get(),
        GetBoolName(sActiveChildInputContext.mInPrivateBrowsing)));
 
   sInstalledMenuKeyboardListener = aInstalling;
 
@@ -877,18 +813,18 @@ void IMEStateManager::UpdateIMEState(con
                                      nsIContent* aContent,
                                      EditorBase* aEditorBase) {
   MOZ_LOG(
       sISMLog, LogLevel::Info,
       ("UpdateIMEState(aNewIMEState={ mEnabled=%s, "
        "mOpen=%s }, aContent=0x%p, aEditorBase=0x%p), "
        "sPresContext=0x%p, sContent=0x%p, sWidget=0x%p (available: %s), "
        "sActiveIMEContentObserver=0x%p, sIsGettingNewIMEState=%s",
-       GetIMEStateEnabledName(aNewIMEState.mEnabled),
-       GetIMEStateSetOpenName(aNewIMEState.mOpen), aContent, aEditorBase,
+       ToString(aNewIMEState.mEnabled).c_str(),
+       ToString(aNewIMEState.mOpen).c_str(), aContent, aEditorBase,
        sPresContext.get(), sContent.get(), sWidget,
        GetBoolName(sWidget && !sWidget->Destroyed()),
        sActiveIMEContentObserver.get(), GetBoolName(sIsGettingNewIMEState)));
 
   if (sIsGettingNewIMEState) {
     MOZ_LOG(sISMLog, LogLevel::Debug,
             ("  UpdateIMEState(), "
              "does nothing because of called while getting new IME state"));
@@ -1081,18 +1017,18 @@ IMEState IMEStateManager::GetNewIMEState
   // For avoiding such nested IME state updates, we should set
   // sIsGettingNewIMEState here and UpdateIMEState() should check it.
   GettingNewIMEStateBlocker blocker;
 
   IMEState newIMEState = aContent->GetDesiredIMEState();
   MOZ_LOG(sISMLog, LogLevel::Debug,
           ("  GetNewIMEState() returns { mEnabled=%s, "
            "mOpen=%s }",
-           GetIMEStateEnabledName(newIMEState.mEnabled),
-           GetIMEStateSetOpenName(newIMEState.mOpen)));
+           ToString(newIMEState.mEnabled).c_str(),
+           ToString(newIMEState.mOpen).c_str()));
   return newIMEState;
 }
 
 static bool MayBeIMEUnawareWebApp(nsINode* aNode) {
   bool haveKeyEventsListener = false;
 
   while (aNode) {
     EventListenerManager* const mgr = aNode->GetExistingListenerManager();
@@ -1125,25 +1061,24 @@ void IMEStateManager::SetInputContextFor
   MOZ_LOG(
       sISMLog, LogLevel::Info,
       ("SetInputContextForChildProcess(aBrowserParent=0x%p, "
        "aInputContext={ mIMEState={ mEnabled=%s, mOpen=%s }, "
        "mHTMLInputType=\"%s\", mHTMLInputInputmode=\"%s\", mActionHint=\"%s\", "
        "mInPrivateBrowsing=%s }, aAction={ mCause=%s, mAction=%s }), "
        "sPresContext=0x%p (available: %s), sWidget=0x%p (available: %s), "
        "BrowserParent::GetFocused()=0x%p, sInstalledMenuKeyboardListener=%s",
-       aBrowserParent, GetIMEStateEnabledName(aInputContext.mIMEState.mEnabled),
-       GetIMEStateSetOpenName(aInputContext.mIMEState.mOpen),
+       aBrowserParent, ToString(aInputContext.mIMEState.mEnabled).c_str(),
+       ToString(aInputContext.mIMEState.mOpen).c_str(),
        NS_ConvertUTF16toUTF8(aInputContext.mHTMLInputType).get(),
        NS_ConvertUTF16toUTF8(aInputContext.mHTMLInputInputmode).get(),
        NS_ConvertUTF16toUTF8(aInputContext.mActionHint).get(),
        GetBoolName(aInputContext.mInPrivateBrowsing),
-       GetActionCauseName(aAction.mCause),
-       GetActionFocusChangeName(aAction.mFocusChange), sPresContext.get(),
-       GetBoolName(CanHandleWith(sPresContext)), sWidget,
+       ToString(aAction.mCause).c_str(), ToString(aAction.mFocusChange).c_str(),
+       sPresContext.get(), GetBoolName(CanHandleWith(sPresContext)), sWidget,
        GetBoolName(sWidget && !sWidget->Destroyed()),
        BrowserParent::GetFocused(),
        GetBoolName(sInstalledMenuKeyboardListener)));
 
   if (aBrowserParent != BrowserParent::GetFocused()) {
     MOZ_LOG(sISMLog, LogLevel::Error,
             ("  SetInputContextForChildProcess(), FAILED, "
              "because non-focused tab parent tries to set input context"));
@@ -1310,21 +1245,20 @@ void IMEStateManager::SetIMEState(const 
                                   nsIContent* aContent, nsIWidget* aWidget,
                                   InputContextAction aAction,
                                   InputContext::Origin aOrigin) {
   MOZ_LOG(
       sISMLog, LogLevel::Info,
       ("SetIMEState(aState={ mEnabled=%s, mOpen=%s }, "
        "aContent=0x%p (BrowserParent=0x%p), aWidget=0x%p, aAction={ mCause=%s, "
        "mFocusChange=%s }, aOrigin=%s)",
-       GetIMEStateEnabledName(aState.mEnabled),
-       GetIMEStateSetOpenName(aState.mOpen), aContent,
-       BrowserParent::GetFrom(aContent), aWidget,
-       GetActionCauseName(aAction.mCause),
-       GetActionFocusChangeName(aAction.mFocusChange), ToChar(aOrigin)));
+       ToString(aState.mEnabled).c_str(), ToString(aState.mOpen).c_str(),
+       aContent, BrowserParent::GetFrom(aContent), aWidget,
+       ToString(aAction.mCause).c_str(), ToString(aAction.mFocusChange).c_str(),
+       ToChar(aOrigin)));
 
   NS_ENSURE_TRUE_VOID(aWidget);
 
   InputContext context;
   context.mIMEState = aState;
   context.mOrigin = aOrigin;
   context.mMayBeIMEUnaware = context.mIMEState.IsEditable() &&
                              sCheckForIMEUnawareWebApps &&
@@ -1399,24 +1333,23 @@ void IMEStateManager::SetInputContext(ns
                                       const InputContextAction& aAction) {
   MOZ_LOG(
       sISMLog, LogLevel::Info,
       ("SetInputContext(aWidget=0x%p, aInputContext={ "
        "mIMEState={ mEnabled=%s, mOpen=%s }, mHTMLInputType=\"%s\", "
        "mHTMLInputInputmode=\"%s\", mActionHint=\"%s\", "
        "mInPrivateBrowsing=%s }, "
        "aAction={ mCause=%s, mAction=%s }), BrowserParent::GetFocused()=0x%p",
-       aWidget, GetIMEStateEnabledName(aInputContext.mIMEState.mEnabled),
-       GetIMEStateSetOpenName(aInputContext.mIMEState.mOpen),
+       aWidget, ToString(aInputContext.mIMEState.mEnabled).c_str(),
+       ToString(aInputContext.mIMEState.mOpen).c_str(),
        NS_ConvertUTF16toUTF8(aInputContext.mHTMLInputType).get(),
        NS_ConvertUTF16toUTF8(aInputContext.mHTMLInputInputmode).get(),
        NS_ConvertUTF16toUTF8(aInputContext.mActionHint).get(),
        GetBoolName(aInputContext.mInPrivateBrowsing),
-       GetActionCauseName(aAction.mCause),
-       GetActionFocusChangeName(aAction.mFocusChange),
+       ToString(aAction.mCause).c_str(), ToString(aAction.mFocusChange).c_str(),
        BrowserParent::GetFocused()));
 
   MOZ_RELEASE_ASSERT(aWidget);
 
   nsCOMPtr<nsIWidget> widget(aWidget);
   widget->SetInputContext(aInputContext, aAction);
   sActiveInputContextWidget = widget;
 }
--- a/dom/flex/Flex.cpp
+++ b/dom/flex/Flex.cpp
@@ -24,18 +24,23 @@ NS_INTERFACE_MAP_END
 Flex::Flex(Element* aParent, nsFlexContainerFrame* aFrame) : mParent(aParent) {
   MOZ_ASSERT(aFrame,
              "Should never be instantiated with a null nsFlexContainerFrame");
 
   // Eagerly create property values from aFrame, because we're not
   // going to keep it around.
   const ComputedFlexContainerInfo* containerInfo =
       aFrame->GetFlexContainerInfo();
-  MOZ_ASSERT(containerInfo, "Should only be passed a frame with info.");
-
+  if (!containerInfo) {
+    // It's weird but possible to fail to get a ComputedFlexContainerInfo
+    // structure. Assign sensible default values.
+    mMainAxisDirection = FlexPhysicalDirection::Horizontal_lr;
+    mCrossAxisDirection = FlexPhysicalDirection::Vertical_tb;
+    return;
+  }
   mLines.SetLength(containerInfo->mLines.Length());
   uint32_t index = 0;
   for (auto&& l : containerInfo->mLines) {
     FlexLineValues* line = new FlexLineValues(this, &l);
     mLines.ElementAt(index) = line;
     index++;
   }
 
--- a/dom/ipc/BrowserChild.h
+++ b/dom/ipc/BrowserChild.h
@@ -541,17 +541,17 @@ class BrowserChild final : public nsMess
   bool IPCOpen() const { return mIPCOpen; }
 
   bool ParentIsActive() const { return mParentIsActive; }
 
   const mozilla::layers::CompositorOptions& GetCompositorOptions() const;
   bool AsyncPanZoomEnabled() const;
 
   ScreenIntSize GetInnerSize();
-  CSSIntSize GetUnscaledInnerSize() { return RoundedToInt(mUnscaledInnerSize); }
+  CSSSize GetUnscaledInnerSize() { return mUnscaledInnerSize; }
 
   Maybe<LayoutDeviceIntRect> GetVisibleRect() const;
 
   // Call RecvShow(nsIntSize(0, 0)) and block future calls to RecvShow().
   void DoFakeShow(const ShowInfo& aShowInfo);
 
   void ContentReceivedInputBlock(uint64_t aInputBlockId,
                                  bool aPreventDefault) const;
--- a/extensions/permissions/nsPermissionManager.cpp
+++ b/extensions/permissions/nsPermissionManager.cpp
@@ -29,16 +29,19 @@
 #include "nsXULAppAPI.h"
 #include "nsIPrincipal.h"
 #include "nsIURIMutator.h"
 #include "nsContentUtils.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsIEffectiveTLDService.h"
 #include "nsPIDOMWindow.h"
 #include "mozilla/dom/Document.h"
+#ifdef ANDROID
+#  include "mozilla/jni/Utils.h"  // for jni::IsFennec
+#endif
 #include "mozilla/net/NeckoMessageUtils.h"
 #include "mozilla/Preferences.h"
 #include "nsReadLine.h"
 #include "mozilla/Telemetry.h"
 #include "nsIConsoleService.h"
 #include "nsINavHistoryService.h"
 #include "nsToolkitCompsCID.h"
 #include "nsIObserverService.h"
@@ -122,16 +125,29 @@ static const nsLiteralCString kPreloadPe
     // interception when a user has disabled storage for a specific site.  Once
     // service worker interception moves to the parent process this should be
     // removed.  See bug 1428130.
     NS_LITERAL_CSTRING("cookie"), NS_LITERAL_CSTRING("trackingprotection"),
     NS_LITERAL_CSTRING("trackingprotection-pb"),
 
     USER_INTERACTION_PERM};
 
+// Certain permissions should never be persisted to disk under GeckoView; it's
+// the responsibility of the app to manage storing these beyond the scope of
+// a single session.
+#ifdef ANDROID
+static const nsLiteralCString kGeckoViewRestrictedPermissions[] = {
+    NS_LITERAL_CSTRING("MediaManagerVideo"),
+    NS_LITERAL_CSTRING("geolocation"),
+    NS_LITERAL_CSTRING("desktop-notification"),
+    NS_LITERAL_CSTRING("persistent-storage"),
+    NS_LITERAL_CSTRING("trackingprotection"),
+    NS_LITERAL_CSTRING("trackingprotection-pb")};
+#endif
+
 // NOTE: nullptr can be passed as aType - if it is this function will return
 // "false" unconditionally.
 bool IsPreloadPermission(const nsACString& aType) {
   if (!aType.IsEmpty()) {
     for (const auto& perm : kPreloadPermissions) {
       if (perm.Equals(aType)) {
         return true;
       }
@@ -678,19 +694,25 @@ nsresult UpgradeHostToOriginAndInsert(
 
 static bool IsExpandedPrincipal(nsIPrincipal* aPrincipal) {
   nsCOMPtr<nsIExpandedPrincipal> ep = do_QueryInterface(aPrincipal);
   return !!ep;
 }
 
 // We only want to persist permissions which don't have session or policy
 // expiration.
-static bool IsPersistentExpire(uint32_t aExpire) {
-  return aExpire != nsIPermissionManager::EXPIRE_SESSION &&
-         aExpire != nsIPermissionManager::EXPIRE_POLICY;
+static bool IsPersistentExpire(uint32_t aExpire, const nsACString& aType) {
+  bool res = (aExpire != nsIPermissionManager::EXPIRE_SESSION &&
+              aExpire != nsIPermissionManager::EXPIRE_POLICY);
+#ifdef ANDROID
+  for (const auto& perm : kGeckoViewRestrictedPermissions) {
+    res = res && !perm.Equals(aType);
+  }
+#endif
+  return res;
 }
 
 static void UpdateAutoplayTelemetry(const nsACString& aType,
                                     uint32_t aOldPermission,
                                     uint32_t aNewPermission,
                                     uint32_t aExpireType) {
   if (!aType.EqualsLiteral("autoplay-media")) {
     return;
@@ -1847,17 +1869,18 @@ nsresult nsPermissionManager::AddInterna
                           aModificationTime));
 
       // Record a count of the number of preload permissions present in the
       // content process.
       if (IsPreloadPermission(mTypeArray[typeIndex])) {
         sPreloadPermissionCount++;
       }
 
-      if (aDBOperation == eWriteToDB && IsPersistentExpire(aExpireType)) {
+      if (aDBOperation == eWriteToDB &&
+          IsPersistentExpire(aExpireType, aType)) {
         UpdateDB(op, mStmtInsert, id, origin, aType, aPermission, aExpireType,
                  aExpireTime, aModificationTime);
       }
 
       if (aNotifyOperation == eNotify) {
         NotifyObserversWithPermission(aPrincipal, mTypeArray[typeIndex],
                                       aPermission, aExpireType, aExpireTime,
                                       aModificationTime, u"added");
@@ -1940,22 +1963,24 @@ nsresult nsPermissionManager::AddInterna
         entry->GetPermissions()[index].mNonSessionExpireTime = aExpireTime;
       }
 
       entry->GetPermissions()[index].mPermission = aPermission;
       entry->GetPermissions()[index].mExpireType = aExpireType;
       entry->GetPermissions()[index].mExpireTime = aExpireTime;
       entry->GetPermissions()[index].mModificationTime = aModificationTime;
 
-      if (aDBOperation == eWriteToDB && IsPersistentExpire(aExpireType))
+      if (aDBOperation == eWriteToDB &&
+          IsPersistentExpire(aExpireType, aType)) {
         // We care only about the id, the permission and
         // expireType/expireTime/modificationTime here. We pass dummy values for
         // all other parameters.
         UpdateDB(op, mStmtUpdate, id, EmptyCString(), EmptyCString(),
                  aPermission, aExpireType, aExpireTime, aModificationTime);
+      }
 
       if (aNotifyOperation == eNotify) {
         NotifyObserversWithPermission(aPrincipal, mTypeArray[typeIndex],
                                       aPermission, aExpireType, aExpireTime,
                                       aModificationTime, u"changed");
       }
 
       break;
@@ -1991,17 +2016,18 @@ nsresult nsPermissionManager::AddInterna
       // update the existing entry in memory.
       entry->GetPermissions()[index].mID = id;
       entry->GetPermissions()[index].mPermission = aPermission;
       entry->GetPermissions()[index].mExpireType = aExpireType;
       entry->GetPermissions()[index].mExpireTime = aExpireTime;
       entry->GetPermissions()[index].mModificationTime = aModificationTime;
 
       // If requested, create the entry in the DB.
-      if (aDBOperation == eWriteToDB && IsPersistentExpire(aExpireType)) {
+      if (aDBOperation == eWriteToDB &&
+          IsPersistentExpire(aExpireType, aType)) {
         UpdateDB(eOperationAdding, mStmtInsert, id, origin, aType, aPermission,
                  aExpireType, aExpireTime, aModificationTime);
       }
 
       if (aNotifyOperation == eNotify) {
         NotifyObserversWithPermission(aPrincipal, mTypeArray[typeIndex],
                                       aPermission, aExpireType, aExpireTime,
                                       aModificationTime, u"changed");
--- a/gfx/gl/GLBlitHelper.cpp
+++ b/gfx/gl/GLBlitHelper.cpp
@@ -1080,73 +1080,73 @@ void GLBlitHelper::DrawBlitTextureToFram
   const bool yFlip = false;
   const DrawBlitProg::BaseArgs baseArgs = {texMatrix0, yFlip, destSize,
                                            Nothing()};
   prog->Draw(baseArgs);
 }
 
 // -----------------------------------------------------------------------------
 
-void GLBlitHelper::BlitFramebuffer(const gfx::IntSize& srcSize,
-                                   const gfx::IntSize& destSize,
+void GLBlitHelper::BlitFramebuffer(const gfx::IntRect& srcRect,
+                                   const gfx::IntRect& destRect,
                                    GLuint filter) const {
   MOZ_ASSERT(mGL->IsSupported(GLFeature::framebuffer_blit));
 
   const ScopedGLState scissor(mGL, LOCAL_GL_SCISSOR_TEST, false);
-  mGL->fBlitFramebuffer(0, 0, srcSize.width, srcSize.height, 0, 0,
-                        destSize.width, destSize.height,
-                        LOCAL_GL_COLOR_BUFFER_BIT, filter);
+  mGL->fBlitFramebuffer(srcRect.x, srcRect.y, srcRect.XMost(), srcRect.YMost(),
+                        destRect.x, destRect.y, destRect.XMost(),
+                        destRect.YMost(), LOCAL_GL_COLOR_BUFFER_BIT, filter);
 }
 
 // --
 
 void GLBlitHelper::BlitFramebufferToFramebuffer(const GLuint srcFB,
                                                 const GLuint destFB,
-                                                const gfx::IntSize& srcSize,
-                                                const gfx::IntSize& destSize,
+                                                const gfx::IntRect& srcRect,
+                                                const gfx::IntRect& destRect,
                                                 GLuint filter) const {
   MOZ_ASSERT(mGL->IsSupported(GLFeature::framebuffer_blit));
   MOZ_GL_ASSERT(mGL, !srcFB || mGL->fIsFramebuffer(srcFB));
   MOZ_GL_ASSERT(mGL, !destFB || mGL->fIsFramebuffer(destFB));
 
   const ScopedBindFramebuffer boundFB(mGL);
   mGL->fBindFramebuffer(LOCAL_GL_READ_FRAMEBUFFER, srcFB);
   mGL->fBindFramebuffer(LOCAL_GL_DRAW_FRAMEBUFFER, destFB);
 
-  BlitFramebuffer(srcSize, destSize, filter);
+  BlitFramebuffer(srcRect, destRect, filter);
 }
 
 void GLBlitHelper::BlitTextureToFramebuffer(GLuint srcTex,
                                             const gfx::IntSize& srcSize,
                                             const gfx::IntSize& destSize,
                                             GLenum srcTarget) const {
   MOZ_GL_ASSERT(mGL, mGL->fIsTexture(srcTex));
 
   if (mGL->IsSupported(GLFeature::framebuffer_blit)) {
     const ScopedFramebufferForTexture srcWrapper(mGL, srcTex, srcTarget);
     const ScopedBindFramebuffer bindFB(mGL);
     mGL->fBindFramebuffer(LOCAL_GL_READ_FRAMEBUFFER, srcWrapper.FB());
-    BlitFramebuffer(srcSize, destSize);
+    BlitFramebuffer(gfx::IntRect({}, srcSize), gfx::IntRect({}, destSize));
     return;
   }
 
   DrawBlitTextureToFramebuffer(srcTex, srcSize, destSize, srcTarget);
 }
 
 void GLBlitHelper::BlitFramebufferToTexture(GLuint destTex,
                                             const gfx::IntSize& srcSize,
                                             const gfx::IntSize& destSize,
                                             GLenum destTarget) const {
   MOZ_GL_ASSERT(mGL, mGL->fIsTexture(destTex));
 
   if (mGL->IsSupported(GLFeature::framebuffer_blit)) {
     const ScopedFramebufferForTexture destWrapper(mGL, destTex, destTarget);
     const ScopedBindFramebuffer bindFB(mGL);
     mGL->fBindFramebuffer(LOCAL_GL_DRAW_FRAMEBUFFER, destWrapper.FB());
-    BlitFramebuffer(srcSize, destSize);
+    BlitFramebuffer(gfx::IntRect({}, srcSize), gfx::IntRect({}, destSize));
     return;
   }
 
   ScopedBindTexture autoTex(mGL, destTex, destTarget);
   ScopedGLState scissor(mGL, LOCAL_GL_SCISSOR_TEST, false);
   mGL->fCopyTexSubImage2D(destTarget, 0, 0, 0, 0, 0, srcSize.width,
                           srcSize.height);
 }
--- a/gfx/gl/GLBlitHelper.h
+++ b/gfx/gl/GLBlitHelper.h
@@ -153,22 +153,22 @@ class GLBlitHelper final {
                  const gfx::IntSize& destSize, OriginPos destOrigin) const;
 #endif
 
   explicit GLBlitHelper(GLContext* gl);
 
  public:
   ~GLBlitHelper();
 
-  void BlitFramebuffer(const gfx::IntSize& srcSize,
-                       const gfx::IntSize& destSize,
+  void BlitFramebuffer(const gfx::IntRect& srcRect,
+                       const gfx::IntRect& destRect,
                        GLuint filter = LOCAL_GL_NEAREST) const;
   void BlitFramebufferToFramebuffer(GLuint srcFB, GLuint destFB,
-                                    const gfx::IntSize& srcSize,
-                                    const gfx::IntSize& destSize,
+                                    const gfx::IntRect& srcRect,
+                                    const gfx::IntRect& destRect,
                                     GLuint filter = LOCAL_GL_NEAREST) const;
   void BlitFramebufferToTexture(GLuint destTex, const gfx::IntSize& srcSize,
                                 const gfx::IntSize& destSize,
                                 GLenum destTarget = LOCAL_GL_TEXTURE_2D) const;
   void BlitTextureToFramebuffer(GLuint srcTex, const gfx::IntSize& srcSize,
                                 const gfx::IntSize& destSize,
                                 GLenum srcTarget = LOCAL_GL_TEXTURE_2D) const;
   void BlitTextureToTexture(GLuint srcTex, GLuint destTex,
--- a/gfx/gl/SharedSurface.cpp
+++ b/gfx/gl/SharedSurface.cpp
@@ -71,18 +71,19 @@ bool SharedSurface::ProdCopy(SharedSurfa
       const ScopedBindFramebuffer bindFB(gl, 0);
 
       gl->BlitHelper()->BlitFramebufferToTexture(destTex, src->mSize,
                                                  dest->mSize, destTarget);
     } else if (dest->mAttachType == AttachmentType::GLRenderbuffer) {
       GLuint destRB = dest->ProdRenderbuffer();
       ScopedFramebufferForRenderbuffer destWrapper(gl, destRB);
 
-      gl->BlitHelper()->BlitFramebufferToFramebuffer(0, destWrapper.FB(),
-                                                     src->mSize, dest->mSize);
+      gl->BlitHelper()->BlitFramebufferToFramebuffer(
+          0, destWrapper.FB(), gfx::IntRect({}, src->mSize),
+          gfx::IntRect({}, dest->mSize));
     } else {
       MOZ_CRASH("GFX: Unhandled dest->mAttachType 1.");
     }
 
     if (srcNeedsUnlock) src->UnlockProd();
 
     if (origNeedsRelock) origLocked->LockProd();
 
@@ -110,18 +111,19 @@ bool SharedSurface::ProdCopy(SharedSurfa
       const ScopedBindFramebuffer bindFB(gl, 0);
 
       gl->BlitHelper()->BlitTextureToFramebuffer(srcTex, src->mSize,
                                                  dest->mSize, srcTarget);
     } else if (src->mAttachType == AttachmentType::GLRenderbuffer) {
       GLuint srcRB = src->ProdRenderbuffer();
       ScopedFramebufferForRenderbuffer srcWrapper(gl, srcRB);
 
-      gl->BlitHelper()->BlitFramebufferToFramebuffer(srcWrapper.FB(), 0,
-                                                     src->mSize, dest->mSize);
+      gl->BlitHelper()->BlitFramebufferToFramebuffer(
+          srcWrapper.FB(), 0, gfx::IntRect({}, src->mSize),
+          gfx::IntRect({}, dest->mSize));
     } else {
       MOZ_CRASH("GFX: Unhandled src->mAttachType 2.");
     }
 
     if (destNeedsUnlock) dest->UnlockProd();
 
     if (origNeedsRelock) origLocked->LockProd();
 
@@ -173,17 +175,18 @@ bool SharedSurface::ProdCopy(SharedSurfa
       return true;
     }
 
     if (dest->mAttachType == AttachmentType::GLRenderbuffer) {
       GLuint destRB = dest->ProdRenderbuffer();
       ScopedFramebufferForRenderbuffer destWrapper(gl, destRB);
 
       gl->BlitHelper()->BlitFramebufferToFramebuffer(
-          srcWrapper.FB(), destWrapper.FB(), src->mSize, dest->mSize);
+          srcWrapper.FB(), destWrapper.FB(), gfx::IntRect({}, src->mSize),
+          gfx::IntRect({}, dest->mSize));
 
       return true;
     }
 
     MOZ_CRASH("GFX: Unhandled dest->mAttachType 4.");
   }
 
   MOZ_CRASH("GFX: Unhandled src->mAttachType 5.");
--- a/gfx/layers/Compositor.cpp
+++ b/gfx/layers/Compositor.cpp
@@ -8,16 +8,17 @@
 #include "base/message_loop.h"                      // for MessageLoop
 #include "mozilla/layers/CompositorBridgeParent.h"  // for CompositorBridgeParent
 #include "mozilla/layers/Diagnostics.h"
 #include "mozilla/layers/Effects.h"  // for Effect, EffectChain, etc
 #include "mozilla/layers/TextureClient.h"
 #include "mozilla/layers/TextureHost.h"
 #include "mozilla/layers/CompositorThread.h"
 #include "mozilla/mozalloc.h"  // for operator delete, etc
+#include "GeckoProfiler.h"
 #include "gfx2DGlue.h"
 #include "nsAppRunner.h"
 #include "LayersHelpers.h"
 
 namespace mozilla {
 
 namespace layers {
 
@@ -592,10 +593,19 @@ already_AddRefed<RecordedFrame> Composit
 
   if (!ReadbackRenderTarget(renderTarget, buffer)) {
     return nullptr;
   }
 
   return MakeAndAddRef<CompositorRecordedFrame>(aTimeStamp, std::move(buffer));
 }
 
+bool Compositor::ShouldRecordFrames() const {
+#ifdef MOZ_GECKO_PROFILER
+  if (profiler_feature_active(ProfilerFeature::Screenshots)) {
+    return true;
+  }
+#endif
+  return mRecordFrames;
+}
+
 }  // namespace layers
 }  // namespace mozilla
--- a/gfx/layers/Compositor.h
+++ b/gfx/layers/Compositor.h
@@ -167,18 +167,19 @@ enum SurfaceInitMode { INIT_MODE_NONE, I
  *    call MakeCurrent if necessary (not necessary if no other context has been
  *      made current),
  *    take care of any texture upload required to composite the quad, this step
  *      is backend-dependent,
  *    construct an EffectChain for the quad,
  *    call DrawQuad,
  *  call EndFrame.
  *
- * By default, the compositor will render to the screen, to render to a target,
- * call SetTargetContext or SetRenderTarget, the latter with a target created
+ * By default, the compositor will render to the screen if BeginFrameForWindow
+ * is called. To render to a target, call BeginFrameForTarget or
+ * or SetRenderTarget, the latter with a target created
  * by CreateRenderTarget or CreateRenderTargetFromSource.
  *
  * The target and viewport methods can be called before any DrawQuad call and
  * affect any subsequent DrawQuad calls.
  */
 class Compositor : public TextureSourceProvider {
  protected:
   virtual ~Compositor();
@@ -198,30 +199,16 @@ class Compositor : public TextureSourceP
    */
   virtual TextureFactoryIdentifier GetTextureFactoryIdentifier() = 0;
 
   /**
    * Properties of the compositor.
    */
   virtual bool CanUseCanvasLayerForSize(const gfx::IntSize& aSize) = 0;
 
-  /**
-   * Set the target for rendering. Results will have been written to aTarget by
-   * the time that EndFrame returns.
-   *
-   * If this method is not used, or we pass in nullptr, we target the
-   * compositor's usual swap chain and render to the screen.
-   */
-  void SetTargetContext(gfx::DrawTarget* aTarget, const gfx::IntRect& aRect) {
-    mTarget = aTarget;
-    mTargetBounds = aRect;
-  }
-  gfx::DrawTarget* GetTargetContext() const { return mTarget; }
-  void ClearTargetContext() { mTarget = nullptr; }
-
   typedef uint32_t MakeCurrentFlags;
   static const MakeCurrentFlags ForceMakeCurrent = 0x1;
   /**
    * Make this compositor's rendering context the current context for the
    * underlying graphics API. This may be a global operation, depending on the
    * API. Our context will remain the current one until someone else changes it.
    *
    * Clients of the compositor should call this at the start of the compositing
@@ -393,44 +380,129 @@ class Compositor : public TextureSourceP
   void SetClearColorToDefault() { mClearColor = mDefaultClearColor; }
 
   /*
    * Clear aRect on current render target.
    */
   virtual void ClearRect(const gfx::Rect& aRect) = 0;
 
   /**
-   * Start a new frame.
+   * Start a new frame for rendering to the window.
+   * Needs to be paired with a call to EndFrame() if the return value is not
+   * Nothing().
    *
-   * aInvalidRect is the invalid region of the screen; it can be ignored for
-   * compositors where the performance for compositing the entire window is
-   * sufficient.
-   *
-   * aClipRect is the clip rect for the window in window space (optional).
-   * aRenderBounds bounding rect for rendering, in user space.
+   * aInvalidRegion is the invalid region of the window.
+   * aClipRect is the clip rect for all drawing (optional).
+   * aRenderBounds is the bounding rect for rendering.
    * aOpaqueRegion is the area that contains opaque content.
+   * All coordinates are in window space.
    *
    * Returns the non-empty render bounds actually used by the compositor in
    * window space, or Nothing() if composition should be aborted.
    */
-  virtual Maybe<gfx::IntRect> BeginFrame(const nsIntRegion& aInvalidRegion,
-                                         const Maybe<gfx::IntRect>& aClipRect,
-                                         const gfx::IntRect& aRenderBounds,
-                                         const nsIntRegion& aOpaqueRegion,
-                                         NativeLayer* aNativeLayer) = 0;
+  virtual Maybe<gfx::IntRect> BeginFrameForWindow(
+      const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+      const gfx::IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion) = 0;
+
+  /**
+   * Start a new frame for rendering to a DrawTarget. Rendering can happen
+   * directly into the DrawTarget, or it can happen in an offscreen GPU buffer
+   * and read back into the DrawTarget in EndFrame, or it can happen inside the
+   * window and read back into the DrawTarget in EndFrame.
+   * Needs to be paired with a call to EndFrame() if the return value is not
+   * Nothing().
+   *
+   * aInvalidRegion is the invalid region in the target.
+   * aClipRect is the clip rect for all drawing (optional).
+   * aRenderBounds is the bounding rect for rendering.
+   * aOpaqueRegion is the area that contains opaque content.
+   * aTarget is the DrawTarget which should contain the rendering after
+   *         EndFrame() has been called.
+   * aTargetBounds are the DrawTarget's bounds.
+   * All coordinates are in window space.
+   *
+   * Returns the non-empty render bounds actually used by the compositor in
+   * window space, or Nothing() if composition should be aborted.
+   *
+   * If BeginFrame succeeds, the compositor keeps a reference to aTarget until
+   * EndFrame is called.
+   */
+  virtual Maybe<gfx::IntRect> BeginFrameForTarget(
+      const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+      const gfx::IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion,
+      gfx::DrawTarget* aTarget, const gfx::IntRect& aTargetBounds) = 0;
+
+  /**
+   * Start a new frame for rendering to one or more native layers. Needs to be
+   * paired with a call to EndFrame().
+   *
+   * This puts the compositor in a state where offscreen rendering is allowed.
+   * Rendering an actual native layer is only possible via a call to
+   * BeginRenderingToNativeLayer(), after BeginFrameForNativeLayers() has run.
+   *
+   * The following is true for the entire time between
+   * BeginFrameForNativeLayers() and EndFrame(), even outside pairs of calls to
+   * Begin/EndRenderingToNativeLayer():
+   *  - GetCurrentRenderTarget() will return something non-null.
+   *  - CreateRenderTarget() and SetRenderTarget() can be called, in order to
+   *    facilitate offscreen rendering.
+   * The render target that this method sets as the current render target is not
+   * useful. Do not render to it. It exists so that calls of the form
+   * SetRenderTarget(previousTarget) do not crash.
+   *
+   * Do not call on platforms that do not support native layers.
+   */
+  virtual void BeginFrameForNativeLayers() = 0;
+
+  /**
+   * Start rendering into aNativeLayer.
+   * Needs to be paired with a call to EndRenderingToNativeLayer() if the return
+   * value is not Nothing().
+   *
+   * Must be called between BeginFrameForNativeLayers() and EndFrame().
+   *
+   * aInvalidRegion is the invalid region in the native layer.
+   * aClipRect is the clip rect for all drawing (optional).
+   * aOpaqueRegion is the area that contains opaque content.
+   * aNativeLayer is the native layer.
+   * All coordinates, including aNativeLayer->GetRect(), are in window space.
+   *
+   * Returns the non-empty layer rect, or Nothing() if rendering to this layer
+   * should be skipped.
+   *
+   * If BeginRenderingToNativeLayer succeeds, the compositor keeps a reference
+   * to aNativeLayer until EndRenderingToNativeLayer is called.
+   *
+   * Do not call on platforms that do not support native layers.
+   */
+  virtual Maybe<gfx::IntRect> BeginRenderingToNativeLayer(
+      const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+      const nsIntRegion& aOpaqueRegion, NativeLayer* aNativeLayer) = 0;
+
+  /**
+   * Stop rendering to the native layer and submit the rendering as the layer's
+   * new content.
+   *
+   * Do not call on platforms that do not support native layers.
+   */
+  virtual void EndRenderingToNativeLayer() = 0;
 
   /**
    * Notification that we've finished issuing draw commands for normal
    * layers (as opposed to the diagnostic overlay which comes after).
-   * This is called between BeginFrame and EndFrame, and it's called before
+   * This is called between BeginFrame* and EndFrame, and it's called before
    * GetWindowRenderTarget() is called for the purposes of screenshot capturing.
    * That next call to GetWindowRenderTarget() expects up-to-date contents for
    * the current frame.
-   * Called at a time when the current render target is the one that BeginFrame
-   * put in place.
+   * When rendering to native layers, this should be called for every layer,
+   * between BeginRenderingToNativeLayer and EndRenderingToNativeLayer, at a
+   * time at which the current render target is the one that
+   * BeginRenderingToNativeLayer has put in place.
+   * When not rendering to native layers, this should be called at a time when
+   * the current render target is the one that BeginFrameForWindow put in place.
    */
   virtual void NormalDrawingDone() {}
 
   /**
    * Flush the current frame to the screen and tidy up.
    *
    * Derived class overriding this should call Compositor::EndFrame.
    */
@@ -519,17 +591,19 @@ class Compositor : public TextureSourceP
   bool IsValid() const override;
   CompositorBridgeParent* GetCompositorBridgeParent() const { return mParent; }
 
   /**
    * Request the compositor to allow recording its frames.
    *
    * This is a noop on |CompositorOGL|.
    */
-  virtual void RequestAllowFrameRecording(bool aWillRecord) {}
+  virtual void RequestAllowFrameRecording(bool aWillRecord) {
+    mRecordFrames = aWillRecord;
+  }
 
   /**
    * Record the current frame for readback by the |CompositionRecorder|.
    *
    * If this compositor does not support this feature, a null pointer is
    * returned instead.
    */
   already_AddRefed<RecordedFrame> RecordFrame(const TimeStamp& aTimeStamp);
@@ -573,16 +647,28 @@ class Compositor : public TextureSourceP
 
   virtual void DrawPolygon(const gfx::Polygon& aPolygon, const gfx::Rect& aRect,
                            const gfx::IntRect& aClipRect,
                            const EffectChain& aEffectChain, gfx::Float aOpacity,
                            const gfx::Matrix4x4& aTransform,
                            const gfx::Rect& aVisibleRect);
 
   /**
+   * Whether or not the compositor should be prepared to record frames. While
+   * this returns true, compositors are expected to maintain a full window
+   * render target that they return from GetWindowRenderTarget() between
+   * NormalDrawingDone() and EndFrame().
+   *
+   * This will be true when either we are recording a profile with screenshots
+   * enabled or the |LayerManagerComposite| has requested us to record frames
+   * for the |CompositionRecorder|.
+   */
+  bool ShouldRecordFrames() const;
+
+  /**
    * Last Composition end time.
    */
   TimeStamp mLastCompositionEndTime;
 
   DiagnosticTypes mDiagnosticTypes;
   CompositorBridgeParent* mParent;
 
   /**
@@ -590,26 +676,25 @@ class Compositor : public TextureSourceP
    * current frame. This value is an approximation and is not accurate,
    * especially in the presence of transforms.
    */
   size_t mPixelsPerFrame;
   size_t mPixelsFilled;
 
   ScreenRotation mScreenRotation;
 
-  RefPtr<gfx::DrawTarget> mTarget;
-  gfx::IntRect mTargetBounds;
-
   widget::CompositorWidget* mWidget;
 
   bool mIsDestroyed;
 
   gfx::Color mClearColor;
   gfx::Color mDefaultClearColor;
 
+  bool mRecordFrames = false;
+
  private:
   static LayersBackend sBackend;
 };
 
 // Returns the number of rects. (Up to 4)
 typedef gfx::Rect decomposedRectArrayT[4];
 size_t DecomposeIntoNoRepeatRects(const gfx::Rect& aRect,
                                   const gfx::Rect& aTexCoordRect,
--- a/gfx/layers/basic/BasicCompositor.cpp
+++ b/gfx/layers/basic/BasicCompositor.cpp
@@ -238,17 +238,17 @@ BasicCompositor::~BasicCompositor() { MO
 bool BasicCompositor::Initialize(nsCString* const out_failureReason) {
   return mWidget ? mWidget->InitCompositor(this) : false;
 };
 
 int32_t BasicCompositor::GetMaxTextureSize() const { return mMaxTextureSize; }
 
 void BasicCompositingRenderTarget::BindRenderTarget() {
   if (mClearOnBind) {
-    mDrawTarget->ClearRect(Rect(0, 0, mSize.width, mSize.height));
+    mDrawTarget->ClearRect(Rect(GetRect()));
     mClearOnBind = false;
   }
 }
 
 void BasicCompositor::Destroy() {
   if (mIsPendingEndRemoteDrawing) {
     // Force to end previous remote drawing.
     EndRemoteDrawing();
@@ -278,35 +278,37 @@ already_AddRefed<CompositingRenderTarget
       mRenderTarget->mDrawTarget->CreateSimilarDrawTarget(
           aRect.Size(), SurfaceFormat::B8G8R8A8);
 
   if (!target) {
     return nullptr;
   }
 
   RefPtr<BasicCompositingRenderTarget> rt =
-      new BasicCompositingRenderTarget(target, aRect);
+      new BasicCompositingRenderTarget(target, aRect, aRect.TopLeft());
+
+  rt->mDrawTarget->SetTransform(Matrix::Translation(-rt->GetOrigin()));
 
   return rt.forget();
 }
 
 already_AddRefed<CompositingRenderTarget>
 BasicCompositor::CreateRenderTargetFromSource(
     const IntRect& aRect, const CompositingRenderTarget* aSource,
     const IntPoint& aSourcePoint) {
   MOZ_CRASH("GFX: Shouldn't be called!");
   return nullptr;
 }
 
 already_AddRefed<CompositingRenderTarget>
-BasicCompositor::CreateRenderTargetAndClear(DrawTarget* aDrawTarget,
-                                            const IntRect& aDrawTargetRect,
-                                            const IntRegion& aClearRegion) {
-  RefPtr<BasicCompositingRenderTarget> rt =
-      new BasicCompositingRenderTarget(aDrawTarget, aDrawTargetRect);
+BasicCompositor::CreateRootRenderTarget(DrawTarget* aDrawTarget,
+                                        const IntRect& aDrawTargetRect,
+                                        const IntRegion& aClearRegion) {
+  RefPtr<BasicCompositingRenderTarget> rt = new BasicCompositingRenderTarget(
+      aDrawTarget, aDrawTargetRect, IntPoint());
 
   rt->mDrawTarget->SetTransform(Matrix::Translation(-rt->GetOrigin()));
 
   if (!aClearRegion.IsEmpty()) {
     gfx::IntRect clearRect = aClearRegion.GetBounds();
     gfxUtils::ClipToRegion(rt->mDrawTarget, aClearRegion);
     rt->mDrawTarget->ClearRect(gfx::Rect(clearRect));
     rt->mDrawTarget->PopClip();
@@ -656,22 +658,25 @@ void BasicCompositor::DrawGeometry(
     newTransform = Matrix();
 
     // When we apply the 3D transformation, we do it against a temporary
     // surface, so undo the coordinate offset.
     new3DTransform = aTransform;
     new3DTransform.PreTranslate(aRect.X(), aRect.Y(), 0);
   }
 
-  // XXX the transform is probably just an integer offset so this whole
-  // business here is a bit silly.
-  Rect transformedClipRect =
-      buffer->GetTransform().TransformBounds(Rect(aClipRect));
-
-  buffer->PushClipRect(Rect(aClipRect));
+  // The current transform on buffer is always only a translation by `-offset`.
+  // aClipRect is relative to mRenderTarget->GetClipSpaceOrigin().
+  // For non-root render targets, the clip space origin is equal to `offset`.
+  // For the root render target, the clip space origin is at (0, 0) and the
+  // offset can be anywhere.
+  IntRect clipRectInRenderTargetSpace =
+      aClipRect + mRenderTarget->GetClipSpaceOrigin();
+  buffer->PushClipRect(Rect(clipRectInRenderTargetSpace));
+  Rect deviceSpaceClipRect(clipRectInRenderTargetSpace - offset);
 
   newTransform.PostTranslate(-offset.x, -offset.y);
   buffer->SetTransform(newTransform);
 
   RefPtr<SourceSurface> sourceMask;
   Matrix maskTransform;
   if (aTransform.Is2D()) {
     SetupMask(aEffectChain, dest, offset, sourceMask, maskTransform);
@@ -712,24 +717,24 @@ void BasicCompositor::DrawGeometry(
       TextureSourceBasic* source = texturedEffect->mTexture->AsSourceBasic();
 
       if (source && texturedEffect->mPremultiplied) {
         // we have a fast path for video here
         if (source->mFromYCBCR &&
             AttemptVideoConvertAndScale(texturedEffect->mTexture, sourceMask,
                                         aOpacity, blendMode, texturedEffect,
                                         newTransform, aRect,
-                                        transformedClipRect, dest, buffer)) {
+                                        deviceSpaceClipRect, dest, buffer)) {
           // we succeeded in convert and scaling
         } else if (source->mFromYCBCR && !source->GetSurface(dest)) {
           gfxWarning() << "Failed to get YCbCr to rgb surface.";
         } else if (source->mFromYCBCR &&
                    AttemptVideoScale(source, sourceMask, aOpacity, blendMode,
                                      texturedEffect, newTransform, aRect,
-                                     transformedClipRect, dest, buffer)) {
+                                     deviceSpaceClipRect, dest, buffer)) {
           // we succeeded in scaling
         } else {
           DrawSurfaceWithTextureCoords(
               dest, aGeometry, aRect, source->GetSurface(dest),
               texturedEffect->mTextureCoords, texturedEffect->mSamplingFilter,
               drawOptions, sourceMask, &maskTransform);
         }
       } else if (source) {
@@ -861,179 +866,278 @@ bool BasicCompositor::BlitRenderTarget(C
       static_cast<BasicCompositingRenderTarget*>(aSource)
           ->mDrawTarget->Snapshot();
   mRenderTarget->mDrawTarget->DrawSurface(
       surface, Rect(Point(), Size(aDestSize)), Rect(Point(), Size(aSourceSize)),
       DrawSurfaceOptions(), DrawOptions(1.0f, CompositionOp::OP_SOURCE));
   return true;
 }
 
-Maybe<gfx::IntRect> BasicCompositor::BeginFrame(
+Maybe<gfx::IntRect> BasicCompositor::BeginFrameForWindow(
     const nsIntRegion& aInvalidRegion, const Maybe<IntRect>& aClipRect,
-    const IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion,
-    NativeLayer* aNativeLayer) {
+    const IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion) {
   if (mIsPendingEndRemoteDrawing) {
     // Force to end previous remote drawing.
     EndRemoteDrawing();
     MOZ_ASSERT(!mIsPendingEndRemoteDrawing);
   }
 
+  MOZ_RELEASE_ASSERT(mCurrentFrameDest == FrameDestination::NO_CURRENT_FRAME,
+                     "mCurrentFrameDest not restored properly");
+
   IntRect rect(IntPoint(), mWidget->GetClientSize().ToUnknownSize());
 
-  const bool shouldInvalidateWindow = NeedToRecreateFullWindowRenderTarget();
+  mShouldInvalidateWindow = NeedToRecreateFullWindowRenderTarget();
 
-  if (shouldInvalidateWindow) {
+  if (mShouldInvalidateWindow) {
     mInvalidRegion = rect;
   } else {
     IntRegion invalidRegionSafe;
     // Sometimes the invalid region is larger than we want to draw.
     invalidRegionSafe.And(aInvalidRegion, rect);
 
     mInvalidRegion = invalidRegionSafe;
   }
 
-  RefPtr<CompositingRenderTarget> target;
-  if (mTarget) {
-    MOZ_RELEASE_ASSERT(!mInvalidRegion.IsEmpty());
+  LayoutDeviceIntRegion invalidRegion =
+      LayoutDeviceIntRegion::FromUnknownRegion(mInvalidRegion);
+  BufferMode bufferMode = BufferMode::BUFFERED;
+  // StartRemoteDrawingInRegion can mutate invalidRegion.
+  RefPtr<DrawTarget> dt =
+      mWidget->StartRemoteDrawingInRegion(invalidRegion, &bufferMode);
+  if (!dt) {
+    return Nothing();
+  }
+  if (invalidRegion.IsEmpty()) {
+    mWidget->EndRemoteDrawingInRegion(dt, invalidRegion);
+    return Nothing();
+  }
 
-    // If we have a copy target, render into that DrawTarget directly without
-    // any intermediate buffer. We don't need to call StartRemoteDrawingInRegion
-    // because we don't need a widget-provided DrawTarget.
-    IntRegion clearRegion;
-    clearRegion.Sub(mInvalidRegion, aOpaqueRegion);
-    // Set up a render target for drawing directly to mTarget.
-    target = CreateRenderTargetAndClear(mTarget, mTargetBounds, clearRegion);
-  } else if (aNativeLayer) {
-#ifdef XP_MACOSX
-    if (mInvalidRegion.IsEmpty()) {
-      return Nothing();
-    }
-    NativeLayerCA* nativeLayer = aNativeLayer->AsNativeLayerCA();
-    MOZ_RELEASE_ASSERT(nativeLayer, "Unexpected native layer type");
-    nativeLayer->SetSurfaceIsFlipped(false);
-    CFTypeRefPtr<IOSurfaceRef> surf = nativeLayer->NextSurface();
-    if (!surf) {
-      return Nothing();
-    }
-    nativeLayer->InvalidateRegionThroughoutSwapchain(mInvalidRegion);
-    mInvalidRegion = nativeLayer->CurrentSurfaceInvalidRegion();
-    MOZ_RELEASE_ASSERT(!mInvalidRegion.IsEmpty());
-    mCurrentNativeLayer = aNativeLayer;
-    mCurrentIOSurface = new MacIOSurface(std::move(surf));
-    mCurrentIOSurface->Lock(false);
-    RefPtr<DrawTarget> dt =
-        mCurrentIOSurface->GetAsDrawTargetLocked(BackendType::SKIA);
-    IntRect dtBounds(IntPoint(0, 0), dt->GetSize());
-    IntRegion clearRegion;
-    clearRegion.Sub(mInvalidRegion, aOpaqueRegion);
-    // Set up a render target for drawing directly to dt.
-    target = CreateRenderTargetAndClear(dt, dtBounds, clearRegion);
-#else
-    MOZ_CRASH("Unexpected native layer on this platform");
-#endif
-  } else {
-    LayoutDeviceIntRegion invalidRegion =
-        LayoutDeviceIntRegion::FromUnknownRegion(mInvalidRegion);
-    BufferMode bufferMode = BufferMode::BUFFERED;
-    // StartRemoteDrawingInRegion can mutate invalidRegion.
-    RefPtr<DrawTarget> dt =
-        mWidget->StartRemoteDrawingInRegion(invalidRegion, &bufferMode);
-    if (!dt) {
-      return Nothing();
-    }
-    mInvalidRegion = invalidRegion.ToUnknownRegion();
-    if (mInvalidRegion.IsEmpty()) {
+  mInvalidRegion = invalidRegion.ToUnknownRegion();
+  IntRegion clearRegion;
+  clearRegion.Sub(mInvalidRegion, aOpaqueRegion);
+
+  RefPtr<CompositingRenderTarget> target;
+  if (bufferMode == BufferMode::BUFFERED) {
+    // Buffer drawing via a back buffer.
+    IntRect backBufferRect = mInvalidRegion.GetBounds();
+    bool isCleared = false;
+    RefPtr<DrawTarget> backBuffer =
+        mWidget->GetBackBufferDrawTarget(dt, backBufferRect, &isCleared);
+    if (!backBuffer) {
       mWidget->EndRemoteDrawingInRegion(dt, invalidRegion);
       return Nothing();
     }
-
-    IntRegion clearRegion;
-    clearRegion.Sub(mInvalidRegion, aOpaqueRegion);
+    // Set up a render target for drawirg to the back buffer.
+    target = CreateRootRenderTarget(backBuffer, backBufferRect,
+                                    isCleared ? IntRegion() : clearRegion);
+    mFrontBuffer = dt;
+    // We will copy the drawing from the back buffer into mFrontBuffer (the
+    // widget) in EndRemoteDrawing().
+  } else {
+    // In BufferMode::BUFFER_NONE, the DrawTarget returned by
+    // StartRemoteDrawingInRegion can cover different rectangles in window
+    // space. It can either cover the entire window, or it can cover just the
+    // invalid region. We discern between the two cases by comparing the
+    // DrawTarget's size with the invalild region's size.
+    IntRect invalidRect = mInvalidRegion.GetBounds();
+    IntPoint dtLocation = dt->GetSize() == invalidRect.Size()
+                              ? invalidRect.TopLeft()
+                              : IntPoint(0, 0);
+    IntRect dtBounds(dtLocation, dt->GetSize());
 
-    if (bufferMode == BufferMode::BUFFERED) {
-      // Buffer drawing via a back buffer.
-      IntRect backBufferRect = mInvalidRegion.GetBounds();
-      bool isCleared = false;
-      RefPtr<DrawTarget> backBuffer =
-          mWidget->GetBackBufferDrawTarget(dt, backBufferRect, &isCleared);
-      if (!backBuffer) {
-        mWidget->EndRemoteDrawingInRegion(dt, invalidRegion);
-        return Nothing();
-      }
-      // Set up a render target for drawirg to the back buffer.
-      target = CreateRenderTargetAndClear(
-          backBuffer, backBufferRect, isCleared ? IntRegion() : clearRegion);
-      mFrontBuffer = dt;
-      // We will copy the drawing from the back buffer into mFrontBuffer (the
-      // widget) in EndRemoteDrawing().
-    } else {
-      // In BufferMode::BUFFER_NONE, the DrawTarget returned by
-      // StartRemoteDrawingInRegion can cover different rectangles in window
-      // space. It can either cover the entire window, or it can cover just the
-      // invalid region. We discern between the two cases by comparing the
-      // DrawTarget's size with the invalild region's size.
-      IntRect invalidRect = mInvalidRegion.GetBounds();
-      IntPoint dtLocation = dt->GetSize() == invalidRect.Size()
-                                ? invalidRect.TopLeft()
-                                : IntPoint(0, 0);
-      IntRect dtBounds(dtLocation, dt->GetSize());
+    // Set up a render target for drawing directly to dt.
+    target = CreateRootRenderTarget(dt, dtBounds, clearRegion);
+  }
 
-      // Set up a render target for drawing directly to dt.
-      target = CreateRenderTargetAndClear(dt, dtBounds, clearRegion);
-    }
-  }
+  mCurrentFrameDest = FrameDestination::WINDOW;
 
   MOZ_RELEASE_ASSERT(target);
   SetRenderTarget(target);
 
   gfxUtils::ClipToRegion(mRenderTarget->mDrawTarget, mInvalidRegion);
 
   mRenderTarget->mDrawTarget->PushClipRect(Rect(aClipRect.valueOr(rect)));
 
   return Some(rect);
 }
 
-void BasicCompositor::EndFrame() {
-  Compositor::EndFrame();
+Maybe<gfx::IntRect> BasicCompositor::BeginFrameForTarget(
+    const nsIntRegion& aInvalidRegion, const Maybe<IntRect>& aClipRect,
+    const IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion,
+    DrawTarget* aTarget, const IntRect& aTargetBounds) {
+  if (mIsPendingEndRemoteDrawing) {
+    // Force to end previous remote drawing.
+    EndRemoteDrawing();
+    MOZ_ASSERT(!mIsPendingEndRemoteDrawing);
+  }
+
+  MOZ_RELEASE_ASSERT(mCurrentFrameDest == FrameDestination::NO_CURRENT_FRAME,
+                     "mCurrentFrameDest not restored properly");
+
+  mInvalidRegion.And(aInvalidRegion, aTargetBounds);
+  MOZ_RELEASE_ASSERT(!mInvalidRegion.IsEmpty());
+
+  IntRegion clearRegion;
+  clearRegion.Sub(mInvalidRegion, aOpaqueRegion);
+
+  // Set up a render target for drawing directly to aTarget.
+  RefPtr<CompositingRenderTarget> target =
+      CreateRootRenderTarget(aTarget, aTargetBounds, clearRegion);
+  MOZ_RELEASE_ASSERT(target);
+  SetRenderTarget(target);
+
+  mCurrentFrameDest = FrameDestination::TARGET;
+
+  gfxUtils::ClipToRegion(mRenderTarget->mDrawTarget, mInvalidRegion);
+
+  mRenderTarget->mDrawTarget->PushClipRect(
+      Rect(aClipRect.valueOr(aTargetBounds)));
+
+  return Some(aTargetBounds);
+}
+
+void BasicCompositor::BeginFrameForNativeLayers() {
+  if (mIsPendingEndRemoteDrawing) {
+    // Force to end previous remote drawing.
+    EndRemoteDrawing();
+    MOZ_ASSERT(!mIsPendingEndRemoteDrawing);
+  }
+
+  MOZ_RELEASE_ASSERT(mCurrentFrameDest == FrameDestination::NO_CURRENT_FRAME,
+                     "mCurrentFrameDest not restored properly");
+
+  mShouldInvalidateWindow = NeedToRecreateFullWindowRenderTarget();
 
+  // Make a 1x1 dummy render target so that GetCurrentRenderTarget() returns
+  // something non-null even outside of calls to
+  // Begin/EndRenderingToNativeLayer.
+  if (!mNativeLayersReferenceRT) {
+    RefPtr<DrawTarget> dt = Factory::CreateDrawTarget(
+        gfxVars::ContentBackend(), IntSize(1, 1), SurfaceFormat::B8G8R8A8);
+    mNativeLayersReferenceRT =
+        new BasicCompositingRenderTarget(dt, IntRect(0, 0, 1, 1), IntPoint());
+  }
+  SetRenderTarget(mNativeLayersReferenceRT);
+
+  mCurrentFrameDest = FrameDestination::NATIVE_LAYERS;
+}
+
+Maybe<gfx::IntRect> BasicCompositor::BeginRenderingToNativeLayer(
+    const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+    const nsIntRegion& aOpaqueRegion, NativeLayer* aNativeLayer) {
+  IntRect rect = aNativeLayer->GetRect();
+
+  if (mShouldInvalidateWindow) {
+    mInvalidRegion = rect;
+  } else {
+    mInvalidRegion.And(aInvalidRegion, rect);
+  }
+
+  if (mInvalidRegion.IsEmpty()) {
+    return Nothing();
+  }
+
+  RefPtr<CompositingRenderTarget> target;
+#ifdef XP_MACOSX
+  NativeLayerCA* nativeLayer = aNativeLayer->AsNativeLayerCA();
+  MOZ_RELEASE_ASSERT(nativeLayer, "Unexpected native layer type");
+  nativeLayer->SetSurfaceIsFlipped(false);
+  CFTypeRefPtr<IOSurfaceRef> surf = nativeLayer->NextSurface();
+  if (!surf) {
+    return Nothing();
+  }
+  IntRegion invalidRelativeToLayer = mInvalidRegion.MovedBy(-rect.TopLeft());
+  nativeLayer->InvalidateRegionThroughoutSwapchain(invalidRelativeToLayer);
+  invalidRelativeToLayer = nativeLayer->CurrentSurfaceInvalidRegion();
+  mInvalidRegion = invalidRelativeToLayer.MovedBy(rect.TopLeft());
+  MOZ_RELEASE_ASSERT(!mInvalidRegion.IsEmpty());
+  mCurrentNativeLayer = aNativeLayer;
+  mCurrentIOSurface = new MacIOSurface(std::move(surf));
+  mCurrentIOSurface->Lock(false);
+  RefPtr<DrawTarget> dt =
+      mCurrentIOSurface->GetAsDrawTargetLocked(BackendType::SKIA);
+  IntRegion clearRegion;
+  clearRegion.Sub(mInvalidRegion, aOpaqueRegion);
+  // Set up a render target for drawing directly to dt.
+  target = CreateRootRenderTarget(dt, rect, clearRegion);
+#else
+  MOZ_CRASH("Unexpected native layer on this platform");
+#endif
+
+  MOZ_RELEASE_ASSERT(target);
+  SetRenderTarget(target);
+
+  gfxUtils::ClipToRegion(mRenderTarget->mDrawTarget, mInvalidRegion);
+
+  mRenderTarget->mDrawTarget->PushClipRect(Rect(aClipRect.valueOr(rect)));
+
+  return Some(rect);
+}
+
+void BasicCompositor::EndRenderingToNativeLayer() {
   // Pop aClipRect/bounds rect
   mRenderTarget->mDrawTarget->PopClip();
 
-  if (StaticPrefs::nglayout_debug_widget_update_flashing()) {
-    float r = float(rand()) / float(RAND_MAX);
-    float g = float(rand()) / float(RAND_MAX);
-    float b = float(rand()) / float(RAND_MAX);
-    // We're still clipped to mInvalidRegion, so just fill the bounds.
-    mRenderTarget->mDrawTarget->FillRect(Rect(mInvalidRegion.GetBounds()),
-                                         ColorPattern(Color(r, g, b, 0.2f)));
+  // Pop mInvalidRegion
+  mRenderTarget->mDrawTarget->PopClip();
+
+  MOZ_RELEASE_ASSERT(mCurrentNativeLayer);
+
+  SetRenderTarget(mNativeLayersReferenceRT);
+
+#ifdef XP_MACOSX
+  NativeLayerCA* nativeLayer = mCurrentNativeLayer->AsNativeLayerCA();
+  MOZ_RELEASE_ASSERT(nativeLayer, "Unexpected native layer type");
+  mCurrentIOSurface->Unlock(false);
+  mCurrentIOSurface = nullptr;
+  nativeLayer->NotifySurfaceReady();
+  mCurrentNativeLayer = nullptr;
+#else
+  MOZ_CRASH("Unexpected native layer on this platform");
+#endif
+}
+
+void BasicCompositor::EndFrame() {
+  Compositor::EndFrame();
+
+  if (mCurrentFrameDest != FrameDestination::NATIVE_LAYERS) {
+    // Pop aClipRect/bounds rect
+    mRenderTarget->mDrawTarget->PopClip();
+
+    if (StaticPrefs::nglayout_debug_widget_update_flashing()) {
+      float r = float(rand()) / float(RAND_MAX);
+      float g = float(rand()) / float(RAND_MAX);
+      float b = float(rand()) / float(RAND_MAX);
+      // We're still clipped to mInvalidRegion, so just fill the bounds.
+      mRenderTarget->mDrawTarget->FillRect(Rect(mInvalidRegion.GetBounds()),
+                                           ColorPattern(Color(r, g, b, 0.2f)));
+    }
+
+    // Pop aInvalidRegion
+    mRenderTarget->mDrawTarget->PopClip();
   }
 
-  // Pop aInvalidregion
-  mRenderTarget->mDrawTarget->PopClip();
-
-  // Reset the translation that was applied in CreateRenderTargetAndClear.
+  // Reset the translation that was applied in CreateRootRenderTarget.
   mRenderTarget->mDrawTarget->SetTransform(gfx::Matrix());
 
-  if (mTarget) {
-    mRenderTarget = nullptr;
-  } else if (mCurrentNativeLayer) {
-#ifdef XP_MACOSX
-    NativeLayerCA* nativeLayer = mCurrentNativeLayer->AsNativeLayerCA();
-    MOZ_RELEASE_ASSERT(nativeLayer, "Unexpected native layer type");
-    mRenderTarget = nullptr;
-    mCurrentIOSurface->Unlock(false);
-    mCurrentIOSurface = nullptr;
-    nativeLayer->NotifySurfaceReady();
-    mCurrentNativeLayer = nullptr;
-#else
-    MOZ_CRASH("Unexpected native layer on this platform");
-#endif
-  } else {
-    TryToEndRemoteDrawing();
+  switch (mCurrentFrameDest) {
+    case FrameDestination::NO_CURRENT_FRAME:
+      MOZ_CRASH("EndFrame being called without BeginFrameForXYZ?");
+      break;
+    case FrameDestination::WINDOW:
+      TryToEndRemoteDrawing();
+      break;
+    case FrameDestination::TARGET:
+    case FrameDestination::NATIVE_LAYERS:
+      mRenderTarget = nullptr;
+      break;
   }
+  mCurrentFrameDest = FrameDestination::NO_CURRENT_FRAME;
+  mShouldInvalidateWindow = false;
 }
 
 void BasicCompositor::TryToEndRemoteDrawing() {
   if (mIsDestroyed || !mRenderTarget) {
     return;
   }
 
   // If it is not a good time to call EndRemoteDrawing, defer it.
@@ -1103,17 +1207,17 @@ void BasicCompositor::NormalDrawingDone(
     // either case, we need a new render target.
     IntRect windowRect(IntPoint(0, 0),
                        mWidget->GetClientSize().ToUnknownSize());
     RefPtr<gfx::DrawTarget> drawTarget =
         mRenderTarget->mDrawTarget->CreateSimilarDrawTarget(
             windowRect.Size(), mRenderTarget->mDrawTarget->GetFormat());
 
     mFullWindowRenderTarget =
-        new BasicCompositingRenderTarget(drawTarget, windowRect);
+        new BasicCompositingRenderTarget(drawTarget, windowRect, IntPoint());
   }
 
   RefPtr<SourceSurface> source = mRenderTarget->mDrawTarget->Snapshot();
   IntPoint srcOffset = mRenderTarget->GetOrigin();
   for (auto iter = mInvalidRegion.RectIter(); !iter.Done(); iter.Next()) {
     const IntRect& r = iter.Get();
     mFullWindowRenderTarget->mDrawTarget->CopySurface(source, r - srcOffset,
                                                       r.TopLeft());
@@ -1133,19 +1237,10 @@ bool BasicCompositor::NeedToRecreateFull
   }
   if (!mFullWindowRenderTarget) {
     return true;
   }
   IntSize windowSize = mWidget->GetClientSize().ToUnknownSize();
   return mFullWindowRenderTarget->mDrawTarget->GetSize() != windowSize;
 }
 
-bool BasicCompositor::ShouldRecordFrames() const {
-#ifdef MOZ_GECKO_PROFILER
-  if (profiler_feature_active(ProfilerFeature::Screenshots)) {
-    return true;
-  }
-#endif
-  return mRecordFrames;
-}
-
 }  // namespace layers
 }  // namespace mozilla
--- a/gfx/layers/basic/BasicCompositor.h
+++ b/gfx/layers/basic/BasicCompositor.h
@@ -18,34 +18,41 @@ class MacIOSurface;
 #endif
 
 namespace mozilla {
 namespace layers {
 
 class BasicCompositingRenderTarget : public CompositingRenderTarget {
  public:
   BasicCompositingRenderTarget(gfx::DrawTarget* aDrawTarget,
-                               const gfx::IntRect& aRect)
+                               const gfx::IntRect& aRect,
+                               const gfx::IntPoint& aClipSpaceOrigin)
       : CompositingRenderTarget(aRect.TopLeft()),
         mDrawTarget(aDrawTarget),
-        mSize(aRect.Size()) {}
+        mSize(aRect.Size()),
+        mClipSpaceOrigin(aClipSpaceOrigin) {}
 
   const char* Name() const override { return "BasicCompositingRenderTarget"; }
 
   gfx::IntSize GetSize() const override { return mSize; }
 
+  // The point that DrawGeometry's aClipRect is relative to. Will be (0, 0) for
+  // root render targets and equal to GetOrigin() for non-root render targets.
+  gfx::IntPoint GetClipSpaceOrigin() const { return mClipSpaceOrigin; }
+
   void BindRenderTarget();
 
   gfx::SurfaceFormat GetFormat() const override {
     return mDrawTarget ? mDrawTarget->GetFormat()
                        : gfx::SurfaceFormat(gfx::SurfaceFormat::UNKNOWN);
   }
 
   RefPtr<gfx::DrawTarget> mDrawTarget;
   gfx::IntSize mSize;
+  gfx::IntPoint mClipSpaceOrigin;
 };
 
 class BasicCompositor : public Compositor {
  public:
   BasicCompositor(CompositorBridgeParent* aParent,
                   widget::CompositorWidget* aWidget);
 
  protected:
@@ -62,17 +69,17 @@ class BasicCompositor : public Composito
 
   already_AddRefed<CompositingRenderTarget> CreateRenderTarget(
       const gfx::IntRect& aRect, SurfaceInitMode aInit) override;
 
   already_AddRefed<CompositingRenderTarget> CreateRenderTargetFromSource(
       const gfx::IntRect& aRect, const CompositingRenderTarget* aSource,
       const gfx::IntPoint& aSourcePoint) override;
 
-  virtual already_AddRefed<CompositingRenderTarget> CreateRenderTargetAndClear(
+  virtual already_AddRefed<CompositingRenderTarget> CreateRootRenderTarget(
       gfx::DrawTarget* aDrawTarget, const gfx::IntRect& aDrawTargetRect,
       const gfx::IntRegion& aClearRegion);
 
   already_AddRefed<DataTextureSource> CreateDataTextureSource(
       TextureFlags aFlags = TextureFlags::NO_FLAGS) override;
 
   already_AddRefed<DataTextureSource> CreateDataTextureSourceAround(
       gfx::DataSourceSurface* aSurface) override;
@@ -111,21 +118,34 @@ class BasicCompositor : public Composito
 
   void DrawQuad(const gfx::Rect& aRect, const gfx::IntRect& aClipRect,
                 const EffectChain& aEffectChain, gfx::Float aOpacity,
                 const gfx::Matrix4x4& aTransform,
                 const gfx::Rect& aVisibleRect) override;
 
   void ClearRect(const gfx::Rect& aRect) override;
 
-  Maybe<gfx::IntRect> BeginFrame(const nsIntRegion& aInvalidRegion,
-                                 const Maybe<gfx::IntRect>& aClipRect,
-                                 const gfx::IntRect& aRenderBounds,
-                                 const nsIntRegion& aOpaqueRegion,
-                                 NativeLayer* aNativeLayer) override;
+  Maybe<gfx::IntRect> BeginFrameForWindow(
+      const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+      const gfx::IntRect& aRenderBounds,
+      const nsIntRegion& aOpaqueRegion) override;
+
+  Maybe<gfx::IntRect> BeginFrameForTarget(
+      const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+      const gfx::IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion,
+      gfx::DrawTarget* aTarget, const gfx::IntRect& aTargetBounds) override;
+
+  void BeginFrameForNativeLayers() override;
+
+  Maybe<gfx::IntRect> BeginRenderingToNativeLayer(
+      const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+      const nsIntRegion& aOpaqueRegion, NativeLayer* aNativeLayer) override;
+
+  void EndRenderingToNativeLayer() override;
+
   void NormalDrawingDone() override;
   void EndFrame() override;
 
   bool SupportsPartialTextureUpdate() override { return true; }
   bool CanUseCanvasLayerForSize(const gfx::IntSize& aSize) override {
     return true;
   }
   int32_t GetMaxTextureSize() const override;
@@ -142,20 +162,16 @@ class BasicCompositor : public Composito
   LayersBackend GetBackendType() const override {
     return LayersBackend::LAYERS_BASIC;
   }
 
   bool IsPendingComposite() override { return mIsPendingEndRemoteDrawing; }
 
   void FinishPendingComposite() override;
 
-  virtual void RequestAllowFrameRecording(bool aWillRecord) override {
-    mRecordFrames = aWillRecord;
-  }
-
  private:
   template <typename Geometry>
   void DrawGeometry(const Geometry& aGeometry, const gfx::Rect& aRect,
                     const gfx::IntRect& aClipRect,
                     const EffectChain& aEffectChain, gfx::Float aOpacity,
                     const gfx::Matrix4x4& aTransform,
                     const gfx::Rect& aVisibleRect, const bool aEnableAA);
 
@@ -165,60 +181,62 @@ class BasicCompositor : public Composito
                    const gfx::Matrix4x4& aTransform,
                    const gfx::Rect& aVisibleRect) override;
 
   void TryToEndRemoteDrawing();
   void EndRemoteDrawing();
 
   bool NeedsToDeferEndRemoteDrawing();
 
-  /**
-   * Whether or not the compositor should be recording frames.
-   *
-   * When this returns true, the BasicCompositor will keep the
-   * |mFullWindowRenderTarget| as an up-to-date copy of the entire rendered
-   * window. This copy is maintained in NormalDrawingDone().
-   *
-   * This will be true when either we are recording a profile with screenshots
-   * enabled or the |LayerManagerComposite| has requested us to record frames
-   * for the |CompositionRecorder|.
-   */
-  bool ShouldRecordFrames() const;
-
   bool NeedToRecreateFullWindowRenderTarget() const;
 
   // When rendering to a back buffer, this is the front buffer that the contents
-  // of the back buffer need to be copied to. Only non-null between BeginFrame
-  // and EndRemoteDrawing, and only when using a back buffer.
+  // of the back buffer need to be copied to. Only non-null between
+  // BeginFrameForWindow and EndRemoteDrawing, and only when using a back
+  // buffer.
   RefPtr<gfx::DrawTarget> mFrontBuffer;
 
   // The current render target for drawing
   RefPtr<BasicCompositingRenderTarget> mRenderTarget;
 
   // The native layer that we're currently rendering to, if any.
-  // Non-null only between BeginFrame and EndFrame if BeginFrame has been called
-  // with a non-null aNativeLayer and mTarget is null.
+  // Non-null only between BeginFrameForWindow and EndFrame if
+  // BeginFrameForWindow has been called with a non-null aNativeLayer.
   RefPtr<NativeLayer> mCurrentNativeLayer;
 
 #ifdef XP_MACOSX
   // The MacIOSurface that we're currently rendering to, if any.
   // Non-null in the same cases as mCurrentNativeLayer.
   RefPtr<MacIOSurface> mCurrentIOSurface;
 #endif
 
   gfx::IntRegion mInvalidRegion;
 
   uint32_t mMaxTextureSize;
   bool mIsPendingEndRemoteDrawing;
-  bool mRecordFrames;
+  bool mShouldInvalidateWindow = false;
+
+  // Where the current frame is being rendered to.
+  enum class FrameDestination : uint8_t {
+    NO_CURRENT_FRAME,  // before BeginFrameForXYZ or after EndFrame
+    WINDOW,            // between BeginFrameForWindow and EndFrame
+    TARGET,            // between BeginFrameForTarget and EndFrame
+    NATIVE_LAYERS      // between BeginFrameForNativeLayers and EndFrame
+  };
+  FrameDestination mCurrentFrameDest = FrameDestination::NO_CURRENT_FRAME;
 
   // mDrawTarget will not be the full window on all platforms. We therefore need
   // to keep a full window render target around when we are capturing
   // screenshots on those platforms.
   RefPtr<BasicCompositingRenderTarget> mFullWindowRenderTarget;
+
+  // The 1x1 dummy render target that's the "current" render target between
+  // BeginFrameForNativeLayers and EndFrame but outside pairs of
+  // Begin/EndRenderingToNativeLayer. Created on demand.
+  RefPtr<CompositingRenderTarget> mNativeLayersReferenceRT;
 };
 
 BasicCompositor* AssertBasicCompositor(Compositor* aCompositor);
 
 }  // namespace layers
 }  // namespace mozilla
 
 #endif /* MOZILLA_GFX_BASICCOMPOSITOR_H */
--- a/gfx/layers/composite/LayerManagerComposite.cpp
+++ b/gfx/layers/composite/LayerManagerComposite.cpp
@@ -225,17 +225,16 @@ void LayerManagerComposite::BeginTransac
 #endif
 
   if (mDestroyed) {
     NS_WARNING("Call on destroyed layer manager");
     return;
   }
 
   mIsCompositorReady = true;
-  mCompositor->SetTargetContext(aTarget, aRect);
   mTarget = aTarget;
   mTargetBounds = aRect;
 }
 
 template <typename Units>
 static IntRectTyped<Units> TransformRect(const IntRectTyped<Units>& aRect,
                                          const Matrix& aTransform,
                                          bool aRoundIn = false) {
@@ -536,17 +535,16 @@ void LayerManagerComposite::EndTransacti
   SetCompositionTime(aTimeStamp);
 
   if (mRoot && !(aFlags & END_NO_IMMEDIATE_REDRAW)) {
     MOZ_ASSERT(!aTimeStamp.IsNull());
     UpdateAndRender();
     mCompositor->FlushPendingNotifyNotUsed();
   }
 
-  mCompositor->ClearTargetContext();
   mTarget = nullptr;
 
 #ifdef MOZ_LAYERS_HAVE_LOG
   Log();
   MOZ_LAYERS_LOG(("]----- EndTransaction"));
 #endif
 }
 
@@ -972,29 +970,35 @@ bool LayerManagerComposite::Render(const
   LayerMetricsWrapper wrapper = GetRootContentLayer();
   if (wrapper) {
     mCompositor->SetClearColor(wrapper.Metadata().GetBackgroundColor());
   } else {
     mCompositor->SetClearColorToDefault();
   }
 #endif
 
-  if (mNativeLayerForEntireWindow) {
-    mNativeLayerForEntireWindow->SetRect(mRenderBounds);
-#ifdef XP_MACOSX
-    mNativeLayerForEntireWindow->SetOpaqueRegion(
-        mCompositor->GetWidget()->GetOpaqueWidgetRegion().ToUnknownRegion());
-#endif
+  Maybe<IntRect> rootLayerClip = mRoot->GetClipRect().map(
+      [](const ParentLayerIntRect& r) { return r.ToUnknownRect(); });
+  Maybe<IntRect> maybeBounds;
+  bool usingNativeLayers = false;
+  if (mTarget) {
+    maybeBounds = mCompositor->BeginFrameForTarget(
+        aInvalidRegion, rootLayerClip, mRenderBounds, aOpaqueRegion, mTarget,
+        mTargetBounds);
+  } else if (mNativeLayerRoot) {
+    if (aInvalidRegion.Intersects(mRenderBounds)) {
+      mCompositor->BeginFrameForNativeLayers();
+      maybeBounds = Some(mRenderBounds);
+      usingNativeLayers = true;
+    }
+  } else {
+    maybeBounds = mCompositor->BeginFrameForWindow(
+        aInvalidRegion, rootLayerClip, mRenderBounds, aOpaqueRegion);
   }
 
-  Maybe<IntRect> rootLayerClip = mRoot->GetClipRect().map(
-      [](const ParentLayerIntRect& r) { return r.ToUnknownRect(); });
-  Maybe<IntRect> maybeBounds =
-      mCompositor->BeginFrame(aInvalidRegion, rootLayerClip, mRenderBounds,
-                              aOpaqueRegion, mNativeLayerForEntireWindow);
   if (!maybeBounds) {
     mProfilerScreenshotGrabber.NotifyEmptyFrame();
     mCompositor->GetWidget()->PostRender(&widgetContext);
 
     // Discard the current payloads. These payloads did not require a composite
     // (they caused no changes to anything visible), so we don't want to measure
     // their latency.
     mPayload.Clear();
@@ -1005,86 +1009,129 @@ bool LayerManagerComposite::Render(const
   IntRect bounds = *maybeBounds;
   IntRect clipRect = rootLayerClip.valueOr(bounds);
 #if defined(MOZ_WIDGET_ANDROID)
   ScreenCoord offset = GetContentShiftForToolbar();
   ScopedCompositorRenderOffset scopedOffset(mCompositor->AsCompositorOGL(),
                                             ScreenPoint(0.0f, offset));
 #endif
 
-  RefPtr<CompositingRenderTarget> previousTarget;
-  if (haveLayerEffects) {
-    previousTarget = PushGroupForLayerEffects();
-  } else {
-    mTwoPassTmpTarget = nullptr;
-  }
-
-  // Render our layers.
+  // Prepare our layers.
   {
     Diagnostics::Record record(mRenderStartTime);
     RootLayer()->Prepare(RenderTargetIntRect::FromUnknownRect(clipRect));
     if (record.Recording()) {
       mDiagnostics->RecordPrepareTime(record.Duration());
     }
   }
-  // Execute draw commands.
+
+  auto RenderOnce = [&](const IntRect& aClipRect) {
+    RefPtr<CompositingRenderTarget> previousTarget;
+    if (haveLayerEffects) {
+      previousTarget = PushGroupForLayerEffects();
+    } else {
+      mTwoPassTmpTarget = nullptr;
+    }
+
+    // Execute draw commands.
+    RootLayer()->RenderLayer(aClipRect, Nothing());
+
+    if (mTwoPassTmpTarget) {
+      MOZ_ASSERT(haveLayerEffects);
+      PopGroupForLayerEffects(previousTarget, aClipRect, grayscaleVal,
+                              invertVal, contrastVal);
+    }
+    if (!mRegionToClear.IsEmpty()) {
+      for (auto iter = mRegionToClear.RectIter(); !iter.Done(); iter.Next()) {
+        mCompositor->ClearRect(Rect(iter.Get()));
+      }
+    }
+    mCompositor->NormalDrawingDone();
+  };
+
   {
     Diagnostics::Record record;
-    RootLayer()->RenderLayer(clipRect, Nothing());
+
+    if (usingNativeLayers) {
+      mNativeLayerForEntireWindow->SetRect(mRenderBounds);
+#ifdef XP_MACOSX
+      IntRegion opaqueRegion =
+          mCompositor->GetWidget()->GetOpaqueWidgetRegion().ToUnknownRegion();
+      opaqueRegion.AndWith(mRenderBounds);
+      mNativeLayerForEntireWindow->SetOpaqueRegion(
+          opaqueRegion.MovedBy(-mRenderBounds.TopLeft()));
+#endif
+
+      do {
+        Maybe<IntRect> maybeLayerRect =
+            mCompositor->BeginRenderingToNativeLayer(
+                aInvalidRegion, rootLayerClip, aOpaqueRegion,
+                mNativeLayerForEntireWindow);
+        if (!maybeLayerRect) {
+          continue;
+        }
+
+        if (rootLayerClip) {
+          RenderOnce(rootLayerClip->Intersect(*maybeLayerRect));
+        } else {
+          RenderOnce(*maybeLayerRect);
+        }
+        mCompositor->EndRenderingToNativeLayer();
+      } while (0);
+    } else {
+      RenderOnce(clipRect);
+    }
+
     if (record.Recording()) {
       mDiagnostics->RecordCompositeTime(record.Duration());
     }
   }
+
   RootLayer()->Cleanup();
 
-  if (!mRegionToClear.IsEmpty()) {
-    for (auto iter = mRegionToClear.RectIter(); !iter.Done(); iter.Next()) {
-      mCompositor->ClearRect(Rect(iter.Get()));
-    }
-  }
-
-  if (mTwoPassTmpTarget) {
-    MOZ_ASSERT(haveLayerEffects);
-    PopGroupForLayerEffects(previousTarget, clipRect, grayscaleVal, invertVal,
-                            contrastVal);
-  }
-
-  // Allow widget to render a custom foreground.
-  mCompositor->GetWidget()->DrawWindowOverlay(
-      &widgetContext, LayoutDeviceIntRect::FromUnknownRect(bounds));
-
-  mCompositor->NormalDrawingDone();
-
   mProfilerScreenshotGrabber.MaybeGrabScreenshot(mCompositor);
 
   if (mCompositionRecorder) {
     bool hasContentPaint = std::any_of(
         mPayload.begin(), mPayload.end(), [](CompositionPayload& payload) {
           return payload.mType == CompositionPayloadType::eContentPaint;
         });
 
     if (hasContentPaint) {
       if (RefPtr<RecordedFrame> frame =
               mCompositor->RecordFrame(TimeStamp::Now())) {
         mCompositionRecorder->RecordFrame(frame);
       }
     }
   }
 
+  if (!usingNativeLayers) {
+    // Allow widget to render a custom foreground.
+    mCompositor->GetWidget()->DrawWindowOverlay(
+        &widgetContext, LayoutDeviceIntRect::FromUnknownRect(bounds));
+
 #if defined(MOZ_WIDGET_ANDROID)
-  // Depending on the content shift the toolbar may be rendered on top of
-  // some of the content so it must be rendered after the content.
-  if (jni::IsFennec()) {
-    RenderToolbar();
-  }
-  HandlePixelsTarget();
+    // Depending on the content shift the toolbar may be rendered on top of
+    // some of the content so it must be rendered after the content.
+    if (jni::IsFennec()) {
+      RenderToolbar();
+    }
+    HandlePixelsTarget();
 #endif  // defined(MOZ_WIDGET_ANDROID)
 
-  // Debugging
-  RenderDebugOverlay(bounds);
+    // Debugging
+    // FIXME: We should render the debug overlay when using native layers, too.
+    // But we can't split the debug overlay rendering into multiple tiles
+    // because of a cyclic dependency: We want to display stats about the
+    // rendering of the entire window, but at the time when we render into the
+    // native layers, we do not know all the information about this frame yet.
+    // So we need to render the debug layer into an additional native layer on
+    // top, probably.
+    RenderDebugOverlay(bounds);
+  }
 
   {
     AUTO_PROFILER_LABEL("LayerManagerComposite::Render:EndFrame", GRAPHICS);
 
     mCompositor->EndFrame();
   }
 
   mCompositor->GetWidget()->PostRender(&widgetContext);
@@ -1220,18 +1267,18 @@ void LayerManagerComposite::RenderToPres
 
   mRoot->ComputeEffectiveTransforms(matrix);
   nsIntRegion opaque;
   PostProcessLayers(opaque);
 
   nsIntRegion invalid;
   IntRect bounds = IntRect::Truncate(0, 0, scale * pageWidth, actualHeight);
   MOZ_ASSERT(mRoot->GetOpacity() == 1);
-  Unused << mCompositor->BeginFrame(invalid, Nothing(), bounds, nsIntRegion(),
-                                    nullptr);
+  Unused << mCompositor->BeginFrameForWindow(invalid, Nothing(), bounds,
+                                             nsIntRegion());
 
   // The Java side of Fennec sets a scissor rect that accounts for
   // chrome such as the URL bar. Override that so that the entire frame buffer
   // is cleared.
   ScopedScissorRect scissorRect(egl, 0, 0, actualWidth, actualHeight);
   egl->fClearColor(0.0, 0.0, 0.0, 0.0);
   egl->fClear(LOCAL_GL_COLOR_BUFFER_BIT);
 
@@ -1245,36 +1292,36 @@ void LayerManagerComposite::RenderToPres
 
 ScreenCoord LayerManagerComposite::GetContentShiftForToolbar() {
   ScreenCoord result(0.0f);
   // If we're not in Fennec, we don't have a dynamic toolbar so there isn't a
   // content offset.
   if (!jni::IsFennec()) {
     return result;
   }
-  // If GetTargetContext return is not null we are not drawing to the screen so
+  // If mTarget not null we are not drawing to the screen so
   // there will not be any content offset.
-  if (mCompositor->GetTargetContext() != nullptr) {
+  if (mTarget) {
     return result;
   }
 
   if (CompositorBridgeParent* bridge =
           mCompositor->GetCompositorBridgeParent()) {
     AndroidDynamicToolbarAnimator* animator =
         bridge->GetAndroidDynamicToolbarAnimator();
     MOZ_RELEASE_ASSERT(animator);
     result.value = (float)animator->GetCurrentContentOffset().value;
   }
   return result;
 }
 
 void LayerManagerComposite::RenderToolbar() {
-  // If GetTargetContext return is not null we are not drawing to the screen so
+  // If mTarget is not null we are not drawing to the screen so
   // don't draw the toolbar.
-  if (mCompositor->GetTargetContext() != nullptr) {
+  if (mTarget) {
     return;
   }
 
   if (CompositorBridgeParent* bridge =
           mCompositor->GetCompositorBridgeParent()) {
     AndroidDynamicToolbarAnimator* animator =
         bridge->GetAndroidDynamicToolbarAnimator();
     MOZ_RELEASE_ASSERT(animator);
@@ -1401,22 +1448,17 @@ LayerManagerComposite::AutoAddMaskEffect
 LayerManagerComposite::AutoAddMaskEffect::~AutoAddMaskEffect() {
   if (!mCompositable) {
     return;
   }
 
   mCompositable->RemoveMaskEffect();
 }
 
-bool LayerManagerComposite::IsCompositingToScreen() const {
-  if (!mCompositor) {
-    return true;
-  }
-  return !mCompositor->GetTargetContext();
-}
+bool LayerManagerComposite::IsCompositingToScreen() const { return !mTarget; }
 
 LayerComposite::LayerComposite(LayerManagerComposite* aManager)
     : HostLayer(aManager),
       mCompositeManager(aManager),
       mCompositor(aManager->GetCompositor()),
       mDestroyed(false),
       mLayerComposited(false) {}
 
--- a/gfx/layers/composite/TextureHost.h
+++ b/gfx/layers/composite/TextureHost.h
@@ -954,16 +954,17 @@ class CompositingRenderTarget : public T
 #endif
 
   /**
    * Perform a clear when recycling a non opaque surface.
    * The clear is deferred to when the render target is bound.
    */
   void ClearOnBind() { mClearOnBind = true; }
 
+  void SetOrigin(const gfx::IntPoint& aOrigin) { mOrigin = aOrigin; }
   const gfx::IntPoint& GetOrigin() const { return mOrigin; }
   gfx::IntRect GetRect() { return gfx::IntRect(GetOrigin(), GetSize()); }
 
   /**
    * If a Projection matrix is set, then it is used for rendering to
    * this render target instead of generating one.  If no explicit
    * projection is set, Compositors are expected to generate an
    * orthogonal maaping that maps 0..1 to the full size of the render
--- a/gfx/layers/d3d11/CompositorD3D11.cpp
+++ b/gfx/layers/d3d11/CompositorD3D11.cpp
@@ -1089,23 +1089,57 @@ void CompositorD3D11::DrawGeometry(const
   Draw(aGeometry, pTexCoordRect);
 
   if (restoreBlendMode) {
     mContext->OMSetBlendState(mAttachments->mPremulBlendState, sBlendFactor,
                               0xFFFFFFFF);
   }
 }
 
+Maybe<IntRect> CompositorD3D11::BeginFrameForWindow(
+    const nsIntRegion& aInvalidRegion, const Maybe<IntRect>& aClipRect,
+    const IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion) {
+  MOZ_RELEASE_ASSERT(!mTarget, "mTarget not cleared properly");
+  return BeginFrame(aInvalidRegion, aClipRect, aRenderBounds, aOpaqueRegion);
+}
+
+Maybe<IntRect> CompositorD3D11::BeginFrameForTarget(
+    const nsIntRegion& aInvalidRegion, const Maybe<IntRect>& aClipRect,
+    const IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion,
+    DrawTarget* aTarget, const IntRect& aTargetBounds) {
+  MOZ_RELEASE_ASSERT(!mTarget, "mTarget not cleared properly");
+  mTarget = aTarget;  // Will be cleared in EndFrame().
+  mTargetBounds = aTargetBounds;
+  Maybe<IntRect> result =
+      BeginFrame(aInvalidRegion, aClipRect, aRenderBounds, aOpaqueRegion);
+  if (!result) {
+    // Composition has been aborted. Reset mTarget.
+    mTarget = nullptr;
+  }
+  return result;
+}
+
+void CompositorD3D11::BeginFrameForNativeLayers() {
+  MOZ_CRASH("Native layers are not implemented on Windows.");
+}
+
+Maybe<gfx::IntRect> CompositorD3D11::BeginRenderingToNativeLayer(
+    const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+    const nsIntRegion& aOpaqueRegion, NativeLayer* aNativeLayer) {
+  MOZ_CRASH("Native layers are not implemented on Windows.");
+}
+
+void CompositorD3D11::EndRenderingToNativeLayer() {
+  MOZ_CRASH("Native layers are not implemented on Windows.");
+}
+
 Maybe<IntRect> CompositorD3D11::BeginFrame(const nsIntRegion& aInvalidRegion,
                                            const Maybe<IntRect>& aClipRect,
                                            const IntRect& aRenderBounds,
-                                           const nsIntRegion& aOpaqueRegion,
-                                           NativeLayer* aNativeLayer) {
-  MOZ_RELEASE_ASSERT(!aNativeLayer, "Unexpected native layer on this platform");
-
+                                           const nsIntRegion& aOpaqueRegion) {
   // Don't composite if we are minimised. Other than for the sake of efficency,
   // this is important because resizing our buffers when mimised will fail and
   // cause a crash when we're restored.
   NS_ASSERTION(mHwnd, "Couldn't find an HWND when initialising?");
   if (mWidget->IsHidden()) {
     // We are not going to render, and not going to call EndFrame so we have to
     // read-unlock our textures to prevent them from accumulating.
     ReadUnlockTextures();
@@ -1207,30 +1241,33 @@ void CompositorD3D11::NormalDrawingDone(
 
 void CompositorD3D11::EndFrame() {
   if (!profiler_feature_active(ProfilerFeature::Screenshots) && mWindowRTCopy) {
     mWindowRTCopy = nullptr;
   }
 
   if (!mDefaultRT) {
     Compositor::EndFrame();
+    mTarget = nullptr;
     return;
   }
 
   if (XRE_IsParentProcess() && mDevice->GetDeviceRemovedReason() != S_OK) {
     gfxCriticalNote << "GFX: D3D11 skip EndFrame with device-removed.";
     Compositor::EndFrame();
+    mTarget = nullptr;
     mCurrentRT = nullptr;
     return;
   }
 
   LayoutDeviceIntSize oldSize = mSize;
   EnsureSize();
   if (mSize.width <= 0 || mSize.height <= 0) {
     Compositor::EndFrame();
+    mTarget = nullptr;
     return;
   }
 
   RefPtr<ID3D11Query> query;
   CD3D11_QUERY_DESC desc(D3D11_QUERY_EVENT);
   mDevice->CreateQuery(&desc, getter_AddRefs(query));
   if (query) {
     mContext->End(query);
@@ -1249,17 +1286,17 @@ void CompositorD3D11::EndFrame() {
   if (mQuery) {
     BOOL result;
     WaitForFrameGPUQuery(mDevice, mContext, mQuery, &result);
   }
   // Store the query for this frame so we can flush it next time.
   mQuery = query;
 
   Compositor::EndFrame();
-
+  mTarget = nullptr;
   mCurrentRT = nullptr;
 }
 
 void CompositorD3D11::GetFrameStats(GPUStats* aStats) {
   mDiagnostics->Query(aStats);
 }
 
 void CompositorD3D11::Present() {
--- a/gfx/layers/d3d11/CompositorD3D11.h
+++ b/gfx/layers/d3d11/CompositorD3D11.h
@@ -87,21 +87,33 @@ class CompositorD3D11 : public Composito
   void DrawQuad(const gfx::Rect& aRect, const gfx::IntRect& aClipRect,
                 const EffectChain& aEffectChain, gfx::Float aOpacity,
                 const gfx::Matrix4x4& aTransform,
                 const gfx::Rect& aVisibleRect) override;
 
   /**
    * Start a new frame.
    */
-  Maybe<gfx::IntRect> BeginFrame(const nsIntRegion& aInvalidRegion,
-                                 const Maybe<gfx::IntRect>& aClipRect,
-                                 const gfx::IntRect& aRenderBounds,
-                                 const nsIntRegion& aOpaqueRegion,
-                                 NativeLayer* aNativeLayer) override;
+  Maybe<gfx::IntRect> BeginFrameForWindow(
+      const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+      const gfx::IntRect& aRenderBounds,
+      const nsIntRegion& aOpaqueRegion) override;
+
+  Maybe<gfx::IntRect> BeginFrameForTarget(
+      const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+      const gfx::IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion,
+      gfx::DrawTarget* aTarget, const gfx::IntRect& aTargetBounds) override;
+
+  void BeginFrameForNativeLayers() override;
+
+  Maybe<gfx::IntRect> BeginRenderingToNativeLayer(
+      const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+      const nsIntRegion& aOpaqueRegion, NativeLayer* aNativeLayer) override;
+
+  void EndRenderingToNativeLayer() override;
 
   void NormalDrawingDone() override;
 
   /**
    * Flush the current frame to the screen.
    */
   void EndFrame() override;
 
@@ -158,16 +170,20 @@ class CompositorD3D11 : public Composito
   void EnsureSize();
   bool VerifyBufferSize();
   bool UpdateRenderTarget();
   bool UpdateConstantBuffers();
   void SetSamplerForSamplingFilter(gfx::SamplingFilter aSamplingFilter);
 
   ID3D11PixelShader* GetPSForEffect(Effect* aEffect, const bool aUseBlendShader,
                                     const MaskType aMaskType);
+  Maybe<gfx::IntRect> BeginFrame(const nsIntRegion& aInvalidRegion,
+                                 const Maybe<gfx::IntRect>& aClipRect,
+                                 const gfx::IntRect& aRenderBounds,
+                                 const nsIntRegion& aOpaqueRegion);
   void PaintToTarget();
   RefPtr<ID3D11Texture2D> CreateTexture(const gfx::IntRect& aRect,
                                         const CompositingRenderTarget* aSource,
                                         const gfx::IntPoint& aSourcePoint);
   bool CopyBackdrop(const gfx::IntRect& aRect,
                     RefPtr<ID3D11Texture2D>* aOutTexture,
                     RefPtr<ID3D11ShaderResourceView>* aOutView);
 
@@ -219,16 +235,22 @@ class CompositorD3D11 : public Composito
    * nullptr.
    *
    * This will be true when either we are recording a profile with screenshots
    * enabled or the |LayerManagerComposite| has requested us to record frames
    * for the |CompositionRecorder|.
    */
   bool ShouldAllowFrameRecording() const;
 
+  // The DrawTarget from BeginFrameForTarget, which EndFrame needs to copy the
+  // window contents into.
+  // Only non-null between BeginFrameForTarget and EndFrame.
+  RefPtr<gfx::DrawTarget> mTarget;
+  gfx::IntRect mTargetBounds;
+
   RefPtr<ID3D11DeviceContext> mContext;
   RefPtr<ID3D11Device> mDevice;
   RefPtr<IDXGISwapChain> mSwapChain;
   RefPtr<CompositingRenderTargetD3D11> mDefaultRT;
   RefPtr<CompositingRenderTargetD3D11> mCurrentRT;
   mutable RefPtr<CompositingRenderTargetD3D11> mWindowRTCopy;
 
   RefPtr<ID3D11Query> mQuery;
--- a/gfx/layers/opengl/CompositingRenderTargetOGL.h
+++ b/gfx/layers/opengl/CompositingRenderTargetOGL.h
@@ -63,39 +63,41 @@ class CompositingRenderTargetOGL : publi
     gfx::IntSize mSize;     // Logical size, the expected by callers.
     gfx::IntSize mPhySize;  // Physical size, the real size of the surface.
     GLenum mFBOTextureTarget;
     SurfaceInitMode mInit;
   };
 
  public:
   CompositingRenderTargetOGL(CompositorOGL* aCompositor,
-                             const gfx::IntPoint& aOrigin, GLuint aTexure,
-                             GLuint aFBO)
+                             const gfx::IntPoint& aOrigin,
+                             const gfx::IntPoint& aClipSpaceOrigin,
+                             GLuint aTexure, GLuint aFBO)
       : CompositingRenderTarget(aOrigin),
         mInitParams(),
         mCompositor(aCompositor),
         mGL(aCompositor->gl()),
+        mClipSpaceOrigin(aClipSpaceOrigin),
         mTextureHandle(aTexure),
         mFBO(aFBO) {
     MOZ_ASSERT(mGL);
   }
 
   ~CompositingRenderTargetOGL();
 
   const char* Name() const override { return "CompositingRenderTargetOGL"; }
 
   /**
    * Create a render target around the default FBO, for rendering straight to
    * the window.
    */
   static already_AddRefed<CompositingRenderTargetOGL> RenderTargetForWindow(
       CompositorOGL* aCompositor, const gfx::IntSize& aSize) {
-    RefPtr<CompositingRenderTargetOGL> result =
-        new CompositingRenderTargetOGL(aCompositor, gfx::IntPoint(), 0, 0);
+    RefPtr<CompositingRenderTargetOGL> result = new CompositingRenderTargetOGL(
+        aCompositor, gfx::IntPoint(), gfx::IntPoint(), 0, 0);
     result->mInitParams = InitParams(aSize, aSize, 0, INIT_MODE_NONE);
     result->mInitParams.mStatus = InitParams::INITIALIZED;
     return result.forget();
   }
 
   /**
    * Some initialisation work on the backing FBO and texture.
    * We do this lazily so that when we first set this render target on the
@@ -131,22 +133,28 @@ class CompositingRenderTargetOGL : publi
     // XXX - Bug 900770
     MOZ_ASSERT(
         false,
         "CompositingRenderTargetOGL should not be used as a TextureSource");
     return nullptr;
   }
   gfx::IntSize GetSize() const override { return mInitParams.mSize; }
 
+  // The point that DrawGeometry's aClipRect is relative to. Will be (0, 0) for
+  // root render targets and equal to GetOrigin() for non-root render targets.
+  gfx::IntPoint GetClipSpaceOrigin() const { return mClipSpaceOrigin; }
+
   gfx::SurfaceFormat GetFormat() const override {
     // XXX - Should it be implemented ? is the above assert true ?
     MOZ_ASSERT(false, "Not implemented");
     return gfx::SurfaceFormat::UNKNOWN;
   }
 
+  // In render target coordinates, i.e. the same space as GetOrigin().
+  // NOT relative to mClipSpaceOrigin!
   void SetClipRect(const Maybe<gfx::IntRect>& aRect) { mClipRect = aRect; }
   const Maybe<gfx::IntRect>& GetClipRect() const { return mClipRect; }
 
 #ifdef MOZ_DUMP_PAINTING
   already_AddRefed<gfx::DataSourceSurface> Dump(
       Compositor* aCompositor) override;
 #endif
 
@@ -163,16 +171,17 @@ class CompositingRenderTargetOGL : publi
   /**
    * There is temporary a cycle between the compositor and the render target,
    * each having a strong ref to the other. The compositor's reference to
    * the target is always cleared at the end of a frame.
    */
   RefPtr<CompositorOGL> mCompositor;
   RefPtr<GLContext> mGL;
   Maybe<gfx::IntRect> mClipRect;
+  gfx::IntPoint mClipSpaceOrigin;
   GLuint mTextureHandle;
   GLuint mFBO;
 };
 
 }  // namespace layers
 }  // namespace mozilla
 
 #endif /* MOZILLA_GFX_SURFACEOGL_H */
--- a/gfx/layers/opengl/CompositorOGL.cpp
+++ b/gfx/layers/opengl/CompositorOGL.cpp
@@ -299,25 +299,29 @@ void CompositorOGL::CleanupResources() {
   if (!ctx->MakeCurrent()) {
     // Leak resources!
     mQuadVBO = 0;
     mTriangleVBO = 0;
     mPreviousFrameDoneSync = nullptr;
     mThisFrameDoneSync = nullptr;
     mGLContext = nullptr;
     mPrograms.clear();
+    mNativeLayersReferenceRT = nullptr;
+    mFullWindowRenderTarget = nullptr;
     return;
   }
 
   for (std::map<ShaderConfigOGL, ShaderProgramOGL*>::iterator iter =
            mPrograms.begin();
        iter != mPrograms.end(); iter++) {
     delete iter->second;
   }
   mPrograms.clear();
+  mNativeLayersReferenceRT = nullptr;
+  mFullWindowRenderTarget = nullptr;
 
 #ifdef MOZ_WIDGET_GTK
   // TextureSources might hold RefPtr<gl::GLContext>.
   // All of them needs to be released to destroy GLContext.
   // GLContextGLX has to be destroyed before related gtk window is destroyed.
   for (auto textureSource : mRegisteredTextureSources) {
     textureSource->DeallocateDeviceData();
   }
@@ -633,18 +637,18 @@ already_AddRefed<CompositingRenderTarget
     return nullptr;
   }
 
   GLuint tex = 0;
   GLuint fbo = 0;
   IntRect rect = aRect;
   IntSize FBOSize;
   CreateFBOWithTexture(rect, false, 0, &fbo, &tex, &FBOSize);
-  RefPtr<CompositingRenderTargetOGL> surface =
-      new CompositingRenderTargetOGL(this, aRect.TopLeft(), tex, fbo);
+  RefPtr<CompositingRenderTargetOGL> surface = new CompositingRenderTargetOGL(
+      this, aRect.TopLeft(), aRect.TopLeft(), tex, fbo);
   surface->Initialize(aRect.Size(), FBOSize, mFBOTextureTarget, aInit);
   return surface.forget();
 }
 
 already_AddRefed<CompositingRenderTarget>
 CompositorOGL::CreateRenderTargetFromSource(
     const IntRect& aRect, const CompositingRenderTarget* aSource,
     const IntPoint& aSourcePoint) {
@@ -662,18 +666,18 @@ CompositorOGL::CreateRenderTargetFromSou
 
   GLuint tex = 0;
   GLuint fbo = 0;
   const CompositingRenderTargetOGL* sourceSurface =
       static_cast<const CompositingRenderTargetOGL*>(aSource);
   IntRect sourceRect(aSourcePoint, aRect.Size());
   CreateFBOWithTexture(sourceRect, true, sourceSurface->GetFBO(), &fbo, &tex);
 
-  RefPtr<CompositingRenderTargetOGL> surface =
-      new CompositingRenderTargetOGL(this, aRect.TopLeft(), tex, fbo);
+  RefPtr<CompositingRenderTargetOGL> surface = new CompositingRenderTargetOGL(
+      this, aRect.TopLeft(), aRect.TopLeft(), tex, fbo);
   surface->Initialize(aRect.Size(), sourceRect.Size(), mFBOTextureTarget,
                       INIT_MODE_NONE);
   return surface.forget();
 }
 
 void CompositorOGL::SetRenderTarget(CompositingRenderTarget* aSurface) {
   MOZ_ASSERT(aSurface);
   CompositingRenderTargetOGL* surface =
@@ -729,17 +733,18 @@ bool CompositorOGL::BlitRenderTarget(Com
   if (!mGLContext->IsSupported(GLFeature::framebuffer_blit)) {
     return false;
   }
   CompositingRenderTargetOGL* source =
       static_cast<CompositingRenderTargetOGL*>(aSource);
   GLuint srcFBO = source->GetFBO();
   GLuint destFBO = mCurrentRenderTarget->GetFBO();
   mGLContext->BlitHelper()->BlitFramebufferToFramebuffer(
-      srcFBO, destFBO, aSourceSize, aDestSize, LOCAL_GL_LINEAR);
+      srcFBO, destFBO, IntRect(IntPoint(), aSourceSize),
+      IntRect(IntPoint(), aDestSize), LOCAL_GL_LINEAR);
   return true;
 }
 
 static GLenum GetFrameBufferInternalFormat(
     GLContext* gl, GLuint aFrameBuffer,
     mozilla::widget::CompositorWidget* aWidget) {
   if (aFrameBuffer == 0) {  // default framebuffer
     return aWidget->GetGLFrameBufferFormat();
@@ -798,18 +803,18 @@ void CompositorOGL::RegisterIOSurface(IO
   GLuint fbo = mGLContext->CreateFramebuffer();
   {
     const ScopedBindFramebuffer bindFB(mGLContext, fbo);
     mGLContext->fFramebufferTexture2D(LOCAL_GL_FRAMEBUFFER,
                                       LOCAL_GL_COLOR_ATTACHMENT0,
                                       LOCAL_GL_TEXTURE_RECTANGLE_ARB, tex, 0);
   }
 
-  RefPtr<CompositingRenderTargetOGL> rt =
-      new CompositingRenderTargetOGL(this, gfx::IntPoint(), tex, fbo);
+  RefPtr<CompositingRenderTargetOGL> rt = new CompositingRenderTargetOGL(
+      this, gfx::IntPoint(), gfx::IntPoint(), tex, fbo);
   rt->Initialize(size, size, LOCAL_GL_TEXTURE_RECTANGLE_ARB, INIT_MODE_NONE);
 
   mRegisteredIOSurfaceRenderTargets.insert({aSurface, rt});
 }
 
 void CompositorOGL::UnregisterIOSurface(IOSurfacePtr aSurface) {
   size_t removeCount = mRegisteredIOSurfaceRenderTargets.erase(aSurface);
   MOZ_RELEASE_ASSERT(removeCount == 1,
@@ -851,33 +856,212 @@ CompositorOGL::RenderTargetForNativeLaye
   nativeLayer->InvalidateRegionThroughoutSwapchain(invalidRelativeToLayer);
   invalidRelativeToLayer = nativeLayer->CurrentSurfaceInvalidRegion();
   aInvalidRegion = invalidRelativeToLayer.MovedBy(layerRect.TopLeft());
 
   auto match = mRegisteredIOSurfaceRenderTargets.find((IOSurfacePtr)surf.get());
   MOZ_RELEASE_ASSERT(match != mRegisteredIOSurfaceRenderTargets.end(),
                      "IOSurface has not been registered with this Compositor");
   RefPtr<CompositingRenderTargetOGL> rt = match->second;
+  rt->SetOrigin(layerRect.TopLeft());
 
   // Clip the render target to the invalid rect. This conserves memory bandwidth
   // and power.
   IntRect invalidRect = aInvalidRegion.GetBounds();
   rt->SetClipRect(invalidRect == layerRect ? Nothing() : Some(invalidRect));
 
   return rt.forget();
 #else
   MOZ_CRASH("Unexpected native layer on this platform");
 #endif
 }
 
+Maybe<IntRect> CompositorOGL::BeginFrameForWindow(
+    const nsIntRegion& aInvalidRegion, const Maybe<IntRect>& aClipRect,
+    const IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion) {
+  MOZ_RELEASE_ASSERT(!mTarget, "mTarget not cleared properly");
+  return BeginFrame(aInvalidRegion, aClipRect, aRenderBounds, aOpaqueRegion);
+}
+
+Maybe<IntRect> CompositorOGL::BeginFrameForTarget(
+    const nsIntRegion& aInvalidRegion, const Maybe<IntRect>& aClipRect,
+    const IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion,
+    DrawTarget* aTarget, const IntRect& aTargetBounds) {
+  MOZ_RELEASE_ASSERT(!mTarget, "mTarget not cleared properly");
+  mTarget = aTarget;  // Will be cleared in EndFrame().
+  mTargetBounds = aTargetBounds;
+  Maybe<IntRect> result =
+      BeginFrame(aInvalidRegion, aClipRect, aRenderBounds, aOpaqueRegion);
+  if (!result) {
+    // Composition has been aborted. Reset mTarget.
+    mTarget = nullptr;
+  }
+  return result;
+}
+
+void CompositorOGL::BeginFrameForNativeLayers() {
+  MakeCurrent();
+  mPixelsPerFrame = 0;
+  mPixelsFilled = 0;
+
+  // Default blend function implements "OVER"
+  mGLContext->fBlendFuncSeparate(LOCAL_GL_ONE, LOCAL_GL_ONE_MINUS_SRC_ALPHA,
+                                 LOCAL_GL_ONE, LOCAL_GL_ONE_MINUS_SRC_ALPHA);
+  mGLContext->fEnable(LOCAL_GL_BLEND);
+
+  mFrameInProgress = true;
+  mShouldInvalidateWindow = NeedToRecreateFullWindowRenderTarget();
+
+  // Make a 1x1 dummy render target so that GetCurrentRenderTarget() returns
+  // something non-null even outside of calls to
+  // Begin/EndRenderingToNativeLayer.
+  if (!mNativeLayersReferenceRT) {
+    mNativeLayersReferenceRT =
+        CreateRenderTarget(IntRect(0, 0, 1, 1), INIT_MODE_CLEAR);
+  }
+  SetRenderTarget(mNativeLayersReferenceRT);
+  mWindowRenderTarget = mFullWindowRenderTarget;
+}
+
+Maybe<gfx::IntRect> CompositorOGL::BeginRenderingToNativeLayer(
+    const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+    const nsIntRegion& aOpaqueRegion, NativeLayer* aNativeLayer) {
+  MOZ_RELEASE_ASSERT(aNativeLayer);
+  MOZ_RELEASE_ASSERT(mCurrentRenderTarget == mNativeLayersReferenceRT,
+                     "Please restore the current render target to the one that "
+                     "was in place after the call to BeginFrameForNativeLayers "
+                     "before calling BeginRenderingToNativeLayer.");
+
+  IntRect rect = aNativeLayer->GetRect();
+  IntRegion layerInvalid;
+  if (mShouldInvalidateWindow) {
+    layerInvalid = rect;
+  } else {
+    layerInvalid.And(aInvalidRegion, rect);
+  }
+
+  RefPtr<CompositingRenderTarget> rt =
+      RenderTargetForNativeLayer(aNativeLayer, layerInvalid);
+  if (!rt) {
+    return Nothing();
+  }
+  SetRenderTarget(rt);
+  mCurrentNativeLayer = aNativeLayer;
+  mPixelsPerFrame += rect.Area();
+
+  mGLContext->fClearColor(mClearColor.r, mClearColor.g, mClearColor.b,
+                          mClearColor.a);
+  if (const Maybe<IntRect>& rtClip = mCurrentRenderTarget->GetClipRect()) {
+    // We need to apply a scissor rect during the clear. And since clears with
+    // scissor rects are usually treated differently by the GPU than regular
+    // clears, let's try to clear as little as possible in order to conserve
+    // memory bandwidth.
+    IntRegion clearRegion;
+    clearRegion.Sub(*rtClip, aOpaqueRegion);
+    if (!clearRegion.IsEmpty()) {
+      IntRect clearRect =
+          clearRegion.GetBounds() - mCurrentRenderTarget->GetOrigin();
+      ScopedGLState scopedScissorTestState(mGLContext, LOCAL_GL_SCISSOR_TEST,
+                                           true);
+      ScopedScissorRect autoScissorRect(mGLContext, clearRect.x,
+                                        FlipY(clearRect.YMost()),
+                                        clearRect.Width(), clearRect.Height());
+      mGLContext->fClear(LOCAL_GL_COLOR_BUFFER_BIT | LOCAL_GL_DEPTH_BUFFER_BIT);
+    }
+  } else {
+    mGLContext->fClear(LOCAL_GL_COLOR_BUFFER_BIT | LOCAL_GL_DEPTH_BUFFER_BIT);
+  }
+
+  return Some(rect);
+}
+
+void CompositorOGL::NormalDrawingDone() {
+  // Now is a good time to update mFullWindowRenderTarget.
+  if (!mCurrentNativeLayer) {
+    return;
+  }
+
+  if (!mGLContext->IsSupported(GLFeature::framebuffer_blit)) {
+    return;
+  }
+
+  if (!ShouldRecordFrames()) {
+    // If we are no longer recording a profile, we can drop the render target if
+    // it exists.
+    mWindowRenderTarget = nullptr;
+    mFullWindowRenderTarget = nullptr;
+    return;
+  }
+
+  if (NeedToRecreateFullWindowRenderTarget()) {
+    // We have either (1) just started recording and not yet allocated a
+    // buffer or (2) are already recording and have resized the window. In
+    // either case, we need a new render target.
+    IntRect windowRect(IntPoint(0, 0),
+                       mWidget->GetClientSize().ToUnknownSize());
+    RefPtr<CompositingRenderTarget> rt =
+        CreateRenderTarget(windowRect, INIT_MODE_NONE);
+    mFullWindowRenderTarget =
+        static_cast<CompositingRenderTargetOGL*>(rt.get());
+    mWindowRenderTarget = mFullWindowRenderTarget;
+
+    // Initialize the render target by binding it.
+    RefPtr<CompositingRenderTarget> previousTarget = mCurrentRenderTarget;
+    SetRenderTarget(mFullWindowRenderTarget);
+    SetRenderTarget(previousTarget);
+  }
+
+  // Copy the appropriate rectangle from the layer to mFullWindowRenderTarget.
+  RefPtr<CompositingRenderTargetOGL> layerRT = mCurrentRenderTarget;
+  IntRect copyRect = layerRT->GetClipRect().valueOr(layerRT->GetRect());
+  IntRect sourceRect = copyRect - layerRT->GetOrigin();
+  sourceRect.y = layerRT->GetSize().height - sourceRect.YMost();
+  IntRect destRect = copyRect;
+  destRect.y = mFullWindowRenderTarget->GetSize().height - destRect.YMost();
+  GLuint sourceFBO = layerRT->GetFBO();
+  GLuint destFBO = mFullWindowRenderTarget->GetFBO();
+  mGLContext->BlitHelper()->BlitFramebufferToFramebuffer(
+      sourceFBO, destFBO, sourceRect, destRect, LOCAL_GL_NEAREST);
+}
+
+void CompositorOGL::EndRenderingToNativeLayer() {
+  MOZ_RELEASE_ASSERT(mCurrentNativeLayer,
+                     "EndRenderingToNativeLayer not paired with a call to "
+                     "BeginRenderingToNativeLayer?");
+
+  if (StaticPrefs::nglayout_debug_widget_update_flashing()) {
+    float r = float(rand()) / RAND_MAX;
+    float g = float(rand()) / RAND_MAX;
+    float b = float(rand()) / RAND_MAX;
+    EffectChain effectChain;
+    effectChain.mPrimaryEffect = new EffectSolidColor(Color(r, g, b, 0.2f));
+    // If we're clipping the render target to the invalid rect, then the
+    // current render target is still clipped, so just fill the bounds.
+    IntRect rect = mCurrentRenderTarget->GetRect();
+    DrawQuad(Rect(rect), rect - rect.TopLeft(), effectChain, 1.0, Matrix4x4(),
+             Rect(rect));
+  }
+
+  mCurrentRenderTarget->SetClipRect(Nothing());
+  SetRenderTarget(mNativeLayersReferenceRT);
+
+#ifdef XP_MACOSX
+  NativeLayerCA* nativeLayer = mCurrentNativeLayer->AsNativeLayerCA();
+  MOZ_RELEASE_ASSERT(nativeLayer, "Unexpected native layer type");
+  nativeLayer->NotifySurfaceReady();
+  mCurrentNativeLayer = nullptr;
+#else
+  MOZ_CRASH("Unexpected native layer on this platform");
+#endif
+}
+
 Maybe<IntRect> CompositorOGL::BeginFrame(const nsIntRegion& aInvalidRegion,
                                          const Maybe<IntRect>& aClipRect,
                                          const IntRect& aRenderBounds,
-                                         const nsIntRegion& aOpaqueRegion,
-                                         NativeLayer* aNativeLayer) {
+                                         const nsIntRegion& aOpaqueRegion) {
   AUTO_PROFILER_LABEL("CompositorOGL::BeginFrame", GRAPHICS);
 
   MOZ_ASSERT(!mFrameInProgress,
              "frame still in progress (should have called EndFrame");
 
   IntRect rect;
   if (mUseExternalSurfaceSize) {
     rect = IntRect(IntPoint(), mSurfaceSize);
@@ -910,34 +1094,25 @@ Maybe<IntRect> CompositorOGL::BeginFrame
 #endif
 
   // Default blend function implements "OVER"
   mGLContext->fBlendFuncSeparate(LOCAL_GL_ONE, LOCAL_GL_ONE_MINUS_SRC_ALPHA,
                                  LOCAL_GL_ONE, LOCAL_GL_ONE_MINUS_SRC_ALPHA);
   mGLContext->fEnable(LOCAL_GL_BLEND);
 
   RefPtr<CompositingRenderTarget> rt;
-  if (mTarget) {
-    if (mCanRenderToDefaultFramebuffer) {
-      rt = CompositingRenderTargetOGL::RenderTargetForWindow(this, rect.Size());
-    } else {
-      rt = CreateRenderTarget(rect, INIT_MODE_CLEAR);
-    }
-  } else if (aNativeLayer) {
-    IntRegion layerInvalid;
-    layerInvalid.And(aInvalidRegion, rect);
-    rt = RenderTargetForNativeLayer(aNativeLayer, layerInvalid);
-    mCurrentNativeLayer = aNativeLayer;
+  if (mCanRenderToDefaultFramebuffer) {
+    rt = CompositingRenderTargetOGL::RenderTargetForWindow(this, rect.Size());
+  } else if (mTarget) {
+    rt = CreateRenderTarget(rect, INIT_MODE_CLEAR);
   } else {
-    MOZ_RELEASE_ASSERT(mCanRenderToDefaultFramebuffer);
-    rt = CompositingRenderTargetOGL::RenderTargetForWindow(this, rect.Size());
+    MOZ_CRASH("Unexpected call");
   }
 
   if (!rt) {
-    mCurrentNativeLayer = nullptr;
     return Nothing();
   }
 
   // We're about to actually draw a frame.
   mFrameInProgress = true;
 
   SetRenderTarget(rt);
   mWindowRenderTarget = mCurrentRenderTarget;
@@ -952,36 +1127,17 @@ Maybe<IntRect> CompositorOGL::BeginFrame
   } else {
     mGLContext->fClearColor(mClearColor.r, mClearColor.g, mClearColor.b,
                             mClearColor.a);
   }
 #else
   mGLContext->fClearColor(mClearColor.r, mClearColor.g, mClearColor.b,
                           mClearColor.a);
 #endif  // defined(MOZ_WIDGET_ANDROID)
-
-  if (const Maybe<IntRect>& rtClip = mCurrentRenderTarget->GetClipRect()) {
-    // We need to apply a scissor rect during the clear. And since clears with
-    // scissor rects are usually treated differently by the GPU than regular
-    // clears, let's try to clear as little as possible in order to conserve
-    // memory bandwidth.
-    IntRegion clearRegion;
-    clearRegion.Sub(*rtClip, aOpaqueRegion);
-    if (!clearRegion.IsEmpty()) {
-      IntRect clearRect = clearRegion.GetBounds();
-      ScopedGLState scopedScissorTestState(mGLContext, LOCAL_GL_SCISSOR_TEST,
-                                           true);
-      ScopedScissorRect autoScissorRect(mGLContext, clearRect.x,
-                                        FlipY(clearRect.YMost()),
-                                        clearRect.Width(), clearRect.Height());
-      mGLContext->fClear(LOCAL_GL_COLOR_BUFFER_BIT | LOCAL_GL_DEPTH_BUFFER_BIT);
-    }
-  } else {
-    mGLContext->fClear(LOCAL_GL_COLOR_BUFFER_BIT | LOCAL_GL_DEPTH_BUFFER_BIT);
-  }
+  mGLContext->fClear(LOCAL_GL_COLOR_BUFFER_BIT | LOCAL_GL_DEPTH_BUFFER_BIT);
 
   return Some(rect);
 }
 
 void CompositorOGL::CreateFBOWithTexture(const gfx::IntRect& aRect,
                                          bool aCopyFromSource,
                                          GLuint aSourceFrameBuffer,
                                          GLuint* aFBO, GLuint* aTexture,
@@ -1279,42 +1435,35 @@ void CompositorOGL::DrawGeometry(const G
                                  gfx::Float aOpacity,
                                  const gfx::Matrix4x4& aTransform,
                                  const gfx::Rect& aVisibleRect) {
   MOZ_ASSERT(mFrameInProgress, "frame not started");
   MOZ_ASSERT(mCurrentRenderTarget, "No destination");
 
   MakeCurrent();
 
-  IntPoint offset = mCurrentRenderTarget->GetOrigin();
-  IntSize size = mCurrentRenderTarget->GetSize();
+  // Convert aClipRect into render target space, and intersect it with the
+  // render target's clip.
+  IntRect clipRect = aClipRect + mCurrentRenderTarget->GetClipSpaceOrigin();
+  if (Maybe<IntRect> rtClip = mCurrentRenderTarget->GetClipRect()) {
+    clipRect = clipRect.Intersect(*rtClip);
+  }
 
-  Rect renderBound(0, 0, size.width, size.height);
-  renderBound.IntersectRect(renderBound, Rect(aClipRect));
-  renderBound.MoveBy(offset);
-
-  Rect destRect = aTransform.TransformAndClipBounds(aRect, renderBound);
+  Rect destRect = aTransform.TransformAndClipBounds(
+      aRect, Rect(mCurrentRenderTarget->GetRect().Intersect(clipRect)));
+  if (destRect.IsEmpty()) {
+    return;
+  }
 
   // XXX: This doesn't handle 3D transforms. It also doesn't handled rotated
   //      quads. Fix me.
   mPixelsFilled += destRect.Area();
 
-  // Do a simple culling if this rect is out of target buffer.
-  // Inflate a small size to avoid some numerical imprecision issue.
-  destRect.Inflate(1, 1);
-  destRect.MoveBy(-offset);
-  renderBound = Rect(0, 0, size.width, size.height);
-  if (!renderBound.Intersects(destRect)) {
-    return;
-  }
-
   LayerScope::DrawBegin();
 
-  IntRect clipRect = aClipRect;
-
   EffectMask* effectMask;
   Rect maskBounds;
   if (aEffectChain.mSecondaryEffects[EffectTypes::MASK]) {
     effectMask = static_cast<EffectMask*>(
         aEffectChain.mSecondaryEffects[EffectTypes::MASK].get());
 
     // We're assuming that the gl backend won't cheat and use NPOT
     // textures when glContext says it can't (which seems to happen
@@ -1322,24 +1471,24 @@ void CompositorOGL::DrawGeometry(const G
     IntSize maskSize = CalculatePOTSize(effectMask->mSize, mGLContext);
 
     const gfx::Matrix4x4& maskTransform = effectMask->mMaskTransform;
     NS_ASSERTION(maskTransform.Is2D(),
                  "How did we end up with a 3D transform here?!");
     maskBounds = Rect(Point(), Size(maskSize));
     maskBounds = maskTransform.As2D().TransformBounds(maskBounds);
 
-    clipRect = clipRect.Intersect(RoundedOut(maskBounds) - offset);
+    clipRect = clipRect.Intersect(RoundedOut(maskBounds));
   }
 
-  if (Maybe<IntRect> rtClip = mCurrentRenderTarget->GetClipRect()) {
-    clipRect = clipRect.Intersect(*rtClip);
-  }
+  // Move clipRect into device space.
+  IntPoint offset = mCurrentRenderTarget->GetOrigin();
+  clipRect -= offset;
 
-  // aClipRect is in destination coordinate space (after all
+  // clipRect is in destination coordinate space (after all
   // transforms and offsets have been applied) so if our
   // drawing is going to be shifted by mRenderOffset then we need
   // to shift the clip rect by the same amount.
   if (!mTarget && mCurrentRenderTarget->IsWindow()) {
     clipRect.MoveBy(mRenderOffset.x + mSurfaceOrigin.x,
                     mRenderOffset.y - mSurfaceOrigin.y);
   }
 
@@ -1430,17 +1579,17 @@ void CompositorOGL::DrawGeometry(const G
     if (gl()->IsExtensionSupported(GLContext::NV_texture_barrier)) {
       // The NV_texture_barrier extension lets us read directly from the
       // backbuffer. Let's do that.
       // We need to tell OpenGL about this, so that it can make sure everything
       // on the GPU is happening in the right order.
       gl()->fTextureBarrier();
       mixBlendBackdrop = mCurrentRenderTarget->GetTextureHandle();
     } else {
-      gfx::IntRect rect = ComputeBackdropCopyRect(aRect, aClipRect, aTransform,
+      gfx::IntRect rect = ComputeBackdropCopyRect(aRect, clipRect, aTransform,
                                                   &backdropTransform);
       mixBlendBackdrop =
           CreateTexture(rect, true, mCurrentRenderTarget->GetFBO());
       createdMixBlendBackdropTexture = true;
     }
     program->SetBackdropTransform(backdropTransform);
   }
 
@@ -1865,19 +2014,16 @@ void CompositorOGL::InitializeVAO(const 
                                    LOCAL_GL_FALSE, aStride,
                                    reinterpret_cast<GLvoid*>(aOffset));
   mGLContext->fEnableVertexAttribArray(aAttrib);
 }
 
 void CompositorOGL::EndFrame() {
   AUTO_PROFILER_LABEL("CompositorOGL::EndFrame", GRAPHICS);
 
-  MOZ_ASSERT(mCurrentRenderTarget == mWindowRenderTarget,
-             "Rendering target not properly restored");
-
 #ifdef MOZ_DUMP_PAINTING
   if (gfxEnv::DumpCompositorTextures()) {
     LayoutDeviceIntSize size;
     if (mUseExternalSurfaceSize) {
       size = LayoutDeviceIntSize(mSurfaceSize.width, mSurfaceSize.height);
     } else {
       size = mWidget->GetClientSize();
     }
@@ -1886,47 +2032,23 @@ void CompositorOGL::EndFrame() {
             IntSize(size.width, size.height), SurfaceFormat::B8G8R8A8);
     if (target) {
       CopyToTarget(target, nsIntPoint(), Matrix());
       WriteSnapshotToDumpFile(this, target);
     }
   }
 #endif
 
-  if (StaticPrefs::nglayout_debug_widget_update_flashing()) {
-    float r = float(rand()) / float(RAND_MAX);
-    float g = float(rand()) / float(RAND_MAX);
-    float b = float(rand()) / float(RAND_MAX);
-    EffectChain effectChain;
-    effectChain.mPrimaryEffect = new EffectSolidColor(Color(r, g, b, 0.2f));
-    // If we're clipping the render target to the invalid rect, then the
-    // current render target is still clipped, so just fill the bounds.
-    IntRect rect = mCurrentRenderTarget->GetRect();
-    DrawQuad(Rect(rect), rect - rect.TopLeft(), effectChain, 1.0, Matrix4x4(),
-             Rect(rect));
-  }
-
-  mCurrentRenderTarget->SetClipRect(Nothing());
-
   mFrameInProgress = false;
-
-  if (mCurrentNativeLayer) {
-#ifdef XP_MACOSX
-    NativeLayerCA* nativeLayer = mCurrentNativeLayer->AsNativeLayerCA();
-    MOZ_RELEASE_ASSERT(nativeLayer, "Unexpected native layer type");
-    nativeLayer->NotifySurfaceReady();
-    mCurrentNativeLayer = nullptr;
-#else
-    MOZ_CRASH("Unexpected native layer on this platform");
-#endif
-  }
+  mShouldInvalidateWindow = false;
 
   if (mTarget) {
     CopyToTarget(mTarget, mTargetBounds.TopLeft(), Matrix());
     mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, 0);
+    mTarget = nullptr;
     mWindowRenderTarget = nullptr;
     mCurrentRenderTarget = nullptr;
     Compositor::EndFrame();
     return;
   }
 
   mWindowRenderTarget = nullptr;
   mCurrentRenderTarget = nullptr;
@@ -1973,16 +2095,27 @@ void CompositorOGL::WaitForGPU() {
                                 LOCAL_GL_SYNC_FLUSH_COMMANDS_BIT,
                                 LOCAL_GL_TIMEOUT_IGNORED);
     mGLContext->fDeleteSync(mPreviousFrameDoneSync);
   }
   mPreviousFrameDoneSync = mThisFrameDoneSync;
   mThisFrameDoneSync = nullptr;
 }
 
+bool CompositorOGL::NeedToRecreateFullWindowRenderTarget() const {
+  if (!ShouldRecordFrames()) {
+    return false;
+  }
+  if (!mFullWindowRenderTarget) {
+    return true;
+  }
+  IntSize windowSize = mWidget->GetClientSize().ToUnknownSize();
+  return mFullWindowRenderTarget->GetSize() != windowSize;
+}
+
 void CompositorOGL::SetDestinationSurfaceSize(const IntSize& aSize) {
   mSurfaceSize.width = aSize.width;
   mSurfaceSize.height = aSize.height;
 }
 
 void CompositorOGL::CopyToTarget(DrawTarget* aTarget,
                                  const nsIntPoint& aTopLeft,
                                  const gfx::Matrix& aTransform) {
--- a/gfx/layers/opengl/CompositorOGL.h
+++ b/gfx/layers/opengl/CompositorOGL.h
@@ -190,16 +190,18 @@ class CompositorOGL final : public Compo
   void DrawTriangles(const nsTArray<gfx::TexturedTriangle>& aTriangles,
                      const gfx::Rect& aRect, const gfx::IntRect& aClipRect,
                      const EffectChain& aEffectChain, gfx::Float aOpacity,
                      const gfx::Matrix4x4& aTransform,
                      const gfx::Rect& aVisibleRect) override;
 
   bool SupportsLayerGeometry() const override;
 
+  void NormalDrawingDone() override;
+
   void EndFrame() override;
 
   void WaitForGPU() override;
 
   bool SupportsPartialTextureUpdate() override;
 
   bool CanUseCanvasLayerForSize(const gfx::IntSize& aSize) override {
     if (!mGLContext) return false;
@@ -296,16 +298,18 @@ class CompositorOGL final : public Compo
                     const gfx::Rect& aVisibleRect);
 
   void PrepareViewport(CompositingRenderTargetOGL* aRenderTarget);
 
   bool SupportsTextureDirectMapping();
 
   void InsertFrameDoneSync();
 
+  bool NeedToRecreateFullWindowRenderTarget() const;
+
   /** Widget associated with this compositor */
   LayoutDeviceIntSize mWidgetSize;
   RefPtr<GLContext> mGLContext;
   UniquePtr<GLBlitTextureImageHelper> mBlitTextureImageHelper;
   gfx::Matrix4x4 mProjMatrix;
   bool mCanRenderToDefaultFramebuffer = true;
 
 #ifdef XP_DARWIN
@@ -323,17 +327,30 @@ class CompositorOGL final : public Compo
   already_AddRefed<mozilla::gl::GLContext> CreateContext();
 
   /** Texture target to use for FBOs */
   GLenum mFBOTextureTarget;
 
   /** Currently bound render target */
   RefPtr<CompositingRenderTargetOGL> mCurrentRenderTarget;
 
-  CompositingRenderTargetOGL* mWindowRenderTarget;
+  // The 1x1 dummy render target that's the "current" render target between
+  // BeginFrameForNativeLayers and EndFrame but outside pairs of
+  // Begin/EndRenderingToNativeLayer. Created on demand.
+  RefPtr<CompositingRenderTarget> mNativeLayersReferenceRT;
+
+  // The render target that profiler screenshots / frame recording read from.
+  // This will be the actual window framebuffer when rendering to a window, and
+  // it will be mFullWindowRenderTarget when rendering to native layers.
+  RefPtr<CompositingRenderTargetOGL> mWindowRenderTarget;
+
+  // Non-null when using native layers and frame recording is requested.
+  // EndNormalDrawing() maintains a copy of the entire window contents in this
+  // render target, by copying from the native layer render targets.
+  RefPtr<CompositingRenderTargetOGL> mFullWindowRenderTarget;
 
   /**
    * VBO that has some basics in it for a textured quad, including vertex
    * coords and texcoords.
    */
   GLuint mQuadVBO;
 
   /**
@@ -354,28 +371,49 @@ class CompositorOGL final : public Compo
    */
   bool mUseExternalSurfaceSize;
 
   /**
    * Have we had DrawQuad calls since the last frame was rendered?
    */
   bool mFrameInProgress;
 
+  // Only true between BeginFromeForNativeLayers and EndFrame, and only if the
+  // full window render target needed to be recreated in the current frame.
+  bool mShouldInvalidateWindow = false;
+
   /*
    * Clear aRect on current render target.
    */
   void ClearRect(const gfx::Rect& aRect) override;
 
   /* Start a new frame.
    */
+  Maybe<gfx::IntRect> BeginFrameForWindow(
+      const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+      const gfx::IntRect& aRenderBounds,
+      const nsIntRegion& aOpaqueRegion) override;
+
+  Maybe<gfx::IntRect> BeginFrameForTarget(
+      const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+      const gfx::IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion,
+      gfx::DrawTarget* aTarget, const gfx::IntRect& aTargetBounds) override;
+
+  void BeginFrameForNativeLayers() override;
+
+  Maybe<gfx::IntRect> BeginRenderingToNativeLayer(
+      const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+      const nsIntRegion& aOpaqueRegion, NativeLayer* aNativeLayer) override;
+
+  void EndRenderingToNativeLayer() override;
+
   Maybe<gfx::IntRect> BeginFrame(const nsIntRegion& aInvalidRegion,
                                  const Maybe<gfx::IntRect>& aClipRect,
                                  const gfx::IntRect& aRenderBounds,
-                                 const nsIntRegion& aOpaqueRegion,
-                                 NativeLayer* aNativeLayer) override;
+                                 const nsIntRegion& aOpaqueRegion);
 
   ShaderConfigOGL GetShaderConfigFor(
       Effect* aEffect, TextureSourceOGL* aSourceMask = nullptr,
       gfx::CompositionOp aOp = gfx::CompositionOp::OP_OVER,
       bool aColorMatrix = false, bool aDEAAEnabled = false) const;
 
   ShaderProgramOGL* GetShaderProgramFor(const ShaderConfigOGL& aConfig);
 
@@ -466,16 +504,22 @@ class CompositorOGL final : public Compo
    *
    * Indeed, the only coordinate system that OpenGL knows has the y-axis
    * pointing upwards, but the layers/compositor coordinate system has the
    * y-axis pointing downwards, for good reason as Web pages are typically
    * scrolled downwards. So, some flipping has to take place; FlippedY does it.
    */
   GLint FlipY(GLint y) const { return mViewportSize.height - y; }
 
+  // The DrawTarget from BeginFrameForTarget, which EndFrame needs to copy the
+  // window contents into.
+  // Only non-null between BeginFrameForTarget and EndFrame.
+  RefPtr<gfx::DrawTarget> mTarget;
+  gfx::IntRect mTargetBounds;
+
   RefPtr<CompositorTexturePoolOGL> mTexturePool;
 
   // The native layer that we're currently rendering to, if any.
   // Non-null only between BeginFrame and EndFrame if BeginFrame has been called
   // with a non-null aNativeLayer.
   RefPtr<NativeLayer> mCurrentNativeLayer;
 
 #ifdef MOZ_WIDGET_GTK
--- a/image/decoders/nsPNGDecoder.cpp
+++ b/image/decoders/nsPNGDecoder.cpp
@@ -818,29 +818,31 @@ void nsPNGDecoder::row_callback(png_stru
     // didn't, we might overflow the deinterlacing buffer.
     MOZ_ASSERT_UNREACHABLE("libpng producing extra rows?");
     return;
   }
 
   // Note that |new_row| may be null here, indicating that this is an interlaced
   // image and |row_callback| is being called for a row that hasn't changed.
   MOZ_ASSERT_IF(!new_row, decoder->interlacebuf);
-  uint8_t* rowToWrite = new_row;
 
   if (decoder->interlacebuf) {
     uint32_t width = uint32_t(decoder->mFrameRect.Width());
 
     // We'll output the deinterlaced version of the row.
-    rowToWrite = decoder->interlacebuf + (row_num * decoder->mChannels * width);
+    uint8_t* rowToWrite =
+        decoder->interlacebuf + (row_num * decoder->mChannels * width);
 
     // Update the deinterlaced version of this row with the new data.
     png_progressive_combine_row(png_ptr, rowToWrite, new_row);
+
+    decoder->WriteRow(rowToWrite);
+  } else {
+    decoder->WriteRow(new_row);
   }
-
-  decoder->WriteRow(rowToWrite);
 }
 
 void nsPNGDecoder::WriteRow(uint8_t* aRow) {
   MOZ_ASSERT(aRow);
 
   uint8_t* rowToWrite = aRow;
   uint32_t width = uint32_t(mFrameRect.Width());
 
--- a/image/imgLoader.cpp
+++ b/image/imgLoader.cpp
@@ -2,16 +2,18 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // Undefine windows version of LoadImage because our code uses that name.
 #undef LoadImage
 
+#include <algorithm>
+
 #include "ImageLogging.h"
 #include "imgLoader.h"
 
 #include "mozilla/Attributes.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/Move.h"
 #include "mozilla/NullPrincipal.h"
 #include "mozilla/Preferences.h"
@@ -993,19 +995,16 @@ void imgCacheEntry::SetHasNoProxies(bool
 }
 
 imgCacheQueue::imgCacheQueue() : mDirty(false), mSize(0) {}
 
 void imgCacheQueue::UpdateSize(int32_t diff) { mSize += diff; }
 
 uint32_t imgCacheQueue::GetSize() const { return mSize; }
 
-#include <algorithm>
-using namespace std;
-
 void imgCacheQueue::Remove(imgCacheEntry* entry) {
   uint64_t index = mQueue.IndexOf(entry);
   if (index == queueContainer::NoIndex) {
     return;
   }
 
   mSize -= mQueue[index]->GetDataSize();
 
--- a/image/test/mochitest/test_bug415761.html
+++ b/image/test/mochitest/test_bug415761.html
@@ -23,28 +23,34 @@ const { Services } = ChromeUtils.import(
 var io = Services.io;
 chromeURI = io.newURI(chromeURI);
 var chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]
                   .getService(Ci.nsIChromeRegistry);
 var fileURI = chromeReg.convertChromeURL(chromeURI);
 fileURI.QueryInterface(Ci.nsIFileURL);
 var self = fileURI.file;
 
-// Check if the non-ascii-named icon is still hanging around from a previous test
-var dest = self.parent;
-dest.append("\u263a.ico");
-if (dest.exists()) {
-  dest.remove(false);
+// Check if the ref or test icon are still hanging around from a previous test
+var testDest = self.parent;
+var refDest = self.parent;
+testDest.append("\u263a.ico");
+refDest.append("bug415761-ref.ico");
+if (testDest.exists()) {
+  testDest.remove(false);
+}
+if (refDest.exists()) {
+  refDest.remove(false);
 }
 
-// Copy the source icon so that we have an identical icon with non-ascii characters
-// in its name
+// Copy the source icon so that we have two identical icons with, one with
+// non-ascii characters in its name.
 var src = self.parent;
 src.append("bug415761.ico");
-src.copyTo(null, dest.leafName);
+src.copyTo(null, testDest.leafName);
+src.copyTo(null, refDest.leafName);
 
 // Now load both icons in an Image() with a moz-icon URI
 var testImage = new Image();
 var refImage = new Image();
 
 var loadedImages = 0;
 testImage.onload = refImage.onload = function() {
   loadedImages++;
@@ -69,29 +75,32 @@ function finishTest() {
 
   // Render the icon with a non-ascii character in its name to a canvas
   var testCanvas = document.createElement("canvas");
   testCanvas.setAttribute("height", 32);
   testCanvas.setAttribute("width", 32);
   testCanvas.getContext('2d').drawImage(testImage, 0, 0, 32, 32);
 
   // Assert that they should be the same.
-  assertSnapshots(refCanvas, testCanvas, true, 0, "icon", "reference icon");
+  assertSnapshots(testCanvas, refCanvas, true, 0, "icon", "reference icon");
   SimpleTest.finish();
 };
 
-var testURI = io.newFileURI(dest).spec;
-var refURI = io.newFileURI(src).spec;
+var testURI = io.newFileURI(testDest).spec;
+var refURI = io.newFileURI(refDest).spec;
 testImage.src = "moz-icon:" + testURI;
 refImage.src = "moz-icon:" + refURI;
 
 SimpleTest.registerCleanupFunction(function() {
-  // Remove the copied file if it exists.
-  if (dest.exists()) {
-    dest.remove(false);
+  // Remove the copied files if they exist.
+  if (testDest.exists()) {
+    testDest.remove(false);
+  }
+  if (refDest.exists()) {
+    refDest.remove(false);
   }
 });
 
 </script>
 </pre>
 </body>
 
 </html>
--- a/js/src/builtin/Stream.cpp
+++ b/js/src/builtin/Stream.cpp
@@ -6,126 +6,39 @@
 
 #include "builtin/Stream.h"
 
 #include "js/Stream.h"
 
 #include <stdint.h>  // int32_t
 
 #include "builtin/streams/ClassSpecMacro.h"           // JS_STREAMS_CLASS_SPEC
-#include "builtin/streams/MiscellaneousOperations.h"  // js::CreateAlgorithmFromUnderlyingMethod, js::InvokeOrNoop, js::MakeSizeAlgorithmFromSizeFunction, js::PromiseCall, js::PromiseRejectedWithPendingError, js::ReturnPromiseRejectedWithPendingError, js::ValidateAndNormalizeHighWaterMark
-#include "builtin/streams/QueueWithSizes.h"  // js::{DequeueValue,EnqueueValueWithSize,ResetQueue}
+#include "builtin/streams/MiscellaneousOperations.h"  // js::CreateAlgorithmFromUnderlyingMethod, js::InvokeOrNoop, js::IsMaybeWrapped, js::PromiseCall, js::PromiseRejectedWithPendingError
+#include "builtin/streams/PullIntoDescriptor.h"       // js::PullIntoDescriptor
+#include "builtin/streams/QueueWithSizes.h"  // js::{EnqueueValueWithSize,ResetQueue}
+#include "builtin/streams/ReadableStream.h"  // js::ReadableStream, js::SetUpExternalReadableByteStreamController
+#include "builtin/streams/ReadableStreamController.h"  // js::ReadableStream{,Default}Controller, js::ReadableStreamDefaultControllerPullSteps, js::ControllerStart{,Failed}Handler
+#include "builtin/streams/ReadableStreamDefaultControllerOperations.h"  // js::ReadableStreamControllerClearAlgorithms
+#include "builtin/streams/ReadableStreamInternals.h"  // js::ReadableStream{AddReadOrReadIntoRequest,CloseInternal,CreateReadResult,ErrorInternal,FulfillReadOrReadIntoRequest,GetNumReadRequests,HasDefaultReader}
+#include "builtin/streams/ReadableStreamReader.h"  // js::ReadableStream{,Default}Reader, js::CreateReadableStreamDefaultReader, js::ReadableStreamReaderGeneric{Cancel,Initialize,Release}, js::ReadableStreamDefaultReaderRead
 #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 "builtin/streams/HandlerFunction-inl.h"  // js::NewHandler, js::TargetFromHandler
+#include "builtin/streams/ReadableStreamReader-inl.h"  // js::Unwrap{ReaderFromStream{,NoThrow},StreamFromReader}
 #include "vm/Compartment-inl.h"
 #include "vm/List-inl.h"  // js::ListObject, js::StoreNewListInFixedSlot
 #include "vm/NativeObject-inl.h"
 
 using namespace js;
 
-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) {
-  return v.isObject() && v.toObject().canUnwrapAs<T>();
-}
-
-JS::ReadableStreamMode ReadableStream::mode() const {
-  ReadableStreamController* controller = this->controller();
-  if (controller->is<ReadableStreamDefaultController>()) {
-    return JS::ReadableStreamMode::Default;
-  }
-  return controller->as<ReadableByteStreamController>().hasExternalSource()
-             ? JS::ReadableStreamMode::ExternalSource
-             : JS::ReadableStreamMode::Byte;
-}
-
-/**
- * Returns the stream associated with the given reader.
- */
-static MOZ_MUST_USE ReadableStream* UnwrapStreamFromReader(
-    JSContext* cx, Handle<ReadableStreamReader*> reader) {
-  MOZ_ASSERT(reader->hasStream());
-  return UnwrapInternalSlot<ReadableStream>(cx, reader,
-                                            ReadableStreamReader::Slot_Stream);
-}
-
-/**
- * Returns the reader associated with the given stream.
- *
- * Must only be called on ReadableStreams that already have a reader
- * associated with them.
- *
- * If the reader is a wrapper, it will be unwrapped, so the result might not be
- * an object from the currently active compartment.
- */
-static MOZ_MUST_USE ReadableStreamReader* UnwrapReaderFromStream(
-    JSContext* cx, Handle<ReadableStream*> stream) {
-  return UnwrapInternalSlot<ReadableStreamReader>(cx, stream,
-                                                  ReadableStream::Slot_Reader);
-}
-
-static MOZ_MUST_USE ReadableStreamReader* UnwrapReaderFromStreamNoThrow(
-    ReadableStream* stream) {
-  JSObject* readerObj =
-      &stream->getFixedSlot(ReadableStream::Slot_Reader).toObject();
-  if (IsProxy(readerObj)) {
-    if (JS_IsDeadWrapper(readerObj)) {
-      return nullptr;
-    }
-
-    readerObj = readerObj->maybeUnwrapAs<ReadableStreamReader>();
-    if (!readerObj) {
-      return nullptr;
-    }
-  }
-
-  return &readerObj->as<ReadableStreamReader>();
-}
-
-constexpr size_t StreamHandlerFunctionSlot_Target = 0;
-
-inline static MOZ_MUST_USE JSFunction* NewHandler(JSContext* cx, Native handler,
-                                                  HandleObject target) {
-  cx->check(target);
-
-  HandlePropertyName funName = cx->names().empty;
-  RootedFunction handlerFun(
-      cx, NewNativeFunction(cx, handler, 0, funName,
-                            gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
-  if (!handlerFun) {
-    return nullptr;
-  }
-  handlerFun->setExtendedSlot(StreamHandlerFunctionSlot_Target,
-                              ObjectValue(*target));
-  return handlerFun;
-}
-
-/**
- * Helper for handler functions that "close over" a value that is always a
- * direct reference to an object of class T, never a wrapper.
- */
-template <class T>
-inline static MOZ_MUST_USE T* TargetFromHandler(CallArgs& args) {
-  JSFunction& func = args.callee().as<JSFunction>();
-  return &func.getExtendedSlot(StreamHandlerFunctionSlot_Target)
-              .toObject()
-              .as<T>();
-}
-
 #if 0  // disable user-defined byte streams
 
 class ByteStreamChunk : public NativeObject
 {
   private:
     enum Slots {
         Slot_Buffer = 0,
         Slot_ByteOffset,
@@ -165,3119 +78,24 @@ class ByteStreamChunk : public NativeObj
 
 const JSClass ByteStreamChunk::class_ = {
     "ByteStreamChunk",
     JSCLASS_HAS_RESERVED_SLOTS(SlotCount)
 };
 
 #endif  // user-defined byte streams
 
-class PullIntoDescriptor : public NativeObject {
- private:
-  enum Slots {
-    Slot_buffer,
-    Slot_ByteOffset,
-    Slot_ByteLength,
-    Slot_BytesFilled,
-    Slot_ElementSize,
-    Slot_Ctor,
-    Slot_ReaderType,
-    SlotCount
-  };
-
- public:
-  static const JSClass class_;
-
-  ArrayBufferObject* buffer() {
-    return &getFixedSlot(Slot_buffer).toObject().as<ArrayBufferObject>();
-  }
-  void setBuffer(ArrayBufferObject* buffer) {
-    setFixedSlot(Slot_buffer, ObjectValue(*buffer));
-  }
-  JSObject* ctor() { return getFixedSlot(Slot_Ctor).toObjectOrNull(); }
-  uint32_t byteOffset() const {
-    return getFixedSlot(Slot_ByteOffset).toInt32();
-  }
-  uint32_t byteLength() const {
-    return getFixedSlot(Slot_ByteLength).toInt32();
-  }
-  uint32_t bytesFilled() const {
-    return getFixedSlot(Slot_BytesFilled).toInt32();
-  }
-  void setBytesFilled(int32_t bytes) {
-    setFixedSlot(Slot_BytesFilled, Int32Value(bytes));
-  }
-  uint32_t elementSize() const {
-    return getFixedSlot(Slot_ElementSize).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, 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(static_cast<int32_t>(readerType)));
-    return descriptor;
-  }
-};
-
-const JSClass PullIntoDescriptor::class_ = {
-    "PullIntoDescriptor", JSCLASS_HAS_RESERVED_SLOTS(SlotCount)};
-
-/**
- * TeeState objects implement the local variables in Streams spec 3.3.9
- * ReadableStreamTee, which are accessed by several algorithms.
- */
-class TeeState : public NativeObject {
- public:
-  /**
-   * Memory layout for TeeState instances.
-   *
-   * The Reason1 and Reason2 slots store opaque values, which might be
-   * wrapped objects from other compartments. Since we don't treat them as
-   * objects in Streams-specific code, we don't have to worry about that
-   * apart from ensuring that the values are properly wrapped before storing
-   * them.
-   *
-   * CancelPromise is always created in TeeState::create below, so is
-   * guaranteed to be in the same compartment as the TeeState instance
-   * itself.
-   *
-   * Stream can be from another compartment. It is automatically wrapped
-   * before storing it and unwrapped upon retrieval. That means that
-   * TeeState consumers need to be able to deal with unwrapped
-   * ReadableStream instances from non-current compartments.
-   *
-   * Branch1 and Branch2 are always created in the same compartment as the
-   * TeeState instance, so cannot be from another compartment.
-   */
-  enum Slots {
-    Slot_Flags = 0,
-    Slot_Reason1,
-    Slot_Reason2,
-    Slot_CancelPromise,
-    Slot_Stream,
-    Slot_Branch1,
-    Slot_Branch2,
-    SlotCount
-  };
-
- private:
-  enum Flags {
-    Flag_ClosedOrErrored = 1 << 0,
-    Flag_Canceled1 = 1 << 1,
-    Flag_Canceled2 = 1 << 2,
-    Flag_CloneForBranch2 = 1 << 3,
-  };
-  uint32_t flags() const { return getFixedSlot(Slot_Flags).toInt32(); }
-  void setFlags(uint32_t flags) { setFixedSlot(Slot_Flags, Int32Value(flags)); }
-
- public:
-  static const JSClass class_;
-
-  bool cloneForBranch2() const { return flags() & Flag_CloneForBranch2; }
-
-  bool closedOrErrored() const { return flags() & Flag_ClosedOrErrored; }
-  void setClosedOrErrored() {
-    MOZ_ASSERT(!(flags() & Flag_ClosedOrErrored));
-    setFlags(flags() | Flag_ClosedOrErrored);
-  }
-
-  bool canceled1() const { return flags() & Flag_Canceled1; }
-  void setCanceled1(HandleValue reason) {
-    MOZ_ASSERT(!(flags() & Flag_Canceled1));
-    setFlags(flags() | Flag_Canceled1);
-    setFixedSlot(Slot_Reason1, reason);
-  }
-
-  bool canceled2() const { return flags() & Flag_Canceled2; }
-  void setCanceled2(HandleValue reason) {
-    MOZ_ASSERT(!(flags() & Flag_Canceled2));
-    setFlags(flags() | Flag_Canceled2);
-    setFixedSlot(Slot_Reason2, reason);
-  }
-
-  Value reason1() const {
-    MOZ_ASSERT(canceled1());
-    return getFixedSlot(Slot_Reason1);
-  }
-
-  Value reason2() const {
-    MOZ_ASSERT(canceled2());
-    return getFixedSlot(Slot_Reason2);
-  }
-
-  PromiseObject* cancelPromise() {
-    return &getFixedSlot(Slot_CancelPromise).toObject().as<PromiseObject>();
-  }
-
-  ReadableStreamDefaultController* branch1() {
-    ReadableStreamDefaultController* controller =
-        &getFixedSlot(Slot_Branch1)
-             .toObject()
-             .as<ReadableStreamDefaultController>();
-    MOZ_ASSERT(controller->isTeeBranch1());
-    return controller;
-  }
-  void setBranch1(ReadableStreamDefaultController* controller) {
-    MOZ_ASSERT(controller->isTeeBranch1());
-    setFixedSlot(Slot_Branch1, ObjectValue(*controller));
-  }
-
-  ReadableStreamDefaultController* branch2() {
-    ReadableStreamDefaultController* controller =
-        &getFixedSlot(Slot_Branch2)
-             .toObject()
-             .as<ReadableStreamDefaultController>();
-    MOZ_ASSERT(controller->isTeeBranch2());
-    return controller;
-  }
-  void setBranch2(ReadableStreamDefaultController* controller) {
-    MOZ_ASSERT(controller->isTeeBranch2());
-    setFixedSlot(Slot_Branch2, ObjectValue(*controller));
-  }
-
-  static TeeState* create(JSContext* cx,
-                          Handle<ReadableStream*> unwrappedStream) {
-    Rooted<TeeState*> state(cx, NewBuiltinClassInstance<TeeState>(cx));
-    if (!state) {
-      return nullptr;
-    }
-
-    Rooted<PromiseObject*> cancelPromise(
-        cx, PromiseObject::createSkippingExecutor(cx));
-    if (!cancelPromise) {
-      return nullptr;
-    }
-
-    state->setFixedSlot(Slot_Flags, Int32Value(0));
-    state->setFixedSlot(Slot_CancelPromise, ObjectValue(*cancelPromise));
-    RootedObject wrappedStream(cx, unwrappedStream);
-    if (!cx->compartment()->wrap(cx, &wrappedStream)) {
-      return nullptr;
-    }
-    state->setFixedSlot(Slot_Stream, ObjectValue(*wrappedStream));
-
-    return state;
-  }
-};
-
-const JSClass TeeState::class_ = {"TeeState",
-                                  JSCLASS_HAS_RESERVED_SLOTS(SlotCount)};
-
-/*** 3.2. Class ReadableStream **********************************************/
-
-static MOZ_MUST_USE bool SetUpExternalReadableByteStreamController(
-    JSContext* cx, Handle<ReadableStream*> stream,
-    JS::ReadableStreamUnderlyingSource* source);
-
-ReadableStream* ReadableStream::createExternalSourceStream(
-    JSContext* cx, JS::ReadableStreamUnderlyingSource* source,
-    void* nsISupportsObject_alreadyAddreffed /* = nullptr */,
-    HandleObject proto /* = nullptr */) {
-  Rooted<ReadableStream*> stream(
-      cx, create(cx, nsISupportsObject_alreadyAddreffed, proto));
-  if (!stream) {
-    return nullptr;
-  }
-
-  if (!SetUpExternalReadableByteStreamController(cx, stream, source)) {
-    return nullptr;
-  }
-
-  return stream;
-}
-
-static MOZ_MUST_USE bool
-SetUpReadableStreamDefaultControllerFromUnderlyingSource(
-    JSContext* cx, Handle<ReadableStream*> stream, HandleValue underlyingSource,
-    double highWaterMark, HandleValue sizeAlgorithm);
-
-/**
- * Streams spec, 3.2.3. new ReadableStream(underlyingSource = {}, strategy = {})
- */
-bool ReadableStream::constructor(JSContext* cx, unsigned argc, Value* vp) {
-  CallArgs args = CallArgsFromVp(argc, vp);
-
-  if (!ThrowIfNotConstructing(cx, args, "ReadableStream")) {
-    return false;
-  }
-
-  // Implicit in the spec: argument default values.
-  RootedValue underlyingSource(cx, args.get(0));
-  if (underlyingSource.isUndefined()) {
-    JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx);
-    if (!emptyObj) {
-      return false;
-    }
-    underlyingSource = ObjectValue(*emptyObj);
-  }
-
-  RootedValue strategy(cx, args.get(1));
-  if (strategy.isUndefined()) {
-    JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx);
-    if (!emptyObj) {
-      return false;
-    }
-    strategy = ObjectValue(*emptyObj);
-  }
-
-  // Implicit in the spec: Set this to
-  //     OrdinaryCreateFromConstructor(NewTarget, ...).
-  // Step 1: Perform ! InitializeReadableStream(this).
-  RootedObject proto(cx);
-  if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_ReadableStream,
-                                          &proto)) {
-    return false;
-  }
-  Rooted<ReadableStream*> stream(cx,
-                                 ReadableStream::create(cx, nullptr, proto));
-  if (!stream) {
-    return false;
-  }
-
-  // Step 2: Let size be ? GetV(strategy, "size").
-  RootedValue size(cx);
-  if (!GetProperty(cx, strategy, cx->names().size, &size)) {
-    return false;
-  }
-
-  // Step 3: Let highWaterMark be ? GetV(strategy, "highWaterMark").
-  RootedValue highWaterMarkVal(cx);
-  if (!GetProperty(cx, strategy, cx->names().highWaterMark,
-                   &highWaterMarkVal)) {
-    return false;
-  }
-
-  // Step 4: Let type be ? GetV(underlyingSource, "type").
-  RootedValue type(cx);
-  if (!GetProperty(cx, underlyingSource, cx->names().type, &type)) {
-    return false;
-  }
-
-  // Step 5: Let typeString be ? ToString(type).
-  RootedString typeString(cx, ToString<CanGC>(cx, type));
-  if (!typeString) {
-    return false;
-  }
-
-  // Step 6: If typeString is "bytes",
-  bool equal;
-  if (!EqualStrings(cx, typeString, cx->names().bytes, &equal)) {
-    return false;
-  }
-  if (equal) {
-    // The rest of step 6 is unimplemented, since we don't support
-    // user-defined byte streams yet.
-    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                              JSMSG_READABLESTREAM_BYTES_TYPE_NOT_IMPLEMENTED);
-    return false;
-  }
-
-  // Step 7: Otherwise, if type is undefined,
-  if (type.isUndefined()) {
-    // Step 7.a: Let sizeAlgorithm be ? MakeSizeAlgorithmFromSizeFunction(size).
-    if (!MakeSizeAlgorithmFromSizeFunction(cx, size)) {
-      return false;
-    }
-
-    // Step 7.b: If highWaterMark is undefined, let highWaterMark be 1.
-    double highWaterMark;
-    if (highWaterMarkVal.isUndefined()) {
-      highWaterMark = 1;
-    } else {
-      // Step 7.c: Set highWaterMark to ?
-      // ValidateAndNormalizeHighWaterMark(highWaterMark).
-      if (!ValidateAndNormalizeHighWaterMark(cx, highWaterMarkVal,
-                                             &highWaterMark)) {
-        return false;
-      }
-    }
-
-    // Step 7.d: Perform
-    //           ? SetUpReadableStreamDefaultControllerFromUnderlyingSource(
-    //           this, underlyingSource, highWaterMark, sizeAlgorithm).
-    if (!SetUpReadableStreamDefaultControllerFromUnderlyingSource(
-            cx, stream, underlyingSource, highWaterMark, size)) {
-      return false;
-    }
-
-    args.rval().setObject(*stream);
-    return true;
-  }
-
-  // Step 8: Otherwise, throw a RangeError exception.
-  JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                            JSMSG_READABLESTREAM_UNDERLYINGSOURCE_TYPE_WRONG);
-  return false;
-}
-
-/**
- * Streams spec, 3.2.5.1. get locked
- */
-static bool ReadableStream_locked(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, "get locked"));
-  if (!unwrappedStream) {
-    return false;
-  }
-
-  // Step 2: Return ! IsReadableStreamLocked(this).
-  args.rval().setBoolean(unwrappedStream->locked());
-  return true;
-}
-
-static MOZ_MUST_USE JSObject* ReadableStreamCancel(
-    JSContext* cx, Handle<ReadableStream*> unwrappedStream, HandleValue reason);
-
-/**
- * Streams spec, 3.2.5.2. cancel ( reason )
- */
-static MOZ_MUST_USE bool ReadableStream_cancel(JSContext* cx, unsigned argc,
-                                               Value* vp) {
-  CallArgs args = CallArgsFromVp(argc, vp);
-
-  // Step 1: If ! IsReadableStream(this) is false, return a promise rejected
-  //         with a TypeError exception.
-  Rooted<ReadableStream*> unwrappedStream(
-      cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "cancel"));
-  if (!unwrappedStream) {
-    return ReturnPromiseRejectedWithPendingError(cx, args);
-  }
-
-  // Step 2: If ! IsReadableStreamLocked(this) is true, return a promise
-  //         rejected with a TypeError exception.
-  if (unwrappedStream->locked()) {
-    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                              JSMSG_READABLESTREAM_LOCKED_METHOD, "cancel");
-    return ReturnPromiseRejectedWithPendingError(cx, args);
-  }
-
-  // Step 3: Return ! ReadableStreamCancel(this, reason).
-  RootedObject cancelPromise(
-      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.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);
-    if (!emptyObj) {
-      return false;
-    }
-    optionsVal.setObject(*emptyObj);
-  }
-  RootedValue modeVal(cx);
-  if (!GetProperty(cx, optionsVal, cx->names().mode, &modeVal)) {
-    return false;
-  }
-
-  // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
-  Rooted<ReadableStream*> unwrappedStream(
-      cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "getReader"));
-  if (!unwrappedStream) {
-    return false;
-  }
-
-  // Step 2: If mode is undefined, return
-  //         ? AcquireReadableStreamDefaultReader(this).
-  RootedObject reader(cx);
-  if (modeVal.isUndefined()) {
-    reader = CreateReadableStreamDefaultReader(cx, unwrappedStream,
-                                               ForAuthorCodeBool::Yes);
-  } else {
-    // Step 3: Set mode to ? ToString(mode) (implicit).
-    RootedString mode(cx, ToString<CanGC>(cx, modeVal));
-    if (!mode) {
-      return false;
-    }
-
-    // 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,
-                              JSMSG_READABLESTREAM_INVALID_READER_MODE);
-    return false;
-  }
-
-  // Reordered second part of steps 2 and 4.
-  if (!reader) {
-    return false;
-  }
-  args.rval().setObject(*reader);
-  return true;
-}
-
-// Streams spec, 3.2.5.5.
-//      pipeThrough({ writable, readable },
-//                  { preventClose, preventAbort, preventCancel, signal })
-//
-// Not implemented.
-
-// 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.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) {
-    return false;
-  }
-
-  // Step 2: Let branches be ? ReadableStreamTee(this, false).
-  Rooted<ReadableStream*> branch1(cx);
-  Rooted<ReadableStream*> branch2(cx);
-  if (!ReadableStreamTee(cx, unwrappedStream, false, &branch1, &branch2)) {
-    return false;
-  }
-
-  // Step 3: Return ! CreateArrayFromList(branches).
-  RootedNativeObject branches(cx, NewDenseFullyAllocatedArray(cx, 2));
-  if (!branches) {
-    return false;
-  }
-  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};
-
-JS_STREAMS_CLASS_SPEC(ReadableStream, 0, SlotCount, 0,
-                      JSCLASS_PRIVATE_IS_NSISUPPORTS | JSCLASS_HAS_PRIVATE,
-                      JS_NULL_CLASS_OPS);
-
 /*** 3.3. ReadableStreamAsyncIteratorPrototype ******************************/
 
 // Not implemented.
 
-/*** 3.4. General readable stream abstract operations ***********************/
-
-// Streams spec, 3.4.1. AcquireReadableStreamBYOBReader ( stream )
-// Always inlined.
-
-// 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().
- */
-enum class SourceAlgorithms {
-  Script,
-  Tee,
-};
-
-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.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(
-    JSContext* cx, SourceAlgorithms sourceAlgorithms,
-    HandleValue underlyingSource, HandleValue pullMethod = UndefinedHandleValue,
-    HandleValue cancelMethod = UndefinedHandleValue, double highWaterMark = 1,
-    HandleValue sizeAlgorithm = UndefinedHandleValue,
-    HandleObject proto = nullptr) {
-  cx->check(underlyingSource, sizeAlgorithm, proto);
-  MOZ_ASSERT(sizeAlgorithm.isUndefined() || IsCallable(sizeAlgorithm));
-
-  // Step 1: If highWaterMark was not passed, set it to 1 (implicit).
-  // Step 2: If sizeAlgorithm was not passed, set it to an algorithm that
-  //         returns 1 (implicit).
-  // Step 3: Assert: ! IsNonNegativeNumber(highWaterMark) is true.
-  MOZ_ASSERT(highWaterMark >= 0);
-
-  // Step 4: Let stream be ObjectCreate(the original value of ReadableStream's
-  //         prototype property).
-  // Step 5: Perform ! InitializeReadableStream(stream).
-  Rooted<ReadableStream*> stream(cx,
-                                 ReadableStream::create(cx, nullptr, proto));
-  if (!stream) {
-    return nullptr;
-  }
-
-  // Step 6: Let controller be ObjectCreate(the original value of
-  //         ReadableStreamDefaultController's prototype property).
-  // Step 7: Perform ? SetUpReadableStreamDefaultController(stream,
-  //         controller, startAlgorithm, pullAlgorithm, cancelAlgorithm,
-  //         highWaterMark, sizeAlgorithm).
-  if (!SetUpReadableStreamDefaultController(
-          cx, stream, sourceAlgorithms, underlyingSource, pullMethod,
-          cancelMethod, highWaterMark, sizeAlgorithm)) {
-    return nullptr;
-  }
-
-  // Step 8: Return stream.
-  return stream;
-}
-
-// Streams spec, 3.4.4. CreateReadableByteStream (
-//                          startAlgorithm, pullAlgorithm, cancelAlgorithm
-//                          [, highWaterMark [, autoAllocateChunkSize ] ] )
-// Not implemented.
-
-/**
- * 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.
-  Rooted<ReadableStream*> stream(
-      cx, NewObjectWithClassProto<ReadableStream>(cx, proto));
-  if (!stream) {
-    return nullptr;
-  }
-
-  JS_SetPrivate(stream, nsISupportsObject_alreadyAddreffed);
-
-  // Step 1: Set stream.[[state]] to "readable".
-  stream->initStateBits(Readable);
-  MOZ_ASSERT(stream->readable());
-
-  // Step 2: Set stream.[[reader]] and stream.[[storedError]] to
-  //         undefined (implicit).
-  MOZ_ASSERT(!stream->hasReader());
-  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.4.6. IsReadableStream ( x )
-// Using UnwrapAndTypeCheck templates instead.
-
-// Streams spec, 3.4.7. IsReadableStreamDisturbed ( stream )
-// Using stream->disturbed() instead.
-
-/**
- * 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.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;
-  }
-
-  // Step iii: Let done be ? Get(result, "done").
-  RootedValue doneVal(cx);
-  if (!GetProperty(cx, result, result, cx->names().done, &doneVal)) {
-    return false;
-  }
-
-  // Step iv: Assert: Type(done) is Boolean.
-  bool done = doneVal.toBoolean();
-
-  // Step v: If done is true and closedOrErrored is false,
-  if (done && !unwrappedTeeState->closedOrErrored()) {
-    // Step v.1: If canceled1 is false,
-    if (!unwrappedTeeState->canceled1()) {
-      // Step v.1.a: Perform ! ReadableStreamDefaultControllerClose(
-      //             branch1.[[readableStreamController]]).
-      Rooted<ReadableStreamDefaultController*> unwrappedBranch1(
-          cx, unwrappedTeeState->branch1());
-      if (!ReadableStreamDefaultControllerClose(cx, unwrappedBranch1)) {
-        return false;
-      }
-    }
-
-    // Step v.2: If teeState.[[canceled2]] is false,
-    if (!unwrappedTeeState->canceled2()) {
-      // Step v.2.a: Perform ! ReadableStreamDefaultControllerClose(
-      //             branch2.[[readableStreamController]]).
-      Rooted<ReadableStreamDefaultController*> unwrappedBranch2(
-          cx, unwrappedTeeState->branch2());
-      if (!ReadableStreamDefaultControllerClose(cx, unwrappedBranch2)) {
-        return false;
-      }
-    }
-
-    // Step v.3: Set closedOrErrored to true.
-    unwrappedTeeState->setClosedOrErrored();
-  }
-
-  // Step vi: If closedOrErrored is true, return.
-  if (unwrappedTeeState->closedOrErrored()) {
-    return true;
-  }
-
-  // Step vii: Let value1 and value2 be value.
-  RootedValue value1(cx, value);
-  RootedValue value2(cx, value);
-
-  // Step viii: If canceled2 is false and cloneForBranch2 is true,
-  //            set value2 to
-  //            ? StructuredDeserialize(? StructuredSerialize(value2),
-  //                                    the current Realm Record).
-  // We don't yet support any specifications that use cloneForBranch2, and
-  // the Streams spec doesn't offer any way for author code to enable it,
-  // so it's always false here.
-  MOZ_ASSERT(!unwrappedTeeState->cloneForBranch2());
-
-  // Step ix: If canceled1 is false, perform
-  //          ? ReadableStreamDefaultControllerEnqueue(
-  //                branch1.[[readableStreamController]], value1).
-  Rooted<ReadableStreamDefaultController*> unwrappedController(cx);
-  if (!unwrappedTeeState->canceled1()) {
-    unwrappedController = unwrappedTeeState->branch1();
-    if (!ReadableStreamDefaultControllerEnqueue(cx, unwrappedController,
-                                                value1)) {
-      return false;
-    }
-  }
-
-  // Step x: If canceled2 is false, perform
-  //         ? ReadableStreamDefaultControllerEnqueue(
-  //               branch2.[[readableStreamController]], value2).
-  if (!unwrappedTeeState->canceled2()) {
-    unwrappedController = unwrappedTeeState->branch2();
-    if (!ReadableStreamDefaultControllerEnqueue(cx, unwrappedController,
-                                                value2)) {
-      return false;
-    }
-  }
-
-  args.rval().setUndefined();
-  return true;
-}
-
-static MOZ_MUST_USE JSObject* ReadableStreamDefaultReaderRead(
-    JSContext* cx, Handle<ReadableStreamDefaultReader*> unwrappedReader);
-
-/**
- * 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,
-                                             TeeState::Slot_Stream));
-  if (!unwrappedStream) {
-    return nullptr;
-  }
-  Rooted<ReadableStreamReader*> unwrappedReaderObj(
-      cx, UnwrapReaderFromStream(cx, unwrappedStream));
-  if (!unwrappedReaderObj) {
-    return nullptr;
-  }
-  Rooted<ReadableStreamDefaultReader*> unwrappedReader(
-      cx, &unwrappedReaderObj->as<ReadableStreamDefaultReader>());
-
-  // Step 12.a: Return the result of transforming
-  // ! ReadableStreamDefaultReaderRead(reader) with a fulfillment handler
-  // which takes the argument result and performs the following steps:
-  //
-  // The steps under 12.a are implemented in TeeReaderReadHandler.
-  RootedObject readPromise(
-      cx, ::ReadableStreamDefaultReaderRead(cx, unwrappedReader));
-  if (!readPromise) {
-    return nullptr;
-  }
-
-  RootedObject teeState(cx, unwrappedTeeState);
-  if (!cx->compartment()->wrap(cx, &teeState)) {
-    return nullptr;
-  }
-  RootedObject onFulfilled(cx, NewHandler(cx, TeeReaderReadHandler, teeState));
-  if (!onFulfilled) {
-    return nullptr;
-  }
-
-  return JS::CallOriginalPromiseThen(cx, readPromise, onFulfilled, nullptr);
-}
-
-/**
- * Cancel one branch of a tee'd stream with the given |reason_|.
- *
- * 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(
-      cx, UnwrapInternalSlot<ReadableStream>(cx, unwrappedTeeState,
-                                             TeeState::Slot_Stream));
-  if (!unwrappedStream) {
-    return nullptr;
-  }
-
-  bool bothBranchesCanceled = false;
-
-  // Step 13/14.a: Set canceled1/canceled2 to true.
-  // Step 13/14.b: Set reason1/reason2 to reason.
-  {
-    RootedValue unwrappedReason(cx, reason);
-    {
-      AutoRealm ar(cx, unwrappedTeeState);
-      if (!cx->compartment()->wrap(cx, &unwrappedReason)) {
-        return nullptr;
-      }
-    }
-    if (unwrappedBranch->isTeeBranch1()) {
-      unwrappedTeeState->setCanceled1(unwrappedReason);
-      bothBranchesCanceled = unwrappedTeeState->canceled2();
-    } else {
-      MOZ_ASSERT(unwrappedBranch->isTeeBranch2());
-      unwrappedTeeState->setCanceled2(unwrappedReason);
-      bothBranchesCanceled = unwrappedTeeState->canceled1();
-    }
-  }
-
-  // Step 13/14.c: If canceled2/canceled1 is true,
-  if (bothBranchesCanceled) {
-    // Step 13/14.c.i: Let compositeReason be
-    //                 ! CreateArrayFromList(« reason1, reason2 »).
-    RootedValue reason1(cx, unwrappedTeeState->reason1());
-    RootedValue reason2(cx, unwrappedTeeState->reason2());
-    if (!cx->compartment()->wrap(cx, &reason1) ||
-        !cx->compartment()->wrap(cx, &reason2)) {
-      return nullptr;
-    }
-
-    RootedNativeObject compositeReason(cx, NewDenseFullyAllocatedArray(cx, 2));
-    if (!compositeReason) {
-      return nullptr;
-    }
-    compositeReason->setDenseInitializedLength(2);
-    compositeReason->initDenseElement(0, reason1);
-    compositeReason->initDenseElement(1, reason2);
-    RootedValue compositeReasonVal(cx, ObjectValue(*compositeReason));
-
-    // Step 13/14.c.ii: Let cancelResult be
-    //                  ! ReadableStreamCancel(stream, compositeReason).
-    // In our implementation, this can fail with OOM. The best course then
-    // is to reject cancelPromise with an OOM error.
-    RootedObject cancelResult(
-        cx, ::ReadableStreamCancel(cx, unwrappedStream, compositeReasonVal));
-    {
-      Rooted<PromiseObject*> cancelPromise(cx,
-                                           unwrappedTeeState->cancelPromise());
-      AutoRealm ar(cx, cancelPromise);
-
-      if (!cancelResult) {
-        // Handle the OOM case mentioned above.
-        if (!RejectPromiseWithPendingError(cx, cancelPromise)) {
-          return nullptr;
-        }
-      } else {
-        // Step 13/14.c.iii: Resolve cancelPromise with cancelResult.
-        RootedValue resultVal(cx, ObjectValue(*cancelResult));
-        if (!cx->compartment()->wrap(cx, &resultVal)) {
-          return nullptr;
-        }
-        if (!PromiseObject::resolve(cx, cancelPromise, resultVal)) {
-          return nullptr;
-        }
-      }
-    }
-  }
-
-  // Step 13/14.d: Return cancelPromise.
-  RootedObject cancelPromise(cx, unwrappedTeeState->cancelPromise());
-  if (!cx->compartment()->wrap(cx, &cancelPromise)) {
-    return nullptr;
-  }
-  return cancelPromise;
-}
-
-static MOZ_MUST_USE bool ReadableStreamControllerError(
-    JSContext* cx, Handle<ReadableStreamController*> unwrappedController,
-    HandleValue e);
-
-/**
- * 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:
-  if (!teeState->closedOrErrored()) {
-    // Step a.iii: Set closedOrErrored to true.
-    // Reordered to ensure that internal errors in the other steps don't
-    // leave the teeState in an undefined state.
-    teeState->setClosedOrErrored();
-
-    // Step a.i: Perform
-    //           ! ReadableStreamDefaultControllerError(
-    //               branch1.[[readableStreamController]], r).
-    Rooted<ReadableStreamDefaultController*> branch1(cx, teeState->branch1());
-    if (!ReadableStreamControllerError(cx, branch1, reason)) {
-      return false;
-    }
-
-    // Step a.ii: Perform
-    //            ! ReadableStreamDefaultControllerError(
-    //                branch2.[[readableStreamController]], r).
-    Rooted<ReadableStreamDefaultController*> branch2(cx, teeState->branch2());
-    if (!ReadableStreamControllerError(cx, branch2, reason)) {
-      return false;
-    }
-  }
-
-  args.rval().setUndefined();
-  return true;
-}
-
-/**
- * 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).
-
-  // Step 3: Let reader be ? AcquireReadableStreamDefaultReader(stream).
-  Rooted<ReadableStreamDefaultReader*> reader(
-      cx, CreateReadableStreamDefaultReader(cx, unwrappedStream));
-  if (!reader) {
-    return false;
-  }
-
-  // Several algorithms close over the variables initialized in the next few
-  // steps, so we allocate them in an object, the TeeState. The algorithms
-  // also close over `stream` and `reader`, so TeeState gets a reference to
-  // the stream.
-  //
-  // Step 4: Let closedOrErrored be false.
-  // Step 5: Let canceled1 be false.
-  // Step 6: Let canceled2 be false.
-  // Step 7: Let reason1 be undefined.
-  // Step 8: Let reason2 be undefined.
-  // Step 9: Let branch1 be undefined.
-  // Step 10: Let branch2 be undefined.
-  // Step 11: Let cancelPromise be a new promise.
-  Rooted<TeeState*> teeState(cx, TeeState::create(cx, unwrappedStream));
-  if (!teeState) {
-    return false;
-  }
-
-  // Step 12: Let pullAlgorithm be the following steps: [...]
-  // Step 13: Let cancel1Algorithm be the following steps: [...]
-  // Step 14: Let cancel2Algorithm be the following steps: [...]
-  // Step 15: Let startAlgorithm be an algorithm that returns undefined.
-  //
-  // Implicit. Our implementation does not use objects to represent
-  // [[pullAlgorithm]], [[cancelAlgorithm]], and so on. Instead, we decide
-  // which one to perform based on class checks. For example, our
-  // implementation of ReadableStreamControllerCallPullIfNeeded checks
-  // whether the stream's underlyingSource is a TeeState object.
-
-  // Step 16: Set branch1 to
-  //          ! CreateReadableStream(startAlgorithm, pullAlgorithm,
-  //                                 cancel1Algorithm).
-  RootedValue underlyingSource(cx, ObjectValue(*teeState));
-  branch1Stream.set(
-      CreateReadableStream(cx, SourceAlgorithms::Tee, underlyingSource));
-  if (!branch1Stream) {
-    return false;
-  }
-
-  Rooted<ReadableStreamDefaultController*> branch1(cx);
-  branch1 = &branch1Stream->controller()->as<ReadableStreamDefaultController>();
-  branch1->setTeeBranch1();
-  teeState->setBranch1(branch1);
-
-  // Step 17: Set branch2 to
-  //          ! CreateReadableStream(startAlgorithm, pullAlgorithm,
-  //                                 cancel2Algorithm).
-  branch2Stream.set(
-      CreateReadableStream(cx, SourceAlgorithms::Tee, underlyingSource));
-  if (!branch2Stream) {
-    return false;
-  }
-
-  Rooted<ReadableStreamDefaultController*> branch2(cx);
-  branch2 = &branch2Stream->controller()->as<ReadableStreamDefaultController>();
-  branch2->setTeeBranch2();
-  teeState->setBranch2(branch2);
-
-  // Step 18: Upon rejection of reader.[[closedPromise]] with reason r, [...]
-  RootedObject closedPromise(cx, reader->closedPromise());
-
-  RootedObject onRejected(cx,
-                          NewHandler(cx, TeeReaderErroredHandler, teeState));
-  if (!onRejected) {
-    return false;
-  }
-
-  if (!JS::AddPromiseReactions(cx, closedPromise, nullptr, onRejected)) {
-    return false;
-  }
-
-  // Step 19: Return « branch1, branch2 ».
-  return true;
-}
-
-// Streams spec, 3.4.11.
-//      ReadableStreamPipeTo ( source, dest, preventClose, preventAbort,
-//                             preventCancel, signal )
-//
-// Not implemented.
-
-/*** 3.5. The interface between readable streams and controllers ************/
-
-/**
- * Streams spec, 3.5.1.
- *      ReadableStreamAddReadIntoRequest ( stream, forAuthorCode )
- * 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) {
-  // Step 1: Assert: ! IsReadableStream{BYOB,Default}Reader(stream.[[reader]])
-  //         is true.
-  // (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.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;
-  }
-
-  // Step 4: Let read{Into}Request be
-  //         Record {[[promise]]: promise, [[forAuthorCode]]: forAuthorCode}.
-  // Step 5: Append read{Into}Request as the last element of
-  //         stream.[[reader]].[[read{Into}Requests]].
-  // Since we don't need the [[forAuthorCode]] field (see the comment on
-  // `ReadableStreamReader::forAuthorCode()`), we elide the Record and store
-  // only the promise.
-  if (!AppendToListInFixedSlot(cx, unwrappedReader,
-                               ReadableStreamReader::Slot_Requests, promise)) {
-    return nullptr;
-  }
-
-  // Step 6: Return promise.
-  return promise;
-}
-
-static MOZ_MUST_USE JSObject* ReadableStreamControllerCancelSteps(
-    JSContext* cx, Handle<ReadableStreamController*> unwrappedController,
-    HandleValue reason);
-
-/**
- * Used for transforming the result of promise fulfillment/rejection.
- */
-static bool ReturnUndefined(JSContext* cx, unsigned argc, Value* vp) {
-  CallArgs args = CallArgsFromVp(argc, vp);
-  args.rval().setUndefined();
-  return true;
-}
-
-MOZ_MUST_USE bool ReadableStreamCloseInternal(
-    JSContext* cx, Handle<ReadableStream*> unwrappedStream);
-
-/**
- * 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();
-
-  // Step 2: If stream.[[state]] is "closed", return a new promise resolved
-  //         with undefined.
-  if (unwrappedStream->closed()) {
-    return PromiseObject::unforgeableResolve(cx, UndefinedHandleValue);
-  }
-
-  // Step 3: If stream.[[state]] is "errored", return a new promise rejected
-  //         with stream.[[storedError]].
-  if (unwrappedStream->errored()) {
-    RootedValue storedError(cx, unwrappedStream->storedError());
-    if (!cx->compartment()->wrap(cx, &storedError)) {
-      return nullptr;
-    }
-    return PromiseObject::unforgeableReject(cx, storedError);
-  }
-
-  // Step 4: Perform ! ReadableStreamClose(stream).
-  if (!ReadableStreamCloseInternal(cx, unwrappedStream)) {
-    return nullptr;
-  }
-
-  // Step 5: Let sourceCancelPromise be
-  //         ! stream.[[readableStreamController]].[[CancelSteps]](reason).
-  Rooted<ReadableStreamController*> unwrappedController(
-      cx, unwrappedStream->controller());
-  RootedObject sourceCancelPromise(
-      cx, ReadableStreamControllerCancelSteps(cx, unwrappedController, reason));
-  if (!sourceCancelPromise) {
-    return nullptr;
-  }
-
-  // Step 6: Return the result of transforming sourceCancelPromise with a
-  //         fulfillment handler that returns undefined.
-  HandlePropertyName funName = cx->names().empty;
-  RootedFunction returnUndefined(
-      cx, NewNativeFunction(cx, ReturnUndefined, 0, funName,
-                            gc::AllocKind::FUNCTION, GenericObject));
-  if (!returnUndefined) {
-    return nullptr;
-  }
-  return JS::CallOriginalPromiseThen(cx, sourceCancelPromise, returnUndefined,
-                                     nullptr);
-}
-
-static MOZ_MUST_USE JSObject* ReadableStreamCreateReadResult(
-    JSContext* cx, HandleValue value, bool done,
-    ForAuthorCodeBool forAuthorCode);
-
-/**
- * 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();
-
-  // Step 4: If reader is undefined, return (reordered).
-  if (!unwrappedStream->hasReader()) {
-    return true;
-  }
-
-  // Step 3: Let reader be stream.[[reader]].
-  Rooted<ReadableStreamReader*> unwrappedReader(
-      cx, UnwrapReaderFromStream(cx, unwrappedStream));
-  if (!unwrappedReader) {
-    return false;
-  }
-
-  // Step 5: If ! IsReadableStreamDefaultReader(reader) is true,
-  if (unwrappedReader->is<ReadableStreamDefaultReader>()) {
-    ForAuthorCodeBool forAuthorCode = unwrappedReader->forAuthorCode();
-
-    // Step a: Repeat for each readRequest that is an element of
-    //         reader.[[readRequests]],
-    Rooted<ListObject*> unwrappedReadRequests(cx, unwrappedReader->requests());
-    uint32_t len = unwrappedReadRequests->length();
-    RootedObject readRequest(cx);
-    RootedObject resultObj(cx);
-    RootedValue resultVal(cx);
-    for (uint32_t i = 0; i < len; i++) {
-      // Step i: Resolve readRequest.[[promise]] with
-      //         ! ReadableStreamCreateReadResult(undefined, true,
-      //                                          readRequest.[[forAuthorCode]]).
-      readRequest = &unwrappedReadRequests->getAs<JSObject>(i);
-      if (!cx->compartment()->wrap(cx, &readRequest)) {
-        return false;
-      }
-
-      resultObj = ReadableStreamCreateReadResult(cx, UndefinedHandleValue, true,
-                                                 forAuthorCode);
-      if (!resultObj) {
-        return false;
-      }
-      resultVal = ObjectValue(*resultObj);
-      if (!ResolvePromise(cx, readRequest, resultVal)) {
-        return false;
-      }
-    }
-
-    // Step b: Set reader.[[readRequests]] to an empty List.
-    unwrappedReader->clearRequests();
-  }
-
-  // Step 6: Resolve reader.[[closedPromise]] with undefined.
-  RootedObject closedPromise(cx, unwrappedReader->closedPromise());
-  if (!cx->compartment()->wrap(cx, &closedPromise)) {
-    return false;
-  }
-  if (!ResolvePromise(cx, closedPromise, UndefinedHandleValue)) {
-    return false;
-  }
-
-  if (unwrappedStream->mode() == JS::ReadableStreamMode::ExternalSource) {
-    // Make sure we're in the stream's compartment.
-    AutoRealm ar(cx, unwrappedStream);
-    JS::ReadableStreamUnderlyingSource* source =
-        unwrappedStream->controller()->externalSource();
-    source->onClosed(cx, unwrappedStream);
-  }
-
-  return true;
-}
-
-/**
- * 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,
-      forAuthorCode == ForAuthorCodeBool::Yes
-          ? cx->realm()->getOrCreateIterResultTemplateObject(cx)
-          : cx->realm()->getOrCreateIterResultWithoutPrototypeTemplateObject(
-                cx));
-  if (!templateObject) {
-    return nullptr;
-  }
-
-  // Step 3: Assert: Type(done) is Boolean (implicit).
-
-  // Step 4: Let obj be ObjectCreate(prototype).
-  NativeObject* obj;
-  JS_TRY_VAR_OR_RETURN_NULL(
-      cx, obj, NativeObject::createWithTemplate(cx, templateObject));
-
-  // Step 5: Perform CreateDataProperty(obj, "value", value).
-  obj->setSlot(Realm::IterResultObjectValueSlot, value);
-
-  // Step 6: Perform CreateDataProperty(obj, "done", done).
-  obj->setSlot(Realm::IterResultObjectDoneSlot,
-               done ? TrueHandleValue : FalseHandleValue);
-
-  // Step 7: Return obj.
-  return obj;
-}
-
-/**
- * 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());
-
-  // Step 3: Set stream.[[state]] to "errored".
-  unwrappedStream->setErrored();
-
-  // Step 4: Set stream.[[storedError]] to e.
-  {
-    AutoRealm ar(cx, unwrappedStream);
-    RootedValue wrappedError(cx, e);
-    if (!cx->compartment()->wrap(cx, &wrappedError)) {
-      return false;
-    }
-    unwrappedStream->setStoredError(wrappedError);
-  }
-
-  // Step 6: If reader is undefined, return (reordered).
-  if (!unwrappedStream->hasReader()) {
-    return true;
-  }
-
-  // Step 5: Let reader be stream.[[reader]].
-  Rooted<ReadableStreamReader*> unwrappedReader(
-      cx, UnwrapReaderFromStream(cx, unwrappedStream));
-  if (!unwrappedReader) {
-    return false;
-  }
-
-  // Steps 7-8: (Identical in our implementation.)
-  // Step 7.a/8.b: Repeat for each read{Into}Request that is an element of
-  //               reader.[[read{Into}Requests]],
-  Rooted<ListObject*> unwrappedReadRequests(cx, unwrappedReader->requests());
-  RootedObject readRequest(cx);
-  RootedValue val(cx);
-  uint32_t len = unwrappedReadRequests->length();
-  for (uint32_t i = 0; i < len; i++) {
-    // Step i: Reject read{Into}Request.[[promise]] with e.
-    val = unwrappedReadRequests->get(i);
-    readRequest = &val.toObject();
-
-    // Responses have to be created in the compartment from which the
-    // error was triggered, which might not be the same as the one the
-    // request was created in, so we have to wrap requests here.
-    if (!cx->compartment()->wrap(cx, &readRequest)) {
-      return false;
-    }
-
-    if (!RejectPromise(cx, readRequest, e)) {
-      return false;
-    }
-  }
-
-  // Step 7.b/8.c: Set reader.[[read{Into}Requests]] to a new empty List.
-  if (!StoreNewListInFixedSlot(cx, unwrappedReader,
-                               ReadableStreamReader::Slot_Requests)) {
-    return false;
-  }
-
-  // Step 9: Reject reader.[[closedPromise]] with e.
-  //
-  // The closedPromise might have been created in another compartment.
-  // RejectPromise can deal with wrapped Promise objects, but all its arguments
-  // must be same-compartment with cx, so we do need to wrap the Promise.
-  RootedObject closedPromise(cx, unwrappedReader->closedPromise());
-  if (!cx->compartment()->wrap(cx, &closedPromise)) {
-    return false;
-  }
-  if (!RejectPromise(cx, closedPromise, e)) {
-    return false;
-  }
-
-  if (unwrappedStream->mode() == JS::ReadableStreamMode::ExternalSource) {
-    // Make sure we're in the stream's compartment.
-    AutoRealm ar(cx, unwrappedStream);
-    JS::ReadableStreamUnderlyingSource* source =
-        unwrappedStream->controller()->externalSource();
-
-    // Ensure that the embedding doesn't have to deal with
-    // mixed-compartment arguments to the callback.
-    RootedValue error(cx, e);
-    if (!cx->compartment()->wrap(cx, &error)) {
-      return false;
-    }
-    source->onErrored(cx, unwrappedStream, error);
-  }
-
-  return true;
-}
-
-/**
- * Streams spec, 3.5.7.
- *      ReadableStreamFulfillReadIntoRequest( stream, chunk, done )
- * 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);
-
-  // Step 1: Let reader be stream.[[reader]].
-  Rooted<ReadableStreamReader*> unwrappedReader(
-      cx, UnwrapReaderFromStream(cx, unwrappedStream));
-  if (!unwrappedReader) {
-    return false;
-  }
-
-  // Step 2: Let read{Into}Request be the first element of
-  //         reader.[[read{Into}Requests]].
-  // Step 3: Remove read{Into}Request from reader.[[read{Into}Requests]],
-  //         shifting all other elements downward (so that the second becomes
-  //         the first, and so on).
-  Rooted<ListObject*> unwrappedReadIntoRequests(cx,
-                                                unwrappedReader->requests());
-  RootedObject readIntoRequest(
-      cx, &unwrappedReadIntoRequests->popFirstAs<JSObject>(cx));
-  MOZ_ASSERT(readIntoRequest);
-  if (!cx->compartment()->wrap(cx, &readIntoRequest)) {
-    return false;
-  }
-
-  // Step 4: Resolve read{Into}Request.[[promise]] with
-  //         ! ReadableStreamCreateReadResult(chunk, done,
-  //         readIntoRequest.[[forAuthorCode]]).
-  RootedObject iterResult(
-      cx, ReadableStreamCreateReadResult(cx, chunk, done,
-                                         unwrappedReader->forAuthorCode()));
-  if (!iterResult) {
-    return false;
-  }
-  RootedValue val(cx, ObjectValue(*iterResult));
-  return ResolvePromise(cx, readIntoRequest, val);
-}
-
-/**
- * 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;
-  }
-
-  JS::AutoSuppressGCAnalysis nogc;
-  ReadableStreamReader* reader = UnwrapReaderFromStreamNoThrow(stream);
-
-  // 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.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;
-  }
-  Rooted<ReadableStreamReader*> unwrappedReader(
-      cx, UnwrapReaderFromStream(cx, unwrappedStream));
-  if (!unwrappedReader) {
-    return false;
-  }
-
-  // Step 3: If ! ReadableStreamDefaultReader(reader) is false, return false.
-  // Step 4: Return true.
-  *result = unwrappedReader->is<ReadableStreamDefaultReader>();
-  return true;
-}
-
-/*** 3.6. Class ReadableStreamDefaultReader *********************************/
-
-static MOZ_MUST_USE bool ReadableStreamReaderGenericInitialize(
-    JSContext* cx, Handle<ReadableStreamReader*> reader,
-    Handle<ReadableStream*> unwrappedStream, ForAuthorCodeBool forAuthorCode);
-
-/**
- * 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(
-      cx, NewObjectWithClassProto<ReadableStreamDefaultReader>(cx, proto));
-  if (!reader) {
-    return nullptr;
-  }
-
-  // Step 2: If ! IsReadableStreamLocked(stream) is true, throw a TypeError
-  //         exception.
-  if (unwrappedStream->locked()) {
-    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                              JSMSG_READABLESTREAM_LOCKED);
-    return nullptr;
-  }
-
-  // Step 3: Perform ! ReadableStreamReaderGenericInitialize(this, stream).
-  // Step 4: Set this.[[readRequests]] to a new empty List.
-  if (!ReadableStreamReaderGenericInitialize(cx, reader, unwrappedStream,
-                                             forAuthorCode)) {
-    return nullptr;
-  }
-
-  return reader;
-}
-
-/**
- * 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;
-  }
-
-  // Implicit in the spec: Find the prototype object to use.
-  RootedObject proto(cx);
-  if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Null, &proto)) {
-    return false;
-  }
-
-  // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError
-  //         exception.
-  Rooted<ReadableStream*> unwrappedStream(
-      cx, UnwrapAndTypeCheckArgument<ReadableStream>(
-              cx, args, "ReadableStreamDefaultReader constructor", 0));
-  if (!unwrappedStream) {
-    return false;
-  }
-
-  RootedObject reader(
-      cx, CreateReadableStreamDefaultReader(cx, unwrappedStream,
-                                            ForAuthorCodeBool::Yes, proto));
-  if (!reader) {
-    return false;
-  }
-
-  args.rval().setObject(*reader);
-  return true;
-}
-
-/**
- * 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.
-  Rooted<ReadableStreamDefaultReader*> unwrappedReader(
-      cx, UnwrapAndTypeCheckThis<ReadableStreamDefaultReader>(cx, args,
-                                                              "get closed"));
-  if (!unwrappedReader) {
-    return ReturnPromiseRejectedWithPendingError(cx, args);
-  }
-
-  // Step 2: Return this.[[closedPromise]].
-  RootedObject closedPromise(cx, unwrappedReader->closedPromise());
-  if (!cx->compartment()->wrap(cx, &closedPromise)) {
-    return false;
-  }
-
-  args.rval().setObject(*closedPromise);
-  return true;
-}
-
-static MOZ_MUST_USE JSObject* ReadableStreamReaderGenericCancel(
-    JSContext* cx, Handle<ReadableStreamReader*> unwrappedReader,
-    HandleValue 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.
-  Rooted<ReadableStreamDefaultReader*> unwrappedReader(
-      cx,
-      UnwrapAndTypeCheckThis<ReadableStreamDefaultReader>(cx, args, "cancel"));
-  if (!unwrappedReader) {
-    return ReturnPromiseRejectedWithPendingError(cx, args);
-  }
-
-  // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise
-  //         rejected with a TypeError exception.
-  if (!unwrappedReader->hasStream()) {
-    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                              JSMSG_READABLESTREAMREADER_NOT_OWNED, "cancel");
-    return ReturnPromiseRejectedWithPendingError(cx, args);
-  }
-
-  // Step 3: Return ! ReadableStreamReaderGenericCancel(this, reason).
-  JSObject* cancelPromise =
-      ReadableStreamReaderGenericCancel(cx, unwrappedReader, args.get(0));
-  if (!cancelPromise) {
-    return false;
-  }
-  args.rval().setObject(*cancelPromise);
-  return true;
-}
-
-/**
- * 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.
-  Rooted<ReadableStreamDefaultReader*> unwrappedReader(
-      cx,
-      UnwrapAndTypeCheckThis<ReadableStreamDefaultReader>(cx, args, "read"));
-  if (!unwrappedReader) {
-    return ReturnPromiseRejectedWithPendingError(cx, args);
-  }
-
-  // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise
-  //         rejected with a TypeError exception.
-  if (!unwrappedReader->hasStream()) {
-    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                              JSMSG_READABLESTREAMREADER_NOT_OWNED, "read");
-    return ReturnPromiseRejectedWithPendingError(cx, args);
-  }
-
-  // Step 3: Return ! ReadableStreamDefaultReaderRead(this, true).
-  JSObject* readPromise =
-      ::ReadableStreamDefaultReaderRead(cx, unwrappedReader);
-  if (!readPromise) {
-    return false;
-  }
-  args.rval().setObject(*readPromise);
-  return true;
-}
-
-static MOZ_MUST_USE bool ReadableStreamReaderGenericRelease(
-    JSContext* cx, Handle<ReadableStreamReader*> reader);
-
-/**
- * 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,
-                                                              "releaseLock"));
-  if (!reader) {
-    return false;
-  }
-
-  // Step 2: If this.[[ownerReadableStream]] is undefined, return.
-  if (!reader->hasStream()) {
-    args.rval().setUndefined();
-    return true;
-  }
-
-  // Step 3: If this.[[readRequests]] is not empty, throw a TypeError exception.
-  Value val = reader->getFixedSlot(ReadableStreamReader::Slot_Requests);
-  if (!val.isUndefined()) {
-    ListObject* readRequests = &val.toObject().as<ListObject>();
-    if (readRequests->length() != 0) {
-      JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                JSMSG_READABLESTREAMREADER_NOT_EMPTY,
-                                "releaseLock");
-      return false;
-    }
-  }
-
-  // Step 4: Perform ! ReadableStreamReaderGenericRelease(this).
-  if (!ReadableStreamReaderGenericRelease(cx, reader)) {
-    return false;
-  }
-
-  args.rval().setUndefined();
-  return true;
-}
-
-static const JSFunctionSpec ReadableStreamDefaultReader_methods[] = {
-    JS_FN("cancel", ReadableStreamDefaultReader_cancel, 1, 0),
-    JS_FN("read", ReadableStreamDefaultReader_read, 0, 0),
-    JS_FN("releaseLock", ReadableStreamDefaultReader_releaseLock, 0, 0),
-    JS_FS_END};
-
-static const JSPropertySpec ReadableStreamDefaultReader_properties[] = {
-    JS_PSG("closed", ReadableStreamDefaultReader_closed, 0), JS_PS_END};
-
-const JSClass ReadableStreamReader::class_ = {"ReadableStreamReader"};
-
-JS_STREAMS_CLASS_SPEC(ReadableStreamDefaultReader, 1, SlotCount,
-                      ClassSpec::DontDefineConstructor, 0, JS_NULL_CLASS_OPS);
-
 /*** 3.7. Class ReadableStreamBYOBReader ************************************/
 
 // Not implemented.
 
-/*** 3.8. Readable stream reader abstract operations ************************/
-
-// Streams spec, 3.8.1. IsReadableStreamDefaultReader ( x )
-// Implemented via is<ReadableStreamDefaultReader>()
-<