Merge autoland to mozilla-central. a=merge
authorIulian Moraru <imoraru@mozilla.com>
Thu, 14 Apr 2022 06:43:41 +0300
changeset 684726 7483423001f5d1710b2cd0ab94614a4a2e8045bc
parent 684707 4ddd4f1a1b2758a33b531378b2f011c3063fed1a (current diff)
parent 684716 f88eb6ce6cfb7df0d8ca7cc4c9d9413103bf7c3e (diff)
child 684727 162d06ca646c034a516607f3b97e95489240543f
child 684732 30a7bf9f22842af79045e4d44bd4364e62ed2c9c
push id16598
push userffxbld-merge
push dateMon, 02 May 2022 14:23:32 +0000
treeherdermozilla-beta@de86a81c7a63 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone101.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
--- a/.cargo/config.in
+++ b/.cargo/config.in
@@ -30,17 +30,17 @@ rev = "4c11f0ffb5d6a10de4aff40a7b81218b3
 [source."https://github.com/mozilla/cubeb-pulse-rs"]
 git = "https://github.com/mozilla/cubeb-pulse-rs"
 replace-with = "vendored-sources"
 rev = "df4dc0288b07b865440f4c7e41ca49ca9ccffc63"
 
 [source."https://github.com/mozilla/cubeb-coreaudio-rs"]
 git = "https://github.com/mozilla/cubeb-coreaudio-rs"
 replace-with = "vendored-sources"
-rev = "39f7e696c11ff45503b50a49a36cd9f39900c27b"
+rev = "a5a21ccbcc1fb46877b231e6a815cf8a824b1c5c"
 
 [source."https://github.com/mozilla/audioipc-2"]
 git = "https://github.com/mozilla/audioipc-2"
 replace-with = "vendored-sources"
 rev = "c144368c4e084ec0f076af6262970655c2d99e8d"
 
 [source."https://github.com/mozilla/application-services"]
 git = "https://github.com/mozilla/application-services"
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -839,17 +839,17 @@ source = "registry+https://github.com/ru
 checksum = "2b7e3347be6a09b46aba228d6608386739fb70beff4f61e07422da87b0bb31fa"
 dependencies = [
  "bindgen",
 ]
 
 [[package]]
 name = "coreaudio-sys-utils"
 version = "0.1.0"
-source = "git+https://github.com/mozilla/cubeb-coreaudio-rs?rev=39f7e696c11ff45503b50a49a36cd9f39900c27b#39f7e696c11ff45503b50a49a36cd9f39900c27b"
+source = "git+https://github.com/mozilla/cubeb-coreaudio-rs?rev=a5a21ccbcc1fb46877b231e6a815cf8a824b1c5c#a5a21ccbcc1fb46877b231e6a815cf8a824b1c5c"
 dependencies = [
  "core-foundation-sys",
  "coreaudio-sys",
 ]
 
 [[package]]
 name = "coremidi"
 version = "0.6.0"
@@ -1140,17 +1140,17 @@ checksum = "04aabcd7fa088330b5f25b2f92cd
 dependencies = [
  "bitflags",
  "cubeb-sys",
 ]
 
 [[package]]
 name = "cubeb-coreaudio"
 version = "0.1.0"
-source = "git+https://github.com/mozilla/cubeb-coreaudio-rs?rev=39f7e696c11ff45503b50a49a36cd9f39900c27b#39f7e696c11ff45503b50a49a36cd9f39900c27b"
+source = "git+https://github.com/mozilla/cubeb-coreaudio-rs?rev=a5a21ccbcc1fb46877b231e6a815cf8a824b1c5c#a5a21ccbcc1fb46877b231e6a815cf8a824b1c5c"
 dependencies = [
  "atomic",
  "audio-mixer",
  "bitflags",
  "coreaudio-sys-utils",
  "cubeb-backend",
  "float-cmp",
  "lazy_static",
--- a/accessible/basetypes/HyperTextAccessibleBase.cpp
+++ b/accessible/basetypes/HyperTextAccessibleBase.cpp
@@ -545,22 +545,28 @@ already_AddRefed<AccAttributes> HyperTex
                                               /* aIsEndOffset */ true);
   return attributes.forget();
 }
 
 void HyperTextAccessibleBase::CroppedSelectionRanges(
     nsTArray<TextRange>& aRanges) const {
   SelectionRanges(&aRanges);
   const Accessible* acc = Acc();
-  if (!acc->IsDoc()) {
-    aRanges.RemoveElementsBy([acc](auto& range) {
-      return range.StartPoint() == range.EndPoint() ||
-             !range.Crop(const_cast<Accessible*>(acc));
-    });
-  }
+  aRanges.RemoveElementsBy([acc](auto& range) {
+    if (range.StartPoint() == range.EndPoint()) {
+      return true;  // Collapsed, so remove this range.
+    }
+    // If this is the document, it contains all ranges, so there's no need to
+    // crop.
+    if (!acc->IsDoc()) {
+      // If we fail to crop, the range is outside acc, so remove it.
+      return !range.Crop(const_cast<Accessible*>(acc));
+    }
+    return false;
+  });
 }
 
 int32_t HyperTextAccessibleBase::SelectionCount() {
   nsTArray<TextRange> ranges;
   CroppedSelectionRanges(ranges);
   return static_cast<int32_t>(ranges.Length());
 }
 
--- a/accessible/tests/browser/e10s/browser_caching_text.js
+++ b/accessible/tests/browser/e10s/browser_caching_text.js
@@ -750,25 +750,28 @@ addAccessibleTask(
 <textarea id="textarea">ab</textarea>
 <div id="editable" contenteditable>
   <p id="p1">a</p>
   <p id="p2">bc</p>
   <p id="pWithLink">d<a id="link" href="https://example.com/">e</a><span id="textAfterLink">f</span></p>
 </div>
   `,
   async function(browser, docAcc) {
+    queryInterfaces(docAcc, [nsIAccessibleText]);
+
     const textarea = findAccessibleChildByID(docAcc, "textarea", [
       nsIAccessibleText,
     ]);
     info("Focusing textarea");
     let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
     textarea.takeFocus();
     await caretMoved;
     testSelectionRange(browser, textarea, textarea, 0, textarea, 0);
     is(textarea.selectionCount, 0, "textarea selectionCount is 0");
+    is(docAcc.selectionCount, 0, "document selectionCount is 0");
 
     info("Selecting a in textarea");
     let selChanged = waitForSelectionChange(textarea);
     EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
     await selChanged;
     testSelectionRange(browser, textarea, textarea, 0, textarea, 1);
     testTextGetSelection(textarea, 0, 1, 0);
 
@@ -800,16 +803,17 @@ addAccessibleTask(
     const p1 = findAccessibleChildByID(docAcc, "p1", [nsIAccessibleText]);
     info("Focusing editable, caret to start");
     caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, p1);
     await changeDomSelection(browser, "p1", 0, "p1", 0);
     await caretMoved;
     testSelectionRange(browser, editable, p1, 0, p1, 0);
     is(editable.selectionCount, 0, "editable selectionCount is 0");
     is(p1.selectionCount, 0, "p1 selectionCount is 0");
+    is(docAcc.selectionCount, 0, "document selectionCount is 0");
 
     info("Selecting a in editable");
     selChanged = waitForSelectionChange(p1);
     await changeDomSelection(browser, "p1", 0, "p1", 1);
     await selChanged;
     testSelectionRange(browser, editable, p1, 0, p1, 1);
     testTextGetSelection(editable, 0, 1, 0);
     testTextGetSelection(p1, 0, 1, 0);
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -432,16 +432,20 @@ pref("browser.urlbar.quicksuggest.allowP
 // Whether non-sponsored quick suggest results are subject to impression
 // frequency caps.
 pref("browser.urlbar.quicksuggest.impressionCaps.nonSponsoredEnabled", false);
 
 // Whether sponsored quick suggest results are subject to impression frequency
 // caps.
 pref("browser.urlbar.quicksuggest.impressionCaps.sponsoredEnabled", false);
 
+// Whether the usual non-best-match quick suggest results can be blocked. This
+// pref is a fallback for the Nimbus variable `quickSuggestBlockingEnabled`.
+pref("browser.urlbar.quicksuggest.blockingEnabled", false);
+
 // Whether unit conversion is enabled.
 #ifdef NIGHTLY_BUILD
 pref("browser.urlbar.unitConversion.enabled", true);
 #else
 pref("browser.urlbar.unitConversion.enabled", false);
 #endif
 
 // Whether to show search suggestions before general results like history and
@@ -519,17 +523,18 @@ pref("browser.urlbar.merino.timeoutMs", 
 pref("browser.urlbar.merino.providers", "");
 
 // Comma-separated list of client variants to send to Merino
 pref("browser.urlbar.merino.clientVariants", "");
 
 // Whether the best match feature in the urlbar is enabled.
 pref("browser.urlbar.bestMatch.enabled", false);
 
-// Whether best match results can be blocked.
+// Whether best match results can be blocked. This pref is a fallback for the
+// Nimbus variable `bestMatchBlockingEnabled`.
 pref("browser.urlbar.bestMatch.blockingEnabled", false);
 
 pref("browser.altClickSave", false);
 
 // Enable logging downloads operations to the Console.
 pref("browser.download.loglevel", "Error");
 
 // Number of milliseconds to wait for the http headers (and thus
--- a/browser/components/newtab/aboutwelcome/content/aboutwelcome.bundle.js
+++ b/browser/components/newtab/aboutwelcome/content/aboutwelcome.bundle.js
@@ -1197,20 +1197,21 @@ function useLanguageSwitcher(appAndSyste
     setLangPackInstallPhase("installing");
     window.AWEnsureLangPackInstalled(negotiatedLanguage.langPack).then(() => {
       setLangPackInstallPhase("installed");
     }, error => {
       console.error(error);
       setLangPackInstallPhase("installation-error");
     });
   }, [negotiatedLanguage]);
-  const shouldHideLanguageSwitcher = screen && (appAndSystemLocaleInfo === null || appAndSystemLocaleInfo === void 0 ? void 0 : appAndSystemLocaleInfo.matchType) !== "language-mismatch";
   const [languageFilteredScreens, setLanguageFilteredScreens] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(screens);
   (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(function filterScreen() {
-    if (shouldHideLanguageSwitcher || (negotiatedLanguage === null || negotiatedLanguage === void 0 ? void 0 : negotiatedLanguage.langPack) === null) {
+    // Remove the language screen if it exists (already removed for no live
+    // reload) and we either don't-need-to or can't switch.
+    if (screen && ((appAndSystemLocaleInfo === null || appAndSystemLocaleInfo === void 0 ? void 0 : appAndSystemLocaleInfo.matchType) !== "language-mismatch" || (negotiatedLanguage === null || negotiatedLanguage === void 0 ? void 0 : negotiatedLanguage.langPack) === null)) {
       if (screenIndex > languageMismatchScreenIndex) {
         setScreenIndex(screenIndex - 1);
       }
 
       setLanguageFilteredScreens(screens.filter(s => s.id !== "AW_LANGUAGE_MISMATCH"));
     } else {
       setLanguageFilteredScreens(screens);
     }
--- a/browser/components/newtab/content-src/aboutwelcome/components/LanguageSwitcher.jsx
+++ b/browser/components/newtab/content-src/aboutwelcome/components/LanguageSwitcher.jsx
@@ -93,25 +93,28 @@ export function useLanguageSwitcher(
           console.error(error);
           setLangPackInstallPhase("installation-error");
         }
       );
     },
     [negotiatedLanguage]
   );
 
-  const shouldHideLanguageSwitcher =
-    screen && appAndSystemLocaleInfo?.matchType !== "language-mismatch";
-
   const [languageFilteredScreens, setLanguageFilteredScreens] = useState(
     screens
   );
   useEffect(
     function filterScreen() {
-      if (shouldHideLanguageSwitcher || negotiatedLanguage?.langPack === null) {
+      // Remove the language screen if it exists (already removed for no live
+      // reload) and we either don't-need-to or can't switch.
+      if (
+        screen &&
+        (appAndSystemLocaleInfo?.matchType !== "language-mismatch" ||
+          negotiatedLanguage?.langPack === null)
+      ) {
         if (screenIndex > languageMismatchScreenIndex) {
           setScreenIndex(screenIndex - 1);
         }
         setLanguageFilteredScreens(
           screens.filter(s => s.id !== "AW_LANGUAGE_MISMATCH")
         );
       } else {
         setLanguageFilteredScreens(screens);
--- a/browser/components/newtab/lib/ASRouter.jsm
+++ b/browser/components/newtab/lib/ASRouter.jsm
@@ -86,16 +86,17 @@ const RS_FLUENT_RECORD_PREFIX = `cfr-${R
 const RS_DOWNLOAD_MAX_RETRIES = 2;
 // This is the list of providers for which we want to cache the targeting
 // expression result and reuse between calls. Cache duration is defined in
 // ASRouterTargeting where evaluation takes place.
 const JEXL_PROVIDER_CACHE = new Set(["snippets"]);
 
 // To observe the app locale change notification.
 const TOPIC_INTL_LOCALE_CHANGED = "intl:app-locales-changed";
+const TOPIC_EXPERIMENT_FORCE_ENROLLED = "nimbus:force-enroll";
 // To observe the pref that controls if ASRouter should use the remote Fluent files for l10n.
 const USE_REMOTE_L10N_PREF =
   "browser.newtabpage.activity-stream.asrouter.useRemoteL10n";
 
 // Experiment groups that need to report the reach event in Messaging-Experiments.
 // If you're adding new groups to it, make sure they're also added in the
 // `messaging_experiments.reach.objects` defined in "toolkit/components/telemetry/Events.yaml"
 const REACH_EVENT_GROUPS = ["cfr", "moments-page", "infobar", "spotlight"];
@@ -535,16 +536,19 @@ class _ASRouter {
     this.handleMessageRequest = this.handleMessageRequest.bind(this);
     this.addImpression = this.addImpression.bind(this);
     this._handleTargetingError = this._handleTargetingError.bind(this);
     this.onPrefChange = this.onPrefChange.bind(this);
     this._onLocaleChanged = this._onLocaleChanged.bind(this);
     this.isUnblockedMessage = this.isUnblockedMessage.bind(this);
     this.unblockAll = this.unblockAll.bind(this);
     this.forceWNPanel = this.forceWNPanel.bind(this);
+    this._onExperimentForceEnrolled = this._onExperimentForceEnrolled.bind(
+      this
+    );
     Services.telemetry.setEventRecordingEnabled(REACH_EVENT_CATEGORY, true);
   }
 
   async onPrefChange(prefName) {
     if (TARGETING_PREFERENCES.includes(prefName)) {
       let invalidMessages = [];
       // Notify all tabs of messages that have become invalid after pref change
       const context = this._getMessagesContext();
@@ -743,22 +747,26 @@ class _ASRouter {
       // If fetching remote messages fails we default to existing state.groups.
       groups: (remoteMessages || state.groups).map(this._checkGroupEnabled),
     }));
   }
 
   /**
    * loadMessagesFromAllProviders - Loads messages from all providers if they require updates.
    *                                Checks the .lastUpdated field on each provider to see if updates are needed
+   * @param toUpdate  An optional list of providers to update. This overrides
+   *                  the checks to determine which providers to update.
    * @memberof _ASRouter
    */
-  async loadMessagesFromAllProviders() {
-    const needsUpdate = this.state.providers.filter(provider =>
-      MessageLoaderUtils.shouldProviderUpdate(provider)
-    );
+  async loadMessagesFromAllProviders(toUpdate = undefined) {
+    const needsUpdate = Array.isArray(toUpdate)
+      ? toUpdate
+      : this.state.providers.filter(provider =>
+          MessageLoaderUtils.shouldProviderUpdate(provider)
+        );
     await this.loadAllMessageGroups();
     // Don't do extra work if we don't need any updates
     if (needsUpdate.length) {
       let newState = { messages: [], providers: [] };
       for (const provider of this.state.providers) {
         if (needsUpdate.includes(provider)) {
           const {
             messages,
@@ -912,16 +920,20 @@ class _ASRouter {
       initialized: false,
     });
     await this._updateMessageProviders();
     await this.loadMessagesFromAllProviders();
     await MessageLoaderUtils.cleanupCache(this.state.providers, storage);
 
     SpecialMessageActions.blockMessageById = this.blockMessageById;
     Services.obs.addObserver(this._onLocaleChanged, TOPIC_INTL_LOCALE_CHANGED);
+    Services.obs.addObserver(
+      this._onExperimentForceEnrolled,
+      TOPIC_EXPERIMENT_FORCE_ENROLLED
+    );
     Services.prefs.addObserver(USE_REMOTE_L10N_PREF, this);
     // sets .initialized to true and resolves .waitForInitialized promise
     this._finishInitializing();
     return this.state;
   }
 
   uninit() {
     this._storage.set("previousSessionEnd", Date.now());
@@ -941,16 +953,20 @@ class _ASRouter {
     // Uninitialise all trigger listeners
     for (const listener of ASRouterTriggerListeners.values()) {
       listener.uninit();
     }
     Services.obs.removeObserver(
       this._onLocaleChanged,
       TOPIC_INTL_LOCALE_CHANGED
     );
+    Services.obs.removeObserver(
+      this._onExperimentForceEnrolled,
+      TOPIC_EXPERIMENT_FORCE_ENROLLED
+    );
     Services.prefs.removeObserver(USE_REMOTE_L10N_PREF, this);
     // If we added any CFR recommendations, they need to be removed
     CFRPageActions.clearRecommendations();
     this._resetInitialization();
   }
 
   setState(callbackOrObj) {
     const newState =
@@ -1729,16 +1745,27 @@ class _ASRouter {
   async closeWNPanel(browser) {
     let win = browser.ownerGlobal;
     let panel = win.document.getElementById("customizationui-widget-panel");
     // Set the attribute to allow the panel to close
     panel.setAttribute("noautohide", false);
     // Removing the button is enough to close the panel.
     await ToolbarPanelHub._hideToolbarButton(win);
   }
+
+  async _onExperimentForceEnrolled(subject, topic, data) {
+    const experimentProvider = this.state.providers.find(
+      p => p.id === "messaging-experiments"
+    );
+    if (!experimentProvider.enabled) {
+      return;
+    }
+
+    await this.loadMessagesFromAllProviders([experimentProvider]);
+  }
 }
 this._ASRouter = _ASRouter;
 
 /**
  * ASRouter - singleton instance of _ASRouter that controls all messages
  * in the new tab page.
  */
 this.ASRouter = new _ASRouter();
--- a/browser/components/newtab/test/browser/browser_aboutwelcome_multistage_languageSwitcher.js
+++ b/browser/components/newtab/test/browser/browser_aboutwelcome_multistage_languageSwitcher.js
@@ -552,16 +552,51 @@ add_task(async function test_aboutwelcom
       `[data-l10n-id="onboarding-live-language-header"]`,
     ]
   );
 
   sinon.assert.notCalled(mockable.setRequestedAppLocales);
 });
 
 /**
+ * Test when bidi live reloading is not supported and no langpacks.
+ */
+add_task(
+  async function test_aboutwelcome_languageSwitcher_bidiNotSupported_noLangPacks() {
+    sandbox.restore();
+    await pushPrefs(["intl.multilingual.liveReloadBidirectional", false]);
+
+    const { resolveLangPacks, mockable } = mockAddonAndLocaleAPIs({
+      systemLocale: "ar-EG", // Arabic (Egypt)
+      appLocale: "en-US",
+    });
+    resolveLangPacks([]);
+
+    const { browser } = await openAboutWelcome();
+
+    info("Clicking the primary button to start installing the langpack.");
+    await clickVisibleButton(browser, "button.primary");
+
+    await testScreenContent(
+      browser,
+      "Language selection skipped for bidi",
+      // Expected selectors:
+      [`.screen-1`],
+      // Unexpected selectors:
+      [
+        `[data-l10n-id*="onboarding-live-language"]`,
+        `[data-l10n-id="onboarding-live-language-header"]`,
+      ]
+    );
+
+    sinon.assert.notCalled(mockable.setRequestedAppLocales);
+  }
+);
+
+/**
  * Test when bidi live reloading is supported.
  */
 add_task(async function test_aboutwelcome_languageSwitcher_bidiNotSupported() {
   sandbox.restore();
   await pushPrefs(["intl.multilingual.liveReloadBidirectional", true]);
 
   const { resolveLangPacks, mockable } = mockAddonAndLocaleAPIs({
     systemLocale: "ar-EG", // Arabic (Egypt)
--- a/browser/components/newtab/test/browser/browser_asrouter_experimentsAPILoader.js
+++ b/browser/components/newtab/test/browser/browser_asrouter_experimentsAPILoader.js
@@ -1,23 +1,29 @@
+const { BrowserTestUtils } = ChromeUtils.import(
+  "resource://testing-common/BrowserTestUtils.jsm"
+);
 const { RemoteSettings } = ChromeUtils.import(
   "resource://services-settings/remote-settings.js"
 );
 const { ASRouter } = ChromeUtils.import(
   "resource://activity-stream/lib/ASRouter.jsm"
 );
 const { RemoteSettingsExperimentLoader } = ChromeUtils.import(
   "resource://nimbus/lib/RemoteSettingsExperimentLoader.jsm"
 );
 const { ExperimentAPI } = ChromeUtils.import(
   "resource://nimbus/ExperimentAPI.jsm"
 );
 const { ExperimentFakes, ExperimentTestUtils } = ChromeUtils.import(
   "resource://testing-common/NimbusTestUtils.jsm"
 );
+const { ExperimentManager } = ChromeUtils.import(
+  "resource://nimbus/lib/ExperimentManager.jsm"
+);
 const { TelemetryFeed } = ChromeUtils.import(
   "resource://activity-stream/lib/TelemetryFeed.jsm"
 );
 const { TelemetryTestUtils } = ChromeUtils.import(
   "resource://testing-common/TelemetryTestUtils.jsm"
 );
 
 const MESSAGE_CONTENT = {
@@ -330,8 +336,46 @@ add_task(async function test_exposure_pi
     "telemetry.event_counts",
     "normandy#expose#nimbus_experiment",
     1
   );
 
   exposureSpy.restore();
   await cleanup();
 });
+
+add_task(async function test_forceEnrollUpdatesMessages() {
+  const experiment = await getCFRExperiment();
+
+  await setup(experiment);
+  await SpecialPowers.pushPrefEnv({
+    set: [["nimbus.debug", true]],
+  });
+  registerCleanupFunction(async () => {
+    await ExperimentManager.unenroll(`optin-${experiment.slug}`, "cleanup");
+    await SpecialPowers.popPrefEnv();
+    await cleanup();
+  });
+
+  Assert.equal(
+    ASRouter.state.messages.filter(m => m.id === "xman_test_message").length,
+    0,
+    "Experiment message should not be found until we opt in"
+  );
+
+  await RemoteSettingsExperimentLoader.optInToExperiment({
+    slug: experiment.slug,
+    branch: experiment.branches[0].slug,
+  });
+
+  await BrowserTestUtils.waitForCondition(
+    () =>
+      !!ASRouter.state.messages.filter(m => m.id === "xman_test_message")
+        .length,
+    "waiting for ASRouter to update messages"
+  );
+
+  Assert.equal(
+    ASRouter.state.messages.filter(m => m.id === "xman_test_message").length,
+    1,
+    "Experiment message should be found after opt in"
+  );
+});
--- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
@@ -496,19 +496,19 @@ describe("ASRouter", () => {
     it("should assign ASRouterPreferences.specialConditions to state", async () => {
       assert.isTrue(ASRouterPreferences.specialConditions.someCondition);
       assert.isTrue(Router.state.someCondition);
     });
     it("should add observer for `intl:app-locales-changed`", async () => {
       sandbox.spy(global.Services.obs, "addObserver");
       await createRouterAndInit();
 
-      assert.calledOnce(global.Services.obs.addObserver);
-      assert.equal(
-        global.Services.obs.addObserver.args[0][1],
+      assert.calledWithExactly(
+        global.Services.obs.addObserver,
+        Router._onLocaleChanged,
         "intl:app-locales-changed"
       );
     });
     it("should add a pref observer", async () => {
       sandbox.spy(global.Services.prefs, "addObserver");
       await createRouterAndInit();
 
       assert.calledOnce(global.Services.prefs.addObserver);
@@ -1410,19 +1410,19 @@ describe("ASRouter", () => {
         "previousSessionEnd",
         sinon.match.number
       );
     });
     it("should remove the observer for `intl:app-locales-changed`", () => {
       sandbox.spy(global.Services.obs, "removeObserver");
       Router.uninit();
 
-      assert.calledOnce(global.Services.obs.removeObserver);
-      assert.equal(
-        global.Services.obs.removeObserver.args[0][1],
+      assert.calledWithExactly(
+        global.Services.obs.removeObserver,
+        Router._onLocaleChanged,
         "intl:app-locales-changed"
       );
     });
     it("should remove the pref observer for `USE_REMOTE_L10N_PREF`", async () => {
       sandbox.spy(global.Services.prefs, "removeObserver");
       Router.uninit();
 
       // Grab the last call as #uninit() also involves multiple calls of `Services.prefs.removeObserver`.
--- a/browser/components/pocket/content/panels/css/global.scss
+++ b/browser/components/pocket/content/panels/css/global.scss
@@ -7,25 +7,30 @@
 }
 
 html {
   font: menu;
 }
 
 body {
   &.theme_dark {
+    background: #42414c;
     color: #FBFBFE;
   }
 }
 
 hr {
   margin: 12px -8px;
   background-color: #F0F0F4;
   height: 1px;
   border: none;
+
+  @include theme_dark {
+    background-color: #52525E;
+  }
 }
 
 .header_large {
   margin: 12px 0 8px;
   font-size: 1.25rem;
   line-height: 1.65rem;
 
   .stp_button {
--- a/browser/components/pocket/content/panels/css/main.compiled.css
+++ b/browser/components/pocket/content/panels/css/main.compiled.css
@@ -391,25 +391,29 @@ a:active {
   color: #006b9d;
 }
 
 html {
   font: menu;
 }
 
 body.theme_dark {
+  background: #42414c;
   color: #FBFBFE;
 }
 
 hr {
   margin: 12px -8px;
   background-color: #F0F0F4;
   height: 1px;
   border: none;
 }
+body.theme_dark hr {
+  background-color: #52525E;
+}
 
 .header_large {
   margin: 12px 0 8px;
   font-size: 1.25rem;
   line-height: 1.65rem;
 }
 .header_large .stp_button {
   margin: 0;
--- a/browser/components/pocket/content/panels/js/home/overlay.js
+++ b/browser/components/pocket/content/panels/js/home/overlay.js
@@ -71,16 +71,20 @@ HomeOverlay.prototype = {
             { title: "Career", topic: "career" },
             { title: "Health", topic: "health" },
             { title: "Travel", topic: "travel" },
             { title: "Must-Reads", topic: "must-reads" },
           ]}
         />,
         document.querySelector(`body`)
       );
+
+      if (window?.matchMedia(`(prefers-color-scheme: dark)`).matches) {
+        document.querySelector(`body`).classList.add(`theme_dark`);
+      }
     } else {
       // For English, we have a discover topics link.
       // For non English, we don't have a link yet for this.
       // When we do, we can consider flipping this on.
       const enableLocalizedExploreMore = false;
       const templateData = {
         pockethost,
         utmsource: `firefox-button`,
--- a/browser/components/pocket/content/panels/js/main.bundle.js
+++ b/browser/components/pocket/content/panels/js/main.bundle.js
@@ -482,16 +482,20 @@ HomeOverlay.prototype = {
         }, {
           title: "Travel",
           topic: "travel"
         }, {
           title: "Must-Reads",
           topic: "must-reads"
         }]
       }), document.querySelector(`body`));
+
+      if (window?.matchMedia(`(prefers-color-scheme: dark)`).matches) {
+        document.querySelector(`body`).classList.add(`theme_dark`);
+      }
     } else {
       // For English, we have a discover topics link.
       // For non English, we don't have a link yet for this.
       // When we do, we can consider flipping this on.
       const enableLocalizedExploreMore = false;
       const templateData = {
         pockethost,
         utmsource: `firefox-button`
@@ -657,16 +661,20 @@ var SignupOverlay = function (options) {
 
       react_dom.render( /*#__PURE__*/react.createElement(Signup_Signup, {
         pockethost: pockethost,
         utmSource: utmSource,
         utmCampaign: utmCampaign,
         utmContent: utmContent,
         locale: locale
       }), document.querySelector(`body`));
+
+      if (window?.matchMedia(`(prefers-color-scheme: dark)`).matches) {
+        document.querySelector(`body`).classList.add(`theme_dark`);
+      }
     } else {
       const templateData = {
         pockethost,
         utmCampaign: utmCampaign || `firefox_door_hanger_menu`,
         // utmContent is now used for experiment branch in the new layouts,
         // but for backwards comp reasons, we pass it in the old way as utmSource.
         utmSource: utmContent || `control`
       }; // extra modifier class for language
@@ -1551,16 +1559,20 @@ SavedOverlay.prototype = {
 
       react_dom.render( /*#__PURE__*/react.createElement(Saved_Saved, {
         locale: locale,
         pockethost: pockethost,
         utmSource: utmSource,
         utmCampaign: utmCampaign,
         utmContent: utmContent
       }), document.querySelector(`body`));
+
+      if (window?.matchMedia(`(prefers-color-scheme: dark)`).matches) {
+        document.querySelector(`body`).classList.add(`theme_dark`);
+      }
     } else {
       // set host
       const templateData = {
         pockethost
       }; // extra modifier class for language
 
       if (language) {
         document.querySelector(`body`).classList.add(`pkt_ext_saved_${language}`);
--- a/browser/components/pocket/content/panels/js/saved/overlay.js
+++ b/browser/components/pocket/content/panels/js/saved/overlay.js
@@ -683,16 +683,20 @@ SavedOverlay.prototype = {
           locale={locale}
           pockethost={pockethost}
           utmSource={utmSource}
           utmCampaign={utmCampaign}
           utmContent={utmContent}
         />,
         document.querySelector(`body`)
       );
+
+      if (window?.matchMedia(`(prefers-color-scheme: dark)`).matches) {
+        document.querySelector(`body`).classList.add(`theme_dark`);
+      }
     } else {
       // set host
       const templateData = {
         pockethost,
       };
 
       // extra modifier class for language
       if (language) {
--- a/browser/components/pocket/content/panels/js/signup/overlay.js
+++ b/browser/components/pocket/content/panels/js/signup/overlay.js
@@ -68,16 +68,20 @@ var SignupOverlay = function(options) {
           pockethost={pockethost}
           utmSource={utmSource}
           utmCampaign={utmCampaign}
           utmContent={utmContent}
           locale={locale}
         />,
         document.querySelector(`body`)
       );
+
+      if (window?.matchMedia(`(prefers-color-scheme: dark)`).matches) {
+        document.querySelector(`body`).classList.add(`theme_dark`);
+      }
     } else {
       const templateData = {
         pockethost,
         utmCampaign: utmCampaign || `firefox_door_hanger_menu`,
         // utmContent is now used for experiment branch in the new layouts,
         // but for backwards comp reasons, we pass it in the old way as utmSource.
         utmSource: utmContent || `control`,
       };
--- a/browser/components/pocket/content/panels/js/style-guide/entry.js
+++ b/browser/components/pocket/content/panels/js/style-guide/entry.js
@@ -8,17 +8,18 @@ function onDOMLoaded() {
     thePKT_PANEL.initStyleGuide();
   }
   window.thePKT_PANEL.overlay.create();
 
   setupDarkModeUI();
 }
 
 function setupDarkModeUI() {
-  let isDarkModeEnabled = false; // TODO: Use browser pref for starting value
+  let isDarkModeEnabled = window?.matchMedia(`(prefers-color-scheme: dark)`)
+    .matches;
   let elDarkModeToggle = document.querySelector(`#dark_mode_toggle input`);
   let elBody = document.querySelector(`body`);
 
   function setTheme() {
     if (isDarkModeEnabled) {
       elBody.classList.add(`theme_dark`);
       elDarkModeToggle.checked = true;
     } else {
--- a/browser/components/pocket/content/panels/style-guide.html
+++ b/browser/components/pocket/content/panels/style-guide.html
@@ -14,16 +14,18 @@
         <script src="js/vendor/handlebars.runtime.js"></script>
         <script src="js/tmpl.js"></script>
         <script src="js/vendor.bundle.js"></script>
         <script src="js/main.bundle.js"></script>
         <script src="js/style-guide/entry.js"></script>
 
         <div id="stp_style_guide">
             <div id="dark_mode_toggle">
-                <label for="dark_mode_checkbox"><strong>Dark Mode: </strong></label>
-                <input id="dark_mode_checkbox" type="checkbox"/>
+                <form autocomplete="off">
+                    <label for="dark_mode_checkbox"><strong>Dark Mode: </strong></label>
+                    <input id="dark_mode_checkbox" type="checkbox"/>
+                </form>
             </div>
             <h1>Save To Pocket:<br/> Style Guide</h1>
             <div id="stp_style_guide_components"></div>
         </div>
     </body>
 </html>
--- a/browser/components/preferences/home.js
+++ b/browser/components/preferences/home.js
@@ -107,16 +107,17 @@ var gHomePane = {
         true
       );
       let newValue = newtabEnabledPref
         ? this.HOME_MODE_FIREFOX_HOME
         : this.HOME_MODE_BLANK;
       if (newValue !== menulist.value) {
         menulist.value = newValue;
       }
+      menulist.disabled = Preferences.get(this.NEWTAB_ENABLED_PREF).locked;
       // If change was triggered by installing an addon we need to update
       // the value of the menulist to be that addon.
     } else {
       let selectedAddon = ExtensionSettingsStore.getSetting(
         URL_OVERRIDES_TYPE,
         NEW_TAB_KEY
       );
       if (selectedAddon && menulist.value !== selectedAddon.id) {
--- a/browser/components/urlbar/UrlbarController.jsm
+++ b/browser/components/urlbar/UrlbarController.jsm
@@ -538,16 +538,20 @@ class UrlbarController {
     // Do not modify existing telemetry types.  To add a new type:
     //
     // * Set telemetryType appropriately. Since telemetryType is used as the
     //   probe name, it must be alphanumeric with optional underscores.
     // * Add a new keyed scalar probe into the urlbar.picked category for the
     //   newly added telemetryType.
     // * Add a test named browser_UsageTelemetry_urlbar_newType.js to
     //   browser/modules/test/browser.
+    // * Add the telemetryType to UrlbarUtils.SELECTED_RESULT_TYPES, which is
+    //   used by the histograms below. These histograms are deprecated, but the
+    //   code below logs an error if telemetryType is not in
+    //   SELECTED_RESULT_TYPES.
     //
     // The "topsite" type overrides the other ones, because it starts from a
     // unique user interaction, that we want to count apart. We do this here
     // rather than in telemetryTypeFromResult because other consumers, like
     // events telemetry, are reporting this information separately.
     let telemetryType =
       result.providerName == "UrlbarProviderTopSites"
         ? "topsite"
@@ -581,17 +585,17 @@ class UrlbarController {
       .getKeyedHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2")
       .add(telemetryType, resultIndex);
   }
 
   /**
    * Handles deletion of results from the last query context and the view. There
    * are two kinds of results that can be deleted:
    *
-   * - Results for which `provider.blockResult(result)` returns true
+   * - Results for which `provider.blockResult()` returns true
    * - Results whose source is `HISTORY` are handled specially by this method
    *   and can always be removed
    *
    * No other results can be deleted and this method will ignore them.
    *
    * @param {UrlbarResult} [result]
    *   The result to delete. If given, it must be present in the controller's
    *   most recent query context. If not given, the currently selected result
@@ -599,49 +603,53 @@ class UrlbarController {
    * @returns {boolean}
    *   Returns true if the result was deleted and false if not.
    */
   handleDeleteEntry(result = undefined) {
     if (!this._lastQueryContextWrapper) {
       Cu.reportError("Cannot delete - the latest query is not present");
       return false;
     }
+    let { queryContext } = this._lastQueryContextWrapper;
 
     if (!result) {
       // No result specified, so use the currently selected result.
       let { selectedElement } = this.input.view;
       if (selectedElement?.classList.contains("urlbarView-button")) {
         // For results with buttons, delete them only when the main part of the
         // row is selected, not a button.
         return false;
       }
       result = this.input.view.selectedResult;
     }
 
     if (!result || result.heuristic) {
       return false;
     }
 
-    // First call `provider.blockResult(result)`.
+    // First call `provider.blockResult()`.
     let provider = UrlbarProvidersManager.getProvider(result.providerName);
     if (!provider) {
       Cu.reportError(`Provider not found: ${result.providerName}`);
     }
-    let blockedByProvider = provider?.tryMethod("blockResult", result);
+    let blockedByProvider = provider?.tryMethod(
+      "blockResult",
+      queryContext,
+      result
+    );
 
     // If the provider didn't block the result, then continue only if the result
     // is from history.
     if (
       !blockedByProvider &&
       result.source != UrlbarUtils.RESULT_SOURCE.HISTORY
     ) {
       return false;
     }
 
-    let { queryContext } = this._lastQueryContextWrapper;
     let index = queryContext.results.indexOf(result);
     if (index < 0) {
       Cu.reportError("Failed to find the selected result in the results");
       return false;
     }
 
     queryContext.results.splice(index, 1);
     this.notify(NOTIFICATIONS.QUERY_RESULT_REMOVED, index);
@@ -947,13 +955,16 @@ class TelemetryEvent {
     let row = element.closest(".urlbarView-row");
     if (row.result && row.result.providerName != "UrlbarProviderTopSites") {
       // Element handlers go here.
       if (element.classList.contains("urlbarView-button-help")) {
         return row.result.type == UrlbarUtils.RESULT_TYPE.TIP
           ? "tiphelp"
           : "help";
       }
+      if (element.classList.contains("urlbarView-button-block")) {
+        return "block";
+      }
     }
     // Now handle the result.
     return UrlbarUtils.telemetryTypeFromResult(row.result);
   }
 }
--- a/browser/components/urlbar/UrlbarPrefs.jsm
+++ b/browser/components/urlbar/UrlbarPrefs.jsm
@@ -50,17 +50,18 @@ const PREF_URLBAR_DEFAULTS = new Map([
   // autofilled even if the user hasn't actually visited them.
   ["autoFill.searchEngines", false],
 
   // Affects the frecency threshold of the autofill algorithm.  The threshold is
   // the mean of all origin frecencies plus one standard deviation multiplied by
   // this value.  See UrlbarProviderPlaces.
   ["autoFill.stddevMultiplier", [0.0, "float"]],
 
-  // Whether best match results can be blocked.
+  // Whether best match results can be blocked. This pref is a fallback for the
+  // Nimbus variable `bestMatchBlockingEnabled`.
   ["bestMatch.blockingEnabled", false],
 
   // Whether the best match feature is enabled.
   ["bestMatch.enabled", true],
 
   // Whether using `ctrl` when hitting return/enter in the URL bar
   // (or clicking 'go') should prefix 'www.' and suffix
   // browser.fixup.alternate.suffix to the URL bar value prior to
@@ -217,17 +218,21 @@ const PREF_URLBAR_DEFAULTS = new Map([
 
   // Whether results will include search suggestions.
   ["suggest.searches", false],
 
   // Whether results will include top sites and the view will open on focus.
   ["suggest.topsites", true],
 
   // JSON'ed array of blocked quick suggest URL digests.
-  ["quickSuggest.blockedDigests", ""],
+  ["quicksuggest.blockedDigests", ""],
+
+  // Whether the usual non-best-match quick suggest results can be blocked. This
+  // pref is a fallback for the Nimbus variable `quickSuggestBlockingEnabled`.
+  ["quicksuggest.blockingEnabled", false],
 
   // Global toggle for whether the quick suggest feature is enabled, i.e.,
   // sponsored and recommended results related to the user's search string.
   ["quicksuggest.enabled", false],
 
   // Whether non-sponsored quick suggest results are subject to impression
   // frequency caps. This pref is a fallback for the Nimbus variable
   // `quickSuggestImpressionCapsNonSponsoredEnabled`.
--- a/browser/components/urlbar/UrlbarProviderQuickSuggest.jsm
+++ b/browser/components/urlbar/UrlbarProviderQuickSuggest.jsm
@@ -41,31 +41,37 @@ const MERINO_ENDPOINT_PARAM_PROVIDERS = 
 
 const TELEMETRY_MERINO_LATENCY = "FX_URLBAR_MERINO_LATENCY_MS";
 const TELEMETRY_MERINO_RESPONSE = "FX_URLBAR_MERINO_RESPONSE";
 
 const TELEMETRY_REMOTE_SETTINGS_LATENCY =
   "FX_URLBAR_QUICK_SUGGEST_REMOTE_SETTINGS_LATENCY_MS";
 
 const TELEMETRY_SCALARS = {
-  IMPRESSION: "contextual.services.quicksuggest.impression",
-  IMPRESSION_SPONSORED_BEST_MATCH:
-    "contextual.services.quicksuggest.impression_sponsored_bestmatch",
-  IMPRESSION_NONSPONSORED_BEST_MATCH:
-    "contextual.services.quicksuggest.impression_nonsponsored_bestmatch",
+  BLOCK_SPONSORED: "contextual.services.quicksuggest.block_sponsored",
+  BLOCK_SPONSORED_BEST_MATCH:
+    "contextual.services.quicksuggest.block_sponsored_bestmatch",
+  BLOCK_NONSPONSORED: "contextual.services.quicksuggest.block_nonsponsored",
+  BLOCK_NONSPONSORED_BEST_MATCH:
+    "contextual.services.quicksuggest.block_nonsponsored_bestmatch",
   CLICK: "contextual.services.quicksuggest.click",
+  CLICK_NONSPONSORED_BEST_MATCH:
+    "contextual.services.quicksuggest.click_nonsponsored_bestmatch",
   CLICK_SPONSORED_BEST_MATCH:
     "contextual.services.quicksuggest.click_sponsored_bestmatch",
-  CLICK_NONSPONSORED_BEST_MATCH:
-    "contextual.services.quicksuggest.click_nonsponsored_bestmatch",
   HELP: "contextual.services.quicksuggest.help",
+  HELP_NONSPONSORED_BEST_MATCH:
+    "contextual.services.quicksuggest.help_nonsponsored_bestmatch",
   HELP_SPONSORED_BEST_MATCH:
     "contextual.services.quicksuggest.help_sponsored_bestmatch",
-  HELP_NONSPONSORED_BEST_MATCH:
-    "contextual.services.quicksuggest.help_nonsponsored_bestmatch",
+  IMPRESSION: "contextual.services.quicksuggest.impression",
+  IMPRESSION_NONSPONSORED_BEST_MATCH:
+    "contextual.services.quicksuggest.impression_nonsponsored_bestmatch",
+  IMPRESSION_SPONSORED_BEST_MATCH:
+    "contextual.services.quicksuggest.impression_sponsored_bestmatch",
 };
 
 const TELEMETRY_EVENT_CATEGORY = "contextservices.quicksuggest";
 
 // This object maps impression stats object keys to their corresponding keys in
 // the `extra` object of impression cap telemetry events. The main reason this
 // is necessary is because the keys of the `extra` object are limited to 15
 // characters in length, which some stats object keys exceed. It also forces us
@@ -156,17 +162,17 @@ class ProviderQuickSuggest extends Urlba
   /**
    * Whether this provider should be invoked for the given context.
    * If this method returns false, the providers manager won't start a query
    * with this provider, to save on resources.
    * @param {UrlbarQueryContext} queryContext The query context object
    * @returns {boolean} Whether this provider should be invoked for the search.
    */
   isActive(queryContext) {
-    this._addedResultInLastQuery = false;
+    this._resultFromLastQuery = null;
 
     // If the sources don't include search or the user used a restriction
     // character other than search, don't allow any suggestions.
     if (
       !queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.SEARCH) ||
       (queryContext.restrictSource &&
         queryContext.restrictSource != UrlbarUtils.RESULT_SOURCE.SEARCH)
     ) {
@@ -320,17 +326,17 @@ class ProviderQuickSuggest extends Urlba
         suggestion.is_sponsored
           ? "quickSuggestSponsoredIndex"
           : "quickSuggestNonSponsoredIndex"
       );
     }
 
     addCallback(this, result);
 
-    this._addedResultInLastQuery = true;
+    this._resultFromLastQuery = result;
 
     // The user triggered a suggestion. Depending on the experiment the user is
     // enrolled in (if any), we may need to record the Nimbus exposure event.
     //
     // If the user is in a best match experiment:
     //   Record if the suggestion is itself a best match and either of the
     //   following are true:
     //   * The best match feature is enabled (i.e., the user is in a treatment
@@ -356,29 +362,35 @@ class ProviderQuickSuggest extends Urlba
   }
 
   /**
    * Called when the result's block button is picked. If the provider can block
    * the result, it should do so and return true. If the provider cannot block
    * the result, it should return false. The meaning of "blocked" depends on the
    * provider and the type of result.
    *
+   * @param {UrlbarQueryContext} queryContext
    * @param {UrlbarResult} result
-   *   The result that was picked.
+   *   The result that should be blocked.
    * @returns {boolean}
    *   Whether the result was blocked.
    */
-  blockResult(result) {
-    if (!UrlbarPrefs.get("bestMatch.blockingEnabled")) {
-      this.logger.info("Blocking disabled, ignoring key shortcut");
+  blockResult(queryContext, result) {
+    if (
+      (!result.isBestMatch &&
+        !UrlbarPrefs.get("quickSuggestBlockingEnabled")) ||
+      (result.isBestMatch && !UrlbarPrefs.get("bestMatchBlockingEnabled"))
+    ) {
+      this.logger.info("Blocking disabled, ignoring block");
       return false;
     }
 
     this.logger.info("Blocking result: " + JSON.stringify(result));
     this.blockSuggestion(result.payload.originalUrl);
+    this._recordEngagementTelemetry(result, queryContext.isPrivate, "block");
     return true;
   }
 
   /**
    * Blocks a suggestion.
    *
    * @param {string} originalUrl
    *   The suggestion's original URL with its unreplaced timestamp template.
@@ -386,17 +398,22 @@ class ProviderQuickSuggest extends Urlba
   async blockSuggestion(originalUrl) {
     this.logger.debug(`Queueing blockSuggestion: ${originalUrl}`);
     await this._blockTaskQueue.queue(async () => {
       this.logger.info(`Blocking suggestion: ${originalUrl}`);
       let digest = await this._getDigest(originalUrl);
       this.logger.debug(`Got digest for '${originalUrl}': ${digest}`);
       this._blockedDigests.add(digest);
       let json = JSON.stringify([...this._blockedDigests]);
-      UrlbarPrefs.set("quickSuggest.blockedDigests", json);
+      this._updatingBlockedDigests = true;
+      try {
+        UrlbarPrefs.set("quicksuggest.blockedDigests", json);
+      } finally {
+        this._updatingBlockedDigests = false;
+      }
       this.logger.debug(`All blocked suggestions: ${json}`);
     });
   }
 
   /**
    * Gets whether a suggestion is blocked.
    *
    * @param {string} originalUrl
@@ -419,17 +436,17 @@ class ProviderQuickSuggest extends Urlba
   /**
    * Unblocks all suggestions.
    */
   async clearBlockedSuggestions() {
     this.logger.debug(`Queueing clearBlockedSuggestions`);
     await this._blockTaskQueue.queue(() => {
       this.logger.info(`Clearing all blocked suggestions`);
       this._blockedDigests.clear();
-      UrlbarPrefs.clear("quickSuggest.blockedDigests");
+      UrlbarPrefs.clear("quicksuggest.blockedDigests");
     });
   }
 
   /**
    * Called when the user starts and ends an engagement with the urlbar.  For
    * details on parameters, see UrlbarProvider.onEngagement().
    *
    * @param {boolean} isPrivate
@@ -441,50 +458,68 @@ class ProviderQuickSuggest extends Urlba
    *   The engagement's query context.  This is *not* guaranteed to be defined
    *   when `state` is "start".  It will always be defined for "engagement" and
    *   "abandonment".
    * @param {object} details
    *   This is defined only when `state` is "engagement" or "abandonment", and
    *   it describes the search string and picked result.
    */
   onEngagement(isPrivate, state, queryContext, details) {
-    if (!this._addedResultInLastQuery) {
+    if (!this._resultFromLastQuery) {
       return;
     }
-    this._addedResultInLastQuery = false;
+    let result = this._resultFromLastQuery;
+    this._resultFromLastQuery = null;
 
     // Per spec, we count impressions only when the user picks a result, i.e.,
     // when `state` is "engagement".
-    if (state != "engagement") {
-      return;
+    if (state == "engagement") {
+      this._recordEngagementTelemetry(
+        result,
+        isPrivate,
+        details.selIndex == result.rowIndex ? details.selType : ""
+      );
     }
+  }
 
-    // Get the index of the quick suggest result. Usually it will be last, so to
-    // avoid an O(n) lookup in the common case, check the last result first. It
-    // may not be last if `browser.urlbar.showSearchSuggestionsFirst` is false
-    // or its position is configured differently via Nimbus.
-    let resultIndex = queryContext.results.length - 1;
-    let result = queryContext.results[resultIndex];
-    if (result.providerName != this.name) {
-      resultIndex = queryContext.results.findIndex(
-        r => r.providerName == this.name
-      );
-      if (resultIndex < 0) {
-        this.logger.error(`Could not find quick suggest result`);
-        return;
-      }
-      result = queryContext.results[resultIndex];
-    }
-
+  /**
+   * Records engagement telemetry. This should be called only at the end of an
+   * engagement when a quick suggest result is present or when a quick suggest
+   * result is blocked.
+   *
+   * @param {UrlbarResult} result
+   *   The quick suggest result that was present (and possibly picked) at the
+   *   end of the engagement or that was blocked.
+   * @param {boolean} isPrivate
+   *   Whether the engagement is in a private context.
+   * @param {string} selType
+   *   This parameter indicates the part of the row the user picked, if any, and
+   *   should be one of the following values:
+   *
+   *   - "": The user didn't pick the row or any part of it
+   *   - "quicksuggest": The user picked the main part of the row
+   *   - "help": The user picked the help button
+   *   - "block": The user picked the block button or used the key shortcut
+   *
+   *   An empty string means the user picked some other row to end the
+   *   engagement, not the quick suggest row. In that case only impression
+   *   telemetry will be recorded.
+   *
+   *   A non-empty string means the user picked the quick suggest row or some
+   *   part of it, and both impression and click telemetry will be recorded. The
+   *   non-empty-string values come from the `details.selType` passed in to
+   *   `onEngagement()`; see `TelemetryEvent.typeFromElement()`.
+   */
+  _recordEngagementTelemetry(result, isPrivate, selType) {
     // Update impression stats.
     this._updateImpressionStats(result.payload.isSponsored);
 
-    // Record telemetry.  We want to record the 1-based index of the result, so
-    // add 1 to the 0-based resultIndex.
-    let telemetryResultIndex = resultIndex + 1;
+    // Indexes recorded in quick suggest telemetry are 1-based, so add 1 to the
+    // 0-based `result.rowIndex`.
+    let telemetryResultIndex = result.rowIndex + 1;
 
     // impression scalars
     Services.telemetry.keyedScalarAdd(
       TELEMETRY_SCALARS.IMPRESSION,
       telemetryResultIndex,
       1
     );
     if (result.isBestMatch) {
@@ -492,52 +527,88 @@ class ProviderQuickSuggest extends Urlba
         result.payload.isSponsored
           ? TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH
           : TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH,
         telemetryResultIndex,
         1
       );
     }
 
-    if (details.selIndex == resultIndex) {
-      // click or help scalars
-      Services.telemetry.keyedScalarAdd(
-        details.selType == "help"
-          ? TELEMETRY_SCALARS.HELP
-          : TELEMETRY_SCALARS.CLICK,
-        telemetryResultIndex,
-        1
-      );
-      if (result.isBestMatch) {
-        if (details.selType == "help") {
-          Services.telemetry.keyedScalarAdd(
+    // scalars related to clicking the result and other elements in its row
+    let clickScalars = [];
+    switch (selType) {
+      case "quicksuggest":
+        clickScalars.push(TELEMETRY_SCALARS.CLICK);
+        if (result.isBestMatch) {
+          clickScalars.push(
+            result.payload.isSponsored
+              ? TELEMETRY_SCALARS.CLICK_SPONSORED_BEST_MATCH
+              : TELEMETRY_SCALARS.CLICK_NONSPONSORED_BEST_MATCH
+          );
+        }
+        break;
+      case "help":
+        clickScalars.push(TELEMETRY_SCALARS.HELP);
+        if (result.isBestMatch) {
+          clickScalars.push(
             result.payload.isSponsored
               ? TELEMETRY_SCALARS.HELP_SPONSORED_BEST_MATCH
-              : TELEMETRY_SCALARS.HELP_NONSPONSORED_BEST_MATCH,
-            telemetryResultIndex,
-            1
+              : TELEMETRY_SCALARS.HELP_NONSPONSORED_BEST_MATCH
           );
-        } else {
-          Services.telemetry.keyedScalarAdd(
+        }
+        break;
+      case "block":
+        clickScalars.push(
+          result.payload.isSponsored
+            ? TELEMETRY_SCALARS.BLOCK_SPONSORED
+            : TELEMETRY_SCALARS.BLOCK_NONSPONSORED
+        );
+        if (result.isBestMatch) {
+          clickScalars.push(
             result.payload.isSponsored
-              ? TELEMETRY_SCALARS.CLICK_SPONSORED_BEST_MATCH
-              : TELEMETRY_SCALARS.CLICK_NONSPONSORED_BEST_MATCH,
-            telemetryResultIndex,
-            1
+              ? TELEMETRY_SCALARS.BLOCK_SPONSORED_BEST_MATCH
+              : TELEMETRY_SCALARS.BLOCK_NONSPONSORED_BEST_MATCH
           );
         }
+        break;
+      default:
+        if (selType) {
+          this.logger.error(
+            "Engagement telemetry error, unknown selType: " + selType
+          );
+        }
+        break;
+    }
+    for (let scalar of clickScalars) {
+      Services.telemetry.keyedScalarAdd(scalar, telemetryResultIndex, 1);
+    }
+
+    // engagement event
+    let match_type = result.isBestMatch ? "best-match" : "firefox-suggest";
+    Services.telemetry.recordEvent(
+      TELEMETRY_EVENT_CATEGORY,
+      "engagement",
+      selType == "quicksuggest" ? "click" : selType || "impression_only",
+      "",
+      {
+        match_type,
+        position: String(telemetryResultIndex),
+        suggestion_type: result.payload.isSponsored
+          ? "sponsored"
+          : "nonsponsored",
       }
-    }
+    );
 
     // Send the custom impression and click pings
     if (!isPrivate) {
-      let is_clicked =
-        details.selIndex == resultIndex && details.selType !== "help";
+      // `is_clicked` is whether the user clicked the suggestion. `selType` will
+      // be "quicksuggest" in that case. See this method's JSDoc for all
+      // possible `selType` values.
+      let is_clicked = selType == "quicksuggest";
       let scenario = UrlbarPrefs.get("quicksuggest.scenario");
-      let match_type = result.isBestMatch ? "best-match" : "firefox-suggest";
 
       // Always use lowercase to make the reporting consistent
       let advertiser = result.payload.sponsoredAdvertiser.toLocaleLowerCase();
 
       // impression
       PartnerLinkAttribution.sendContextualServicesPing(
         {
           advertiser,
@@ -573,19 +644,23 @@ class ProviderQuickSuggest extends Urlba
   /**
    * Called when a urlbar pref changes.
    *
    * @param {string} pref
    *   The name of the pref relative to `browser.urlbar`.
    */
   onPrefChanged(pref) {
     switch (pref) {
-      case "quickSuggest.blockedDigests":
-        this.logger.info("browser.urlbar.quickSuggest.blockedDigests changed");
-        this._loadBlockedDigests();
+      case "quicksuggest.blockedDigests":
+        if (!this._updatingBlockedDigests) {
+          this.logger.info(
+            "browser.urlbar.quicksuggest.blockedDigests changed"
+          );
+          this._loadBlockedDigests();
+        }
         break;
       case "quicksuggest.impressionCaps.stats":
         if (!this._updatingImpressionStats) {
           this.logger.info(
             "browser.urlbar.quicksuggest.impressionCaps.stats changed"
           );
           this._loadImpressionStats();
         }
@@ -1287,19 +1362,19 @@ class ProviderQuickSuggest extends Urlba
 
   /**
    * Loads blocked suggestion digests from the pref into `_blockedDigests`.
    */
   async _loadBlockedDigests() {
     this.logger.debug(`Queueing _loadBlockedDigests`);
     await this._blockTaskQueue.queue(() => {
       this.logger.info(`Loading blocked suggestion digests`);
-      let json = UrlbarPrefs.get("quickSuggest.blockedDigests");
+      let json = UrlbarPrefs.get("quicksuggest.blockedDigests");
       this.logger.debug(
-        `browser.urlbar.quickSuggest.blockedDigests value: ${json}`
+        `browser.urlbar.quicksuggest.blockedDigests value: ${json}`
       );
       if (!json) {
         this.logger.info(`There are no blocked suggestion digests`);
         this._blockedDigests.clear();
       } else {
         try {
           this._blockedDigests = new Set(JSON.parse(json));
           this.logger.info(`Successfully loaded blocked suggestion digests`);
@@ -1354,18 +1429,18 @@ class ProviderQuickSuggest extends Urlba
   }
 
   // The most recently cached value of `UrlbarPrefs.get("quickSuggestEnabled")`.
   // The purpose of this property is only to detect changes in the feature's
   // enabled status. To determine the current status, call
   // `UrlbarPrefs.get("quickSuggestEnabled")` directly instead.
   _quickSuggestEnabled = false;
 
-  // Whether we added a result during the most recent query.
-  _addedResultInLastQuery = false;
+  // The result we added during the most recent query.
+  _resultFromLastQuery = null;
 
   // An object that keeps track of impression stats per sponsored and
   // non-sponsored suggestion types. It looks like this:
   //
   //   { sponsored: statsArray, nonsponsored: statsArray }
   //
   // The `statsArray` values are arrays of stats objects, one per impression
   // cap, which look like this:
@@ -1414,17 +1489,20 @@ class ProviderQuickSuggest extends Urlba
   //
   // The only reason we use URL digests is that suggestions currently do not
   // have persistent IDs. We could use the URLs themselves but SHA-1 digests are
   // only 40 chars long, so they save a little space. This is also consistent
   // with how blocked tiles on the newtab page are stored, but they use MD5. We
   // do *not* store digests for any security or obfuscation reason.
   //
   // This value is serialized as a JSON'ed array to the
-  // `browser.urlbar.quickSuggest.blockedDigests` pref.
+  // `browser.urlbar.quicksuggest.blockedDigests` pref.
   _blockedDigests = new Set();
 
   // Used to serialize access to blocked suggestions. This is only necessary
   // because getting a suggestion's URL digest is async.
   _blockTaskQueue = new TaskQueue();
+
+  // Whether blocked digests are currently being updated.
+  _updatingBlockedDigests = false;
 }
 
 var UrlbarProviderQuickSuggest = new ProviderQuickSuggest();
--- a/browser/components/urlbar/UrlbarUtils.jsm
+++ b/browser/components/urlbar/UrlbarUtils.jsm
@@ -145,16 +145,17 @@ var UrlbarUtils = {
     remotetab: 9,
     extension: 10,
     "preloaded-top-site": 11, // This is currently unused.
     tip: 12,
     topsite: 13,
     formhistory: 14,
     dynamic: 15,
     tabtosearch: 16,
+    quicksuggest: 17,
     // n_values = 32, so you'll need to create a new histogram if you need more.
   },
 
   // This defines icon locations that are commonly used in the UI.
   ICON: {
     // DEFAULT is defined lazily so it doesn't eagerly initialize PlacesUtils.
     EXTENSION: "chrome://mozapps/skin/extensions/extension.svg",
     HISTORY: "chrome://browser/skin/history.svg",
@@ -1157,16 +1158,19 @@ var UrlbarUtils = {
           return "autofill";
         }
         if (
           result.source == UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL &&
           result.heuristic
         ) {
           return "visiturl";
         }
+        if (result.providerName == "UrlbarProviderQuickSuggest") {
+          return "quicksuggest";
+        }
         return result.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS
           ? "bookmark"
           : "history";
       case UrlbarUtils.RESULT_TYPE.KEYWORD:
         return "keyword";
       case UrlbarUtils.RESULT_TYPE.OMNIBOX:
         return "extension";
       case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
@@ -1839,23 +1843,24 @@ class UrlbarProvider {
   pickResult(result, element) {}
 
   /**
    * Called when the result's block button is picked. If the provider can block
    * the result, it should do so and return true. If the provider cannot block
    * the result, it should return false. The meaning of "blocked" depends on the
    * provider and the type of result.
    *
+   * @param {UrlbarQueryContext} queryContext
    * @param {UrlbarResult} result
-   *   The result that was picked.
+   *   The result that should be blocked.
    * @returns {boolean}
    *   Whether the result was blocked.
    * @abstract
    */
-  blockResult(result) {
+  blockResult(queryContext, result) {
     return false;
   }
 
   /**
    * Called when the user starts and ends an engagement with the urlbar.
    *
    * @param {boolean} isPrivate
    *   True if the engagement is in a private context.
--- a/browser/components/urlbar/UrlbarView.jsm
+++ b/browser/components/urlbar/UrlbarView.jsm
@@ -1146,18 +1146,24 @@ class UrlbarView {
 
     let url = this._createElement("span");
     url.className = "urlbarView-url";
     item._content.appendChild(url);
     item._elements.set("url", url);
 
     // Usually we create all child elements for the row regardless of whether
     // the specific result will use them, but we don't expect the vast majority
-    // of results to have help URLs, so as an optimization, only create the help
-    // button if the result will use it.
+    // of rows to need help or block buttons, so as an optimization, create them
+    // only when necessary.
+    if (
+      result.providerName == "UrlbarProviderQuickSuggest" &&
+      UrlbarPrefs.get("quickSuggestBlockingEnabled")
+    ) {
+      this._addRowButton(item, "block", "firefox-suggest-urlbar-block");
+    }
     if (result.payload.helpUrl) {
       this._addRowButton(item, "help", result.payload.helpL10nId);
     }
   }
 
   _createRowContentForTip(item) {
     // We use role="group" so screen readers will read the group's label when a
     // button inside it gets focus. (Screen readers don't do this for
@@ -1280,17 +1286,17 @@ class UrlbarView {
     top.appendChild(url);
     item._elements.set("url", url);
 
     let bottom = this._createElement("div");
     bottom.className = "urlbarView-row-body-bottom";
     body.appendChild(bottom);
     item._elements.set("bottom", bottom);
 
-    if (UrlbarPrefs.get("bestMatch.blockingEnabled")) {
+    if (UrlbarPrefs.get("bestMatchBlockingEnabled")) {
       this._addRowButton(item, "block", "firefox-suggest-urlbar-block");
     }
     if (result.payload.helpUrl) {
       this._addRowButton(item, "help", result.payload.helpL10nId);
     }
   }
 
   _addRowButton(item, name, l10nID) {
@@ -1331,17 +1337,24 @@ class UrlbarView {
       (oldResultType == UrlbarUtils.RESULT_TYPE.TIP) !=
         (result.type == UrlbarUtils.RESULT_TYPE.TIP) ||
       (oldResultType == UrlbarUtils.RESULT_TYPE.DYNAMIC) !=
         (result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC) ||
       (oldResultType == UrlbarUtils.RESULT_TYPE.DYNAMIC &&
         result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC &&
         oldResult.dynamicType != result.dynamicType) ||
       oldResult.isBestMatch != result.isBestMatch ||
-      !!result.payload.helpUrl != item._buttons.has("help");
+      !!result.payload.helpUrl != item._buttons.has("help") ||
+      (result.isBestMatch &&
+        UrlbarPrefs.get("bestMatchBlockingEnabled") !=
+          item._buttons.has("block")) ||
+      (!result.isBestMatch &&
+        result.providerName == "UrlbarProviderQuickSuggest" &&
+        UrlbarPrefs.get("quickSuggestBlockingEnabled") !=
+          item._buttons.has("block"));
 
     if (needsNewContent) {
       while (item.lastChild) {
         item.lastChild.remove();
       }
       item._elements.clear();
       item._buttons.clear();
       item._content = this._createElement("span");
--- a/browser/components/urlbar/docs/firefox-suggest-telemetry.rst
+++ b/browser/components/urlbar/docs/firefox-suggest-telemetry.rst
@@ -113,16 +113,74 @@ Changelog
   Firefox 99.0
     Introduced firefoxSuggestBestMatch. [Bug 1755100_]
     Introduced firefoxSuggestBestMatchLearnMore. [Bug 1756917_]
 
 .. _1735976: https://bugzilla.mozilla.org/show_bug.cgi?id=1735976
 .. _1755100: https://bugzilla.mozilla.org/show_bug.cgi?id=1755100
 .. _1756917: https://bugzilla.mozilla.org/show_bug.cgi?id=1756917
 
+contextual.services.quicksuggest.block_nonsponsored
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This keyed scalar is incremented each time the user dismisses ("blocks") a
+non-sponsored suggestion, including both best matches and the usual
+non-best-match suggestions. Each key is the index at which a suggestion appeared
+in the results (1-based), and the corresponding value is the number of
+dismissals at that index.
+
+Changelog
+  Firefox 101.0
+    Introduced. [Bug 1761059_]
+
+.. _1761059: https://bugzilla.mozilla.org/show_bug.cgi?id=1761059
+
+contextual.services.quicksuggest.block_nonsponsored_bestmatch
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This keyed scalar is incremented each time the user dismisses ("blocks") a
+non-sponsored best match. Each key is the index at which a suggestion appeared
+in the results (1-based), and the corresponding value is the number of
+dismissals at that index.
+
+Changelog
+  Firefox 101.0
+    Introduced. [Bug 1761059_]
+
+.. _1761059: https://bugzilla.mozilla.org/show_bug.cgi?id=1761059
+
+contextual.services.quicksuggest.block_sponsored
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This keyed scalar is incremented each time the user dismisses ("blocks") a
+sponsored suggestion, including both best matches and the usual non-best-match
+suggestions. Each key is the index at which a suggestion appeared in the results
+(1-based), and the corresponding value is the number of dismissals at that
+index.
+
+Changelog
+  Firefox 101.0
+    Introduced. [Bug 1761059_]
+
+.. _1761059: https://bugzilla.mozilla.org/show_bug.cgi?id=1761059
+
+contextual.services.quicksuggest.block_sponsored_bestmatch
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This keyed scalar is incremented each time the user dismisses ("blocks") a
+sponsored best match. Each key is the index at which a suggestion appeared in
+the results (1-based), and the corresponding value is the number of dismissals
+at that index.
+
+Changelog
+  Firefox 101.0
+    Introduced. [Bug 1761059_]
+
+.. _1761059: https://bugzilla.mozilla.org/show_bug.cgi?id=1761059
+
 contextual.services.quicksuggest.click
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 This keyed scalar is incremented each time the user picks a suggestion. Each key
 is the index at which a suggestion appeared in the results (1-based), and the
 corresponding value is the number of clicks at that index.
 
 Changelog
@@ -328,29 +386,68 @@ Changelog
     The event is no longer recorded when the user interacts with the online
     modal dialog since the ``browser.urlbar.suggest.quicksuggest.nonsponsored``
     pref is no longer set when the user opts in or out. [Bug 1740965_]
 
 .. _1693126: https://bugzilla.mozilla.org/show_bug.cgi?id=1693126
 .. _1735976: https://bugzilla.mozilla.org/show_bug.cgi?id=1735976
 .. _1740965: https://bugzilla.mozilla.org/show_bug.cgi?id=1740965
 
+contextservices.quicksuggest.engagement
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This event is recorded when an engagement occurs in the address bar while a
+Firefox Suggest suggestion is present. In other words, it is recorded in two
+cases:
+
+- The user picks a Firefox Suggest suggestion or a related UI element like its
+  help button.
+- While a Firefox Suggest suggestion is present in the address bar, the user
+  picks some other row.
+
+The event's objects are the following possible values:
+
+:block:
+  The user dismissed ("blocked") the suggestion.
+:click:
+  The user picked the suggestion.
+:help:
+  The user picked the suggestion's help button.
+:impression_only:
+  The user picked some other row.
+
+The event's ``extra`` contains the following properties:
+
+:match_type:
+  "best-match" if the suggestion was a best match or "firefox-suggest" if it was
+  a non-best-match suggestion.
+:position:
+  The index of the suggestion in the list of results (1-based).
+:suggestion_type:
+  The type of suggestion, one of: "sponsored", "nonsponsored"
+
+Changelog
+  Firefox 101.0
+    Introduced. [Bug 1761059_]
+
+.. _1761059: https://bugzilla.mozilla.org/show_bug.cgi?id=1761059
+
 contextservices.quicksuggest.impression_cap
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 This event is recorded when an event related to an impression cap occurs. The
 event's objects are the following possible values:
 
 :hit:
   Recorded when an impression cap is hit.
 :reset:
   Recorded when a cap's counter is reset because its interval period has
   elapsed.
 
-The event's ``extra`` object value contains the following properties:
+The event's ``extra`` contains the following properties:
 
 :count:
   The number of impressions during the cap's interval period.
 :endDate:
   The timestamp at which the cap's interval period will end (for "hit" events)
   or did end (for "reset" events), in number of milliseconds since Unix epoch.
   For lifetime caps, this value will be "Infinity".
 :eventDate:
@@ -363,18 +460,18 @@ The event's ``extra`` object value conta
   The timestamp of the most recent impression, in number of milliseconds since
   Unix epoch.
 :intervalSeconds:
   The number of seconds in the cap's interval period. For lifetime caps, this
   value will be "Infinity".
 :maxCount:
   The maximum number of impressions allowed in the cap's interval period.
 :startDate:
-  The timestamp at which the cap's interval period started, in number of seconds
-  since Unix epoch.
+  The timestamp at which the cap's interval period started, in number of
+  milliseconds since Unix epoch.
 :type:
   The type of cap, one of: "sponsored", "nonsponsored"
 
 Changelog
   Firefox 101.0
     Introduced. [Bug 1761058_]
 
 .. _1761058: https://bugzilla.mozilla.org/show_bug.cgi?id=1761058
@@ -701,27 +798,31 @@ Impression
 An impression ping is recorded when the user is shown a suggestion and the
 following two conditions hold:
 
 - The user has completed an engagement with the address bar by picking a result
   in it or by pressing the Enter key.
 - At the time the user completed the engagement, a suggestion was present in the
   results.
 
+It is also recorded when the user dismisses ("blocks") a suggestion.
+
 The impression ping payload contains the following:
 
 :advertiser:
   The name of the suggestion's advertiser.
 :block_id:
   A unique identifier for the suggestion (a.k.a. a keywords block).
 :context_id:
   A UUID representing this user. Note that it's not client_id, nor can it be
   used to link to a client_id.
 :is_clicked:
-  Whether or not the user also clicked the suggestion.
+  Whether or not the user also clicked the suggestion. When true, we will also
+  send a separate click ping. When the impression ping is recorded because the
+  user dismissed ("blocked") the suggestion, this will be false.
 :matched_keywords (**Removed from Firefox 97**):
   The matched keywords that lead to the suggestion. This is only included when
   the user has opted in to data collection and the suggestion is provided by
   remote settings.
 :match_type:
   "best-match" if the suggestion was a best match or "firefox-suggest" if it was
   a non-best-match suggestion.
 :position:
@@ -768,24 +869,29 @@ Changelog
 
   Firefox 97.0
     - Stop sending ``search_query`` and ``matched_keywords`` in the custom
       impression ping for Firefox Suggest. [Bug 1748348_]
 
   Firefox 99.0
     ``match_type`` is added to the payload. [Bug 1754622_]
 
+  Firefox 101.0
+    The impression ping is now also recorded when the user dismisses ("blocks")
+    a suggestion. [Bug 1761059_]
+
 .. _1689365: https://bugzilla.mozilla.org/show_bug.cgi?id=1689365
 .. _1725492: https://bugzilla.mozilla.org/show_bug.cgi?id=1725492
 .. _1728188: https://bugzilla.mozilla.org/show_bug.cgi?id=1728188
 .. _1729576: https://bugzilla.mozilla.org/show_bug.cgi?id=1729576
 .. _1736117: https://bugzilla.mozilla.org/show_bug.cgi?id=1736117
 .. _1735976: https://bugzilla.mozilla.org/show_bug.cgi?id=1735976
 .. _1748348: https://bugzilla.mozilla.org/show_bug.cgi?id=1748348
 .. _1754622: https://bugzilla.mozilla.org/show_bug.cgi?id=1754622
+.. _1761059: https://bugzilla.mozilla.org/show_bug.cgi?id=1761059
 
 Nimbus Exposure Event
 ---------------------
 
 A `Nimbus exposure event`_ is recorded once per app session when the user first
 encounters the UI of an experiment in which they're enrolled. The timing of the
 event depends on the experiment.
 
--- a/browser/components/urlbar/docs/telemetry.rst
+++ b/browser/components/urlbar/docs/telemetry.rst
@@ -218,16 +218,18 @@ urlbar.picked.*
   - ``extension``
     Added by an add-on through the omnibox WebExtension API.
   - ``formhistory``
     A search suggestion from previous search history.
   - ``history``
     A URL from history.
   - ``keyword``
     A bookmark keyword.
+  - ``quicksuggest``
+    A Firefox Suggest (a.k.a. quick suggest) suggestion.
   - ``remotetab``
     A tab synced from another device.
   - ``searchengine``
     A search result, but not a suggestion. May be the default search action
     or a search alias.
   - ``searchsuggestion``
     A remote search suggestion.
   - ``switchtab``
@@ -348,21 +350,22 @@ Event Extra
     containing spaces in its query parameters, for example.
   - ``selType``
     The type of the selected result at the time of submission.
     This is only present for ``engagement`` events.
     It can be one of: ``none``, ``autofill``, ``visiturl``, ``bookmark``,
     ``history``, ``keyword``, ``searchengine``, ``searchsuggestion``,
     ``switchtab``, ``remotetab``, ``extension``, ``oneoff``, ``keywordoffer``,
     ``canonized``, ``tip``, ``tiphelp``, ``formhistory``, ``tabtosearch``,
-    ``help``, ``unknown``
+    ``help``, ``block``, ``quicksuggest``, ``unknown``
     In practice, ``tabtosearch`` should not appear in real event telemetry.
     Opening a tab-to-search result enters search mode and entering search mode
     does not currently mark the end of an engagement. It is noted here for
-    completeness.
+    completeness. Similarly, ``block`` indicates a result was blocked or deleted
+    but should not appear because blocking a result does not end the engagement.
   - ``selIndex``
     Index of the selected result in the urlbar panel, or -1 for no selection.
     There won't be a selection when a one-off button is the only selection, and
     for the ``paste_go`` or ``drop_go`` objects. There may also not be a
     selection if the system was busy and results arrived too late, then we
     directly decide whether to search or visit the given string without having
     a fully built result.
     This is only present for ``engagement`` events.
@@ -471,17 +474,17 @@ Obsolete histograms
 ~~~~~~~~~~~~~~~~~~~
 
 FX_URLBAR_SELECTED_RESULT_INDEX (OBSOLETE)
   This probe tracked the indexes of picked results in the results list.
   It was an enumerated histogram with 17 groups.
 
 FX_URLBAR_SELECTED_RESULT_TYPE and FX_URLBAR_SELECTED_RESULT_TYPE_2 (from Firefox 78 on) (OBSOLETE)
   This probe tracked the types of picked results.
-  It was an enumerated histogram with 17 groups:
+  It was an enumerated histogram with the following groups:
 
     0. autofill
     1. bookmark
     2. history
     3. keyword
     4. searchengine
     5. searchsuggestion
     6. switchtab
@@ -490,16 +493,17 @@ FX_URLBAR_SELECTED_RESULT_TYPE and FX_UR
     9. remotetab
     10. extension
     11. preloaded-top-site
     12. tip
     13. topsite
     14. formhistory
     15. dynamic
     16. tabtosearch
+    17. quicksuggest
 
 FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE and FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2 (from Firefox 78 on) (OBSOLETE)
   This probe tracked picked result type, for each one it tracked the index where
   it appeared.
   It was a keyed histogram where the keys were result types (see
   FX_URLBAR_SELECTED_RESULT_TYPE above). For each key, this recorded the indexes
   of picked results for that result type.
 
--- a/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.jsm
+++ b/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.jsm
@@ -331,27 +331,27 @@ class QSTestUtils {
     );
 
     let helpButton = row._buttons.get("help");
     this.Assert.ok(helpButton, "The help button should be present");
     this.Assert.equal(result.payload.helpUrl, LEARN_MORE_URL, "Result helpURL");
 
     let blockButton = row._buttons.get("block");
     if (!isBestMatch) {
-      this.Assert.ok(
-        !blockButton,
-        "The block button is not present since the row is not a best match"
-      );
-    } else if (!UrlbarPrefs.get("bestMatch.blockingEnabled")) {
-      this.Assert.ok(
-        !blockButton,
-        "The block button is not present since blocking is disabled"
+      this.Assert.equal(
+        !!blockButton,
+        UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+        "The block button is present iff quick suggest blocking is enabled"
       );
     } else {
-      this.Assert.ok(blockButton, "The block button is present");
+      this.Assert.equal(
+        !!blockButton,
+        UrlbarPrefs.get("bestMatchBlockingEnabled"),
+        "The block button is present iff best match blocking is enabled"
+      );
     }
 
     return details;
   }
 
   /**
    * Asserts a result is not a quick suggest result.
    *
@@ -402,16 +402,41 @@ class QSTestUtils {
           !(scalarName in scalars),
           "Scalar should not be present: " + scalarName
         );
       }
     }
   }
 
   /**
+   * Checks quick suggest telemetry events. This is the same as
+   * `TelemetryTestUtils.assertEvents()` except it filters in only quick suggest
+   * events by default. If you are expecting events that are not in the quick
+   * suggest category, use `TelemetryTestUtils.assertEvents()` directly or pass
+   * in a filter override for `category`.
+   *
+   * @param {array} expectedEvents
+   *   List of expected telemetry events.
+   * @param {object} filterOverrides
+   *   Extra properties to set in the filter object.
+   * @param {object} options
+   *   The options object to pass to `TelemetryTestUtils.assertEvents()`.
+   */
+  assertEvents(expectedEvents, filterOverrides = {}, options = undefined) {
+    TelemetryTestUtils.assertEvents(
+      expectedEvents,
+      {
+        category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY,
+        ...filterOverrides,
+      },
+      options
+    );
+  }
+
+  /**
    * Creates a `sinon.sandbox` and `sinon.spy` that can be used to instrument
    * the quick suggest custom telemetry pings. If `init` was called with a test
    * scope where `registerCleanupFunction` is defined, the sandbox will
    * automically be restored at the end of the test.
    *
    * @returns {object}
    *   An object: { sandbox, spy, spyCleanup }
    *   `spyCleanup` is a cleanup function that should be called if you're in a
@@ -451,21 +476,21 @@ class QSTestUtils {
    * @param {string} [request_id]
    *   The expected request_id in the ping.
    * @param {string} [scenario]
    *   The quick suggest scenario, one of: "history", "offline", "online"
    */
   assertImpressionPing({
     index,
     spy,
-    advertiser = "test-advertiser",
+    advertiser = "testadvertiser",
     block_id = 1,
     is_clicked = false,
     match_type = "firefox-suggest",
-    reporting_url = "http://impression.reporting.test.com/",
+    reporting_url = "http://example.com/impression",
     request_id = null,
     scenario = "offline",
   }) {
     // Find the call for `QS_IMPRESSION`.
     let calls = spy.getCalls().filter(call => {
       let endpoint = call.args[1];
       return endpoint.includes(CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION);
     });
@@ -519,20 +544,20 @@ class QSTestUtils {
    * @param {string} [request_id]
    *   The expected request_id in the ping.
    * @param {string} [scenario]
    *   The quick suggest scenario, one of: "history", "offline", "online"
    */
   assertClickPing({
     index,
     spy,
-    advertiser = "test-advertiser",
+    advertiser = "testadvertiser",
     block_id = 1,
     match_type = "firefox-suggest",
-    reporting_url = "http://click.reporting.test.com/",
+    reporting_url = "http://example.com/click",
     request_id = null,
     scenario = "offline",
   }) {
     // Find the call for `QS_SELECTION`.
     let calls = spy.getCalls().filter(call => {
       let endpoint = call.args[1];
       return endpoint.includes(CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION);
     });
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser.ini
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser.ini
@@ -6,13 +6,14 @@
 support-files =
   head.js
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
   subdialog.xhtml
 
 [browser_quicksuggest.js]
 [browser_quicksuggest_bestMatch.js]
+[browser_quicksuggest_block.js]
 [browser_quicksuggest_configuration.js]
 [browser_quicksuggest_indexes.js]
 [browser_quicksuggest_onboardingDialog.js]
 [browser_quicksuggest_telemetry.js]
 tags = search-telemetry
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_bestMatch.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_bestMatch.js
@@ -1,30 +1,26 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// Tests quick suggest best match results. See also:
+// Browser test for the best match feature as it relates to quick suggest. See
+// also:
 //
 // browser_bestMatch.js
 //   Basic view test for best match rows independent of quick suggest
 // test_quicksuggest_bestMatch.js
 //   Tests triggering quick suggest best matches and things that don't depend on
 //   the view
 
 "use strict";
 
-const { timestampTemplate } = UrlbarProviderQuickSuggest;
-
 const SUGGESTIONS = [1, 2, 3].map(i => ({
   id: i,
   title: `Best match ${i}`,
-  // Include the timestamp template in the suggestion URLs so we can make sure
-  // their original URLs with the unreplaced templates are blocked and not their
-  // URLs with timestamps.
-  url: `http://example.com/bestmatch${i}?t=${timestampTemplate}`,
+  url: `http://example.com/bestmatch${i}`,
   keywords: [`bestmatch${i}`],
   click_url: "http://example.com/click",
   impression_url: "http://example.com/impression",
   advertiser: "TestAdvertiser",
   _test_is_best_match: true,
 }));
 
 const NON_BEST_MATCH_SUGGESTION = {
@@ -34,204 +30,31 @@ const NON_BEST_MATCH_SUGGESTION = {
   keywords: ["non"],
   click_url: "http://example.com/click",
   impression_url: "http://example.com/impression",
   advertiser: "TestAdvertiser",
 };
 
 add_task(async function init() {
   await SpecialPowers.pushPrefEnv({
-    set: [
-      ["browser.urlbar.bestMatch.enabled", true],
-      ["browser.urlbar.bestMatch.blockingEnabled", true],
-    ],
+    set: [["browser.urlbar.bestMatch.enabled", true]],
   });
 
   await PlacesUtils.history.clear();
   await PlacesUtils.bookmarks.eraseEverything();
   await UrlbarTestUtils.formHistory.clear();
 
   await UrlbarProviderQuickSuggest._blockTaskQueue.emptyPromise;
   await UrlbarProviderQuickSuggest.clearBlockedSuggestions();
 
   await QuickSuggestTestUtils.ensureQuickSuggestInit(
     SUGGESTIONS.concat(NON_BEST_MATCH_SUGGESTION)
   );
 });
 
-// Picks the block button with the keyboard.
-add_task(async function basicBlock_keyboard() {
-  await doBasicBlockTest(() => {
-    // Arrow down twice to select the block button: once to select the main
-    // part of the best match row, once to select the block button.
-    EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 2 });
-    EventUtils.synthesizeKey("KEY_Enter");
-  });
-});
-
-// Picks the block button with the mouse.
-add_task(async function basicBlock_mouse() {
-  await doBasicBlockTest(blockButton => {
-    EventUtils.synthesizeMouseAtCenter(blockButton, {});
-  });
-});
-
-// Uses the key shortcut to block a best match.
-add_task(async function basicBlock_keyShortcut() {
-  await doBasicBlockTest(() => {
-    // Arrow down once to select the best match row.
-    EventUtils.synthesizeKey("KEY_ArrowDown");
-    EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
-  });
-});
-
-async function doBasicBlockTest(doBlock) {
-  // Do a search that triggers the best match.
-  await UrlbarTestUtils.promiseAutocompleteResultPopup({
-    window,
-    value: SUGGESTIONS[0].keywords[0],
-  });
-  Assert.equal(
-    UrlbarTestUtils.getResultCount(window),
-    2,
-    "Two rows are present after searching (heuristic + best match)"
-  );
-
-  let details = await QuickSuggestTestUtils.assertIsQuickSuggest({
-    window,
-    originalUrl: SUGGESTIONS[0].url,
-    isBestMatch: true,
-  });
-
-  // Block the suggestion.
-  let blockButton = details.element.row._buttons.get("block");
-  doBlock(blockButton);
-
-  // The row should have been removed.
-  Assert.ok(
-    UrlbarTestUtils.isPopupOpen(window),
-    "View remains open after blocking result"
-  );
-  Assert.equal(
-    UrlbarTestUtils.getResultCount(window),
-    1,
-    "Only one row after blocking best match"
-  );
-  await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
-
-  // The URL should be blocked.
-  Assert.ok(
-    await UrlbarProviderQuickSuggest.isSuggestionBlocked(SUGGESTIONS[0].url),
-    "Suggestion is blocked"
-  );
-
-  await UrlbarTestUtils.promisePopupClose(window);
-  await UrlbarProviderQuickSuggest.clearBlockedSuggestions();
-}
-
-// Blocks multiple suggestions one after the other.
-add_task(async function blockMultiple() {
-  for (let i = 0; i < SUGGESTIONS.length; i++) {
-    // Do a search that triggers the i'th best match.
-    let { keywords, url } = SUGGESTIONS[i];
-    await UrlbarTestUtils.promiseAutocompleteResultPopup({
-      window,
-      value: keywords[0],
-    });
-    await QuickSuggestTestUtils.assertIsQuickSuggest({
-      window,
-      originalUrl: url,
-      isBestMatch: true,
-    });
-
-    // Block it.
-    EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 2 });
-    EventUtils.synthesizeKey("KEY_Enter");
-    Assert.ok(
-      await UrlbarProviderQuickSuggest.isSuggestionBlocked(url),
-      "Suggestion is blocked after picking block button"
-    );
-
-    // Make sure all previous suggestions remain blocked and no other
-    // suggestions are blocked yet.
-    for (let j = 0; j < SUGGESTIONS.length; j++) {
-      Assert.equal(
-        await UrlbarProviderQuickSuggest.isSuggestionBlocked(
-          SUGGESTIONS[j].url
-        ),
-        j <= i,
-        `Suggestion at index ${j} is blocked or not as expected`
-      );
-    }
-  }
-
-  await UrlbarTestUtils.promisePopupClose(window);
-  await UrlbarProviderQuickSuggest.clearBlockedSuggestions();
-});
-
-// Tests with blocking disabled.
-add_task(async function blockingDisabled() {
-  await SpecialPowers.pushPrefEnv({
-    set: [["browser.urlbar.bestMatch.blockingEnabled", false]],
-  });
-
-  await UrlbarTestUtils.promiseAutocompleteResultPopup({
-    window,
-    value: SUGGESTIONS[0].keywords[0],
-  });
-
-  let expectedResultCount = 2;
-  Assert.equal(
-    UrlbarTestUtils.getResultCount(window),
-    expectedResultCount,
-    "Two rows are present after searching (heuristic + best match)"
-  );
-
-  // `assertIsQuickSuggest()` asserts that the block button is not present when
-  // `bestMatch.blockingEnabled` is false, but check it again here since it's
-  // central to this test.
-  let details = await QuickSuggestTestUtils.assertIsQuickSuggest({
-    window,
-    originalUrl: SUGGESTIONS[0].url,
-    isBestMatch: true,
-  });
-  Assert.ok(
-    !details.element.row._buttons.get("block"),
-    "Block button is not present"
-  );
-
-  // Arrow down once to select the best match row and then press the key
-  // shortcut to block.
-  EventUtils.synthesizeKey("KEY_ArrowDown");
-  EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
-
-  // Nothing should happen.
-  Assert.ok(
-    UrlbarTestUtils.isPopupOpen(window),
-    "View remains open after key shortcut"
-  );
-  Assert.equal(
-    UrlbarTestUtils.getResultCount(window),
-    expectedResultCount,
-    "Same number of results after key shortcut"
-  );
-  await QuickSuggestTestUtils.assertIsQuickSuggest({
-    window,
-    originalUrl: SUGGESTIONS[0].url,
-    isBestMatch: true,
-  });
-  Assert.ok(
-    !(await UrlbarProviderQuickSuggest.isSuggestionBlocked(SUGGESTIONS[0].url)),
-    "Suggestion is not blocked"
-  );
-
-  await UrlbarTestUtils.promisePopupClose(window);
-  await SpecialPowers.popPrefEnv();
-});
-
 // When the user is enrolled in a best match experiment with the feature enabled
 // (i.e., the treatment branch), the Nimbus exposure event should be recorded
 // after triggering a best match.
 add_task(async function nimbusExposure_featureEnabled() {
   await doNimbusExposureTest({
     bestMatchEnabled: true,
     bestMatchExpected: true,
     isBestMatchExperiment: true,
copy from browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_bestMatch.js
copy to browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_bestMatch.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js
@@ -1,386 +1,395 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// Tests quick suggest best match results. See also:
+// Tests blocking quick suggest results, including best matches. See also:
 //
 // browser_bestMatch.js
-//   Basic view test for best match rows independent of quick suggest
-// test_quicksuggest_bestMatch.js
-//   Tests triggering quick suggest best matches and things that don't depend on
-//   the view
+//   Includes tests for blocking best match rows independent of quick suggest,
+//   especially the superficial UI part that should be common to all types of
+//   best matches
 
 "use strict";
 
 const { timestampTemplate } = UrlbarProviderQuickSuggest;
 
-const SUGGESTIONS = [1, 2, 3].map(i => ({
-  id: i,
-  title: `Best match ${i}`,
-  // Include the timestamp template in the suggestion URLs so we can make sure
-  // their original URLs with the unreplaced templates are blocked and not their
-  // URLs with timestamps.
-  url: `http://example.com/bestmatch${i}?t=${timestampTemplate}`,
-  keywords: [`bestmatch${i}`],
-  click_url: "http://example.com/click",
-  impression_url: "http://example.com/impression",
-  advertiser: "TestAdvertiser",
-  _test_is_best_match: true,
-}));
+// Include the timestamp template in the suggestion URLs so we can make sure
+// their original URLs with the unreplaced templates are blocked and not their
+// URLs with timestamps.
+const SUGGESTIONS = [
+  {
+    id: 1,
+    url: `http://example.com/sponsored?t=${timestampTemplate}`,
+    title: "Sponsored suggestion",
+    keywords: ["sponsored"],
+    click_url: "http://example.com/click",
+    impression_url: "http://example.com/impression",
+    advertiser: "TestAdvertiser",
+  },
+  {
+    id: 2,
+    url: `http://example.com/nonsponsored?t=${timestampTemplate}`,
+    title: "Non-sponsored suggestion",
+    keywords: ["nonsponsored"],
+    click_url: "http://example.com/click",
+    impression_url: "http://example.com/impression",
+    advertiser: "TestAdvertiser",
+    iab_category: "5 - Education",
+  },
+];
 
-const NON_BEST_MATCH_SUGGESTION = {
-  id: 99,
-  title: "Non-best match",
-  url: "http://example.com/nonbestmatch",
-  keywords: ["non"],
-  click_url: "http://example.com/click",
-  impression_url: "http://example.com/impression",
-  advertiser: "TestAdvertiser",
-};
+// Spy for the custom impression/click sender
+let spy;
 
 add_task(async function init() {
   await SpecialPowers.pushPrefEnv({
     set: [
-      ["browser.urlbar.bestMatch.enabled", true],
       ["browser.urlbar.bestMatch.blockingEnabled", true],
+      ["browser.urlbar.quicksuggest.blockingEnabled", true],
     ],
   });
 
+  ({ spy } = QuickSuggestTestUtils.createTelemetryPingSpy());
+
   await PlacesUtils.history.clear();
   await PlacesUtils.bookmarks.eraseEverything();
   await UrlbarTestUtils.formHistory.clear();
 
   await UrlbarProviderQuickSuggest._blockTaskQueue.emptyPromise;
   await UrlbarProviderQuickSuggest.clearBlockedSuggestions();
 
-  await QuickSuggestTestUtils.ensureQuickSuggestInit(
-    SUGGESTIONS.concat(NON_BEST_MATCH_SUGGESTION)
-  );
+  Services.telemetry.clearScalars();
+  Services.telemetry.clearEvents();
+
+  await QuickSuggestTestUtils.ensureQuickSuggestInit(SUGGESTIONS);
 });
 
+/**
+ * Adds a test task that runs the given callback with combinations of the
+ * following:
+ *
+ * - Best match disabled and enabled
+ * - Each suggestion in `SUGGESTIONS`
+ *
+ * @param {function} fn
+ *   The callback function. It's passed: `{ isBestMatch, suggestion }`
+ */
+function add_combo_task(fn) {
+  let taskFn = async () => {
+    for (let isBestMatch of [false, true]) {
+      UrlbarPrefs.set("bestMatch.enabled", isBestMatch);
+      for (let suggestion of SUGGESTIONS) {
+        info(
+          `Running ${fn.name}: ${JSON.stringify({ isBestMatch, suggestion })}`
+        );
+        await fn({ isBestMatch, suggestion });
+      }
+      UrlbarPrefs.clear("bestMatch.enabled");
+    }
+  };
+  Object.defineProperty(taskFn, "name", { value: fn.name });
+  add_task(taskFn);
+}
+
 // Picks the block button with the keyboard.
-add_task(async function basicBlock_keyboard() {
-  await doBasicBlockTest(() => {
-    // Arrow down twice to select the block button: once to select the main
-    // part of the best match row, once to select the block button.
-    EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 2 });
-    EventUtils.synthesizeKey("KEY_Enter");
+add_combo_task(async function basic_keyboard({ suggestion, isBestMatch }) {
+  await doBasicBlockTest({
+    suggestion,
+    isBestMatch,
+    block: () => {
+      // Arrow down twice to select the block button: once to select the main
+      // part of the row, once to select the block button.
+      EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 2 });
+      EventUtils.synthesizeKey("KEY_Enter");
+    },
   });
 });
 
 // Picks the block button with the mouse.
-add_task(async function basicBlock_mouse() {
-  await doBasicBlockTest(blockButton => {
-    EventUtils.synthesizeMouseAtCenter(blockButton, {});
+add_combo_task(async function basic_mouse({ suggestion, isBestMatch }) {
+  await doBasicBlockTest({
+    suggestion,
+    isBestMatch,
+    block: blockButton => {
+      EventUtils.synthesizeMouseAtCenter(blockButton, {});
+    },
   });
 });
 
-// Uses the key shortcut to block a best match.
-add_task(async function basicBlock_keyShortcut() {
-  await doBasicBlockTest(() => {
-    // Arrow down once to select the best match row.
-    EventUtils.synthesizeKey("KEY_ArrowDown");
-    EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+// Uses the key shortcut to block a suggestion.
+add_combo_task(async function basic_keyShortcut({ suggestion, isBestMatch }) {
+  await doBasicBlockTest({
+    suggestion,
+    isBestMatch,
+    block: () => {
+      // Arrow down once to select the row.
+      EventUtils.synthesizeKey("KEY_ArrowDown");
+      EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+    },
   });
 });
 
-async function doBasicBlockTest(doBlock) {
-  // Do a search that triggers the best match.
+async function doBasicBlockTest({ suggestion, isBestMatch, block }) {
+  spy.resetHistory();
+
+  // Do a search that triggers the suggestion.
   await UrlbarTestUtils.promiseAutocompleteResultPopup({
     window,
-    value: SUGGESTIONS[0].keywords[0],
+    value: suggestion.keywords[0],
   });
   Assert.equal(
     UrlbarTestUtils.getResultCount(window),
     2,
-    "Two rows are present after searching (heuristic + best match)"
+    "Two rows are present after searching (heuristic + suggestion)"
   );
 
+  let isSponsored = suggestion.keywords[0] == "sponsored";
   let details = await QuickSuggestTestUtils.assertIsQuickSuggest({
     window,
-    originalUrl: SUGGESTIONS[0].url,
-    isBestMatch: true,
+    isBestMatch,
+    isSponsored,
+    originalUrl: suggestion.url,
   });
 
   // Block the suggestion.
   let blockButton = details.element.row._buttons.get("block");
-  doBlock(blockButton);
+  await block(blockButton);
 
   // The row should have been removed.
   Assert.ok(
     UrlbarTestUtils.isPopupOpen(window),
     "View remains open after blocking result"
   );
   Assert.equal(
     UrlbarTestUtils.getResultCount(window),
     1,
-    "Only one row after blocking best match"
+    "Only one row after blocking suggestion"
   );
   await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
 
   // The URL should be blocked.
   Assert.ok(
-    await UrlbarProviderQuickSuggest.isSuggestionBlocked(SUGGESTIONS[0].url),
+    await UrlbarProviderQuickSuggest.isSuggestionBlocked(suggestion.url),
     "Suggestion is blocked"
   );
 
+  // Check telemetry scalars.
+  let index = 2;
+  let scalars = {
+    [QuickSuggestTestUtils.SCALARS.IMPRESSION]: index,
+  };
+  if (isSponsored) {
+    scalars[QuickSuggestTestUtils.SCALARS.BLOCK_SPONSORED] = index;
+  } else {
+    scalars[QuickSuggestTestUtils.SCALARS.BLOCK_NONSPONSORED] = index;
+  }
+  if (isBestMatch) {
+    if (isSponsored) {
+      scalars = {
+        ...scalars,
+        [QuickSuggestTestUtils.SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: index,
+        [QuickSuggestTestUtils.SCALARS.BLOCK_SPONSORED_BEST_MATCH]: index,
+      };
+    } else {
+      scalars = {
+        ...scalars,
+        [QuickSuggestTestUtils.SCALARS
+          .IMPRESSION_NONSPONSORED_BEST_MATCH]: index,
+        [QuickSuggestTestUtils.SCALARS.BLOCK_NONSPONSORED_BEST_MATCH]: index,
+      };
+    }
+  }
+  QuickSuggestTestUtils.assertScalars(scalars);
+
+  // Check the engagement event.
+  let match_type = isBestMatch ? "best-match" : "firefox-suggest";
+  QuickSuggestTestUtils.assertEvents([
+    {
+      category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY,
+      method: "engagement",
+      object: "block",
+      extra: {
+        match_type,
+        position: String(index),
+        suggestion_type: isSponsored ? "sponsored" : "nonsponsored",
+      },
+    },
+  ]);
+
+  // Check the custom telemetry pings.
+  QuickSuggestTestUtils.assertImpressionPing({
+    spy,
+    match_type,
+    // `assertImpressionPing()` expects a zero-based result index, not a
+    // 1-based telemetry index, so subtract 1.
+    index: index - 1,
+    block_id: suggestion.id,
+  });
+  QuickSuggestTestUtils.assertNoClickPing(spy);
+
   await UrlbarTestUtils.promisePopupClose(window);
   await UrlbarProviderQuickSuggest.clearBlockedSuggestions();
 }
 
 // Blocks multiple suggestions one after the other.
 add_task(async function blockMultiple() {
-  for (let i = 0; i < SUGGESTIONS.length; i++) {
-    // Do a search that triggers the i'th best match.
-    let { keywords, url } = SUGGESTIONS[i];
-    await UrlbarTestUtils.promiseAutocompleteResultPopup({
-      window,
-      value: keywords[0],
-    });
-    await QuickSuggestTestUtils.assertIsQuickSuggest({
-      window,
-      originalUrl: url,
-      isBestMatch: true,
-    });
+  for (let isBestMatch of [false, true]) {
+    UrlbarPrefs.set("bestMatch.enabled", isBestMatch);
+    info(`Testing with best match enabled: ${isBestMatch}`);
+
+    for (let i = 0; i < SUGGESTIONS.length; i++) {
+      // Do a search that triggers the i'th suggestion.
+      let { keywords, url } = SUGGESTIONS[i];
+      await UrlbarTestUtils.promiseAutocompleteResultPopup({
+        window,
+        value: keywords[0],
+      });
+      await QuickSuggestTestUtils.assertIsQuickSuggest({
+        window,
+        isBestMatch,
+        originalUrl: url,
+        isSponsored: keywords[0] == "sponsored",
+      });
 
-    // Block it.
-    EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 2 });
-    EventUtils.synthesizeKey("KEY_Enter");
-    Assert.ok(
-      await UrlbarProviderQuickSuggest.isSuggestionBlocked(url),
-      "Suggestion is blocked after picking block button"
-    );
+      // Block it.
+      EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 2 });
+      EventUtils.synthesizeKey("KEY_Enter");
+      Assert.ok(
+        await UrlbarProviderQuickSuggest.isSuggestionBlocked(url),
+        "Suggestion is blocked after picking block button"
+      );
 
-    // Make sure all previous suggestions remain blocked and no other
-    // suggestions are blocked yet.
-    for (let j = 0; j < SUGGESTIONS.length; j++) {
-      Assert.equal(
-        await UrlbarProviderQuickSuggest.isSuggestionBlocked(
-          SUGGESTIONS[j].url
-        ),
-        j <= i,
-        `Suggestion at index ${j} is blocked or not as expected`
-      );
+      // Make sure all previous suggestions remain blocked and no other
+      // suggestions are blocked yet.
+      for (let j = 0; j < SUGGESTIONS.length; j++) {
+        Assert.equal(
+          await UrlbarProviderQuickSuggest.isSuggestionBlocked(
+            SUGGESTIONS[j].url
+          ),
+          j <= i,
+          `Suggestion at index ${j} is blocked or not as expected`
+        );
+      }
     }
+
+    await UrlbarTestUtils.promisePopupClose(window);
+    await UrlbarProviderQuickSuggest.clearBlockedSuggestions();
+    UrlbarPrefs.clear("bestMatch.enabled");
   }
-
-  await UrlbarTestUtils.promisePopupClose(window);
-  await UrlbarProviderQuickSuggest.clearBlockedSuggestions();
 });
 
-// Tests with blocking disabled.
-add_task(async function blockingDisabled() {
+// Tests with blocking disabled for both best matches and non-best-matches.
+add_combo_task(async function disabled_both({ suggestion, isBestMatch }) {
+  await doDisabledTest({
+    suggestion,
+    isBestMatch,
+    quickSuggestBlockingEnabled: false,
+    bestMatchBlockingEnabled: false,
+  });
+});
+
+// Tests with blocking disabled only for non-best-matches.
+add_combo_task(async function disabled_quickSuggest({
+  suggestion,
+  isBestMatch,
+}) {
+  await doDisabledTest({
+    suggestion,
+    isBestMatch,
+    quickSuggestBlockingEnabled: false,
+    bestMatchBlockingEnabled: true,
+  });
+});
+
+// Tests with blocking disabled only for best matches.
+add_combo_task(async function disabled_bestMatch({ suggestion, isBestMatch }) {
+  await doDisabledTest({
+    suggestion,
+    isBestMatch,
+    quickSuggestBlockingEnabled: true,
+    bestMatchBlockingEnabled: false,
+  });
+});
+
+async function doDisabledTest({
+  suggestion,
+  isBestMatch,
+  bestMatchBlockingEnabled,
+  quickSuggestBlockingEnabled,
+}) {
   await SpecialPowers.pushPrefEnv({
-    set: [["browser.urlbar.bestMatch.blockingEnabled", false]],
+    set: [
+      ["browser.urlbar.bestMatch.blockingEnabled", bestMatchBlockingEnabled],
+      [
+        "browser.urlbar.quicksuggest.blockingEnabled",
+        quickSuggestBlockingEnabled,
+      ],
+    ],
   });
 
+  // Do a search to show a suggestion.
   await UrlbarTestUtils.promiseAutocompleteResultPopup({
     window,
-    value: SUGGESTIONS[0].keywords[0],
+    value: suggestion.keywords[0],
   });
-
   let expectedResultCount = 2;
   Assert.equal(
     UrlbarTestUtils.getResultCount(window),
     expectedResultCount,
-    "Two rows are present after searching (heuristic + best match)"
+    "Two rows are present after searching (heuristic + suggestion)"
   );
-
-  // `assertIsQuickSuggest()` asserts that the block button is not present when
-  // `bestMatch.blockingEnabled` is false, but check it again here since it's
-  // central to this test.
   let details = await QuickSuggestTestUtils.assertIsQuickSuggest({
     window,
-    originalUrl: SUGGESTIONS[0].url,
-    isBestMatch: true,
+    isBestMatch,
+    originalUrl: suggestion.url,
+    isSponsored: suggestion.keywords[0] == "sponsored",
   });
-  Assert.ok(
-    !details.element.row._buttons.get("block"),
-    "Block button is not present"
-  );
+  let blockButton = details.element.row._buttons.get("block");
 
-  // Arrow down once to select the best match row and then press the key
-  // shortcut to block.
+  // Arrow down to select the suggestion and press the key shortcut to block.
   EventUtils.synthesizeKey("KEY_ArrowDown");
   EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
-
-  // Nothing should happen.
   Assert.ok(
     UrlbarTestUtils.isPopupOpen(window),
-    "View remains open after key shortcut"
-  );
-  Assert.equal(
-    UrlbarTestUtils.getResultCount(window),
-    expectedResultCount,
-    "Same number of results after key shortcut"
+    "View remains open after trying to block result"
   );
-  await QuickSuggestTestUtils.assertIsQuickSuggest({
-    window,
-    originalUrl: SUGGESTIONS[0].url,
-    isBestMatch: true,
-  });
-  Assert.ok(
-    !(await UrlbarProviderQuickSuggest.isSuggestionBlocked(SUGGESTIONS[0].url)),
-    "Suggestion is not blocked"
-  );
+
+  if (
+    (isBestMatch && !bestMatchBlockingEnabled) ||
+    (!isBestMatch && !quickSuggestBlockingEnabled)
+  ) {
+    // Blocking is disabled. The key shortcut shouldn't have done anything.
+    Assert.ok(!blockButton, "Block button is not present");
+    Assert.equal(
+      UrlbarTestUtils.getResultCount(window),
+      expectedResultCount,
+      "Same number of results after key shortcut"
+    );
+    await QuickSuggestTestUtils.assertIsQuickSuggest({
+      window,
+      isBestMatch,
+      originalUrl: suggestion.url,
+      isSponsored: suggestion.keywords[0] == "sponsored",
+    });
+    Assert.ok(
+      !(await UrlbarProviderQuickSuggest.isSuggestionBlocked(suggestion.url)),
+      "Suggestion is not blocked"
+    );
+  } else {
+    // Blocking is enabled. The suggestion should have been blocked.
+    Assert.ok(blockButton, "Block button is present");
+    Assert.equal(
+      UrlbarTestUtils.getResultCount(window),
+      1,
+      "Only one row after blocking suggestion"
+    );
+    await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
+    Assert.ok(
+      await UrlbarProviderQuickSuggest.isSuggestionBlocked(suggestion.url),
+      "Suggestion is blocked"
+    );
+    await UrlbarProviderQuickSuggest.clearBlockedSuggestions();
+  }
 
   await UrlbarTestUtils.promisePopupClose(window);
   await SpecialPowers.popPrefEnv();
-});
-
-// When the user is enrolled in a best match experiment with the feature enabled
-// (i.e., the treatment branch), the Nimbus exposure event should be recorded
-// after triggering a best match.
-add_task(async function nimbusExposure_featureEnabled() {
-  await doNimbusExposureTest({
-    bestMatchEnabled: true,
-    bestMatchExpected: true,
-    isBestMatchExperiment: true,
-    exposureEventExpected: true,
-  });
-  await doNimbusExposureTest({
-    bestMatchEnabled: true,
-    bestMatchExpected: true,
-    experimentType: "best-match",
-    exposureEventExpected: true,
-  });
-});
-
-// When the user is enrolled in a best match experiment with the feature enabled
-// (i.e., the treatment branch) but the user disabled best match, the Nimbus
-// exposure event should not be recorded at all.
-add_task(async function nimbusExposure_featureEnabled_userDisabled() {
-  await SpecialPowers.pushPrefEnv({
-    set: [["browser.urlbar.suggest.bestmatch", false]],
-  });
-
-  await doNimbusExposureTest({
-    bestMatchEnabled: true,
-    bestMatchExpected: false,
-    isBestMatchExperiment: true,
-    exposureEventExpected: false,
-  });
-  await doNimbusExposureTest({
-    bestMatchEnabled: true,
-    bestMatchExpected: false,
-    experimentType: "best-match",
-    exposureEventExpected: false,
-  });
-
-  await SpecialPowers.popPrefEnv();
-});
-
-// When the user is enrolled in a best match experiment with the feature
-// disabled (i.e., the control branch), the Nimbus exposure event should be
-// recorded when the user would have triggered a best match.
-add_task(async function nimbusExposure_featureDisabled() {
-  await doNimbusExposureTest({
-    bestMatchEnabled: false,
-    bestMatchExpected: false,
-    isBestMatchExperiment: true,
-    exposureEventExpected: true,
-  });
-  await doNimbusExposureTest({
-    bestMatchEnabled: false,
-    bestMatchExpected: false,
-    experimentType: "best-match",
-    exposureEventExpected: true,
-  });
-});
-
-add_task(async function nimbusExposure_notBestMatchExperimentType() {
-  await doNimbusExposureTest({
-    bestMatchEnabled: false,
-    bestMatchExpected: false,
-    skipFirstSearch: true,
-    experimentType: "",
-    exposureEventExpected: true,
-  });
-  await doNimbusExposureTest({
-    bestMatchEnabled: false,
-    bestMatchExpected: false,
-    skipFirstSearch: true,
-    exposureEventExpected: true,
-  });
-  await doNimbusExposureTest({
-    bestMatchEnabled: false,
-    bestMatchExpected: false,
-    experimentType: "modal",
-    exposureEventExpected: false,
-  });
-});
-
-/**
- * Installs a mock experiment, triggers best match, and asserts that the Nimbus
- * exposure event was or was not recorded appropriately.
- *
- * @param {boolean} bestMatchEnabled
- *   The value to set for the experiment's `bestMatchEnabled` Nimbus variable.
- * @param {boolean} bestMatchExpected
- *   Whether a best match result is expected to be shown.
- * @param {boolean} exposureEventExpected
- *   Whether an exposure event is expected to be recorded.
- */
-async function doNimbusExposureTest({
-  bestMatchEnabled,
-  bestMatchExpected,
-  experimentType,
-  isBestMatchExperiment,
-  skipFirstSearch,
-  exposureEventExpected,
-}) {
-  await SpecialPowers.pushPrefEnv({
-    set: [["browser.urlbar.bestMatch.enabled", false]],
-  });
-  await QuickSuggestTestUtils.clearExposureEvent();
-  await QuickSuggestTestUtils.withExperiment({
-    valueOverrides: {
-      bestMatchEnabled,
-      experimentType,
-      isBestMatchExperiment,
-    },
-    callback: async () => {
-      // No exposure event should be recorded after only enrolling.
-      await QuickSuggestTestUtils.assertExposureEvent(false);
-
-      // Do a search that doesn't trigger a best match. No exposure event should
-      // be recorded.
-      if (!skipFirstSearch) {
-        info("Doing first search");
-        await UrlbarTestUtils.promiseAutocompleteResultPopup({
-          window,
-          value: NON_BEST_MATCH_SUGGESTION.keywords[0],
-          fireInputEvent: true,
-        });
-        await QuickSuggestTestUtils.assertIsQuickSuggest({
-          window,
-          url: NON_BEST_MATCH_SUGGESTION.url,
-        });
-        await UrlbarTestUtils.promisePopupClose(window);
-
-        await QuickSuggestTestUtils.assertExposureEvent(false);
-      }
-
-      // Do a search that triggers (or would have triggered) a best match. The
-      // exposure event should be recorded.
-      info("Doing second search");
-      await UrlbarTestUtils.promiseAutocompleteResultPopup({
-        window,
-        value: SUGGESTIONS[0].keywords[0],
-        fireInputEvent: true,
-      });
-      await QuickSuggestTestUtils.assertIsQuickSuggest({
-        window,
-        originalUrl: SUGGESTIONS[0].url,
-        isBestMatch: bestMatchExpected,
-      });
-      await QuickSuggestTestUtils.assertExposureEvent(
-        exposureEventExpected,
-        "control"
-      );
-
-      await UrlbarTestUtils.promisePopupClose(window);
-    },
-  });
-
-  await SpecialPowers.popPrefEnv();
 }
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js
@@ -1453,22 +1453,17 @@ async function doDialogTest({
   Assert.equal(
     TelemetryEnvironment.currentEnvironment.settings.userPrefs[
       "browser.urlbar.quicksuggest.onboardingDialogChoice"
     ],
     onboardingDialogChoice,
     "onboardingDialogChoice is correct in TelemetryEnvironment"
   );
 
-  // Even with the `clearEvents` call above, events related to remote settings
-  // can occur in TV tests during the callback, so pass a filter arg to make
-  // sure we get only the events we're interested in.
-  TelemetryTestUtils.assertEvents(telemetryEvents, {
-    category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY,
-  });
+  QuickSuggestTestUtils.assertEvents(telemetryEvents);
 
   Assert.ok(
     UrlbarPrefs.get("quicksuggest.showedOnboardingDialog"),
     "quicksuggest.showedOnboardingDialog is true after showing dialog"
   );
 
   // Clean up.
   for (let [name, value] of Object.entries(originalDefaultBranch)) {
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_telemetry.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_telemetry.js
@@ -8,432 +8,471 @@
 
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm",
   NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm",
 });
 
-const TEST_URL = "http://example.com/quicksuggest?q=frabbits";
-const TEST_SEARCH_STRING = "fra";
-
-const BEST_MATCH_SPONSORED_URL = "http://example.com/sponsored-best-match";
-const BEST_MATCH_SPONSORED_SEARCH_STRING = "sponsoredbestmatch";
-
-const BEST_MATCH_NONSPONSORED_URL =
-  "http://example.com/nonsponsored-best-match";
-const BEST_MATCH_NONSPONSORED_SEARCH_STRING = "nonsponsoredbestmatch";
-
-const TEST_DATA = [
+const SUGGESTIONS = [
   {
     id: 1,
-    url: TEST_URL,
-    title: "frabbits",
-    keywords: [TEST_SEARCH_STRING],
-    click_url: "http://click.reporting.test.com/",
-    impression_url: "http://impression.reporting.test.com/",
-    advertiser: "Test-Advertiser",
+    url: "http://example.com/sponsored",
+    title: "Sponsored suggestion",
+    keywords: ["sponsored"],
+    click_url: "http://example.com/click",
+    impression_url: "http://example.com/impression",
+    advertiser: "TestAdvertiser",
   },
   {
     id: 2,
-    url: BEST_MATCH_SPONSORED_URL,
-    title: "Sponsored best match",
-    keywords: [BEST_MATCH_SPONSORED_SEARCH_STRING],
-    click_url: "http://click.reporting.test.com/",
-    impression_url: "http://impression.reporting.test.com/",
-    advertiser: "Test-Advertiser",
-    _test_is_best_match: true,
-  },
-  {
-    id: 3,
-    url: BEST_MATCH_NONSPONSORED_URL,
-    title: "Non-sponsored best match",
-    keywords: [BEST_MATCH_NONSPONSORED_SEARCH_STRING],
-    click_url: "http://click.reporting.test.com/",
-    impression_url: "http://impression.reporting.test.com/",
-    advertiser: "Test-Advertiser",
+    url: "http://example.com/nonsponsored",
+    title: "Non-sponsored suggestion",
+    keywords: ["nonsponsored"],
+    click_url: "http://example.com/click",
+    impression_url: "http://example.com/impression",
+    advertiser: "TestAdvertiser",
     iab_category: "5 - Education",
-    _test_is_best_match: true,
   },
 ];
 
-const EXPERIMENT_PREF = "browser.urlbar.quicksuggest.enabled";
-const SUGGEST_PREF = "suggest.quicksuggest.nonsponsored";
+const SPONSORED_SUGGESTION = SUGGESTIONS[0];
 
 // Spy for the custom impression/click sender
 let spy;
 
-// Allow more time for Mac machines so they don't time out in verify mode.
-if (AppConstants.platform == "macosx") {
-  requestLongerTimeout(3);
-}
+// This is a thorough test that opens and closes many tabs and it can time out
+// on slower CI machines in verify mode, so request a longer timeout.
+requestLongerTimeout(5);
 
 add_task(async function init() {
-  ({ sandbox, spy } = QuickSuggestTestUtils.createTelemetryPingSpy());
+  ({ spy } = QuickSuggestTestUtils.createTelemetryPingSpy());
 
   await PlacesUtils.history.clear();
   await PlacesUtils.bookmarks.eraseEverything();
   await UrlbarTestUtils.formHistory.clear();
 
+  Services.telemetry.clearScalars();
+  Services.telemetry.clearEvents();
+
   // Add a mock engine so we don't hit the network.
   await SearchTestUtils.installSearchExtension();
   let oldDefaultEngine = await Services.search.getDefault();
   Services.search.setDefault(Services.search.getEngineByName("Example"));
 
-  // Set up Quick Suggest.
-  await QuickSuggestTestUtils.ensureQuickSuggestInit(TEST_DATA);
+  await QuickSuggestTestUtils.ensureQuickSuggestInit(SUGGESTIONS);
 
-  // Enable local telemetry recording for the duration of the test.
-  let oldCanRecord = Services.telemetry.canRecordExtended;
-  Services.telemetry.canRecordExtended = true;
-
-  Services.telemetry.clearScalars();
-
-  registerCleanupFunction(async () => {
+  registerCleanupFunction(() => {
     Services.search.setDefault(oldDefaultEngine);
-    Services.telemetry.canRecordExtended = oldCanRecord;
   });
 });
 
+/**
+ * Adds a test task that runs the given callback with each suggestion in
+ * `SUGGESTIONS`.
+ *
+ * @param {function} fn
+ *   The callback function. It's passed the current suggestion.
+ */
+function add_suggestions_task(fn) {
+  let taskFn = async () => {
+    for (let suggestion of SUGGESTIONS) {
+      info(`Running ${fn.name} with suggestion ${JSON.stringify(suggestion)}`);
+      await fn(suggestion);
+    }
+  };
+  Object.defineProperty(taskFn, "name", { value: fn.name });
+  add_task(taskFn);
+}
+
 // Tests the following:
 // * impression telemetry
 // * offline scenario
 // * data collection disabled
-add_task(async function impression_offline_dataCollectionDisabled() {
+add_suggestions_task(async function impression_offline_dataCollectionDisabled(
+  suggestion
+) {
   await QuickSuggestTestUtils.setScenario("offline");
   UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false);
   await doImpressionTest({
+    suggestion,
     scenario: "offline",
   });
 });
 
 // Tests the following:
 // * impression telemetry
 // * offline scenario
 // * data collection enabled
-add_task(async function impression_offline_dataCollectionEnabled() {
+add_suggestions_task(async function impression_offline_dataCollectionEnabled(
+  suggestion
+) {
   await QuickSuggestTestUtils.setScenario("offline");
   UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
   await doImpressionTest({
+    suggestion,
     scenario: "offline",
   });
 });
 
 // Tests the following:
 // * impression telemetry
 // * online scenario
 // * data collection disabled
-add_task(async function impression_online_dataCollectionDisabled() {
+add_suggestions_task(async function impression_online_dataCollectionDisabled(
+  suggestion
+) {
   await QuickSuggestTestUtils.setScenario("online");
   UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false);
   UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
   UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
   await doImpressionTest({
+    suggestion,
     scenario: "online",
   });
 });
 
 // Tests the following:
 // * impression telemetry
 // * online scenario
 // * data collection enabled
-add_task(async function impression_online_dataCollectionEnabled() {
+add_suggestions_task(async function impression_online_dataCollectionEnabled(
+  suggestion
+) {
   await QuickSuggestTestUtils.setScenario("online");
   UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
   UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
   UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
   await doImpressionTest({
+    suggestion,
     scenario: "online",
   });
 });
 
-async function doImpressionTest({ scenario }) {
+// Tests the following:
+// * impression telemetry
+// * best match
+add_suggestions_task(async function impression_bestMatch(suggestion) {
+  UrlbarPrefs.set("bestMatch.enabled", true);
+  await doImpressionTest({
+    suggestion,
+    scenario: "offline",
+    isBestMatch: true,
+  });
+  UrlbarPrefs.clear("bestMatch.enabled");
+});
+
+async function doImpressionTest({ scenario, suggestion, isBestMatch = false }) {
   await BrowserTestUtils.withNewTab("about:blank", async () => {
     spy.resetHistory();
+    Services.telemetry.clearEvents();
     await UrlbarTestUtils.promiseAutocompleteResultPopup({
       window,
-      value: TEST_SEARCH_STRING,
+      value: suggestion.keywords[0],
       fireInputEvent: true,
     });
     let index = 1;
+    let isSponsored = suggestion.keywords[0] == "sponsored";
     await QuickSuggestTestUtils.assertIsQuickSuggest({
       window,
       index,
-      url: TEST_URL,
+      isSponsored,
+      isBestMatch,
+      url: suggestion.url,
     });
     // Press Enter on the heuristic result, which is not the quick suggest, to
     // make sure we don't record click telemetry.
     await UrlbarTestUtils.promisePopupClose(window, () => {
       EventUtils.synthesizeKey("KEY_Enter");
     });
-    QuickSuggestTestUtils.assertScalars({
+    let scalars = {
       [QuickSuggestTestUtils.SCALARS.IMPRESSION]: index + 1,
-    });
+    };
+    if (isBestMatch) {
+      if (isSponsored) {
+        scalars = {
+          ...scalars,
+          [QuickSuggestTestUtils.SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]:
+            index + 1,
+        };
+      } else {
+        scalars = {
+          ...scalars,
+          [QuickSuggestTestUtils.SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]:
+            index + 1,
+        };
+      }
+    }
+    QuickSuggestTestUtils.assertScalars(scalars);
+    QuickSuggestTestUtils.assertEvents([
+      {
+        category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY,
+        method: "engagement",
+        object: "impression_only",
+        extra: {
+          match_type: isBestMatch ? "best-match" : "firefox-suggest",
+          position: String(index + 1),
+          suggestion_type: isSponsored ? "sponsored" : "nonsponsored",
+        },
+      },
+    ]);
     QuickSuggestTestUtils.assertImpressionPing({
       index,
       spy,
       scenario,
+      block_id: suggestion.id,
+      match_type: isBestMatch ? "best-match" : "firefox-suggest",
     });
     QuickSuggestTestUtils.assertNoClickPing(spy);
   });
 
   await PlacesUtils.history.clear();
+  await UrlbarTestUtils.formHistory.clear();
 
   await QuickSuggestTestUtils.setScenario(null);
   UrlbarPrefs.clear("quicksuggest.dataCollection.enabled");
   UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored");
   UrlbarPrefs.clear("suggest.quicksuggest.sponsored");
 }
 
 // Makes sure impression telemetry is not recorded when the urlbar engagement is
 // abandoned.
 add_task(async function noImpression_abandonment() {
   await BrowserTestUtils.withNewTab("about:blank", async () => {
     spy.resetHistory();
+    Services.telemetry.clearEvents();
     await UrlbarTestUtils.promiseAutocompleteResultPopup({
       window,
-      value: TEST_SEARCH_STRING,
+      value: "sponsored",
       fireInputEvent: true,
     });
     await QuickSuggestTestUtils.assertIsQuickSuggest({
       window,
-      url: TEST_URL,
+      url: SPONSORED_SUGGESTION.url,
     });
     await UrlbarTestUtils.promisePopupClose(window, () => {
       gURLBar.blur();
     });
     QuickSuggestTestUtils.assertScalars({});
+    QuickSuggestTestUtils.assertEvents([]);
     QuickSuggestTestUtils.assertNoImpressionPing(spy);
     QuickSuggestTestUtils.assertNoClickPing(spy);
   });
 });
 
 // Makes sure impression telemetry is not recorded when a quick suggest result
 // is not present.
 add_task(async function noImpression_noQuickSuggestResult() {
   await BrowserTestUtils.withNewTab("about:blank", async () => {
     spy.resetHistory();
+    Services.telemetry.clearEvents();
     await UrlbarTestUtils.promiseAutocompleteResultPopup({
       window,
       value: "noImpression_noQuickSuggestResult",
       fireInputEvent: true,
     });
     await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
     await UrlbarTestUtils.promisePopupClose(window, () => {
       EventUtils.synthesizeKey("KEY_Enter");
     });
     QuickSuggestTestUtils.assertScalars({});
+    QuickSuggestTestUtils.assertEvents([]);
     QuickSuggestTestUtils.assertNoImpressionPing(spy);
     QuickSuggestTestUtils.assertNoClickPing(spy);
   });
   await PlacesUtils.history.clear();
 });
 
 // Tests the following:
 // * click telemetry using keyboard
 // * offline scenario
 // * data collection disabled
-add_task(async function click_keyboard_offline_dataCollectionDisabled() {
-  await QuickSuggestTestUtils.setScenario("offline");
-  UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false);
-  await doClickTest({
-    useKeyboard: true,
-    scenario: "offline",
-  });
-});
+add_suggestions_task(
+  async function click_keyboard_offline_dataCollectionDisabled(suggestion) {
+    await QuickSuggestTestUtils.setScenario("offline");
+    UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false);
+    await doClickTest({
+      suggestion,
+      useKeyboard: true,
+      scenario: "offline",
+    });
+  }
+);
 
 // Tests the following:
 // * click telemetry using keyboard
 // * offline scenario
 // * data collection enabled
-add_task(async function click_keyboard_offline_dataCollectionEnabled() {
-  await QuickSuggestTestUtils.setScenario("offline");
-  UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
-  await doClickTest({
-    useKeyboard: true,
-    scenario: "offline",
-  });
-});
+add_suggestions_task(
+  async function click_keyboard_offline_dataCollectionEnabled(suggestion) {
+    await QuickSuggestTestUtils.setScenario("offline");
+    UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
+    await doClickTest({
+      suggestion,
+      useKeyboard: true,
+      scenario: "offline",
+    });
+  }
+);
 
 // Tests the following:
 // * click telemetry using keyboard
 // * online scenario
 // * data collection disabled
-add_task(async function click_keyboard_online_dataCollectionDisabled() {
-  await QuickSuggestTestUtils.setScenario("online");
-  UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false);
-  UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
-  UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
-  await doClickTest({
-    useKeyboard: true,
-    scenario: "online",
-  });
-});
+add_suggestions_task(
+  async function click_keyboard_online_dataCollectionDisabled(suggestion) {
+    await QuickSuggestTestUtils.setScenario("online");
+    UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false);
+    UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+    UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+    await doClickTest({
+      suggestion,
+      useKeyboard: true,
+      scenario: "online",
+    });
+  }
+);
 
 // Tests the following:
 // * click telemetry using keyboard
 // * online scenario
 // * data collection enabled
-add_task(async function click_keyboard_online_dataCollectionEnabled() {
+add_suggestions_task(async function click_keyboard_online_dataCollectionEnabled(
+  suggestion
+) {
   await QuickSuggestTestUtils.setScenario("online");
   UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
   UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
   UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
   await doClickTest({
+    suggestion,
     useKeyboard: true,
     scenario: "online",
   });
 });
 
 // Tests the following:
 // * click telemetry using mouse
 // * offline scenario
 // * data collection disabled
-add_task(async function click_mouse_offline_dataCollectionDisabled() {
+add_suggestions_task(async function click_mouse_offline_dataCollectionDisabled(
+  suggestion
+) {
   await QuickSuggestTestUtils.setScenario("offline");
   UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false);
   await doClickTest({
+    suggestion,
     useKeyboard: false,
     scenario: "offline",
   });
 });
 
 // Tests the following:
 // * click telemetry using mouse
 // * offline scenario
 // * data collection enabled
-add_task(async function click_mouse_offline_dataCollectionEnabled() {
+add_suggestions_task(async function click_mouse_offline_dataCollectionEnabled(
+  suggestion
+) {
   await QuickSuggestTestUtils.setScenario("offline");
   UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
   await doClickTest({
+    suggestion,
     useKeyboard: false,
     scenario: "offline",
   });
 });
 
 // Tests the following:
 // * click telemetry using mouse
 // * online scenario
 // * data collection disabled
-add_task(async function click_mouse_online_dataCollectionDisabled() {
+add_suggestions_task(async function click_mouse_online_dataCollectionDisabled(
+  suggestion
+) {
   await QuickSuggestTestUtils.setScenario("online");
   UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false);
   UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
   UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
   await doClickTest({
+    suggestion,
     useKeyboard: false,
     scenario: "online",
   });
 });
 
 // Tests the following:
 // * click telemetry using mouse
 // * online scenario
 // * data collection enabled
-add_task(async function click_mouse_online_dataCollectionEnabled() {
+add_suggestions_task(async function click_mouse_online_dataCollectionEnabled(
+  suggestion
+) {
   await QuickSuggestTestUtils.setScenario("online");
   UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
   UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
   UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
   await doClickTest({
+    suggestion,
     useKeyboard: false,
     scenario: "online",
   });
 });
 
-// Tests impression and click (keyboard) telemetry for sponsored best matches.
-add_task(async function bestMatch_sponsored_keyboard() {
+// Tests the following:
+// * click telemetry using keyboard
+// * best match
+add_suggestions_task(async function click_keyboard_bestMatch(suggestion) {
   UrlbarPrefs.set("bestMatch.enabled", true);
-  await QuickSuggestTestUtils.setScenario("offline");
   await doClickTest({
+    suggestion,
     useKeyboard: true,
     scenario: "offline",
-    searchString: BEST_MATCH_SPONSORED_SEARCH_STRING,
-    url: BEST_MATCH_SPONSORED_URL,
-    block_id: 2,
-    isSponsored: true,
     isBestMatch: true,
   });
   UrlbarPrefs.clear("bestMatch.enabled");
 });
 
-// Tests impression and click (mouse) telemetry for sponsored best matches.
-add_task(async function bestMatch_sponsored_mouse() {
+// Tests the following:
+// * click telemetry using mouse
+// * best match
+add_suggestions_task(async function click_mouse_bestMatch(suggestion) {
   UrlbarPrefs.set("bestMatch.enabled", true);
-  await QuickSuggestTestUtils.setScenario("offline");
-  await doClickTest({
-    useKeyboard: false,
-    scenario: "offline",
-    searchString: BEST_MATCH_SPONSORED_SEARCH_STRING,
-    url: BEST_MATCH_SPONSORED_URL,
-    block_id: 2,
-    isSponsored: true,
-    isBestMatch: true,
-  });
-  UrlbarPrefs.clear("bestMatch.enabled");
-});
-
-// Tests impression and click (keyboard) telemetry for non-sponsored best
-// matches.
-add_task(async function bestMatch_nonsponsored_keyboard() {
-  UrlbarPrefs.set("bestMatch.enabled", true);
-  await QuickSuggestTestUtils.setScenario("offline");
   await doClickTest({
-    useKeyboard: true,
+    suggestion,
     scenario: "offline",
-    searchString: BEST_MATCH_NONSPONSORED_SEARCH_STRING,
-    url: BEST_MATCH_NONSPONSORED_URL,
-    block_id: 3,
-    isSponsored: false,
-    isBestMatch: true,
-  });
-  UrlbarPrefs.clear("bestMatch.enabled");
-});
-
-// Tests impression and click (mouse) telemetry for non-sponsored best matches.
-add_task(async function bestMatch_nonsponsored_mouse() {
-  UrlbarPrefs.set("bestMatch.enabled", true);
-  await QuickSuggestTestUtils.setScenario("offline");
-  await doClickTest({
-    useKeyboard: false,
-    scenario: "offline",
-    searchString: BEST_MATCH_NONSPONSORED_SEARCH_STRING,
-    url: BEST_MATCH_NONSPONSORED_URL,
-    block_id: 3,
-    isSponsored: false,
     isBestMatch: true,
   });
   UrlbarPrefs.clear("bestMatch.enabled");
 });
 
 async function doClickTest({
+  suggestion,
   useKeyboard,
   scenario,
-  block_id = undefined,
-  isSponsored = true,
   isBestMatch = false,
-  searchString = TEST_SEARCH_STRING,
-  url = TEST_URL,
 }) {
   await BrowserTestUtils.withNewTab("about:blank", async () => {
     spy.resetHistory();
+    Services.telemetry.clearEvents();
     await UrlbarTestUtils.promiseAutocompleteResultPopup({
       window,
-      value: searchString,
+      value: suggestion.keywords[0],
       fireInputEvent: true,
     });
 
     let index = 1;
+    let isSponsored = suggestion.keywords[0] == "sponsored";
     let result = await QuickSuggestTestUtils.assertIsQuickSuggest({
       window,
       index,
-      url,
       isSponsored,
       isBestMatch,
+      url: suggestion.url,
     });
     await UrlbarTestUtils.promisePopupClose(window, () => {
       if (useKeyboard) {
         EventUtils.synthesizeKey("KEY_ArrowDown");
         EventUtils.synthesizeKey("KEY_Enter");
       } else {
         EventUtils.synthesizeMouseAtCenter(result.element.row, {});
       }
@@ -459,373 +498,257 @@ async function doClickTest({
           [QuickSuggestTestUtils.SCALARS.CLICK_NONSPONSORED_BEST_MATCH]:
             index + 1,
         };
       }
     }
     QuickSuggestTestUtils.assertScalars(scalars);
 
     let match_type = isBestMatch ? "best-match" : "firefox-suggest";
+    QuickSuggestTestUtils.assertEvents([
+      {
+        category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY,
+        method: "engagement",
+        object: "click",
+        extra: {
+          match_type,
+          position: String(index + 1),
+          suggestion_type: isSponsored ? "sponsored" : "nonsponsored",
+        },
+      },
+    ]);
     QuickSuggestTestUtils.assertImpressionPing({
       index,
       spy,
       scenario,
-      block_id,
       match_type,
+      block_id: suggestion.id,
       is_clicked: true,
     });
     QuickSuggestTestUtils.assertClickPing({
       index,
       spy,
       scenario,
-      block_id,
       match_type,
+      block_id: suggestion.id,
     });
   });
 
   await PlacesUtils.history.clear();
 
   await QuickSuggestTestUtils.setScenario(null);
   UrlbarPrefs.clear("quicksuggest.dataCollection.enabled");
   UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored");
   UrlbarPrefs.clear("suggest.quicksuggest.sponsored");
 }
 
-// Tests the impression and click scalars and the custom click ping by picking a
-// Quick Suggest result when it's shown before search suggestions.
+// Tests impression and click telemetry by picking a quick suggest result when
+// it's shown before search suggestions.
 add_task(async function click_beforeSearchSuggestions() {
   await SpecialPowers.pushPrefEnv({
     set: [["browser.urlbar.showSearchSuggestionsFirst", false]],
   });
   await BrowserTestUtils.withNewTab("about:blank", async () => {
     await withSuggestions(async () => {
       spy.resetHistory();
+      Services.telemetry.clearEvents();
       await UrlbarTestUtils.promiseAutocompleteResultPopup({
         window,
-        value: TEST_SEARCH_STRING,
+        value: "sponsored",
         fireInputEvent: true,
       });
       let resultCount = UrlbarTestUtils.getResultCount(window);
-      Assert.greaterOrEqual(
+      Assert.equal(
         resultCount,
         4,
-        "Result count >= 1 heuristic + 1 quick suggest + 2 suggestions"
+        "Result count == 1 heuristic + 1 quick suggest + 2 suggestions"
       );
       let index = resultCount - 3;
       await QuickSuggestTestUtils.assertIsQuickSuggest({
         window,
         index,
-        url: TEST_URL,
+        url: SPONSORED_SUGGESTION.url,
       });
       await UrlbarTestUtils.promisePopupClose(window, () => {
         EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: index });
         EventUtils.synthesizeKey("KEY_Enter");
       });
       // Arrow down to the quick suggest result and press Enter.
       QuickSuggestTestUtils.assertScalars({
         [QuickSuggestTestUtils.SCALARS.IMPRESSION]: index + 1,
         [QuickSuggestTestUtils.SCALARS.CLICK]: index + 1,
       });
+      QuickSuggestTestUtils.assertEvents([
+        {
+          category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY,
+          method: "engagement",
+          object: "click",
+          extra: {
+            match_type: "firefox-suggest",
+            position: String(index + 1),
+            suggestion_type: "sponsored",
+          },
+        },
+      ]);
       QuickSuggestTestUtils.assertImpressionPing({
         index,
         spy,
         is_clicked: true,
       });
       QuickSuggestTestUtils.assertClickPing({ index, spy });
     });
   });
   await PlacesUtils.history.clear();
   await SpecialPowers.popPrefEnv();
 });
 
-// Tests the help scalar by picking a Quick Suggest result help button with the
-// keyboard.
-add_task(async function help_keyboard() {
+// Tests the following:
+// * help telemetry using keyboard
+add_suggestions_task(async function help_keyboard(suggestion) {
+  await doHelpTest({
+    suggestion,
+    useKeyboard: true,
+  });
+});
+
+// Tests the following:
+// * help telemetry using mouse
+add_suggestions_task(async function help_mouse(suggestion) {
+  await doHelpTest({
+    suggestion,
+    useKeyboard: false,
+  });
+});
+
+// Tests the following:
+// * help telemetry using keyboard
+// * best match
+add_suggestions_task(async function help_keyboard_bestMatch(suggestion) {
+  UrlbarPrefs.set("bestMatch.enabled", true);
+  await doHelpTest({
+    suggestion,
+    useKeyboard: true,
+    isBestMatch: true,
+  });
+  UrlbarPrefs.clear("bestMatch.enabled");
+});
+
+// Tests the following:
+// * help telemetry using mouse
+// * best match
+add_suggestions_task(async function help_mouse_bestMatch(suggestion) {
+  UrlbarPrefs.set("bestMatch.enabled", true);
+  await doHelpTest({
+    suggestion,
+    useKeyboard: false,
+    isBestMatch: true,
+  });
+  UrlbarPrefs.clear("bestMatch.enabled");
+});
+
+async function doHelpTest({ suggestion, useKeyboard, isBestMatch = false }) {
   spy.resetHistory();
+  Services.telemetry.clearEvents();
   await UrlbarTestUtils.promiseAutocompleteResultPopup({
     window,
-    value: TEST_SEARCH_STRING,
+    value: suggestion.keywords[0],
     fireInputEvent: true,
   });
   let index = 1;
+  let isSponsored = suggestion.keywords[0] == "sponsored";
   let result = await QuickSuggestTestUtils.assertIsQuickSuggest({
     window,
     index,
-    url: TEST_URL,
+    isSponsored,
+    isBestMatch,
+    url: suggestion.url,
   });
+
   let helpButton = result.element.row._buttons.get("help");
   Assert.ok(helpButton, "The result has a help button");
   let helpLoadPromise = BrowserTestUtils.waitForNewTab(gBrowser);
   await UrlbarTestUtils.promisePopupClose(window, () => {
-    EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 2 });
-    EventUtils.synthesizeKey("KEY_Enter");
-  });
-  await helpLoadPromise;
-  Assert.equal(
-    gBrowser.currentURI.spec,
-    QuickSuggestTestUtils.LEARN_MORE_URL,
-    "Help URL loaded"
-  );
-  QuickSuggestTestUtils.assertScalars({
-    [QuickSuggestTestUtils.SCALARS.IMPRESSION]: index + 1,
-    [QuickSuggestTestUtils.SCALARS.HELP]: index + 1,
-  });
-  QuickSuggestTestUtils.assertImpressionPing({ index, spy });
-  QuickSuggestTestUtils.assertNoClickPing(spy);
-  BrowserTestUtils.removeTab(gBrowser.selectedTab);
-  await PlacesUtils.history.clear();
-});
-
-// Tests the help scalar by picking a Quick Suggest result help button with the
-// mouse.
-add_task(async function help_mouse() {
-  spy.resetHistory();
-  await UrlbarTestUtils.promiseAutocompleteResultPopup({
-    window,
-    value: TEST_SEARCH_STRING,
-    fireInputEvent: true,
-  });
-  let index = 1;
-  let result = await QuickSuggestTestUtils.assertIsQuickSuggest({
-    window,
-    index,
-    url: TEST_URL,
-  });
-  let helpButton = result.element.row._buttons.get("help");
-  Assert.ok(helpButton, "The result has a help button");
-  let helpLoadPromise = BrowserTestUtils.waitForNewTab(gBrowser);
-  await UrlbarTestUtils.promisePopupClose(window, () => {
-    EventUtils.synthesizeMouseAtCenter(helpButton, {});
-  });
-  await helpLoadPromise;
-  Assert.equal(
-    gBrowser.currentURI.spec,
-    QuickSuggestTestUtils.LEARN_MORE_URL,
-    "Help URL loaded"
-  );
-  QuickSuggestTestUtils.assertScalars({
-    [QuickSuggestTestUtils.SCALARS.IMPRESSION]: index + 1,
-    [QuickSuggestTestUtils.SCALARS.HELP]: index + 1,
-  });
-  QuickSuggestTestUtils.assertImpressionPing({ index, spy });
-  QuickSuggestTestUtils.assertNoClickPing(spy);
-  BrowserTestUtils.removeTab(gBrowser.selectedTab);
-  await PlacesUtils.history.clear();
-});
-
-// Tests the sponsored best match help scalar by picking a sponsored best match
-// help button with the keyboard.
-add_task(async function bestMatch_sponsored_help_keyboard() {
-  UrlbarPrefs.set("bestMatch.enabled", true);
-  spy.resetHistory();
-  await UrlbarTestUtils.promiseAutocompleteResultPopup({
-    window,
-    value: BEST_MATCH_SPONSORED_SEARCH_STRING,
-    fireInputEvent: true,
-  });
-  let index = 1;
-  let result = await QuickSuggestTestUtils.assertIsQuickSuggest({
-    window,
-    index,
-    url: BEST_MATCH_SPONSORED_URL,
-    isSponsored: true,
-    isBestMatch: true,
-  });
-  let helpButton = result.element.row._buttons.get("help");
-  Assert.ok(helpButton, "The result has a help button");
-  let helpLoadPromise = BrowserTestUtils.waitForNewTab(gBrowser);
-  await UrlbarTestUtils.promisePopupClose(window, () => {
-    EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 2 });
-    EventUtils.synthesizeKey("KEY_Enter");
+    if (useKeyboard) {
+      EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 2 });
+      EventUtils.synthesizeKey("KEY_Enter");
+    } else {
+      EventUtils.synthesizeMouseAtCenter(helpButton, {});
+    }
   });
   await helpLoadPromise;
   Assert.equal(
     gBrowser.currentURI.spec,
     QuickSuggestTestUtils.LEARN_MORE_URL,
     "Help URL loaded"
   );
-  QuickSuggestTestUtils.assertScalars({
-    [QuickSuggestTestUtils.SCALARS.IMPRESSION]: index + 1,
-    [QuickSuggestTestUtils.SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: index + 1,
-    [QuickSuggestTestUtils.SCALARS.HELP]: index + 1,
-    [QuickSuggestTestUtils.SCALARS.HELP_SPONSORED_BEST_MATCH]: index + 1,
-  });
-  QuickSuggestTestUtils.assertImpressionPing({
-    index,
-    spy,
-    block_id: 2,
-    match_type: "best-match",
-  });
-  QuickSuggestTestUtils.assertNoClickPing(spy);
-  BrowserTestUtils.removeTab(gBrowser.selectedTab);
-  await PlacesUtils.history.clear();
-  UrlbarPrefs.clear("bestMatch.enabled");
-});
 
-// Tests the sponsored best match help scalar by picking a sponsored best match
-// help button with the mouse.
-add_task(async function bestMatch_sponsored_help_mouse() {
-  UrlbarPrefs.set("bestMatch.enabled", true);
-  spy.resetHistory();
-  await UrlbarTestUtils.promiseAutocompleteResultPopup({
-    window,
-    value: BEST_MATCH_SPONSORED_SEARCH_STRING,
-    fireInputEvent: true,
-  });
-  let index = 1;
-  let result = await QuickSuggestTestUtils.assertIsQuickSuggest({
-    window,
-    index,
-    url: BEST_MATCH_SPONSORED_URL,
-    isSponsored: true,
-    isBestMatch: true,
-  });
-  let helpButton = result.element.row._buttons.get("help");
-  Assert.ok(helpButton, "The result has a help button");
-  let helpLoadPromise = BrowserTestUtils.waitForNewTab(gBrowser);
-  await UrlbarTestUtils.promisePopupClose(window, () => {
-    EventUtils.synthesizeMouseAtCenter(helpButton, {});
-  });
-  await helpLoadPromise;
-  Assert.equal(
-    gBrowser.currentURI.spec,
-    QuickSuggestTestUtils.LEARN_MORE_URL,
-    "Help URL loaded"
-  );
-  QuickSuggestTestUtils.assertScalars({
+  let scalars = {
     [QuickSuggestTestUtils.SCALARS.IMPRESSION]: index + 1,
-    [QuickSuggestTestUtils.SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: index + 1,
     [QuickSuggestTestUtils.SCALARS.HELP]: index + 1,
-    [QuickSuggestTestUtils.SCALARS.HELP_SPONSORED_BEST_MATCH]: index + 1,
-  });
+  };
+  if (isBestMatch) {
+    if (isSponsored) {
+      scalars = {
+        ...scalars,
+        [QuickSuggestTestUtils.SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]:
+          index + 1,
+        [QuickSuggestTestUtils.SCALARS.HELP_SPONSORED_BEST_MATCH]: index + 1,
+      };
+    } else {
+      scalars = {
+        ...scalars,
+        [QuickSuggestTestUtils.SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]:
+          index + 1,
+        [QuickSuggestTestUtils.SCALARS.HELP_NONSPONSORED_BEST_MATCH]: index + 1,
+      };
+    }
+  }
+  QuickSuggestTestUtils.assertScalars(scalars);
+
+  let match_type = isBestMatch ? "best-match" : "firefox-suggest";
+  QuickSuggestTestUtils.assertEvents([
+    {
+      category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY,
+      method: "engagement",
+      object: "help",
+      extra: {
+        match_type,
+        position: String(index + 1),
+        suggestion_type: isSponsored ? "sponsored" : "nonsponsored",
+      },
+    },
+  ]);
   QuickSuggestTestUtils.assertImpressionPing({
     index,
     spy,
-    block_id: 2,
-    match_type: "best-match",
-  });
-  QuickSuggestTestUtils.assertNoClickPing(spy);
-  BrowserTestUtils.removeTab(gBrowser.selectedTab);
-  await PlacesUtils.history.clear();
-  UrlbarPrefs.clear("bestMatch.enabled");
-});
-
-// Tests the non-sponsored best match help scalar by picking a non-sponsored
-// best match help button with the keyboard.
-add_task(async function bestMatch_nonsponsored_help_keyboard() {
-  UrlbarPrefs.set("bestMatch.enabled", true);
-  spy.resetHistory();
-  await UrlbarTestUtils.promiseAutocompleteResultPopup({
-    window,
-    value: BEST_MATCH_NONSPONSORED_SEARCH_STRING,
-    fireInputEvent: true,
-  });
-  let index = 1;
-  let result = await QuickSuggestTestUtils.assertIsQuickSuggest({
-    window,
-    index,
-    url: BEST_MATCH_NONSPONSORED_URL,
-    isSponsored: false,
-    isBestMatch: true,
-  });
-  let helpButton = result.element.row._buttons.get("help");
-  Assert.ok(helpButton, "The result has a help button");
-  let helpLoadPromise = BrowserTestUtils.waitForNewTab(gBrowser);
-  await UrlbarTestUtils.promisePopupClose(window, () => {
-    EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 2 });
-    EventUtils.synthesizeKey("KEY_Enter");
-  });
-  await helpLoadPromise;
-  Assert.equal(
-    gBrowser.currentURI.spec,
-    QuickSuggestTestUtils.LEARN_MORE_URL,
-    "Help URL loaded"
-  );
-  QuickSuggestTestUtils.assertScalars({
-    [QuickSuggestTestUtils.SCALARS.IMPRESSION]: index + 1,
-    [QuickSuggestTestUtils.SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]:
-      index + 1,
-    [QuickSuggestTestUtils.SCALARS.HELP]: index + 1,
-    [QuickSuggestTestUtils.SCALARS.HELP_NONSPONSORED_BEST_MATCH]: index + 1,
-  });
-  QuickSuggestTestUtils.assertImpressionPing({
-    index,
-    spy,
-    block_id: 3,
-    match_type: "best-match",
+    match_type,
+    block_id: suggestion.id,
+    is_clicked: false,
+    scenario: "offline",
   });
   QuickSuggestTestUtils.assertNoClickPing(spy);
+
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
   await PlacesUtils.history.clear();
-  UrlbarPrefs.clear("bestMatch.enabled");
-});
-
-// Tests the non-sponsored best match help scalar by picking a non-sponsored
-// best match help button with the mouse.
-add_task(async function bestMatch_nonsponsored_help_mouse() {
-  UrlbarPrefs.set("bestMatch.enabled", true);
-  spy.resetHistory();
-  await UrlbarTestUtils.promiseAutocompleteResultPopup({
-    window,
-    value: BEST_MATCH_NONSPONSORED_SEARCH_STRING,
-    fireInputEvent: true,
-  });
-  let index = 1;
-  let result = await QuickSuggestTestUtils.assertIsQuickSuggest({
-    window,
-    index,
-    url: BEST_MATCH_NONSPONSORED_URL,
-    isSponsored: false,
-    isBestMatch: true,
-  });
-  let helpButton = result.element.row._buttons.get("help");
-  Assert.ok(helpButton, "The result has a help button");
-  let helpLoadPromise = BrowserTestUtils.waitForNewTab(gBrowser);
-  await UrlbarTestUtils.promisePopupClose(window, () => {
-    EventUtils.synthesizeMouseAtCenter(helpButton, {});
-  });
-  await helpLoadPromise;
-  Assert.equal(
-    gBrowser.currentURI.spec,
-    QuickSuggestTestUtils.LEARN_MORE_URL,
-    "Help URL loaded"
-  );
-  QuickSuggestTestUtils.assertScalars({
-    [QuickSuggestTestUtils.SCALARS.IMPRESSION]: index + 1,
-    [QuickSuggestTestUtils.SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]:
-      index + 1,
-    [QuickSuggestTestUtils.SCALARS.HELP]: index + 1,
-    [QuickSuggestTestUtils.SCALARS.HELP_NONSPONSORED_BEST_MATCH]: index + 1,
-  });
-  QuickSuggestTestUtils.assertImpressionPing({
-    index,
-    spy,
-    block_id: 3,
-    match_type: "best-match",
-  });
-  QuickSuggestTestUtils.assertNoClickPing(spy);
-  BrowserTestUtils.removeTab(gBrowser.selectedTab);
-  await PlacesUtils.history.clear();
-  UrlbarPrefs.clear("bestMatch.enabled");
-});
+}
 
 // Tests telemetry recorded when toggling the
 // `suggest.quicksuggest.nonsponsored` pref:
 // * contextservices.quicksuggest enable_toggled event telemetry
 // * TelemetryEnvironment
 add_task(async function enableToggled() {
   Services.telemetry.clearEvents();
 
   // Toggle the suggest.quicksuggest.nonsponsored pref twice. We should get two
   // events.
-  let enabled = UrlbarPrefs.get(SUGGEST_PREF);
+  let enabled = UrlbarPrefs.get("suggest.quicksuggest.nonsponsored");
   for (let i = 0; i < 2; i++) {
     enabled = !enabled;
-    UrlbarPrefs.set(SUGGEST_PREF, enabled);
-    TelemetryTestUtils.assertEvents([
+    UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", enabled);
+    QuickSuggestTestUtils.assertEvents([
       {
         category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY,
         method: "enable_toggled",
         object: enabled ? "enabled" : "disabled",
       },
     ]);
     Assert.equal(
       TelemetryEnvironment.currentEnvironment.settings.userPrefs[
@@ -834,43 +757,41 @@ add_task(async function enableToggled() 
       enabled,
       "suggest.quicksuggest.nonsponsored is correct in TelemetryEnvironment"
     );
   }
 
   // Set the main quicksuggest.enabled pref to false and toggle the
   // suggest.quicksuggest.nonsponsored pref again.  We shouldn't get any events.
   await SpecialPowers.pushPrefEnv({
-    set: [[EXPERIMENT_PREF, false]],
+    set: [["browser.urlbar.quicksuggest.enabled", false]],
   });
   enabled = !enabled;
-  UrlbarPrefs.set(SUGGEST_PREF, enabled);
-  TelemetryTestUtils.assertEvents([], {
-    category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY,
-  });
+  UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", enabled);
+  QuickSuggestTestUtils.assertEvents([]);
   await SpecialPowers.popPrefEnv();
 
   // Set the pref back to what it was at the start of the task.
-  UrlbarPrefs.set(SUGGEST_PREF, !enabled);
+  UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", !enabled);
 });
 
 // Tests telemetry recorded when toggling the `suggest.quicksuggest.sponsored`
 // pref:
 // * contextservices.quicksuggest enable_toggled event telemetry
 // * TelemetryEnvironment
 add_task(async function sponsoredToggled() {
   Services.telemetry.clearEvents();
 
   // Toggle the suggest.quicksuggest.sponsored pref twice. We should get two
   // events.
   let enabled = UrlbarPrefs.get("suggest.quicksuggest.sponsored");
   for (let i = 0; i < 2; i++) {
     enabled = !enabled;
     UrlbarPrefs.set("suggest.quicksuggest.sponsored", enabled);
-    TelemetryTestUtils.assertEvents([
+    QuickSuggestTestUtils.assertEvents([
       {
         category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY,
         method: "sponsored_toggled",
         object: enabled ? "enabled" : "disabled",
       },
     ]);
     Assert.equal(
       TelemetryEnvironment.currentEnvironment.settings.userPrefs[
@@ -879,23 +800,21 @@ add_task(async function sponsoredToggled
       enabled,
       "suggest.quicksuggest.sponsored is correct in TelemetryEnvironment"
     );
   }
 
   // Set the main quicksuggest.enabled pref to false and toggle the
   // suggest.quicksuggest.sponsored pref again. We shouldn't get any events.
   await SpecialPowers.pushPrefEnv({
-    set: [[EXPERIMENT_PREF, false]],
+    set: [["browser.urlbar.quicksuggest.enabled", false]],
   });
   enabled = !enabled;
   UrlbarPrefs.set("suggest.quicksuggest.sponsored", enabled);
-  TelemetryTestUtils.assertEvents([], {
-    category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY,
-  });
+  QuickSuggestTestUtils.assertEvents([]);
   await SpecialPowers.popPrefEnv();
 
   // Set the pref back to what it was at the start of the task.
   UrlbarPrefs.set("suggest.quicksuggest.sponsored", !enabled);
 });
 
 // Tests telemetry recorded when toggling the
 // `quicksuggest.dataCollection.enabled` pref:
@@ -905,17 +824,17 @@ add_task(async function dataCollectionTo
   Services.telemetry.clearEvents();
 
   // Toggle the quicksuggest.dataCollection.enabled pref twice. We should get
   // two events.
   let enabled = UrlbarPrefs.get("quicksuggest.dataCollection.enabled");
   for (let i = 0; i < 2; i++) {
     enabled = !enabled;
     UrlbarPrefs.set("quicksuggest.dataCollection.enabled", enabled);
-    TelemetryTestUtils.assertEvents([
+    QuickSuggestTestUtils.assertEvents([
       {
         category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY,
         method: "data_collect_toggled",
         object: enabled ? "enabled" : "disabled",
       },
     ]);
     Assert.equal(
       TelemetryEnvironment.currentEnvironment.settings.userPrefs[
@@ -924,23 +843,21 @@ add_task(async function dataCollectionTo
       enabled,
       "quicksuggest.dataCollection.enabled is correct in TelemetryEnvironment"
     );
   }
 
   // Set the main quicksuggest.enabled pref to false and toggle the data
   // collection pref again. We shouldn't get any events.
   await SpecialPowers.pushPrefEnv({
-    set: [[EXPERIMENT_PREF, false]],
+    set: [["browser.urlbar.quicksuggest.enabled", false]],
   });
   enabled = !enabled;
   UrlbarPrefs.set("quicksuggest.dataCollection.enabled", enabled);
-  TelemetryTestUtils.assertEvents([], {
-    category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY,
-  });
+  QuickSuggestTestUtils.assertEvents([]);
   await SpecialPowers.popPrefEnv();
 
   // Set the pref back to what it was at the start of the task.
   UrlbarPrefs.set("quicksuggest.dataCollection.enabled", !enabled);
 });
 
 // Tests telemetry recorded when clicking the checkbox for best match in
 // preferences UI. The telemetry will be stored as following keyed scalar.
@@ -1077,23 +994,23 @@ add_task(async function nimbusExposure()
       await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
       await UrlbarTestUtils.promisePopupClose(window);
       QuickSuggestTestUtils.assertExposureEvent(false);
 
       // Do a search that does trigger a quick suggest result. The exposure
       // event should be recorded.
       await UrlbarTestUtils.promiseAutocompleteResultPopup({
         window,
-        value: TEST_SEARCH_STRING,
+        value: "sponsored",
         fireInputEvent: true,
       });
       await QuickSuggestTestUtils.assertIsQuickSuggest({
         window,
         index: 1,
-        url: TEST_URL,
+        url: SPONSORED_SUGGESTION.url,
       });
       await QuickSuggestTestUtils.assertExposureEvent(true, "control");
 
       await UrlbarTestUtils.promisePopupClose(window);
     },
   });
 });
 
--- a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest.js
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest.js
@@ -1033,19 +1033,19 @@ add_task(async function blockedSuggestio
   // Start with no blocked suggestions.
   await UrlbarProviderQuickSuggest.clearBlockedSuggestions();
   Assert.equal(
     UrlbarProviderQuickSuggest._blockedDigests.size,
     0,
     "_blockedDigests is empty"
   );
   Assert.equal(
-    UrlbarPrefs.get("quickSuggest.blockedDigests"),
+    UrlbarPrefs.get("quicksuggest.blockedDigests"),
     "",
-    "quickSuggest.blockedDigests is an empty string"
+    "quicksuggest.blockedDigests is an empty string"
   );
 
   // Make some URLs.
   let urls = [];
   for (let i = 0; i < 3; i++) {
     urls.push("http://example.com/" + i);
   }
 
@@ -1065,29 +1065,29 @@ add_task(async function blockedSuggestio
   // Make sure all URLs are blocked for good measure.
   for (let url of urls) {
     Assert.ok(
       await UrlbarProviderQuickSuggest.isSuggestionBlocked(url),
       `Suggestion is blocked: ${url}`
     );
   }
 
-  // Check `_blockedDigests` and `quickSuggest.blockedDigests`.
+  // Check `_blockedDigests` and `quicksuggest.blockedDigests`.
   Assert.equal(
     UrlbarProviderQuickSuggest._blockedDigests.size,
     urls.length,
     "_blockedDigests has correct size"
   );
-  let array = JSON.parse(UrlbarPrefs.get("quickSuggest.blockedDigests"));
+  let array = JSON.parse(UrlbarPrefs.get("quicksuggest.blockedDigests"));
   Assert.ok(Array.isArray(array), "Parsed value of pref is an array");
   Assert.equal(array.length, urls.length, "Array has correct length");
 
-  // Write some junk to `quickSuggest.blockedDigests`. `_blockedDigests` should
+  // Write some junk to `quicksuggest.blockedDigests`. `_blockedDigests` should
   // not be changed and all previously blocked URLs should remain blocked.
-  UrlbarPrefs.set("quickSuggest.blockedDigests", "not a json array");
+  UrlbarPrefs.set("quicksuggest.blockedDigests", "not a json array");
   await UrlbarProviderQuickSuggest._blockTaskQueue.emptyPromise;
   for (let url of urls) {
     Assert.ok(
       await UrlbarProviderQuickSuggest.isSuggestionBlocked(url),
       `Suggestion remains blocked: ${url}`
     );
   }
   Assert.equal(
@@ -1107,43 +1107,43 @@ add_task(async function blockedSuggestio
       `Suggestion is blocked: ${url}`
     );
   }
   Assert.equal(
     UrlbarProviderQuickSuggest._blockedDigests.size,
     urls.length,
     "_blockedDigests has correct size"
   );
-  array = JSON.parse(UrlbarPrefs.get("quickSuggest.blockedDigests"));
+  array = JSON.parse(UrlbarPrefs.get("quicksuggest.blockedDigests"));
   Assert.ok(Array.isArray(array), "Parsed value of pref is an array");
   Assert.equal(array.length, urls.length, "Array has correct length");
 
   // Add a new URL digest directly to the JSON'ed array in the pref.
   newURL = "http://example.com/direct-to-pref";
   urls.push(newURL);
-  array = JSON.parse(UrlbarPrefs.get("quickSuggest.blockedDigests"));
+  array = JSON.parse(UrlbarPrefs.get("quicksuggest.blockedDigests"));
   array.push(await UrlbarProviderQuickSuggest._getDigest(newURL));
-  UrlbarPrefs.set("quickSuggest.blockedDigests", JSON.stringify(array));
+  UrlbarPrefs.set("quicksuggest.blockedDigests", JSON.stringify(array));
   await UrlbarProviderQuickSuggest._blockTaskQueue.emptyPromise;
 
   // All URLs should remain blocked and the new URL should be blocked.
   for (let url of urls) {
     Assert.ok(
       await UrlbarProviderQuickSuggest.isSuggestionBlocked(url),
       `Suggestion is blocked: ${url}`
     );
   }
   Assert.equal(
     UrlbarProviderQuickSuggest._blockedDigests.size,
     urls.length,
     "_blockedDigests has correct size"
   );
 
   // Clear the pref. All URLs should be unblocked.
-  UrlbarPrefs.clear("quickSuggest.blockedDigests");
+  UrlbarPrefs.clear("quicksuggest.blockedDigests");
   await UrlbarProviderQuickSuggest._blockTaskQueue.emptyPromise;
   for (let url of urls) {
     Assert.ok(
       !(await UrlbarProviderQuickSuggest.isSuggestionBlocked(url)),
       `Suggestion is no longer blocked: ${url}`
     );
   }
   Assert.equal(
--- a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js
@@ -3320,17 +3320,18 @@ async function checkSearch({ name, searc
   // "Structured Ingestion ping failure with error: undefined"
   let isPrivate = true;
   UrlbarProviderQuickSuggest.onEngagement(isPrivate, "engagement", context, {
     selIndex: -1,
   });
 }
 
 async function checkTelemetryEvents(expectedEvents) {
-  TelemetryTestUtils.assertEvents(
+  QuickSuggestTestUtils.assertEvents(
     expectedEvents.map(event => ({
       ...event,
       category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY,
       method: "impression_cap",
-    }))
+    })),
+    // Filter in only impression_cap events.
+    { method: "impression_cap" }
   );
-  Services.telemetry.clearEvents();
 }
new file mode 100644
--- /dev/null
+++ b/caps/tests/gtest/TestRedirectChainURITruncation.cpp
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ContentPrincipal.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/SystemPrincipal.h"
+#include "nsContentUtils.h"
+#include "mozilla/LoadInfo.h"
+
+namespace mozilla {
+
+void checkPrincipalTruncation(nsIPrincipal* aPrincipal,
+                              const nsACString& aExpectedSpec) {
+  nsCOMPtr<nsIPrincipal> truncatedPrincipal =
+      net::CreateTruncatedPrincipal(aPrincipal);
+  ASSERT_TRUE(truncatedPrincipal);
+
+  if (aPrincipal->IsSystemPrincipal()) {
+    ASSERT_TRUE(truncatedPrincipal->IsSystemPrincipal());
+    return;
+  }
+
+  if (aPrincipal->GetIsNullPrincipal()) {
+    nsCOMPtr<nsIPrincipal> precursorPrincipal =
+        aPrincipal->GetPrecursorPrincipal();
+
+    nsAutoCString principalSpecEnding("}");
+    nsAutoCString expectedTestSpec(aExpectedSpec);
+    if (!aExpectedSpec.IsEmpty()) {
+      principalSpecEnding += "?"_ns;
+      expectedTestSpec += "/"_ns;
+    }
+
+    if (precursorPrincipal) {
+      nsAutoCString precursorSpec;
+      precursorPrincipal->GetAsciiSpec(precursorSpec);
+      ASSERT_TRUE(precursorSpec.Equals(expectedTestSpec));
+    }
+
+    // NullPrincipals have UUIDs as part of their scheme i.e.
+    // moz-nullprincipal:{9bebdabb-828a-4284-8b00-432a968c6e42}
+    // To avoid having to know the UUID beforehand we check the principal's spec
+    // before and after the UUID
+    nsAutoCString principalSpec;
+    truncatedPrincipal->GetAsciiSpec(principalSpec);
+    ASSERT_TRUE(StringBeginsWith(principalSpec, "moz-nullprincipal:{"_ns));
+    ASSERT_TRUE(
+        StringEndsWith(principalSpec, principalSpecEnding + aExpectedSpec));
+    return;
+  }
+
+  if (aPrincipal->GetIsContentPrincipal()) {
+    nsAutoCString principalSpec;
+    truncatedPrincipal->GetAsciiSpec(principalSpec);
+    ASSERT_TRUE(principalSpec.Equals(aExpectedSpec));
+    return;
+  }
+
+  // Tests should not reach this point
+  ADD_FAILURE();
+}
+
+TEST(RedirectChainURITruncation, ContentPrincipal)
+{
+  // ======================= HTTP Scheme =======================
+  nsAutoCString httpSpec(
+      "http://root:toor@www.example.com:200/foo/bar/baz.html?qux#thud");
+  nsCOMPtr<nsIURI> uri;
+  nsresult rv = NS_NewURI(getter_AddRefs(uri), httpSpec);
+  ASSERT_EQ(rv, NS_OK);
+
+  nsCOMPtr<nsIPrincipal> principal;
+  OriginAttributes attrs;
+  principal = BasePrincipal::CreateContentPrincipal(uri, attrs);
+  ASSERT_TRUE(principal);
+
+  checkPrincipalTruncation(principal,
+                           "http://www.example.com:200/foo/bar/baz.html"_ns);
+
+  // ======================= HTTPS Scheme =======================
+  nsAutoCString httpsSpec(
+      "https://root:toor@www.example.com:200/foo/bar/baz.html?qux#thud");
+  rv = NS_NewURI(getter_AddRefs(uri), httpsSpec);
+  ASSERT_EQ(rv, NS_OK);
+
+  principal = BasePrincipal::CreateContentPrincipal(uri, attrs);
+  ASSERT_TRUE(principal);
+
+  checkPrincipalTruncation(principal,
+                           "https://www.example.com:200/foo/bar/baz.html"_ns);
+
+  // ======================= View Source Scheme =======================
+  nsAutoCString viewSourceSpec(
+      "view-source:https://root:toor@www.example.com:200/foo/bar/"
+      "baz.html?qux#thud");
+  rv = NS_NewURI(getter_AddRefs(uri), viewSourceSpec);
+  ASSERT_EQ(rv, NS_OK);
+
+  principal = BasePrincipal::CreateContentPrincipal(uri, attrs);
+  ASSERT_TRUE(principal);
+
+  checkPrincipalTruncation(
+      principal, "view-source:https://www.example.com:200/foo/bar/baz.html"_ns);
+
+  // ======================= About Scheme =======================
+  nsAutoCString aboutSpec("about:config");
+  rv = NS_NewURI(getter_AddRefs(uri), aboutSpec);
+  ASSERT_EQ(rv, NS_OK);
+
+  principal = BasePrincipal::CreateContentPrincipal(uri, attrs);
+  ASSERT_TRUE(principal);
+
+  checkPrincipalTruncation(principal, "about:config"_ns);
+
+  // ======================= Resource Scheme =======================
+  nsAutoCString resourceSpec("resource://testing/");
+  rv = NS_NewURI(getter_AddRefs(uri), resourceSpec);
+  ASSERT_EQ(rv, NS_OK);
+
+  principal = BasePrincipal::CreateContentPrincipal(uri, attrs);
+  ASSERT_TRUE(principal);
+
+  checkPrincipalTruncation(principal, "resource://testing/"_ns);
+
+  // ======================= Chrome Scheme =======================
+  nsAutoCString chromeSpec("chrome://foo/content/bar.xul");
+  rv = NS_NewURI(getter_AddRefs(uri), chromeSpec);
+  ASSERT_EQ(rv, NS_OK);
+
+  principal = BasePrincipal::CreateContentPrincipal(uri, attrs);
+  ASSERT_TRUE(principal);
+
+  checkPrincipalTruncation(principal, "chrome://foo/content/bar.xul"_ns);
+}
+
+TEST(RedirectChainURITruncation, NullPrincipal)
+{
+  // ======================= NullPrincipal =======================
+  nsCOMPtr<nsIPrincipal> principal =
+      NullPrincipal::CreateWithoutOriginAttributes();
+  ASSERT_TRUE(principal);
+
+  checkPrincipalTruncation(principal, ""_ns);
+
+  // ======================= NullPrincipal & Precursor =======================
+  nsAutoCString precursorSpec(
+      "https://root:toor@www.example.com:200/foo/bar/baz.html?qux#thud");
+
+  nsCOMPtr<nsIURI> precursorURI;
+  nsresult rv = NS_NewURI(getter_AddRefs(precursorURI), precursorSpec);
+  ASSERT_EQ(rv, NS_OK);
+
+  OriginAttributes attrs;
+  nsCOMPtr<nsIPrincipal> precursorPrincipal =
+      BasePrincipal::CreateContentPrincipal(precursorURI, attrs);
+  principal = NullPrincipal::CreateWithInheritedAttributes(precursorPrincipal);
+  ASSERT_TRUE(principal);
+
+  checkPrincipalTruncation(principal, "https://www.example.com:200"_ns);
+}
+
+TEST(RedirectChainURITruncation, SystemPrincipal)
+{
+  nsCOMPtr<nsIPrincipal> principal = nsContentUtils::GetSystemPrincipal();
+  ASSERT_TRUE(principal);
+
+  checkPrincipalTruncation(principal, ""_ns);
+}
+
+}  // namespace mozilla
--- a/caps/tests/gtest/moz.build
+++ b/caps/tests/gtest/moz.build
@@ -4,15 +4,16 @@
 # 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/.
 
 UNIFIED_SOURCES += [
     "TestNullPrincipalPrecursor.cpp",
     "TestOriginAttributes.cpp",
     "TestPrincipalAttributes.cpp",
     "TestPrincipalSerialization.cpp",
+    "TestRedirectChainURITruncation.cpp",
 ]
 
 include("/ipc/chromium/chromium-config.mozbuild")
 
 FINAL_LIBRARY = "xul-gtest"
 
 REQUIRES_UNIFIED_BUILD = True
--- a/docshell/base/BrowsingContext.cpp
+++ b/docshell/base/BrowsingContext.cpp
@@ -3071,16 +3071,20 @@ mozilla::dom::TouchEventsOverride Browsi
     }
 
     bc = bc->GetParent();
   }
 
   return mozilla::dom::TouchEventsOverride::None;
 }
 
+bool BrowsingContext::TargetTopLevelLinkClicksToBlank() const {
+  return Top()->GetTargetTopLevelLinkClicksToBlankInternal();
+}
+
 // We map `watchedByDevTools` WebIDL attribute to `watchedByDevToolsInternal`
 // BC field. And we map it to the top level BrowsingContext.
 bool BrowsingContext::WatchedByDevTools() {
   return Top()->GetWatchedByDevToolsInternal();
 }
 
 // Enforce that the watchedByDevTools BC field can only be set on the top level
 // Browsing Context.
@@ -3470,16 +3474,23 @@ void BrowsingContext::DidSet(FieldIndex<
   MOZ_ASSERT(GetHasSessionHistory() || !aOldValue,
              "We don't support turning off session history.");
 
   if (GetHasSessionHistory() && !aOldValue) {
     CreateChildSHistory();
   }
 }
 
+bool BrowsingContext::CanSet(
+    FieldIndex<IDX_TargetTopLevelLinkClicksToBlankInternal>,
+    const bool& aTargetTopLevelLinkClicksToBlankInternal,
+    ContentParent* aSource) {
+  return XRE_IsParentProcess() && !aSource && IsTop();
+}
+
 bool BrowsingContext::CanSet(FieldIndex<IDX_BrowserId>, const uint32_t& aValue,
                              ContentParent* aSource) {
   // We should only be able to set this for toplevel contexts which don't have
   // an ID yet.
   return GetBrowserId() == 0 && IsTop() && Children().IsEmpty();
 }
 
 bool BrowsingContext::CanSet(FieldIndex<IDX_PendingInitialization>,
--- a/docshell/base/BrowsingContext.h
+++ b/docshell/base/BrowsingContext.h
@@ -154,16 +154,17 @@ enum class ExplicitActiveStatus : uint8_
    * We use it exclusively to block navigation for both of these cases. */    \
   FIELD(IsPrinting, bool)                                                     \
   FIELD(AncestorLoading, bool)                                                \
   FIELD(AllowPlugins, bool)                                                   \
   FIELD(AllowContentRetargeting, bool)                                        \
   FIELD(AllowContentRetargetingOnChildren, bool)                              \
   FIELD(ForceEnableTrackingProtection, bool)                                  \
   FIELD(UseGlobalHistory, bool)                                               \
+  FIELD(TargetTopLevelLinkClicksToBlankInternal, bool)                        \
   FIELD(FullscreenAllowedByOwner, bool)                                       \
   /*                                                                          \
    * "is popup" in the spec.                                                  \
    * Set only on top browsing contexts.                                       \
    * This doesn't indicate whether this is actually a popup or not,           \
    * but whether this browsing context is created by requesting popup or not. \
    * See also: nsWindowWatcher::ShouldOpenPopup.                              \
    */                                                                         \
@@ -552,16 +553,17 @@ class BrowsingContext : public nsILoadCo
                            ErrorResult& aError);
 
   bool InRDMPane() const { return GetInRDMPane(); }
 
   bool WatchedByDevTools();
   void SetWatchedByDevTools(bool aWatchedByDevTools, ErrorResult& aRv);
 
   dom::TouchEventsOverride TouchEventsOverride() const;
+  bool TargetTopLevelLinkClicksToBlank() const;
 
   bool FullscreenAllowed() const;
 
   float FullZoom() const { return GetFullZoom(); }
   float TextZoom() const { return GetTextZoom(); }
 
   float OverrideDPPX() const { return Top()->GetOverrideDPPX(); }
 
@@ -1134,16 +1136,20 @@ class BrowsingContext : public nsILoadCo
   CanSetResult CanSet(FieldIndex<IDX_DefaultLoadFlags>,
                       const uint32_t& aDefaultLoadFlags,
                       ContentParent* aSource);
   void DidSet(FieldIndex<IDX_DefaultLoadFlags>);
 
   bool CanSet(FieldIndex<IDX_UseGlobalHistory>, const bool& aUseGlobalHistory,
               ContentParent* aSource);
 
+  bool CanSet(FieldIndex<IDX_TargetTopLevelLinkClicksToBlankInternal>,
+              const bool& aTargetTopLevelLinkClicksToBlankInternal,
+              ContentParent* aSource);
+
   void DidSet(FieldIndex<IDX_HasSessionHistory>, bool aOldValue);
 
   bool CanSet(FieldIndex<IDX_BrowserId>, const uint32_t& aValue,
               ContentParent* aSource);
 
   bool CanSet(FieldIndex<IDX_UseErrorPages>, const bool& aUseErrorPages,
               ContentParent* aSource);
 
--- a/docshell/base/CanonicalBrowsingContext.cpp
+++ b/docshell/base/CanonicalBrowsingContext.cpp
@@ -312,16 +312,18 @@ void CanonicalBrowsingContext::ReplacedB
   txn.SetEmbedderColorScheme(GetEmbedderColorScheme());
   txn.SetHasRestoreData(GetHasRestoreData());
   txn.SetShouldDelayMediaFromStart(GetShouldDelayMediaFromStart());
   // As this is a different BrowsingContext, set InitialSandboxFlags to the
   // current flags in the new context so that they also apply to any initial
   // about:blank documents created in it.
   txn.SetSandboxFlags(GetSandboxFlags());
   txn.SetInitialSandboxFlags(GetSandboxFlags());
+  txn.SetTargetTopLevelLinkClicksToBlankInternal(
+      TargetTopLevelLinkClicksToBlank());
   if (aNewContext->EverAttached()) {
     MOZ_ALWAYS_SUCCEEDS(txn.Commit(aNewContext));
   } else {
     txn.CommitWithoutSyncing(aNewContext);
   }
 
   aNewContext->mRestoreState = mRestoreState.forget();
   MOZ_ALWAYS_SUCCEEDS(SetHasRestoreData(false));
@@ -2673,16 +2675,22 @@ bool CanonicalBrowsingContext::AllowedIn
   return bfcacheCombo == 0;
 }
 
 void CanonicalBrowsingContext::SetTouchEventsOverride(
     dom::TouchEventsOverride aOverride, ErrorResult& aRv) {
   SetTouchEventsOverrideInternal(aOverride, aRv);
 }
 
+void CanonicalBrowsingContext::SetTargetTopLevelLinkClicksToBlank(
+    bool aTargetTopLevelLinkClicksToBlank, ErrorResult& aRv) {
+  SetTargetTopLevelLinkClicksToBlankInternal(aTargetTopLevelLinkClicksToBlank,
+                                             aRv);
+}
+
 void CanonicalBrowsingContext::AddPageAwakeRequest() {
   MOZ_ASSERT(IsTop());
   auto count = GetPageAwakeRequestCount();
   MOZ_ASSERT(count < UINT32_MAX);
   Unused << SetPageAwakeRequestCount(++count);
 }
 
 void CanonicalBrowsingContext::RemovePageAwakeRequest() {
--- a/docshell/base/CanonicalBrowsingContext.h
+++ b/docshell/base/CanonicalBrowsingContext.h
@@ -317,16 +317,18 @@ class CanonicalBrowsingContext final : p
     return mPriorityActive;
   }
   void SetPriorityActive(bool aIsActive) {
     MOZ_RELEASE_ASSERT(IsTop());
     mPriorityActive = aIsActive;
   }
 
   void SetTouchEventsOverride(dom::TouchEventsOverride, ErrorResult& aRv);
+  void SetTargetTopLevelLinkClicksToBlank(bool aTargetTopLevelLinkClicksToBlank,
+                                          ErrorResult& aRv);
 
   bool IsReplaced() const { return mIsReplaced; }
 
   const JS::Heap<JS::Value>& PermanentKey() { return mPermanentKey; }
   void ClearPermanentKey() { mPermanentKey.setNull(); }
   void MaybeSetPermanentKey(Element* aEmbedder);
 
   // When request for page awake, it would increase a count that is used to
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -12855,17 +12855,18 @@ nsresult nsDocShell::OnLinkClick(
   }
 
   if (aContent->IsEditable()) {
     return NS_OK;
   }
 
   bool noOpenerImplied = false;
   nsAutoString target(aTargetSpec);
-  if (ShouldOpenInBlankTarget(aTargetSpec, aURI, aContent)) {
+  if (aFileName.IsVoid() &&
+      ShouldOpenInBlankTarget(aTargetSpec, aURI, aContent, aIsUserTriggered)) {
     target = u"_blank";
     if (!aTargetSpec.Equals(target)) {
       noOpenerImplied = true;
     }
   }
 
   RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(aURI);
   loadState->SetTarget(target);
@@ -12881,39 +12882,54 @@ nsresult nsDocShell::OnLinkClick(
 
   nsCOMPtr<nsIRunnable> ev =
       new OnLinkClickEvent(this, aContent, loadState, noOpenerImplied,
                            aIsTrusted, aTriggeringPrincipal);
   return Dispatch(TaskCategory::UI, ev.forget());
 }
 
 bool nsDocShell::ShouldOpenInBlankTarget(const nsAString& aOriginalTarget,
-                                         nsIURI* aLinkURI,
-                                         nsIContent* aContent) {
+                                         nsIURI* aLinkURI, nsIContent* aContent,
+                                         bool aIsUserTriggered) {
+  if (net::SchemeIsJavascript(aLinkURI)) {
+    return false;
+  }
+
+  // External links from within app tabs should always open in new tabs
+  // instead of replacing the app tab's page (Bug 575561)
+  // nsIURI.host can throw for non-nsStandardURL nsIURIs. If we fail to
+  // get either host, just return false to use the original target.
+  nsAutoCString linkHost;
+  if (NS_FAILED(aLinkURI->GetHost(linkHost))) {
+    return false;
+  }
+
+  // The targetTopLevelLinkClicksToBlank property on BrowsingContext allows
+  // privileged code to change the default targeting behaviour. In particular,
+  // if a user-initiated link click for the (or targetting the) top-level frame
+  // is detected, we default the target to "_blank" to give it a new
+  // top-level BrowsingContext.
+  if (mBrowsingContext->TargetTopLevelLinkClicksToBlank() && aIsUserTriggered &&
+      ((aOriginalTarget.IsEmpty() && mBrowsingContext->IsTop()) ||
+       aOriginalTarget == u"_top"_ns)) {
+    return true;
+  }
+
   // Don't modify non-default targets.
   if (!aOriginalTarget.IsEmpty()) {
     return false;
   }
 
   // Only check targets that are in extension panels or app tabs.
   // (isAppTab will be false for app tab subframes).
   nsString mmGroup = mBrowsingContext->Top()->GetMessageManagerGroup();
   if (!mmGroup.EqualsLiteral("webext-browsers") && !mIsAppTab) {
     return false;
   }
 
-  // External links from within app tabs should always open in new tabs
-  // instead of replacing the app tab's page (Bug 575561)
-  // nsIURI.host can throw for non-nsStandardURL nsIURIs. If we fail to
-  // get either host, just return false to use the original target.
-  nsAutoCString linkHost;
-  if (NS_FAILED(aLinkURI->GetHost(linkHost))) {
-    return false;
-  }
-
   nsCOMPtr<nsIURI> docURI = aContent->OwnerDoc()->GetDocumentURIObject();
   if (!docURI) {
     return false;
   }
 
   nsAutoCString docHost;
   if (NS_FAILED(docURI->GetHost(docHost))) {
     return false;
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -1109,17 +1109,18 @@ class nsDocShell final : public nsDocLoa
 
   /**
    * Returns true if `noopener` will be force-enabled by any attempt to create
    * a popup window, even if rel="opener" is requested.
    */
   bool NoopenerForceEnabled();
 
   bool ShouldOpenInBlankTarget(const nsAString& aOriginalTarget,
-                               nsIURI* aLinkURI, nsIContent* aContent);
+                               nsIURI* aLinkURI, nsIContent* aContent,
+                               bool aIsUserTriggered);
 
   void RecordSingleChannelId(bool aStartRequest, nsIRequest* aRequest);
 
   void SetChannelToDisconnectOnPageHide(uint64_t aChannelId) {
     MOZ_ASSERT(mChannelToDisconnectOnPageHide == 0);
     mChannelToDisconnectOnPageHide = aChannelId;
   }
   void MaybeDisconnectChildListenersOnPageHide();
--- a/docshell/test/browser/browser.ini
+++ b/docshell/test/browser/browser.ini
@@ -153,16 +153,17 @@ skip-if = (verify && debug && (os == 'wi
 [browser_fission_maxOrigins.js]
 https_first_disabled = true
 [browser_frameloader_swap_with_bfcache.js]
 [browser_backforward_restore_scroll.js]
 https_first_disabled = true
 support-files =
   file_backforward_restore_scroll.html
   file_backforward_restore_scroll.html^headers^
+[browser_targetTopLevelLinkClicksToBlank.js]
 [browser_title_in_session_history.js]
 skip-if = !sessionHistoryInParent
 [browser_uriFixupIntegration.js]
 [browser_uriFixupAlternateRedirects.js]
 https_first_disabled = true
 support-files =
   redirect_to_example.sjs
 [browser_loadURI_postdata.js]
new file mode 100644
--- /dev/null
+++ b/docshell/test/browser/browser_targetTopLevelLinkClicksToBlank.js
@@ -0,0 +1,285 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test exercises the behaviour where user-initiated link clicks on
+ * the top-level document result in pageloads in a _blank target in a new
+ * browser window.
+ */
+
+const TEST_PAGE = "https://example.com/browser/";
+const TEST_PAGE_2 = "https://example.com/browser/components/";
+const TEST_IFRAME_PAGE =
+  getRootDirectory(gTestPath).replace(
+    "chrome://mochitests/content",
+    "https://example.com"
+  ) + "dummy_iframe_page.html";
+
+// There is an <a> element with this href=".." in the TEST_PAGE
+// that we will click, which should take us up a level.
+const LINK_URL = "https://example.com/";
+
+/**
+ * Test that a user-initiated link click results in targeting to a new
+ * <browser> element, and that this properly sets the referrer on the newly
+ * loaded document.
+ */
+add_task(async function target_to_new_blank_browser() {
+  let win = await BrowserTestUtils.openNewBrowserWindow();
+  let originalTab = win.gBrowser.selectedTab;
+  let originalBrowser = originalTab.linkedBrowser;
+  BrowserTestUtils.loadURI(originalBrowser, TEST_PAGE);
+  await BrowserTestUtils.browserLoaded(originalBrowser, false, TEST_PAGE);
+
+  // Now set the targetTopLevelLinkClicksToBlank property to true, since it
+  // defaults to false.
+  originalBrowser.browsingContext.targetTopLevelLinkClicksToBlank = true;
+
+  let newTabPromise = BrowserTestUtils.waitForNewTab(win.gBrowser, LINK_URL);
+  await SpecialPowers.spawn(originalBrowser, [], async () => {
+    let anchor = content.document.querySelector(`a[href=".."]`);
+    let userInput = content.windowUtils.setHandlingUserInput(true);
+    try {
+      anchor.click();
+    } finally {
+      userInput.destruct();
+    }
+  });
+  let newTab = await newTabPromise;
+  let newBrowser = newTab.linkedBrowser;
+
+  Assert.ok(
+    originalBrowser !== newBrowser,
+    "A new browser should have been created."
+  );
+  await SpecialPowers.spawn(newBrowser, [TEST_PAGE], async referrer => {
+    Assert.equal(
+      content.document.referrer,
+      referrer,
+      "Should have gotten the right referrer set"
+    );
+  });
+  await BrowserTestUtils.switchTab(win.gBrowser, originalTab);
+  BrowserTestUtils.removeTab(newTab);
+
+  // Now do the same thing with a subframe targeting "_top". This should also
+  // get redirected to "_blank".
+  BrowserTestUtils.loadURI(originalBrowser, TEST_IFRAME_PAGE);
+  await BrowserTestUtils.browserLoaded(
+    originalBrowser,
+    false,
+    TEST_IFRAME_PAGE
+  );
+
+  newTabPromise = BrowserTestUtils.waitForNewTab(win.gBrowser, LINK_URL);
+  let frameBC1 = originalBrowser.browsingContext.children[0];
+  Assert.ok(frameBC1, "Should have found a subframe BrowsingContext");
+
+  await SpecialPowers.spawn(frameBC1, [LINK_URL], async linkUrl => {
+    let anchor = content.document.createElement("a");
+    anchor.setAttribute("href", linkUrl);
+    anchor.setAttribute("target", "_top");
+    content.document.body.appendChild(anchor);
+    let userInput = content.windowUtils.setHandlingUserInput(true);
+    try {
+      anchor.click();
+    } finally {
+      userInput.destruct();
+    }
+  });
+  newTab = await newTabPromise;
+  newBrowser = newTab.linkedBrowser;
+
+  Assert.ok(
+    originalBrowser !== newBrowser,
+    "A new browser should have been created."
+  );
+  await SpecialPowers.spawn(
+    newBrowser,
+    [frameBC1.currentURI.spec],
+    async referrer => {
+      Assert.equal(
+        content.document.referrer,
+        referrer,
+        "Should have gotten the right referrer set"
+      );
+    }
+  );
+  await BrowserTestUtils.switchTab(win.gBrowser, originalTab);
+  BrowserTestUtils.removeTab(newTab);
+
+  await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Test that we don't target to _blank loads caused by:
+ * 1. POST requests
+ * 2. Any load that isn't "normal" (in the nsIDocShell.LOAD_CMD_NORMAL sense)
+ * 3. Any loads that are caused by location.replace
+ * 4. Any loads that were caused by setting location.href
+ * 5. Link clicks fired without user interaction.
+ */
+add_task(async function skip_blank_target_for_some_loads() {
+  let win = await BrowserTestUtils.openNewBrowserWindow();
+  let currentBrowser = win.gBrowser.selectedBrowser;
+  BrowserTestUtils.loadURI(currentBrowser, TEST_PAGE);
+  await BrowserTestUtils.browserLoaded(currentBrowser, false, TEST_PAGE);
+
+  // Now set the targetTopLevelLinkClicksToBlank property to true, since it
+  // defaults to false.
+  currentBrowser.browsingContext.targetTopLevelLinkClicksToBlank = true;
+
+  let ensureSingleBrowser = () => {
+    Assert.equal(
+      win.gBrowser.browsers.length,
+      1,
+      "There should only be 1 browser."
+    );
+
+    Assert.ok(
+      currentBrowser.browsingContext.targetTopLevelLinkClicksToBlank,
+      "Should still be targeting top-level clicks to _blank"
+    );
+  };
+
+  // First we'll test a POST request
+  let sameBrowserLoad = BrowserTestUtils.browserLoaded(
+    currentBrowser,
+    false,
+    TEST_PAGE
+  );
+  await SpecialPowers.spawn(currentBrowser, [], async () => {
+    let doc = content.document;
+    let form = doc.createElement("form");
+    form.setAttribute("method", "post");
+    doc.body.appendChild(form);
+    let userInput = content.windowUtils.setHandlingUserInput(true);
+    try {
+      form.submit();
+    } finally {
+      userInput.destruct();
+    }
+  });
+  await sameBrowserLoad;
+  ensureSingleBrowser();
+
+  // Next, we'll try a non-normal load - specifically, we'll try a reload.
+  // Since we've got a page loaded via a POST request, an attempt to reload
+  // will cause the "repost" dialog to appear, so we temporarily allow the
+  // repost to go through with the always_accept testing pref.
+  await SpecialPowers.pushPrefEnv({
+    set: [["dom.confirm_repost.testing.always_accept", true]],
+  });
+  sameBrowserLoad = BrowserTestUtils.browserLoaded(
+    currentBrowser,
+    false,
+    TEST_PAGE
+  );
+  await SpecialPowers.spawn(currentBrowser, [], async () => {
+    let userInput = content.windowUtils.setHandlingUserInput(true);
+    try {
+      content.location.reload();
+    } finally {
+      userInput.destruct();
+    }
+  });
+  await sameBrowserLoad;
+  ensureSingleBrowser();
+  await SpecialPowers.popPrefEnv();
+
+  // Next, we'll try a location.replace
+  sameBrowserLoad = BrowserTestUtils.browserLoaded(
+    currentBrowser,
+    false,
+    TEST_PAGE_2
+  );
+  await SpecialPowers.spawn(currentBrowser, [TEST_PAGE_2], async page2 => {
+    let userInput = content.windowUtils.setHandlingUserInput(true);
+    try {
+      content.location.replace(page2);
+    } finally {
+      userInput.destruct();
+    }
+  });
+  await sameBrowserLoad;
+  ensureSingleBrowser();
+
+  // Finally we'll try setting location.href
+  sameBrowserLoad = BrowserTestUtils.browserLoaded(
+    currentBrowser,
+    false,
+    TEST_PAGE
+  );
+  await SpecialPowers.spawn(currentBrowser, [TEST_PAGE], async page1 => {
+    let userInput = content.windowUtils.setHandlingUserInput(true);
+    try {
+      content.location.href = page1;
+    } finally {
+      userInput.destruct();
+    }
+  });
+  await sameBrowserLoad;
+  ensureSingleBrowser();
+
+  // Now that we're back at TEST_PAGE, let's try a scripted link click. This
+  // shouldn't target to _blank.
+  sameBrowserLoad = BrowserTestUtils.browserLoaded(
+    currentBrowser,
+    false,
+    LINK_URL
+  );
+  await SpecialPowers.spawn(currentBrowser, [], async () => {
+    let anchor = content.document.querySelector(`a[href=".."]`);
+    anchor.click();
+  });
+  await sameBrowserLoad;
+  ensureSingleBrowser();
+
+  // A javascript:void(0); link should also not target to _blank.
+  sameBrowserLoad = BrowserTestUtils.browserLoaded(
+    currentBrowser,
+    false,
+    TEST_PAGE
+  );
+  await SpecialPowers.spawn(currentBrowser, [TEST_PAGE], async newPageURL => {
+    let anchor = content.document.querySelector(`a[href=".."]`);
+    anchor.href = "javascript:void(0);";
+    anchor.addEventListener("click", e => {
+      content.location.href = newPageURL;
+    });
+    let userInput = content.windowUtils.setHandlingUserInput(true);
+    try {
+      anchor.click();
+    } finally {
+      userInput.destruct();
+    }
+  });
+  await sameBrowserLoad;
+  ensureSingleBrowser();
+
+  // Let's also try a non-void javascript: location.
+  sameBrowserLoad = BrowserTestUtils.browserLoaded(
+    currentBrowser,
+    false,
+    TEST_PAGE
+  );
+  await SpecialPowers.spawn(currentBrowser, [TEST_PAGE], async newPageURL => {
+    let anchor = content.document.querySelector(`a[href=".."]`);
+    anchor.href = `javascript:"string-to-navigate-to"`;
+    anchor.addEventListener("click", e => {
+      content.location.href = newPageURL;
+    });
+    let userInput = content.windowUtils.setHandlingUserInput(true);
+    try {
+      anchor.click();
+    } finally {
+      userInput.destruct();
+    }
+  });
+  await sameBrowserLoad;
+  ensureSingleBrowser();
+
+  await BrowserTestUtils.closeWindow(win);
+});
--- a/dom/base/BodyStream.cpp
+++ b/dom/base/BodyStream.cpp
@@ -4,20 +4,18 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "BodyStream.h"
 #include "js/GCAPI.h"
 #include "mozilla/CycleCollectedJSContext.h"
 #include "mozilla/dom/AutoEntryScript.h"
 #include "mozilla/dom/DOMException.h"
-#ifdef MOZ_DOM_STREAMS
-#  include "mozilla/dom/ReadableStream.h"
-#  include "mozilla/dom/ReadableByteStreamController.h"
-#endif
+#include "mozilla/dom/ReadableStream.h"
+#include "mozilla/dom/ReadableByteStreamController.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/WorkerCommon.h"
 #include "mozilla/dom/WorkerPrivate.h"
 #include "mozilla/dom/WorkerRunnable.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/ScopeExit.h"
 #include "mozilla/Services.h"
 #include "mozilla/Unused.h"
@@ -54,23 +52,16 @@ NS_INTERFACE_MAP_END
 BodyStreamHolder::BodyStreamHolder() : mBodyStream(nullptr) {}
 
 void BodyStreamHolder::StoreBodyStream(BodyStream* aBodyStream) {
   MOZ_ASSERT(aBodyStream);
   MOZ_ASSERT(!mBodyStream);
   mBodyStream = aBodyStream;
 }
 
-#ifndef MOZ_DOM_STREAMS
-void BodyStreamHolder::ForgetBodyStream() {
-  MOZ_ASSERT_IF(mStreamCreated, mBodyStream);
-  mBodyStream = nullptr;
-}
-#endif
-
 // BodyStream
 // ---------------------------------------------------------------------------
 
 class BodyStream::WorkerShutdown final : public WorkerControlRunnable {
  public:
   WorkerShutdown(WorkerPrivate* aWorkerPrivate, RefPtr<BodyStream> aStream)
       : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount),
         mStream(aStream) {}
@@ -133,58 +124,42 @@ void BodyStream::Create(JSContext* aCx, 
     }
 
     // Note, this will create a ref-cycle between the holder and the stream.
     // The cycle is broken when the stream is closed or the worker begins
     // shutting down.
     stream->mWorkerRef = std::move(workerRef);
   }
 
-#ifdef MOZ_DOM_STREAMS
   RefPtr<ReadableStream> body =
       ReadableStream::Create(aCx, aGlobal, aStreamHolder, aRv);
   if (aRv.Failed()) {
     return;
   }
-#else
-  aRv.MightThrowJSException();
-  JS::Rooted<JSObject*> body(aCx, JS::NewReadableExternalSourceStreamObject(
-                                      aCx, stream, aStreamHolder));
-  if (!body) {
-    aRv.StealExceptionFromJSContext(aCx);
-    return;
-  }
-
-  // This will be released in BodyStream::FinalizeCallback().  We are
-  // guaranteed the jsapi will call FinalizeCallback when ReadableStream
-  // js object is finalized.
-  NS_ADDREF(stream.get());
-#endif
 
   cleanup.release();
 
   aStreamHolder->StoreBodyStream(stream);
   aStreamHolder->SetReadableStreamBody(body);
 
 #ifdef DEBUG
   aStreamHolder->mStreamCreated = true;
 #endif
 }
 
-#ifdef MOZ_DOM_STREAMS
 // UnderlyingSource.pull, implemented for BodyStream.
 already_AddRefed<Promise> BodyStream::PullCallback(
     JSContext* aCx, ReadableStreamController& aController, ErrorResult& aRv) {
   MOZ_ASSERT(aController.IsByte());
   ReadableStream* stream = aController.AsByte()->Stream();
   MOZ_ASSERT(stream);
 
-#  if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
   MOZ_DIAGNOSTIC_ASSERT(stream->Disturbed());
-#  endif
+#endif
 
   AssertIsOnOwningThread();
 
   MutexSingleWriterAutoLock lock(mMutex);
 
   MOZ_DIAGNOSTIC_ASSERT(mState == eInitializing || mState == eWaiting ||
                         mState == eChecking || mState == eReading);
 
@@ -239,100 +214,22 @@ already_AddRefed<Promise> BodyStream::Pu
   if (NS_WARN_IF(NS_FAILED(rv))) {
     ErrorPropagation(aCx, lock, stream, rv);
     return nullptr;
   }
 
   // All good.
   return resolvedWithUndefinedPromise.forget();
 }
-#else
-void BodyStream::requestData(JSContext* aCx, JS::HandleObject aStream,
-                             size_t aDesiredSize) {
-#  if MOZ_DIAGNOSTIC_ASSERT_ENABLED
-  bool disturbed;
-  if (!JS::ReadableStreamIsDisturbed(aCx, aStream, &disturbed)) {
-    JS_ClearPendingException(aCx);
-  } else {
-    MOZ_DIAGNOSTIC_ASSERT(disturbed);
-  }
-#  endif
 
-  AssertIsOnOwningThread();
-
-  MutexSingleWriterAutoLock lock(mMutex);
-
-  MOZ_DIAGNOSTIC_ASSERT(mState == eInitializing || mState == eWaiting ||
-                        mState == eChecking || mState == eReading);
-
-  if (mState == eReading) {
-    // We are already reading data.
-    return;
-  }
-
-  if (mState == eChecking) {
-    // If we are looking for more data, there is nothing else we should do:
-    // let's move this checking operation in a reading.
-    MOZ_ASSERT(mInputStream);
-    mState = eReading;
-    return;
-  }
-
-  if (mState == eInitializing) {
-    // The stream has been used for the first time.
-    mStreamHolder->MarkAsRead();
-  }
-
-  mState = eReading;
-
-  if (!mInputStream) {
-    // This is the first use of the stream. Let's convert the
-    // mOriginalInputStream into an nsIAsyncInputStream.
-    MOZ_ASSERT(mOriginalInputStream);
-
-    nsCOMPtr<nsIAsyncInputStream> asyncStream;
-    nsresult rv = NS_MakeAsyncNonBlockingInputStream(
-        mOriginalInputStream.forget(), getter_AddRefs(asyncStream));
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      ErrorPropagation(aCx, lock, aStream, rv);
-      return;
-    }
-
-    mInputStream = asyncStream;
-    mOriginalInputStream = nullptr;
-  }
-
-  MOZ_DIAGNOSTIC_ASSERT(mInputStream);
-  MOZ_DIAGNOSTIC_ASSERT(!mOriginalInputStream);
-
-  nsresult rv = mInputStream->AsyncWait(this, 0, 0, mOwningEventTarget);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    ErrorPropagation(aCx, lock, aStream, rv);
-    return;
-  }
-
-  // All good.
-}
-#endif
-
-#ifdef MOZ_DOM_STREAMS
 void BodyStream::WriteIntoReadRequestBuffer(JSContext* aCx,
                                             ReadableStream* aStream,
                                             JS::Handle<JSObject*> aChunk,
                                             uint32_t aLength,
                                             uint32_t* aByteWritten) {
-#else
-// This is passed the buffer directly: It is the responsibility of the
-// caller to ensure the buffer handling is GC Safe.
-void BodyStream::writeIntoReadRequestBuffer(JSContext* aCx,
-                                            JS::HandleObject aStream,
-                                            JS::Handle<JSObject*> aChunk,
-                                            size_t aLength,
-                                            size_t* aByteWritten) {
-#endif
   MOZ_DIAGNOSTIC_ASSERT(aChunk);
   MOZ_DIAGNOSTIC_ASSERT(aByteWritten);
 
   AssertIsOnOwningThread();
 
   MutexSingleWriterAutoLock lock(mMutex);
 
   MOZ_DIAGNOSTIC_ASSERT(mInputStream);
@@ -374,17 +271,16 @@ void BodyStream::writeIntoReadRequestBuf
   if (NS_WARN_IF(NS_FAILED(rv))) {
     ErrorPropagation(aCx, lock, aStream, rv);
     return;
   }
 
   // All good.
 }
 
-#ifdef MOZ_DOM_STREAMS
 // UnderlyingSource.cancel callback, implmented for BodyStream.
 already_AddRefed<Promise> BodyStream::CancelCallback(
     JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason,
     ErrorResult& aRv) {
   mMutex.AssertOnWritingThread();
 
   if (mState == eInitializing) {
     // The stream has been used for the first time.
@@ -407,112 +303,49 @@ already_AddRefed<Promise> BodyStream::Ca
     return nullptr;
   }
 
   // Must come after all uses of members!
   ReleaseObjects();
 
   return promise.forget();
 }
-#else
-JS::Value BodyStream::cancel(JSContext* aCx, JS::HandleObject aStream,
-                             JS::HandleValue aReason) {
-  AssertIsOnOwningThread();
 
-  if (mState == eInitializing) {
-    // The stream has been used for the first time.
-    mStreamHolder->MarkAsRead();
-  }
-
-  if (mInputStream) {
-    mInputStream->CloseWithStatus(NS_BASE_STREAM_CLOSED);
-  }
-
-  // It could be that we don't have mInputStream yet, but we still have the
-  // original stream. We need to close that too.
-  if (mOriginalInputStream) {
-    MOZ_ASSERT(!mInputStream);
-    mOriginalInputStream->Close();
-  }
-
-  ReleaseObjects();
-  return JS::UndefinedValue();
-}
-
-void BodyStream::onClosed(JSContext* aCx, JS::HandleObject aStream) {}
-#endif
-
-#ifdef MOZ_DOM_STREAMS
 // Non-standard UnderlyingSource.error callback.
 void BodyStream::ErrorCallback() {
   mMutex.AssertOnWritingThread();
 
   if (mState == eInitializing) {
     // The stream has been used for the first time.
     mStreamHolder->MarkAsRead();
   }
 
   if (mInputStream) {
     mInputStream->CloseWithStatus(NS_BASE_STREAM_CLOSED);
   }
 
   ReleaseObjects();
 }
-#else
-void BodyStream::onErrored(JSContext* aCx, JS::HandleObject aStream,
-                           JS::HandleValue aReason) {
-  AssertIsOnOwningThread();
-
-  if (mState == eInitializing) {
-    // The stream has been used for the first time.
-    mStreamHolder->MarkAsRead();
-  }
-
-  if (mInputStream) {
-    mInputStream->CloseWithStatus(NS_BASE_STREAM_CLOSED);
-  }
-
-  ReleaseObjects();
-}
-
-#endif
-
-#ifndef MOZ_DOM_STREAMS
-void BodyStream::finalize() {
-  // This can be called in any thread.
-
-  // This takes ownership of the ref created in BodyStream::Create().
-  RefPtr<BodyStream> stream = dont_AddRef(this);
-
-  stream->ReleaseObjects();
-}
-#endif
 
 BodyStream::BodyStream(nsIGlobalObject* aGlobal,
                        BodyStreamHolder* aStreamHolder,
                        nsIInputStream* aInputStream)
     : mMutex("BodyStream::mMutex", this),
       mState(eInitializing),
       mGlobal(aGlobal),
       mStreamHolder(aStreamHolder),
       mOwningEventTarget(aGlobal->EventTargetFor(TaskCategory::Other)),
       mOriginalInputStream(aInputStream) {
   MOZ_DIAGNOSTIC_ASSERT(aInputStream);
   MOZ_DIAGNOSTIC_ASSERT(aStreamHolder);
 }
 
-#ifdef MOZ_DOM_STREAMS
 void BodyStream::ErrorPropagation(JSContext* aCx,
                                   const MutexSingleWriterAutoLock& aProofOfLock,
                                   ReadableStream* aStream, nsresult aError) {
-#else
-void BodyStream::ErrorPropagation(JSContext* aCx,
-                                  const MutexSingleWriterAutoLock& aProofOfLock,
-                                  JS::HandleObject aStream, nsresult aError) {
-#endif
   mMutex.AssertOnWritingThread();
   mMutex.AssertCurrentThreadOwns();
 
   // Nothing to do.
   if (mState == eClosed) {
     return;
   }
 
@@ -529,32 +362,27 @@ void BodyStream::ErrorPropagation(JSCont
   rv.ThrowTypeError("Error in body stream");
 
   JS::Rooted<JS::Value> errorValue(aCx);
   bool ok = ToJSValue(aCx, std::move(rv), &errorValue);
   MOZ_RELEASE_ASSERT(ok, "ToJSValue never fails for ErrorResult");
 
   {
     MutexSingleWriterAutoUnlock unlock(mMutex);
-#ifdef MOZ_DOM_STREAMS
     // Don't re-error an already errored stream.
     if (aStream->State() == ReadableStream::ReaderState::Readable) {
       IgnoredErrorResult rv;
       ReadableStreamError(aCx, aStream, errorValue, rv);
       NS_WARNING_ASSERTION(!rv.Failed(), "Failed to error BodyStream");
     }
-#else
-    JS::ReadableStreamError(aCx, aStream, errorValue);
-#endif
   }
 
   ReleaseObjects(aProofOfLock);
 }
 
-#ifdef MOZ_DOM_STREAMS
 void BodyStream::EnqueueChunkWithSizeIntoStream(JSContext* aCx,
                                                 ReadableStream* aStream,
                                                 uint64_t aAvailableData,
                                                 ErrorResult& aRv) {
   // To avoid OOMing up on huge amounts of available data on a 32 bit system,
   // as well as potentially overflowing nsIInputStream's Read method's
   // parameter, let's limit our maximum chunk size to 256MB.
   uint32_t ableToRead =
@@ -589,17 +417,16 @@ void BodyStream::EnqueueChunkWithSizeInt
   RefPtr<ReadableByteStreamController> byteStreamController =
       aStream->Controller()->AsByte();
 
   ReadableByteStreamControllerEnqueue(aCx, byteStreamController, chunk, aRv);
   if (aRv.Failed()) {
     return;
   }
 }
-#endif
 
 // thread-safety doesn't handle emplace well
 NS_IMETHODIMP
 BodyStream::OnInputStreamReady(nsIAsyncInputStream* aStream)
     NO_THREAD_SAFETY_ANALYSIS {
   AssertIsOnOwningThread();
   MOZ_DIAGNOSTIC_ASSERT(aStream);
 
@@ -619,29 +446,20 @@ BodyStream::OnInputStreamReady(nsIAsyncI
   // destructors run.)
   nsAutoMicroTask mt;
   AutoEntryScript aes(mGlobal, "fetch body data available");
 
   MOZ_DIAGNOSTIC_ASSERT(mInputStream);
   MOZ_DIAGNOSTIC_ASSERT(mState == eReading || mState == eChecking);
 
   JSContext* cx = aes.cx();
-#ifdef MOZ_DOM_STREAMS
   ReadableStream* stream = mStreamHolder->GetReadableStreamBody();
   if (!stream) {
     return NS_ERROR_FAILURE;
   }
-#else
-  JSObject* streamObj = mStreamHolder->GetReadableStreamBody();
-  if (!streamObj) {
-    return NS_ERROR_FAILURE;
-  }
-
-  JS::Rooted<JSObject*> stream(cx, streamObj);
-#endif
 
   uint64_t size = 0;
   nsresult rv = mInputStream->Available(&size);
   if (NS_SUCCEEDED(rv) && size == 0) {
     // In theory this should not happen. If size is 0, the stream should be
     // considered closed.
     rv = NS_BASE_STREAM_CLOSED;
   }
@@ -658,37 +476,32 @@ BodyStream::OnInputStreamReady(nsIAsyncI
     return NS_OK;
   }
 
   mState = eWriting;
 
   // Release the mutex before the call below (which could execute JS), as well
   // as before the microtask checkpoint queued up above occurs.
   lock.reset();
-#ifdef MOZ_DOM_STREAMS
   ErrorResult errorResult;
   EnqueueChunkWithSizeIntoStream(cx, stream, size, errorResult);
   errorResult.WouldReportJSException();
   if (errorResult.Failed()) {
     lock.emplace(mMutex);
     ErrorPropagation(cx, *lock, stream, errorResult.StealNSResult());
     return NS_OK;
   }
-#else
-  Unused << JS::ReadableStreamUpdateDataAvailableFromSource(cx, stream, size);
-#endif
 
   // The previous call can execute JS (even up to running a nested event
   // loop), so |mState| can't be asserted to have any particular value, even
   // if the previous call succeeds.
 
   return NS_OK;
 }
 
-#ifdef MOZ_DOM_STREAMS
 /* static */
 nsresult BodyStream::RetrieveInputStream(BodyStreamHolder* aStream,
                                          nsIInputStream** aInputStream) {
   MOZ_ASSERT(aStream);
   MOZ_ASSERT(aInputStream);
   BodyStream* stream = aStream->GetBodyStream();
   if (NS_WARN_IF(!stream)) {
     return NS_ERROR_DOM_INVALID_STATE_ERR;
@@ -701,75 +514,40 @@ nsresult BodyStream::RetrieveInputStream
   if (NS_WARN_IF(!stream->mOriginalInputStream)) {
     return NS_ERROR_DOM_INVALID_STATE_ERR;
   }
 
   nsCOMPtr<nsIInputStream> inputStream = stream->mOriginalInputStream;
   inputStream.forget(aInputStream);
   return NS_OK;
 }
-#else
-/* static */
-nsresult BodyStream::RetrieveInputStream(
-    JS::ReadableStreamUnderlyingSource* aUnderlyingReadableStreamSource,
-    nsIInputStream** aInputStream) {
-  MOZ_ASSERT(aUnderlyingReadableStreamSource);
-  MOZ_ASSERT(aInputStream);
-
-  RefPtr<BodyStream> stream =
-      static_cast<BodyStream*>(aUnderlyingReadableStreamSource);
-  stream->AssertIsOnOwningThread();
-
-  // if mOriginalInputStream is null, the reading already started. We don't want
-  // to expose the internal inputStream.
-  if (NS_WARN_IF(!stream->mOriginalInputStream)) {
-    return NS_ERROR_DOM_INVALID_STATE_ERR;
-  }
-
-  nsCOMPtr<nsIInputStream> inputStream = stream->mOriginalInputStream;
-  inputStream.forget(aInputStream);
-  return NS_OK;
-}
-#endif
 
 void BodyStream::Close() {
   AssertIsOnOwningThread();
 
   MutexSingleWriterAutoLock lock(mMutex);
 
   if (mState == eClosed) {
     return;
   }
 
   AutoJSAPI jsapi;
   if (NS_WARN_IF(!jsapi.Init(mGlobal))) {
     ReleaseObjects(lock);
     return;
   }
-#ifdef MOZ_DOM_STREAMS
   ReadableStream* stream = mStreamHolder->GetReadableStreamBody();
   if (stream) {
     JSContext* cx = jsapi.cx();
     CloseAndReleaseObjects(cx, lock, stream);
   } else {
     ReleaseObjects(lock);
   }
-#else
-  JSObject* streamObj = mStreamHolder->GetReadableStreamBody();
-  if (streamObj) {
-    JSContext* cx = jsapi.cx();
-    JS::Rooted<JSObject*> stream(cx, streamObj);
-    CloseAndReleaseObjects(cx, lock, stream);
-  } else {
-    ReleaseObjects(lock);
-  }
-#endif
 }
 
-#ifdef MOZ_DOM_STREAMS
 void BodyStream::CloseAndReleaseObjects(
     JSContext* aCx, const MutexSingleWriterAutoLock& aProofOfLock,
     ReadableStream* aStream) {
   AssertIsOnOwningThread();
   mMutex.AssertCurrentThreadOwns();
   MOZ_DIAGNOSTIC_ASSERT(mState != eClosed);
 
   ReleaseObjects(aProofOfLock);
@@ -777,36 +555,16 @@ void BodyStream::CloseAndReleaseObjects(
   MutexSingleWriterAutoUnlock unlock(mMutex);
 
   if (aStream->State() == ReadableStream::ReaderState::Readable) {
     IgnoredErrorResult rv;
     ReadableStreamClose(aCx, aStream, rv);
     NS_WARNING_ASSERTION(!rv.Failed(), "Failed to Close Stream");
   }
 }
-#else
-void BodyStream::CloseAndReleaseObjects(
-    JSContext* aCx, const MutexSingleWriterAutoLock& aProofOfLock,
-    JS::HandleObject aStream) {
-  AssertIsOnOwningThread();
-  mMutex.AssertCurrentThreadOwns();
-  MOZ_DIAGNOSTIC_ASSERT(mState != eClosed);
-
-  ReleaseObjects(aProofOfLock);
-
-  MutexSingleWriterAutoUnlock unlock(mMutex);
-  bool readable;
-  if (!JS::ReadableStreamIsReadable(aCx, aStream, &readable)) {
-    return;
-  }
-  if (readable) {
-    JS::ReadableStreamClose(aCx, aStream);
-  }
-}
-#endif
 
 void BodyStream::ReleaseObjects() {
   MutexSingleWriterAutoLock lock(mMutex);
   ReleaseObjects(lock);
 }
 
 void BodyStream::ReleaseObjects(const MutexSingleWriterAutoLock& aProofOfLock) {
   // This method can be called on 2 possible threads: the owning one and a JS
@@ -842,49 +600,37 @@ void BodyStream::ReleaseObjects(const Mu
 
   if (NS_IsMainThread()) {
     nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
     if (obs) {
       obs->RemoveObserver(this, DOM_WINDOW_DESTROYED_TOPIC);
     }
   }
 
-#ifdef MOZ_DOM_STREAMS
   ReadableStream* stream = mStreamHolder->GetReadableStreamBody();
   if (stream) {
     stream->ReleaseObjects();
   }
-#else
-  JSObject* streamObj = mStreamHolder->GetReadableStreamBody();
-  if (streamObj) {
-    // Let's inform the JSEngine that we are going to be released.
-    JS::ReadableStreamReleaseCCObject(streamObj);
-  }
-#endif
 
   mWorkerRef = nullptr;
   mGlobal = nullptr;
 
-#ifdef MOZ_DOM_STREAMS
   // Since calling ForgetBodyStream can cause our current ref count to drop to
   // zero, which would be bad, because this means we'd be destroying the mutex
   // which aProofOfLock is holding; instead, we do this later by creating an
   // event.
   GetCurrentSerialEventTarget()->Dispatch(NS_NewCancelableRunnableFunction(
       "BodyStream::ReleaseObjects",
       [streamHolder = RefPtr{mStreamHolder->TakeBodyStream()}] {
         // Intentionally left blank: The destruction of this lambda will free
         // free the stream holder, thus releasing the bodystream.
         //
         // This is cancelable because if a worker cancels this, we're still fine
         // as the lambda will be successfully destroyed.
       }));
-#else
-  mStreamHolder->ForgetBodyStream();
-#endif
   mStreamHolder->NullifyStream();
   mStreamHolder = nullptr;
 }
 
 #ifdef DEBUG
 void BodyStream::AssertIsOnOwningThread() const {
   NS_ASSERT_OWNINGTHREAD(BodyStream);
 }
--- a/dom/base/BodyStream.h
+++ b/dom/base/BodyStream.h
@@ -5,20 +5,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_BodyStream_h
 #define mozilla_dom_BodyStream_h
 
 #include "jsapi.h"
 #include "js/Stream.h"
 #include "mozilla/AlreadyAddRefed.h"
-#ifdef MOZ_DOM_STREAMS
-#  include "mozilla/dom/ByteStreamHelpers.h"
-#  include "mozilla/dom/BindingDeclarations.h"
-#endif
+#include "mozilla/dom/ByteStreamHelpers.h"
+#include "mozilla/dom/BindingDeclarations.h"
 #include "nsIAsyncInputStream.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsIObserver.h"
 #include "nsISupportsImpl.h"
 #include "nsNetCID.h"
 #include "nsWeakReference.h"
 #include "mozilla/Mutex.h"
 
@@ -29,19 +27,17 @@ class nsIInputStream;
 namespace mozilla {
 class ErrorResult;
 
 namespace dom {
 
 class BodyStream;
 class WeakWorkerRef;
 class ReadableStream;
-#ifdef MOZ_DOM_STREAMS
 class ReadableStreamController;
-#endif
 
 class BodyStreamUnderlyingSourceAlgorithms;
 
 class BodyStreamHolder : public nsISupports {
   friend class BodyStream;
   friend class BodyStreamUnderlyingSourceAlgorithms;
   friend class BodyStreamUnderlyingSourceErrorCallbackHelper;
 
@@ -50,61 +46,41 @@ class BodyStreamHolder : public nsISuppo
   NS_DECL_CYCLE_COLLECTION_CLASS(BodyStreamHolder)
 
   BodyStreamHolder();
 
   virtual void NullifyStream() = 0;
 
   virtual void MarkAsRead() = 0;
 
-#ifndef MOZ_DOM_STREAMS
-  virtual void SetReadableStreamBody(JSObject* aBody) = 0;
-  virtual JSObject* GetReadableStreamBody() = 0;
-#else
   virtual void SetReadableStreamBody(ReadableStream* aBody) = 0;
   virtual ReadableStream* GetReadableStreamBody() = 0;
-#endif
 
  protected:
   virtual ~BodyStreamHolder() = default;
 
  private:
   void StoreBodyStream(BodyStream* aBodyStream);
-#ifdef MOZ_DOM_STREAMS
   already_AddRefed<BodyStream> TakeBodyStream() {
     MOZ_ASSERT_IF(mStreamCreated, mBodyStream);
     return mBodyStream.forget();
   }
-#else
-  void ForgetBodyStream();
-#endif
   BodyStream* GetBodyStream() { return mBodyStream; }
 
-#ifdef MOZ_DOM_STREAMS
   RefPtr<BodyStream> mBodyStream;
-#else
-  // Raw pointer because BodyStream keeps BodyStreamHolder alive and it
-  // nullifies this stream before being released.
-  BodyStream* mBodyStream;
-#endif
 
 #ifdef DEBUG
   bool mStreamCreated = false;
 #endif
 };
 
 class BodyStream final : public nsIInputStreamCallback,
                          public nsIObserver,
                          public nsSupportsWeakReference,
-                         public SingleWriterLockOwner
-#ifndef MOZ_DOM_STREAMS
-    ,
-                         private JS::ReadableStreamUnderlyingSource
-#endif
-{
+                         public SingleWriterLockOwner {
   friend class BodyStreamHolder;
 
  public:
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIINPUTSTREAMCALLBACK
   NS_DECL_NSIOBSERVER
 
   // This method creates a JS ReadableStream object and it assigns it to the
@@ -119,37 +95,30 @@ class BodyStream final : public nsIInput
   bool OnWritingThread() const override {
 #ifdef MOZ_THREAD_SAFETY_OWNERSHIP_CHECKS_SUPPORTED
     return _mOwningThread.IsCurrentThread();
 #else
     return true;
 #endif
   }
 
-#ifdef MOZ_DOM_STREAMS
   static nsresult RetrieveInputStream(BodyStreamHolder* aStream,
                                       nsIInputStream** aInputStream);
-#else
-  static nsresult RetrieveInputStream(
-      JS::ReadableStreamUnderlyingSource* aUnderlyingReadableStreamSource,
-      nsIInputStream** aInputStream);
-#endif
 
  private:
   BodyStream(nsIGlobalObject* aGlobal, BodyStreamHolder* aStreamHolder,
              nsIInputStream* aInputStream);
   ~BodyStream() = default;
 
 #ifdef DEBUG
   void AssertIsOnOwningThread() const;
 #else
   void AssertIsOnOwningThread() const {}
 #endif
 
-#ifdef MOZ_DOM_STREAMS
  public:
   // Cancel Callback
   already_AddRefed<Promise> CancelCallback(
       JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason,
       ErrorResult& aRv);
 
   // Pull Callback
   already_AddRefed<Promise> PullCallback(JSContext* aCx,
@@ -173,43 +142,16 @@ class BodyStream final : public nsIInput
   void ErrorPropagation(JSContext* aCx,
                         const MutexSingleWriterAutoLock& aProofOfLock,
                         ReadableStream* aStream, nsresult aRv) REQUIRES(mMutex);
 
   // TODO: convert this to MOZ_CAN_RUN_SCRIPT (bug 1750605)
   MOZ_CAN_RUN_SCRIPT_BOUNDARY void CloseAndReleaseObjects(
       JSContext* aCx, const MutexSingleWriterAutoLock& aProofOfLock,
       ReadableStream* aStream) REQUIRES(mMutex);
-#else
-  void requestData(JSContext* aCx, JS::HandleObject aStream,
-                   size_t aDesiredSize) override;
-
-  void writeIntoReadRequestBuffer(JSContext* aCx, JS::HandleObject aStream,
-                                  JS::Handle<JSObject*> aChunk, size_t aLength,
-                                  size_t* aBytesWritten) override;
-
-  JS::Value cancel(JSContext* aCx, JS::HandleObject aStream,
-                   JS::HandleValue aReason) override REQUIRES(mMutex);
-
-  void onClosed(JSContext* aCx, JS::HandleObject aStream) override;
-
-  void onErrored(JSContext* aCx, JS::HandleObject aStream,
-                 JS::HandleValue aReason) override REQUIRES(mMutex);
-
-  void finalize() override;
-
-  void ErrorPropagation(JSContext* aCx,
-                        const MutexSingleWriterAutoLock& aProofOfLock,
-                        JS::HandleObject aStream, nsresult aRv)
-      REQUIRES(mMutex);
-
-  void CloseAndReleaseObjects(JSContext* aCx,
-                              const MutexSingleWriterAutoLock& aProofOfLock,
-                              JS::HandleObject aStream) REQUIRES(mMutex);
-#endif
 
   class WorkerShutdown;
 
   void ReleaseObjects(const MutexSingleWriterAutoLock& aProofOfLock)
       REQUIRES(mMutex);
 
   void ReleaseObjects();
 
--- a/dom/base/nsFrameMessageManager.cpp
+++ b/dom/base/nsFrameMessageManager.cpp
@@ -64,16 +64,17 @@
 #include "mozilla/dom/RootedDictionary.h"
 #include "mozilla/dom/SameProcessMessageQueue.h"
 #include "mozilla/dom/ScriptLoader.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/ToJSValue.h"
 #include "mozilla/dom/MessageManagerCallback.h"
 #include "mozilla/dom/ipc/SharedMap.h"
 #include "mozilla/dom/ipc/StructuredCloneData.h"
+#include "mozilla/scache/StartupCacheUtils.h"
 #include "nsASCIIMask.h"
 #include "nsBaseHashtable.h"
 #include "nsCOMPtr.h"
 #include "nsClassHashtable.h"
 #include "nsComponentManagerUtils.h"
 #include "nsContentUtils.h"
 #include "nsCycleCollectionNoteChild.h"
 #include "nsCycleCollectionParticipant.h"
@@ -125,16 +126,18 @@
 #ifdef FUZZING
 #  include "MessageManagerFuzzer.h"
 #endif
 
 using namespace mozilla;
 using namespace mozilla::dom;
 using namespace mozilla::dom::ipc;
 
+#define CACHE_PREFIX(type) "mm/" type
+
 nsFrameMessageManager::nsFrameMessageManager(MessageManagerCallback* aCallback,
                                              MessageManagerFlags aFlags)
     : mChrome(aFlags & MessageManagerFlags::MM_CHROME),
       mGlobal(aFlags & MessageManagerFlags::MM_GLOBAL),
       mIsProcessManager(aFlags & MessageManagerFlags::MM_PROCESSMANAGER),
       mIsBroadcaster(aFlags & MessageManagerFlags::MM_BROADCASTER),
       mOwnsCallback(aFlags & MessageManagerFlags::MM_OWNSCALLBACK),
       mHandlingMessage(false),
@@ -1267,20 +1270,24 @@ nsMessageManagerScriptExecutor::TryCache
   AutoJSAPI jsapi;
   if (!jsapi.Init(isRunOnce ? aMessageManager : xpc::CompilationScope())) {
     return nullptr;
   }
   JSContext* cx = jsapi.cx();
 
   RefPtr<JS::Stencil> stencil;
   if (useScriptPreloader) {
+    nsAutoCString cachePath;
+    rv = scache::PathifyURI(CACHE_PREFIX("script"), uri, cachePath);
+    NS_ENSURE_SUCCESS(rv, nullptr);
+
     JS::DecodeOptions decodeOptions;
     ScriptPreloader::FillDecodeOptionsForCachedStencil(decodeOptions);
     stencil = ScriptPreloader::GetChildSingleton().GetCachedStencil(
-        cx, decodeOptions, url);
+        cx, decodeOptions, cachePath);
   }
 
   if (!stencil) {
     nsCOMPtr<nsIChannel> channel;
     NS_NewChannel(getter_AddRefs(channel), uri,
                   nsContentUtils::GetSystemPrincipal(),
                   nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
                   nsIContentPolicy::TYPE_INTERNAL_FRAME_MESSAGEMANAGER_SCRIPT);
@@ -1345,17 +1352,20 @@ nsMessageManagerScriptExecutor::TryCache
     JS::InstantiateOptions instantiateOptions(options);
     instantiateOptions.assertDefault();
 #endif
   }
 
   MOZ_ASSERT(stencil);
 
   if (useScriptPreloader) {
-    ScriptPreloader::GetChildSingleton().NoteStencil(url, url, stencil,
+    nsAutoCString cachePath;
+    rv = scache::PathifyURI(CACHE_PREFIX("script"), uri, cachePath);
+    NS_ENSURE_SUCCESS(rv, nullptr);
+    ScriptPreloader::GetChildSingleton().NoteStencil(url, cachePath, stencil,
                                                      isRunOnce);
   }
 
   return stencil.forget();
 }
 
 void nsMessageManagerScriptExecutor::Trace(const TraceCallbacks& aCallbacks,
                                            void* aClosure) {
--- a/dom/base/nsIGlobalObject.cpp
+++ b/dom/base/nsIGlobalObject.cpp
@@ -129,39 +129,35 @@ void nsIGlobalObject::UnlinkObjectsInGlo
       if (NS_FAILED(rv)) {
         NS_WARNING("Failed to dispatch a runnable to the main-thread.");
       }
     }
   }
 
   mReportRecords.Clear();
   mReportingObservers.Clear();
-#ifdef MOZ_DOM_STREAMS
   mCountQueuingStrategySizeFunction = nullptr;
   mByteLengthQueuingStrategySizeFunction = nullptr;
-#endif
 }
 
 void nsIGlobalObject::TraverseObjectsInGlobal(
     nsCycleCollectionTraversalCallback& cb) {
   // Currently we only store BlobImpl objects off the the main-thread and they
   // are not CCed.
   if (!mHostObjectURIs.IsEmpty() && NS_IsMainThread()) {
     for (uint32_t index = 0; index < mHostObjectURIs.Length(); ++index) {
       BlobURLProtocolHandler::Traverse(mHostObjectURIs[index], cb);
     }
   }
 
   nsIGlobalObject* tmp = this;
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReportRecords)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReportingObservers)
-#ifdef MOZ_DOM_STREAMS
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCountQueuingStrategySizeFunction)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mByteLengthQueuingStrategySizeFunction)
-#endif
 }
 
 void nsIGlobalObject::AddEventTargetObject(DOMEventTargetHelper* aObject) {
   MOZ_DIAGNOSTIC_ASSERT(aObject);
   MOZ_ASSERT(!aObject->isInList());
   mEventTargetObjects.insertBack(aObject);
 }
 
@@ -352,17 +348,16 @@ void nsIGlobalObject::NotifyReportingObs
 void nsIGlobalObject::RemoveReportRecords() {
   mReportRecords.Clear();
 
   for (auto& observer : mReportingObservers) {
     observer->ForgetReports();
   }
 }
 
-#ifdef MOZ_DOM_STREAMS
 already_AddRefed<mozilla::dom::Function>
 nsIGlobalObject::GetCountQueuingStrategySizeFunction() {
   return do_AddRef(mCountQueuingStrategySizeFunction);
 }
 
 void nsIGlobalObject::SetCountQueuingStrategySizeFunction(
     mozilla::dom::Function* aFunction) {
   mCountQueuingStrategySizeFunction = aFunction;
@@ -372,13 +367,12 @@ already_AddRefed<mozilla::dom::Function>
 nsIGlobalObject::GetByteLengthQueuingStrategySizeFunction() {
   return do_AddRef(mByteLengthQueuingStrategySizeFunction);
 }
 
 void nsIGlobalObject::SetByteLengthQueuingStrategySizeFunction(
     mozilla::dom::Function* aFunction) {
   mByteLengthQueuingStrategySizeFunction = aFunction;
 }
-#endif
 
 bool nsIGlobalObject::ShouldResistFingerprinting() const {
   return nsContentUtils::ShouldResistFingerprinting();
 }
--- a/dom/base/nsIGlobalObject.h
+++ b/dom/base/nsIGlobalObject.h
@@ -218,28 +218,26 @@ class nsIGlobalObject : public nsISuppor
   void UnregisterReportingObserver(mozilla::dom::ReportingObserver* aObserver);
 
   void BroadcastReport(mozilla::dom::Report* aReport);
 
   MOZ_CAN_RUN_SCRIPT void NotifyReportingObservers();
 
   void RemoveReportRecords();
 
-#ifdef MOZ_DOM_STREAMS
   // https://streams.spec.whatwg.org/#count-queuing-strategy-size-function
   // This function is set once by CountQueuingStrategy::GetSize.
   already_AddRefed<mozilla::dom::Function>
   GetCountQueuingStrategySizeFunction();
   void SetCountQueuingStrategySizeFunction(mozilla::dom::Function* aFunction);
 
   already_AddRefed<mozilla::dom::Function>
   GetByteLengthQueuingStrategySizeFunction();
   void SetByteLengthQueuingStrategySizeFunction(
       mozilla::dom::Function* aFunction);
-#endif
 
   /**
    * Check whether we should avoid leaking distinguishing information to JS/CSS.
    * https://w3c.github.io/fingerprinting-guidance/
    */
   virtual bool ShouldResistFingerprinting() const;
 
   /**
@@ -268,20 +266,18 @@ class nsIGlobalObject : public nsISuppor
 
   size_t ShallowSizeOfExcludingThis(mozilla::MallocSizeOf aSizeOf) const;
 
  private:
   // List of Report objects for ReportingObservers.
   nsTArray<RefPtr<mozilla::dom::ReportingObserver>> mReportingObservers;
   nsTArray<RefPtr<mozilla::dom::Report>> mReportRecords;
 
-#ifdef MOZ_DOM_STREAMS
   // https://streams.spec.whatwg.org/#count-queuing-strategy-size-function
   RefPtr<mozilla::dom::Function> mCountQueuingStrategySizeFunction;
 
   // https://streams.spec.whatwg.org/#byte-length-queuing-strategy-size-function
   RefPtr<mozilla::dom::Function> mByteLengthQueuingStrategySizeFunction;
-#endif
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsIGlobalObject, NS_IGLOBALOBJECT_IID)
 
 #endif  // nsIGlobalObject_h__
--- a/dom/bindings/Codegen.py
+++ b/dom/bindings/Codegen.py
@@ -1420,20 +1420,17 @@ class CGHeaders(CGWrapper):
                 if unrolled.isSpiderMonkeyInterface():
                     bindingHeaders.add("jsfriendapi.h")
                     if jsImplementedDescriptors:
                         # Since we can't forward-declare typed array types
                         # (because they're typedefs), we have to go ahead and
                         # just include their header if we need to have functions
                         # taking references to them declared in that header.
                         headerSet = declareIncludes
-                    if unrolled.isReadableStream():
-                        headerSet.add("mozilla/dom/ReadableStream.h")
-                    else:
-                        headerSet.add("mozilla/dom/TypedArray.h")
+                    headerSet.add("mozilla/dom/TypedArray.h")
                 else:
                     try:
                         typeDesc = config.getDescriptor(unrolled.inner.identifier.name)
                     except NoSuchDescriptorError:
                         return
                     # Dictionaries with interface members rely on the
                     # actual class definition of that interface member
                     # being visible in the binding header, because they
@@ -1668,20 +1665,17 @@ def UnionTypes(unionTypes, config):
                 if f.isPromise():
                     headers.add("mozilla/dom/Promise.h")
                     # We need ToJSValue to do the Promise to JS conversion.
                     headers.add("mozilla/dom/ToJSValue.h")
                 elif f.isInterface():
                     if f.isSpiderMonkeyInterface():
                         headers.add("js/RootingAPI.h")
                         headers.add("js/Value.h")
-                        if f.isReadableStream():
-                            headers.add("mozilla/dom/ReadableStream.h")
-                        else:
-                            headers.add("mozilla/dom/TypedArray.h")
+                        headers.add("mozilla/dom/TypedArray.h")
                     else:
                         try:
                             typeDesc = config.getDescriptor(f.inner.identifier.name)
                         except NoSuchDescriptorError:
                             return
                         if typeDesc.interface.isCallback() or isSequence:
                             # Callback interfaces always use strong refs, so
                             # we need to include the right header to be able
@@ -1783,20 +1777,17 @@ def UnionConversions(unionTypes, config)
                     headers.add("mozilla/dom/BindingCallContext.h")
                 if f.isPromise():
                     headers.add("mozilla/dom/Promise.h")
                     # We need ToJSValue to do the Promise to JS conversion.
                     headers.add("mozilla/dom/ToJSValue.h")
                 elif f.isInterface():
                     if f.isSpiderMonkeyInterface():
                         headers.add("js/RootingAPI.h")
-                        if f.isReadableStream():
-                            headers.add("mozilla/dom/ReadableStream.h")
-                        else:
-                            headers.add("mozilla/dom/TypedArray.h")
+                        headers.add("mozilla/dom/TypedArray.h")
                     elif f.inner.isExternal():
                         try:
                             typeDesc = config.getDescriptor(f.inner.identifier.name)
                         except NoSuchDescriptorError:
                             return
                         headers.add(typeDesc.headerFile)
                     else:
                         headers.add(CGHeaders.getDeclarationFilename(f.inner))
deleted file mode 100644
--- a/dom/bindings/ReadableStream.h
+++ /dev/null
@@ -1,30 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-#ifndef mozilla_dom_ReadableStream_h
-#define mozilla_dom_ReadableStream_h
-
-#ifdef MOZ_DOM_STREAMS
-#  error "This shouldn't have been included"
-#else
-#  include "js/experimental/TypedData.h"
-#  include "mozilla/dom/SpiderMonkeyInterface.h"
-namespace mozilla {
-namespace dom {
-
-class ReadableStream : public SpiderMonkeyInterfaceObjectStorage {
- public:
-  inline bool Init(JSObject* obj) {
-    MOZ_ASSERT(!inited());
-    mImplObj = mWrappedObj = js::UnwrapReadableStream(obj);
-    return inited();
-  }
-};
-
-}  // namespace dom
-}  // namespace mozilla
-#endif
-#endif /* mozilla_dom_ReadableStream_h */
--- a/dom/bindings/moz.build
+++ b/dom/bindings/moz.build
@@ -54,20 +54,16 @@ EXPORTS.mozilla.dom += [
     "SpiderMonkeyInterface.h",
     "ToJSValue.h",
     "TypedArray.h",
     "UnionMember.h",
     "WebIDLGlobalNameHash.h",
     "XrayExpandoClass.h",
 ]
 
-if not CONFIG["MOZ_DOM_STREAMS"]:
-    EXPORTS.mozilla.dom += [
-        "ReadableStream.h",
-    ]
 
 # Generated bindings reference *Binding.h, not mozilla/dom/*Binding.h. And,
 # since we generate exported bindings directly to $(DIST)/include, we need
 # to add that path to the search list.
 #
 # Ideally, binding generation uses the prefixed header file names.
 # Bug 932082 tracks.
 LOCAL_INCLUDES += [
--- a/dom/bindings/mozwebidlcodegen/__init__.py
+++ b/dom/bindings/mozwebidlcodegen/__init__.py
@@ -184,17 +184,16 @@ class WebIDLCodegenManager(LoggingMixin)
         webidl_root,
         inputs,
         exported_header_dir,
         codegen_dir,
         state_path,
         cache_dir=None,
         make_deps_path=None,
         make_deps_target=None,
-        use_builtin_readable_stream=True,
     ):
         """Create an instance that manages WebIDLs in the build system.
 
         config_path refers to a WebIDL config file (e.g. Bindings.conf).
         inputs is a 4-tuple describing the input .webidl files and how to
         process them. Members are:
             (set(.webidl files), set(basenames of exported files),
                 set(basenames of generated events files),
@@ -221,17 +220,16 @@ class WebIDLCodegenManager(LoggingMixin)
         self._generated_events_stems_as_array = generated_events_stems
         self._example_interfaces = set(example_interfaces)
         self._exported_header_dir = exported_header_dir
         self._codegen_dir = codegen_dir
         self._state_path = state_path
         self._cache_dir = cache_dir
         self._make_deps_path = make_deps_path
         self._make_deps_target = make_deps_target
-        self._use_builtin_readable_stream = use_builtin_readable_stream
 
         if (make_deps_path and not make_deps_target) or (
             not make_deps_path and make_deps_target
         ):
             raise Exception(
                 "Must define both make_deps_path and make_deps_target "
                 "if one is defined."
             )
@@ -377,21 +375,17 @@ class WebIDLCodegenManager(LoggingMixin)
         self.log(
             logging.INFO,
             "webidl_parse",
             {"count": len(self._input_paths)},
             "Parsing {count} WebIDL files.",
         )
 
         hashes = {}
-        parser = WebIDL.Parser(
-            self._cache_dir,
-            lexer=None,
-            use_builtin_readable_stream=self._use_builtin_readable_stream,
-        )
+        parser = WebIDL.Parser(self._cache_dir, lexer=None)
 
         for path in sorted(self._input_paths):
             with io.open(path, "r", encoding="utf-8") as fh:
                 data = fh.read()
                 hashes[path] = hashlib.sha1(six.ensure_binary(data)).hexdigest()
                 parser.parse(data, path)
 
         # Only these directories may contain WebIDL files with interfaces
@@ -645,32 +639,25 @@ class WebIDLCodegenManager(LoggingMixin)
         if not existed:
             result[0].add(path)
         elif updated:
             result[1].add(path)
         else:
             result[2].add(path)
 
 
-def create_build_system_manager(
-    topsrcdir=None, topobjdir=None, dist_dir=None, use_builtin_readable_stream=None
-):
+def create_build_system_manager(topsrcdir=None, topobjdir=None, dist_dir=None):
     """Create a WebIDLCodegenManager for use by the build system."""
     if topsrcdir is None:
-        assert (
-            topobjdir is None
-            and dist_dir is None
-            and use_builtin_readable_stream is None
-        )
+        assert topobjdir is None and dist_dir is None
         import buildconfig
 
         topsrcdir = buildconfig.topsrcdir
         topobjdir = buildconfig.topobjdir
         dist_dir = buildconfig.substs["DIST"]
-        use_builtin_readable_stream = not buildconfig.substs.get("MOZ_DOM_STREAMS")
 
     src_dir = os.path.join(topsrcdir, "dom", "bindings")
     obj_dir = os.path.join(topobjdir, "dom", "bindings")
     webidl_root = os.path.join(topsrcdir, "dom", "webidl")
 
     with io.open(os.path.join(obj_dir, "file-lists.json"), "r") as fh:
         files = json.load(fh)
 
@@ -694,10 +681,9 @@ def create_build_system_manager(
         inputs,
         os.path.join(dist_dir, "include", "mozilla", "dom"),
         obj_dir,
         os.path.join(obj_dir, "codegen.json"),
         cache_dir=cache_dir,
         # The make rules include a codegen.pp file containing dependencies.
         make_deps_path=os.path.join(obj_dir, "codegen.pp"),
         make_deps_target="webidl.stub",
-        use_builtin_readable_stream=use_builtin_readable_stream,
     )
--- a/dom/bindings/parser/WebIDL.py
+++ b/dom/bindings/parser/WebIDL.py
@@ -2445,19 +2445,16 @@ class IDLType(IDLObject):
         return self.name == "Void"
 
     def isSequence(self):
         return False
 
     def isRecord(self):
         return False
 
-    def isReadableStream(self):
-        return False
-
     def isArrayBuffer(self):
         return False
 
     def isArrayBufferView(self):
         return False
 
     def isTypedArray(self):
         return False
@@ -2476,17 +2473,17 @@ class IDLType(IDLObject):
         type that is implemented in Gecko. At the moment, this returns
         true for all interface types that are not types from the TypedArray
         spec."""
         return self.isInterface() and not self.isSpiderMonkeyInterface()
 
     def isSpiderMonkeyInterface(self):
         """Returns a boolean indicating whether this type is an 'interface'
         type that is implemented in SpiderMonkey."""
-        return self.isInterface() and (self.isBufferSource() or self.isReadableStream())
+        return self.isInterface() and self.isBufferSource()
 
     def isAny(self):
         return self.tag() == IDLType.Tags.any
 
     def isObject(self):
         return self.tag() == IDLType.Tags.object
 
     def isPromise(self):
@@ -2693,19 +2690,16 @@ class IDLNullableType(IDLParametrizedTyp
         return False
 
     def isSequence(self):
         return self.inner.isSequence()
 
     def isRecord(self):
         return self.inner.isRecord()
 
-    def isReadableStream(self):
-        return self.inner.isReadableStream()
-
     def isArrayBuffer(self):
         return self.inner.isArrayBuffer()
 
     def isArrayBufferView(self):
         return self.inner.isArrayBufferView()
 
     def isTypedArray(self):
         return self.inner.isTypedArray()
@@ -3204,19 +3198,16 @@ class IDLTypedefType(IDLType):
         return self.inner.isJSONType()
 
     def isSequence(self):
         return self.inner.isSequence()
 
     def isRecord(self):
         return self.inner.isRecord()
 
-    def isReadableStream(self):
-        return self.inner.isReadableStream()
-
     def isDictionary(self):
         return self.inner.isDictionary()
 
     def isArrayBuffer(self):
         return self.inner.isArrayBuffer()
 
     def isArrayBufferView(self):
         return self.inner.isArrayBufferView()
@@ -3549,17 +3540,16 @@ class IDLBuiltinType(IDLType):
         "Uint8Array",
         "Uint8ClampedArray",
         "Int16Array",
         "Uint16Array",
         "Int32Array",
         "Uint32Array",
         "Float32Array",
         "Float64Array",
-        "ReadableStream",
     )
 
     TagLookup = {
         Types.byte: IDLType.Tags.int8,
         Types.octet: IDLType.Tags.uint8,
         Types.short: IDLType.Tags.int16,
         Types.unsigned_short: IDLType.Tags.uint16,
         Types.long: IDLType.Tags.int32,
@@ -3585,17 +3575,16 @@ class IDLBuiltinType(IDLType):
         Types.Uint8Array: IDLType.Tags.interface,
         Types.Uint8ClampedArray: IDLType.Tags.interface,
         Types.Int16Array: IDLType.Tags.interface,
         Types.Uint16Array: IDLType.Tags.interface,
         Types.Int32Array: IDLType.Tags.interface,
         Types.Uint32Array: IDLType.Tags.interface,
         Types.Float32Array: IDLType.Tags.interface,
         Types.Float64Array: IDLType.Tags.interface,
-        Types.ReadableStream: IDLType.Tags.interface,
     }
 
     PrettyNames = {
         Types.byte: "byte",
         Types.octet: "octet",
         Types.short: "short",
         Types.unsigned_short: "unsigned short",
         Types.long: "long",
@@ -3621,17 +3610,16 @@ class IDLBuiltinType(IDLType):
         Types.Uint8Array: "Uint8Array",
         Types.Uint8ClampedArray: "Uint8ClampedArray",
         Types.Int16Array: "Int16Array",
         Types.Uint16Array: "Uint16Array",
         Types.Int32Array: "Int32Array",
         Types.Uint32Array: "Uint32Array",
         Types.Float32Array: "Float32Array",
         Types.Float64Array: "Float64Array",
-        Types.ReadableStream: "ReadableStream",
     }
 
     def __init__(
         self,
         location,
         name,
         type,
         clamp=False,
@@ -3782,29 +3770,21 @@ class IDLBuiltinType(IDLType):
         return self._typeTag == IDLBuiltinType.Types.ArrayBufferView
 
     def isTypedArray(self):
         return (
             self._typeTag >= IDLBuiltinType.Types.Int8Array
             and self._typeTag <= IDLBuiltinType.Types.Float64Array
         )
 
-    def isReadableStream(self):
-        return self._typeTag == IDLBuiltinType.Types.ReadableStream
-
     def isInterface(self):
         # TypedArray things are interface types per the TypedArray spec,
         # but we handle them as builtins because SpiderMonkey implements
         # all of it internally.
-        return (
-            self.isArrayBuffer()
-            or self.isArrayBufferView()
-            or self.isTypedArray()
-            or self.isReadableStream()
-        )
+        return self.isArrayBuffer() or self.isArrayBufferView() or self.isTypedArray()
 
     def isNonCallbackInterface(self):
         # All the interfaces we can be are non-callback
         return self.isInterface()
 
     def isFloat(self):
         return (
             self._typeTag == IDLBuiltinType.Types.float
@@ -3888,17 +3868,16 @@ class IDLBuiltinType(IDLType):
             or other.isSequence()
             or other.isRecord()
             or (
                 other.isInterface()
                 and (
                     # ArrayBuffer is distinguishable from everything
                     # that's not an ArrayBuffer or a callback interface
                     (self.isArrayBuffer() and not other.isArrayBuffer())
-                    or (self.isReadableStream() and not other.isReadableStream())
                     or
                     # ArrayBufferView is distinguishable from everything
                     # that's not an ArrayBufferView or typed array.
                     (
                         self.isArrayBufferView()
                         and not other.isArrayBufferView()
                         and not other.isTypedArray()
                     )
@@ -4095,21 +4074,16 @@ BuiltinTypes = {
         "Float32Array",
         IDLBuiltinType.Types.Float32Array,
     ),
     IDLBuiltinType.Types.Float64Array: IDLBuiltinType(
         BuiltinLocation("<builtin type>"),
         "Float64Array",
         IDLBuiltinType.Types.Float64Array,
     ),
-    IDLBuiltinType.Types.ReadableStream: IDLBuiltinType(
-        BuiltinLocation("<builtin type>"),
-        "ReadableStream",
-        IDLBuiltinType.Types.ReadableStream,
-    ),
 }
 
 
 integerTypeSizes = {
     IDLBuiltinType.Types.byte: (-128, 127),
     IDLBuiltinType.Types.octet: (0, 255),
     IDLBuiltinType.Types.short: (-32768, 32767),
     IDLBuiltinType.Types.unsigned_short: (0, 65535),
@@ -6752,19 +6726,16 @@ class Tokenizer(object):
                     )
                 ],
             )
         return t
 
     def t_IDENTIFIER(self, t):
         r"[_-]?[A-Za-z][0-9A-Z_a-z-]*"
         t.type = self.keywords.get(t.value, "IDENTIFIER")
-        # If Builtin readable streams are disabled, mark ReadableStream as an identifier.
-        if t.type == "READABLESTREAM" and not self._use_builtin_readable_streams:
-            t.type = "IDENTIFIER"
         return t
 
     def t_STRING(self, t):
         r'"[^"]*"'
         t.value = t.value[1:-1]
         return t
 
     def t_WHITESPACE(self, t):
@@ -6845,17 +6816,16 @@ class Tokenizer(object):
         "<": "LT",
         ">": "GT",
         "ArrayBuffer": "ARRAYBUFFER",
         "or": "OR",
         "maplike": "MAPLIKE",
         "setlike": "SETLIKE",
         "iterable": "ITERABLE",
         "namespace": "NAMESPACE",
-        "ReadableStream": "READABLESTREAM",
         "constructor": "CONSTRUCTOR",
         "symbol": "SYMBOL",
         "async": "ASYNC",
     }
 
     tokens.extend(keywords.values())
 
     def t_error(self, t):
@@ -6866,18 +6836,17 @@ class Tokenizer(object):
                     lexer=self.lexer,
                     lineno=self.lexer.lineno,
                     lexpos=self.lexer.lexpos,
                     filename=self.filename,
                 )
             ],
         )
 
-    def __init__(self, outputdir, lexer=None, use_builtin_readable_streams=True):
-        self._use_builtin_readable_streams = use_builtin_readable_streams
+    def __init__(self, outputdir, lexer=None):
         if lexer:
             self.lexer = lexer
         else:
             self.lexer = lex.lex(object=self, reflags=re.DOTALL)
 
 
 class SqueakyCleanLogger(object):
     errorWhitelist = [
@@ -8319,25 +8288,22 @@ class Parser(Tokenizer):
         UnionMemberTypes :
         """
         p[0] = []
 
     def p_DistinguishableType(self, p):
         """
         DistinguishableType : PrimitiveType Null
                             | ARRAYBUFFER Null
-                            | READABLESTREAM Null
                             | OBJECT Null
         """
         if p[1] == "object":
             type = BuiltinTypes[IDLBuiltinType.Types.object]
         elif p[1] == "ArrayBuffer":
             type = BuiltinTypes[IDLBuiltinType.Types.ArrayBuffer]
-        elif p[1] == "ReadableStream":
-            type = BuiltinTypes[IDLBuiltinType.Types.ReadableStream]
         else:
             type = BuiltinTypes[p[1]]
 
         p[0] = self.handleNullable(type, p[2])
 
     def p_DistinguishableTypeStringType(self, p):
         """
         DistinguishableType : StringType Null
@@ -8676,18 +8642,18 @@ class Parser(Tokenizer):
                 [self._filename],
             )
         else:
             raise WebIDLError(
                 "invalid syntax",
                 [Location(self.lexer, p.lineno, p.lexpos, self._filename)],
             )
 
-    def __init__(self, outputdir="", lexer=None, use_builtin_readable_stream=True):
-        Tokenizer.__init__(self, outputdir, lexer, use_builtin_readable_stream)
+    def __init__(self, outputdir="", lexer=None):
+        Tokenizer.__init__(self, outputdir, lexer)
 
         logger = SqueakyCleanLogger()
         try:
             self.parser = yacc.yacc(
                 module=self,
                 outputdir=outputdir,
                 errorlog=logger,
                 write_tables=False,
--- a/dom/chrome-webidl/BrowsingContext.webidl
+++ b/dom/chrome-webidl/BrowsingContext.webidl
@@ -200,16 +200,22 @@ interface BrowsingContext {
 
   /**
    * This allows chrome to override the default choice of whether touch events
    * are available in a specific BrowsingContext and its descendents.
    */
   readonly attribute TouchEventsOverride touchEventsOverride;
 
   /**
+   * Returns true if the top-level BrowsingContext has been configured to
+   * default-target user-initiated link clicks to _blank.
+   */
+  readonly attribute boolean targetTopLevelLinkClicksToBlank;
+
+  /**
    * Partially determines whether script execution is allowed in this
    * BrowsingContext. Script execution will be permitted only if this
    * attribute is true and script execution is allowed in the parent
    * WindowContext.
    *
    * May only be set in the parent process.
    */
   [SetterThrows] attribute boolean allowJavascript;
@@ -319,16 +325,22 @@ interface CanonicalBrowsingContext : Bro
 
   /**
    * This allows chrome to override the default choice of whether touch events
    * are available in a specific BrowsingContext and its descendents.
    */
   [SetterThrows] inherit attribute TouchEventsOverride touchEventsOverride;
 
   /**
+   * Set to true to configure the top-level BrowsingContext to default-target
+   * user-initiated link clicks to _blank.
+   */
+  [SetterThrows] inherit attribute boolean targetTopLevelLinkClicksToBlank;
+
+  /**
    * Set the cross-group opener of this BrowsingContext. This is used to
    * retarget the download dialog to an opener window, and close this
    * BrowsingContext, if the first load in a newly created BrowsingContext is a
    * download.
    *
    * This value will be automatically set for documents created using
    * `window.open`.
    */
--- a/dom/fetch/Fetch.cpp
+++ b/dom/fetch/Fetch.cpp
@@ -26,16 +26,17 @@
 #include "mozilla/dom/Exceptions.h"
 #include "mozilla/dom/DOMException.h"
 #include "mozilla/dom/FetchDriver.h"
 #include "mozilla/dom/File.h"
 #include "mozilla/dom/FormData.h"
 #include "mozilla/dom/Headers.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/PromiseWorkerProxy.h"
+#include "mozilla/dom/ReadableStreamDefaultReader.h"
 #include "mozilla/dom/RemoteWorkerChild.h"
 #include "mozilla/dom/Request.h"
 #include "mozilla/dom/Response.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/URLSearchParams.h"
 #include "mozilla/net/CookieJarSettings.h"
 
 #include "BodyExtractor.h"
@@ -43,68 +44,35 @@
 #include "InternalRequest.h"
 #include "InternalResponse.h"
 
 #include "mozilla/dom/WorkerCommon.h"
 #include "mozilla/dom/WorkerRef.h"
 #include "mozilla/dom/WorkerRunnable.h"
 #include "mozilla/dom/WorkerScope.h"
 
-#ifdef MOZ_DOM_STREAMS
-#  include "mozilla/dom/ReadableStreamDefaultReader.h"
-#endif
-
 namespace mozilla::dom {
 
 namespace {
 
-#ifdef MOZ_DOM_STREAMS
 void AbortStream(JSContext* aCx, ReadableStream* aReadableStream,
                  ErrorResult& aRv) {
   if (aReadableStream->State() != ReadableStream::ReaderState::Readable) {
     return;
   }
 
   RefPtr<DOMException> e = DOMException::Create(NS_ERROR_DOM_ABORT_ERR);
   JS::Rooted<JS::Value> value(aCx);
   if (!GetOrCreateDOMReflector(aCx, e, &value)) {
     return;
   }
 
   ReadableStreamError(aCx, aReadableStream, value, aRv);
 }
 
-#else
-
-void AbortStream(JSContext* aCx, JS::Handle<JSObject*> aStream,
-                 ErrorResult& aRv) {
-  aRv.MightThrowJSException();
-
-  bool isReadable;
-  if (!JS::ReadableStreamIsReadable(aCx, aStream, &isReadable)) {
-    aRv.StealExceptionFromJSContext(aCx);
-    return;
-  }
-  if (!isReadable) {
-    return;
-  }
-
-  RefPtr<DOMException> e = DOMException::Create(NS_ERROR_DOM_ABORT_ERR);
-
-  JS::Rooted<JS::Value> value(aCx);
-  if (!GetOrCreateDOMReflector(aCx, e, &value)) {
-    return;
-  }
-
-  if (!JS::ReadableStreamError(aCx, aStream, value)) {
-    aRv.StealExceptionFromJSContext(aCx);
-  }
-}
-#endif
-
 }  // namespace
 
 class AbortSignalMainThread final : public AbortSignalImpl {
  public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AbortSignalMainThread)
 
   explicit AbortSignalMainThread(bool aAborted)
@@ -1128,37 +1096,17 @@ template FetchBody<Response>::~FetchBody
 template <class Derived>
 bool FetchBody<Derived>::GetBodyUsed(ErrorResult& aRv) const {
   if (mBodyUsed) {
     return true;
   }
 
   // If this stream is disturbed, return true.
   if (mReadableStreamBody) {
-#ifdef MOZ_DOM_STREAMS
     return mReadableStreamBody->Disturbed();
-#else
-    aRv.MightThrowJSException();
-
-    AutoJSAPI jsapi;
-    if (!jsapi.Init(mOwner)) {
-      aRv.Throw(NS_ERROR_FAILURE);
-      return true;
-    }
-
-    JSContext* cx = jsapi.cx();
-    JS::Rooted<JSObject*> body(cx, mReadableStreamBody);
-    bool disturbed;
-    if (!JS::ReadableStreamIsDisturbed(cx, body, &disturbed)) {
-      aRv.StealExceptionFromJSContext(cx);
-      return false;
-    }
-
-    return disturbed;
-#endif
   }
 
   return false;
 }
 
 template bool FetchBody<Request>::GetBodyUsed(ErrorResult&) const;
 
 template bool FetchBody<Response>::GetBodyUsed(ErrorResult&) const;
@@ -1184,50 +1132,16 @@ void FetchBody<Derived>::SetBodyUsed(JSC
   MOZ_ASSERT(mOwner->EventTargetFor(TaskCategory::Other)->IsOnCurrentThread());
 
   if (mBodyUsed) {
     return;
   }
 
   mBodyUsed = true;
 
-#ifndef MOZ_DOM_STREAMS
-  // If we already have a ReadableStreamBody and it has been created by DOM, we
-  // have to lock it now because it can have been shared with other objects.
-  if (mReadableStreamBody) {
-    aRv.MightThrowJSException();
-    JSAutoRealm ar(aCx, mOwner->GetGlobalJSObject());
-
-    JS::Rooted<JSObject*> readableStreamObj(aCx, mReadableStreamBody);
-
-    JS::ReadableStreamMode mode;
-    if (!JS::ReadableStreamGetMode(aCx, readableStreamObj, &mode)) {
-      aRv.StealExceptionFromJSContext(aCx);
-      return;
-    }
-
-    if (mode == JS::ReadableStreamMode::ExternalSource) {
-      LockStream(aCx, readableStreamObj, aRv);
-      if (NS_WARN_IF(aRv.Failed())) {
-        return;
-      }
-    } else {
-      // If this is not a native ReadableStream, let's activate the
-      // FetchStreamReader.
-      MOZ_ASSERT(mFetchStreamReader);
-      JS::Rooted<JSObject*> reader(aCx);
-      mFetchStreamReader->StartConsuming(aCx, readableStreamObj, &reader, aRv);
-      if (NS_WARN_IF(aRv.Failed())) {
-        return;
-      }
-
-      mReadableStreamReader = reader;
-    }
-  }
-#else
   // If we already have a ReadableStreamBody and it has been created by DOM, we
   // have to lock it now because it can have been shared with other objects.
   if (mReadableStreamBody) {
     if (mReadableStreamBody->HasNativeUnderlyingSource()) {
       LockStream(aCx, mReadableStreamBody, aRv);
       if (NS_WARN_IF(aRv.Failed())) {
         return;
       }
@@ -1239,17 +1153,16 @@ void FetchBody<Derived>::SetBodyUsed(JSC
                                          getter_AddRefs(reader), aRv);
       if (NS_WARN_IF(aRv.Failed())) {
         return;
       }
 
       mReadableStreamReader = reader.forget();
     }
   }
-#endif
 }
 
 template void FetchBody<Request>::SetBodyUsed(JSContext* aCx, ErrorResult& aRv);
 
 template void FetchBody<Response>::SetBodyUsed(JSContext* aCx,
                                                ErrorResult& aRv);
 
 template <class Derived>
@@ -1379,48 +1292,16 @@ const nsAString& FetchBody<Derived>::Bod
 }
 
 template const nsAString& FetchBody<Request>::BodyLocalPath() const;
 
 template const nsAString& FetchBody<Response>::BodyLocalPath() const;
 
 template const nsAString& FetchBody<EmptyBody>::BodyLocalPath() const;
 
-#ifndef MOZ_DOM_STREAMS
-template <class Derived>
-void FetchBody<Derived>::SetReadableStreamBody(JSContext* aCx,
-                                               JSObject* aBody) {
-  MOZ_ASSERT(!mReadableStreamBody);
-  MOZ_ASSERT(aBody);
-  mReadableStreamBody = aBody;
-
-  RefPtr<AbortSignalImpl> signalImpl = DerivedClass()->GetSignalImpl();
-  if (!signalImpl) {
-    return;
-  }
-
-  bool aborted = signalImpl->Aborted();
-  if (aborted) {
-    JS::Rooted<JSObject*> body(aCx, mReadableStreamBody);
-    IgnoredErrorResult result;
-    AbortStream(aCx, body, result);
-    if (NS_WARN_IF(result.Failed())) {
-      return;
-    }
-  } else if (!IsFollowing()) {
-    Follow(signalImpl);
-  }
-}
-
-template void FetchBody<Request>::SetReadableStreamBody(JSContext* aCx,
-                                                        JSObject* aBody);
-
-template void FetchBody<Response>::SetReadableStreamBody(JSContext* aCx,
-                                                         JSObject* aBody);
-#else
 template <class Derived>
 void FetchBody<Derived>::SetReadableStreamBody(JSContext* aCx,
                                                ReadableStream* aBody) {
   MOZ_ASSERT(!mReadableStreamBody);
   MOZ_ASSERT(aBody);
   mReadableStreamBody = aBody;
 
   RefPtr<AbortSignalImpl> signalImpl = DerivedClass()->GetSignalImpl();
@@ -1440,80 +1321,17 @@ void FetchBody<Derived>::SetReadableStre
   }
 }
 
 template void FetchBody<Request>::SetReadableStreamBody(JSContext* aCx,
                                                         ReadableStream* aBody);
 
 template void FetchBody<Response>::SetReadableStreamBody(JSContext* aCx,
                                                          ReadableStream* aBody);
-#endif
 
-#ifndef MOZ_DOM_STREAMS
-template <class Derived>
-void FetchBody<Derived>::GetBody(JSContext* aCx,
-                                 JS::MutableHandle<JSObject*> aBodyOut,
-                                 ErrorResult& aRv) {
-  if (mReadableStreamBody) {
-    aBodyOut.set(mReadableStreamBody);
-    return;
-  }
-
-  nsCOMPtr<nsIInputStream> inputStream;
-  DerivedClass()->GetBody(getter_AddRefs(inputStream));
-
-  if (!inputStream) {
-    aBodyOut.set(nullptr);
-    return;
-  }
-
-  BodyStream::Create(aCx, this, DerivedClass()->GetParentObject(), inputStream,
-                     aRv);
-  if (NS_WARN_IF(aRv.Failed())) {
-    return;
-  }
-
-  MOZ_ASSERT(mReadableStreamBody);
-
-  JS::Rooted<JSObject*> body(aCx, mReadableStreamBody);
-
-  // If the body has been already consumed, we lock the stream.
-  bool bodyUsed = GetBodyUsed(aRv);
-  if (NS_WARN_IF(aRv.Failed())) {
-    return;
-  }
-  if (bodyUsed) {
-    LockStream(aCx, body, aRv);
-    if (NS_WARN_IF(aRv.Failed())) {
-      return;
-    }
-  }
-
-  RefPtr<AbortSignalImpl> signalImpl = DerivedClass()->GetSignalImpl();
-  if (signalImpl) {
-    if (signalImpl->Aborted()) {
-      AbortStream(aCx, body, aRv);
-      if (NS_WARN_IF(aRv.Failed())) {
-        return;
-      }
-    } else if (!IsFollowing()) {
-      Follow(signalImpl);
-    }
-  }
-
-  aBodyOut.set(mReadableStreamBody);
-}
-
-template void FetchBody<Request>::GetBody(JSContext* aCx,
-                                          JS::MutableHandle<JSObject*> aMessage,
-                                          ErrorResult& aRv);
-
-template void FetchBody<Response>::GetBody(
-    JSContext* aCx, JS::MutableHandle<JSObject*> aMessage, ErrorResult& aRv);
-#else
 template <class Derived>
 already_AddRefed<ReadableStream> FetchBody<Derived>::GetBody(JSContext* aCx,
                                                              ErrorResult& aRv) {
   if (mReadableStreamBody) {
     RefPtr<ReadableStream> body(mReadableStreamBody);
     return body.forget();
   }
 
@@ -1562,19 +1380,16 @@ already_AddRefed<ReadableStream> FetchBo
 }
 
 template already_AddRefed<ReadableStream> FetchBody<Request>::GetBody(
     JSContext* aCx, ErrorResult& aRv);
 
 template already_AddRefed<ReadableStream> FetchBody<Response>::GetBody(
     JSContext* aCx, ErrorResult& aRv);
 
-#endif
-
-#ifdef MOZ_DOM_STREAMS
 template <class Derived>
 void FetchBody<Derived>::LockStream(JSContext* aCx, ReadableStream* aStream,
                                     ErrorResult& aRv) {
   // This is native stream, creating a reader will not execute any JS code.
   RefPtr<ReadableStreamDefaultReader> reader =
       AcquireReadableStreamDefaultReader(aStream, aRv);
   if (aRv.Failed()) {
     return;
@@ -1585,117 +1400,16 @@ void FetchBody<Derived>::LockStream(JSCo
 
 template void FetchBody<Request>::LockStream(JSContext* aCx,
                                              ReadableStream* aStream,
                                              ErrorResult& aRv);
 
 template void FetchBody<Response>::LockStream(JSContext* aCx,
                                               ReadableStream* aStream,
                                               ErrorResult& aRv);
-#else
-template <class Derived>
-void FetchBody<Derived>::LockStream(JSContext* aCx, JS::HandleObject aStream,
-                                    ErrorResult& aRv) {
-  aRv.MightThrowJSException();
-
-#  if DEBUG
-  JS::ReadableStreamMode streamMode;
-  if (!JS::ReadableStreamGetMode(aCx, aStream, &streamMode)) {
-    aRv.StealExceptionFromJSContext(aCx);
-    return;
-  }
-  MOZ_ASSERT(streamMode == JS::ReadableStreamMode::ExternalSource);
-#  endif  // DEBUG
-
-  // This is native stream, creating a reader will not execute any JS code.
-  JS::Rooted<JSObject*> reader(
-      aCx, JS::ReadableStreamGetReader(aCx, aStream,
-                                       JS::ReadableStreamReaderMode::Default));
-  if (!reader) {
-    aRv.StealExceptionFromJSContext(aCx);
-    return;
-  }
-
-  mReadableStreamReader = reader;
-}
-
-template void FetchBody<Request>::LockStream(JSContext* aCx,
-                                             JS::HandleObject aStream,
-                                             ErrorResult& aRv);
-
-template void FetchBody<Response>::LockStream(JSContext* aCx,
-                                              JS::HandleObject aStream,
-                                              ErrorResult& aRv);
-#endif
-
-#ifndef MOZ_DOM_STREAMS
-template <class Derived>
-void FetchBody<Derived>::MaybeTeeReadableStreamBody(
-    JSContext* aCx, JS::MutableHandle<JSObject*> aBodyOut,
-    FetchStreamReader** aStreamReader, nsIInputStream** aInputStream,
-    ErrorResult& aRv) {
-  MOZ_DIAGNOSTIC_ASSERT(aStreamReader);
-  MOZ_DIAGNOSTIC_ASSERT(aInputStream);
-  MOZ_DIAGNOSTIC_ASSERT(!CheckBodyUsed());
-
-  aBodyOut.set(nullptr);
-  *aStreamReader = nullptr;
-  *aInputStream = nullptr;
-
-  if (!mReadableStreamBody) {
-    return;
-  }
-
-  aRv.MightThrowJSException();
-
-  JSAutoRealm ar(aCx, mOwner->GetGlobalJSObject());
-
-  JS::Rooted<JSObject*> stream(aCx, mReadableStreamBody);
-
-  // If this is a ReadableStream with an external source, this has been
-  // generated by a Fetch. In this case, Fetch will be able to recreate it
-  // again when GetBody() is called.
-  JS::ReadableStreamMode streamMode;
-  if (!JS::ReadableStreamGetMode(aCx, stream, &streamMode)) {
-    aRv.StealExceptionFromJSContext(aCx);
-    return;
-  }
-  if (streamMode == JS::ReadableStreamMode::ExternalSource) {
-    aBodyOut.set(nullptr);
-    return;
-  }
-
-  JS::Rooted<JSObject*> branch1(aCx);
-  JS::Rooted<JSObject*> branch2(aCx);
-
-  if (!JS::ReadableStreamTee(aCx, stream, &branch1, &branch2)) {
-    aRv.StealExceptionFromJSContext(aCx);
-    return;
-  }
-
-  mReadableStreamBody = branch1;
-  aBodyOut.set(branch2);
-
-  aRv = FetchStreamReader::Create(aCx, mOwner, aStreamReader, aInputStream);
-  if (NS_WARN_IF(aRv.Failed())) {
-    return;
-  }
-}
-
-template void FetchBody<Request>::MaybeTeeReadableStreamBody(
-    JSContext* aCx, JS::MutableHandle<JSObject*> aMessage,
-    FetchStreamReader** aStreamReader, nsIInputStream** aInputStream,
-    ErrorResult& aRv);
-
-template void FetchBody<Response>::MaybeTeeReadableStreamBody(
-    JSContext* aCx, JS::MutableHandle<JSObject*> aMessage,
-    FetchStreamReader** aStreamReader, nsIInputStream** aInputStream,
-    ErrorResult& aRv);
-
-#else
 
 template <class Derived>
 void FetchBody<Derived>::MaybeTeeReadableStreamBody(
     JSContext* aCx, ReadableStream** aBodyOut,
     FetchStreamReader** aStreamReader, nsIInputStream** aInputStream,
     ErrorResult& aRv) {
   MOZ_DIAGNOSTIC_ASSERT(aStreamReader);
   MOZ_DIAGNOSTIC_ASSERT(aInputStream);
@@ -1736,36 +1450,31 @@ template void FetchBody<Request>::MaybeT
     JSContext* aCx, ReadableStream** aBodyOut,
     FetchStreamReader** aStreamReader, nsIInputStream** aInputStream,
     ErrorResult& aRv);
 
 template void FetchBody<Response>::MaybeTeeReadableStreamBody(
     JSContext* aCx, ReadableStream** aBodyOut,
     FetchStreamReader** aStreamReader, nsIInputStream** aInputStream,
     ErrorResult& aRv);
-#endif
 
 template <class Derived>
 void FetchBody<Derived>::RunAbortAlgorithm() {
   if (!mReadableStreamBody) {
     return;
   }
 
   AutoJSAPI jsapi;
   if (!jsapi.Init(mOwner)) {
     return;
   }
 
   JSContext* cx = jsapi.cx();
 
-#ifdef MOZ_DOM_STREAMS
   RefPtr<ReadableStream> body(mReadableStreamBody);
-#else
-  JS::Rooted<JSObject*> body(cx, mReadableStreamBody);
-#endif
   IgnoredErrorResult result;
   AbortStream(cx, body, result);
 }
 
 template void FetchBody<Request>::RunAbortAlgorithm();
 
 template void FetchBody<Response>::RunAbortAlgorithm();
 
@@ -1773,31 +1482,27 @@ NS_IMPL_ADDREF_INHERITED(EmptyBody, Fetc
 NS_IMPL_RELEASE_INHERITED(EmptyBody, FetchBody<EmptyBody>)
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(EmptyBody)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(EmptyBody, FetchBody<EmptyBody>)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mAbortSignalImpl)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mFetchStreamReader)
-#ifdef MOZ_DOM_STREAMS
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadableStreamBody)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadableStreamReader)
-#endif
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(EmptyBody,
                                                   FetchBody<EmptyBody>)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAbortSignalImpl)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFetchStreamReader)
-#ifdef MOZ_DOM_STREAMS
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReadableStreamBody)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReadableStreamReader)
-#endif
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(EmptyBody, FetchBody<EmptyBody>)
 NS_IMPL_CYCLE_COLLECTION_TRACE_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EmptyBody)
 NS_INTERFACE_MAP_END_INHERITING(FetchBody<EmptyBody>)
 
--- a/dom/fetch/Fetch.h
+++ b/dom/fetch/Fetch.h
@@ -14,20 +14,18 @@
 #include "nsString.h"
 
 #include "mozilla/DebugOnly.h"
 #include "mozilla/dom/AbortSignal.h"
 #include "mozilla/dom/BodyConsumer.h"
 #include "mozilla/dom/BodyStream.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/FetchStreamReader.h"
-#ifdef MOZ_DOM_STREAMS
-#  include "mozilla/dom/ReadableStream.h"
-#  include "mozilla/dom/ReadableStreamDefaultReaderBinding.h"
-#endif
+#include "mozilla/dom/ReadableStream.h"
+#include "mozilla/dom/ReadableStreamDefaultReaderBinding.h"
 #include "mozilla/dom/RequestBinding.h"
 #include "mozilla/dom/workerinternals/RuntimeService.h"
 
 class nsIGlobalObject;
 class nsIEventTarget;
 
 namespace mozilla {
 class ErrorResult;
@@ -159,49 +157,34 @@ class FetchBody : public BodyStreamHolde
   already_AddRefed<Promise> Json(JSContext* aCx, ErrorResult& aRv) {
     return ConsumeBody(aCx, BodyConsumer::CONSUME_JSON, aRv);
   }
 
   already_AddRefed<Promise> Text(JSContext* aCx, ErrorResult& aRv) {
     return ConsumeBody(aCx, BodyConsumer::CONSUME_TEXT, aRv);
   }
 
-#ifdef MOZ_DOM_STREAMS
   already_AddRefed<ReadableStream> GetBody(JSContext* aCx, ErrorResult& aRv);
-#else
-  void GetBody(JSContext* aCx, JS::MutableHandle<JSObject*> aBodyOut,
-               ErrorResult& aRv);
-#endif
   void GetMimeType(nsACString& aMimeType);
 
   const nsACString& BodyBlobURISpec() const;
 
   const nsAString& BodyLocalPath() const;
 
-#ifdef MOZ_DOM_STREAMS
   // If the body contains a ReadableStream body object, this method produces a
   // tee() of it.
   //
   // This is marked as a script boundary minimize changes required for
   // annotation while we work out how to correctly annotate this code.
   // Tracked in Bug 1750650.
   MOZ_CAN_RUN_SCRIPT_BOUNDARY
   void MaybeTeeReadableStreamBody(JSContext* aCx, ReadableStream** aBodyOut,
                                   FetchStreamReader** aStreamReader,
                                   nsIInputStream** aInputStream,
                                   ErrorResult& aRv);
-#else
-  // If the body contains a ReadableStream body object, this method produces a
-  // tee() of it.
-  void MaybeTeeReadableStreamBody(JSContext* aCx,
-                                  JS::MutableHandle<JSObject*> aBodyOut,
-                                  FetchStreamReader** aStreamReader,
-                                  nsIInputStream** aInputStream,
-                                  ErrorResult& aRv);
-#endif
 
   // Utility public methods accessed by various runnables.
 
   // This method _must_ be called in order to set the body as used. If the body
   // is a ReadableStream, this method will start reading the stream.
   // More in details, this method does:
   // 1) It uses an internal flag to track if the body is used.  This is tracked
   // separately from the ReadableStream disturbed state due to purely native
@@ -224,29 +207,22 @@ class FetchBody : public BodyStreamHolde
 
   // BodyStreamHolder
   void NullifyStream() override {
     mReadableStreamBody = nullptr;
     mReadableStreamReader = nullptr;
     mFetchStreamReader = nullptr;
   }
 
-#ifndef MOZ_DOM_STREAMS
-  void SetReadableStreamBody(JSObject* aBody) override {
-    mReadableStreamBody = aBody;
-  }
-  JSObject* GetReadableStreamBody() override { return mReadableStreamBody; }
-#else
   void SetReadableStreamBody(ReadableStream* aBody) override {
     mReadableStreamBody = aBody;
   }
   ReadableStream* GetReadableStreamBody() override {
     return mReadableStreamBody;
   }
-#endif
 
   void MarkAsRead() override { mBodyUsed = true; }
 
   virtual AbortSignalImpl* GetSignalImpl() const = 0;
 
   virtual AbortSignalImpl* GetSignalImplToConsumeBody() const = 0;
 
   // AbortFollower
@@ -254,54 +230,37 @@ class FetchBody : public BodyStreamHolde
 
   already_AddRefed<Promise> ConsumeBody(JSContext* aCx,
                                         BodyConsumer::ConsumeType aType,
                                         ErrorResult& aRv);
 
  protected:
   nsCOMPtr<nsIGlobalObject> mOwner;
 
-#ifdef MOZ_DOM_STREAMS
   // This is the ReadableStream exposed to content. It's underlying source is a
   // BodyStream object. This needs to be traversed by subclasses.
   RefPtr<ReadableStream> mReadableStreamBody;
 
   // This is the Reader used to retrieve data from the body. This needs to be
   // traversed by subclasses.
   RefPtr<ReadableStreamDefaultReader> mReadableStreamReader;
-#else
-  // This is the ReadableStream exposed to content. It's underlying source is a
-  // BodyStream object.
-  JS::Heap<JSObject*> mReadableStreamBody;
-
-  // This is the Reader used to retrieve data from the body.
-  JS::Heap<JSObject*> mReadableStreamReader;
-#endif
   RefPtr<FetchStreamReader> mFetchStreamReader;
 
   explicit FetchBody(nsIGlobalObject* aOwner);
 
   virtual ~FetchBody();
 
-#ifdef MOZ_DOM_STREAMS
   void SetReadableStreamBody(JSContext* aCx, ReadableStream* aBody);
-#else
-  void SetReadableStreamBody(JSContext* aCx, JSObject* aBody);
-#endif
 
  private:
   Derived* DerivedClass() const {
     return static_cast<Derived*>(const_cast<FetchBody*>(this));
   }
 
-#ifdef MOZ_DOM_STREAMS
   void LockStream(JSContext* aCx, ReadableStream* aStream, ErrorResult& aRv);
-#else
-  void LockStream(JSContext* aCx, JS::HandleObject aStream, ErrorResult& aRv);
-#endif
 
   void AssertIsOnTargetThread() {
     MOZ_ASSERT(NS_IsMainThread() == !GetCurrentThreadWorkerPrivate());
   }
 
   // Only ever set once, always on target thread.
   bool mBodyUsed;
 
--- a/dom/fetch/FetchStreamReader.cpp
+++ b/dom/fetch/FetchStreamReader.cpp
@@ -8,21 +8,19 @@
 #include "InternalResponse.h"
 #include "js/Stream.h"
 #include "mozilla/ConsoleReportCollector.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/dom/AutoEntryScript.h"
 #include "mozilla/dom/DOMException.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/PromiseBinding.h"
-#ifdef MOZ_DOM_STREAMS
-#  include "mozilla/dom/ReadableStream.h"
-#  include "mozilla/dom/ReadableStreamDefaultController.h"
-#  include "mozilla/dom/ReadableStreamDefaultReader.h"
-#endif
+#include "mozilla/dom/ReadableStream.h"
+#include "mozilla/dom/ReadableStreamDefaultController.h"
+#include "mozilla/dom/ReadableStreamDefaultReader.h"
 #include "mozilla/dom/WorkerPrivate.h"
 #include "mozilla/dom/WorkerRef.h"
 #include "mozilla/HoldDropJSObjects.h"
 #include "mozilla/TaskCategory.h"
 #include "nsContentUtils.h"
 #include "nsDebug.h"
 #include "nsIAsyncInputStream.h"
 #include "nsIPipe.h"
@@ -34,34 +32,25 @@ namespace mozilla::dom {
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(FetchStreamReader)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(FetchStreamReader)
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(FetchStreamReader)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(FetchStreamReader)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
-#ifdef MOZ_DOM_STREAMS
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mReader)
-#else
-  tmp->mReader = nullptr;
-#endif
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(FetchStreamReader)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
-#ifdef MOZ_DOM_STREAMS
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReader)
-#endif
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(FetchStreamReader)
-#ifndef MOZ_DOM_STREAMS
-  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReader)
-#endif
 NS_IMPL_CYCLE_COLLECTION_TRACE_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FetchStreamReader)
   NS_INTERFACE_MAP_ENTRY(nsIOutputStreamCallback)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIOutputStreamCallback)
 NS_INTERFACE_MAP_END
 
 /* static */
@@ -148,34 +137,25 @@ void FetchStreamReader::CloseAndRelease(
 
   RefPtr<FetchStreamReader> kungFuDeathGrip = this;
 
   if (aCx && mReader) {
     RefPtr<DOMException> error = DOMException::Create(aStatus);
 
     JS::Rooted<JS::Value> errorValue(aCx);
     if (ToJSValue(aCx, error, &errorValue)) {
-#ifdef MOZ_DOM_STREAMS
       IgnoredErrorResult ignoredError;
       // It's currently safe to cancel an already closed reader because, per the
       // comments in ReadableStream::cancel() conveying the spec, step 2 of
       // 3.4.3 that specified ReadableStreamCancel is: If stream.[[state]] is
       // "closed", return a new promise resolved with undefined.
       RefPtr<Promise> ignoredResultPromise =
           MOZ_KnownLive(mReader)->Cancel(aCx, errorValue, ignoredError);
       NS_WARNING_ASSERTION(!ignoredError.Failed(),
                            "Failed to cancel stream during close and release");
-#else
-      JS::Rooted<JSObject*> reader(aCx, mReader);
-      // It's currently safe to cancel an already closed reader because, per the
-      // comments in ReadableStream::cancel() conveying the spec, step 2 of
-      // 3.4.3 that specified ReadableStreamCancel is: If stream.[[state]] is
-      // "closed", return a new promise resolved with undefined.
-      JS::ReadableStreamReaderCancel(aCx, reader, errorValue);
-#endif
     }
 
     // We don't want to propagate exceptions during the cleanup.
     JS_ClearPendingException(aCx);
   }
 
   mStreamClosed = true;
 
@@ -187,17 +167,16 @@ void FetchStreamReader::CloseAndRelease(
   mPipeOut = nullptr;
 
   mWorkerRef = nullptr;
 
   mReader = nullptr;
   mBuffer.Clear();
 }
 
-#ifdef MOZ_DOM_STREAMS
 void FetchStreamReader::StartConsuming(JSContext* aCx, ReadableStream* aStream,
                                        ReadableStreamDefaultReader** aReader,
                                        ErrorResult& aRv) {
   MOZ_DIAGNOSTIC_ASSERT(!mReader);
   MOZ_DIAGNOSTIC_ASSERT(aStream);
 
   RefPtr<ReadableStreamDefaultReader> reader =
       AcquireReadableStreamDefaultReader(aStream, aRv);
@@ -210,52 +189,16 @@ void FetchStreamReader::StartConsuming(J
   reader.forget(aReader);
 
   aRv = mPipeOut->AsyncWait(this, 0, 0, mOwningEventTarget);
   if (NS_WARN_IF(aRv.Failed())) {
     return;
   }
 }
 
-#else
-
-void FetchStreamReader::StartConsuming(JSContext* aCx, JS::HandleObject aStream,
-                                       JS::MutableHandle<JSObject*> aReader,
-                                       ErrorResult& aRv) {
-  MOZ_DIAGNOSTIC_ASSERT(!mReader);
-  MOZ_DIAGNOSTIC_ASSERT(aStream);
-
-  aRv.MightThrowJSException();
-
-  // Here, by spec, we can pick any global we want. Just to avoid extra
-  // cross-compartment steps, we want to create the reader in the same
-  // compartment of the owning Fetch Body object.
-  // The same global will be used to retrieve data from this reader.
-  JSAutoRealm ar(aCx, mGlobal->GetGlobalJSObject());
-
-  JS::Rooted<JSObject*> reader(
-      aCx, JS::ReadableStreamGetReader(aCx, aStream,
-                                       JS::ReadableStreamReaderMode::Default));
-  if (!reader) {
-    aRv.StealExceptionFromJSContext(aCx);
-    CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR);
-    return;
-  }
-
-  mReader = reader;
-  aReader.set(reader);
-
-  aRv = mPipeOut->AsyncWait(this, 0, 0, mOwningEventTarget);
-  if (NS_WARN_IF(aRv.Failed())) {
-    return;
-  }
-}
-#endif
-
-#ifdef MOZ_DOM_STREAMS
 struct FetchReadRequest : public ReadRequest {
  public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FetchReadRequest, ReadRequest)
 
   explicit FetchReadRequest(FetchStreamReader* aReader)
       : mFetchStreamReader(aReader) {}
 
@@ -280,17 +223,16 @@ struct FetchReadRequest : public ReadReq
 };
 
 NS_IMPL_CYCLE_COLLECTION_INHERITED(FetchReadRequest, ReadRequest,
                                    mFetchStreamReader)
 NS_IMPL_ADDREF_INHERITED(FetchReadRequest, ReadRequest)
 NS_IMPL_RELEASE_INHERITED(FetchReadRequest, ReadRequest)
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FetchReadRequest)
 NS_INTERFACE_MAP_END_INHERITING(ReadRequest)
-#endif
 
 // nsIOutputStreamCallback interface
 MOZ_CAN_RUN_SCRIPT_BOUNDARY
 NS_IMETHODIMP
 FetchStreamReader::OnOutputStreamReady(nsIAsyncOutputStream* aStream) {
   NS_ASSERT_OWNINGTHREAD(FetchStreamReader);
   if (mStreamClosed) {
     return NS_OK;
@@ -304,57 +246,34 @@ FetchStreamReader::OnOutputStreamReady(n
     return WriteBuffer();
   }
 
   // Here we can retrieve data from the reader using any global we want because
   // it is not observable. We want to use the reader's global, which is also the
   // Response's one.
   AutoEntryScript aes(mGlobal, "ReadableStreamReader.read", !mWorkerRef);
 
-#ifdef MOZ_DOM_STREAMS
   IgnoredErrorResult rv;
 
   // https://fetch.spec.whatwg.org/#incrementally-read-loop
   // The below very loosely tries to implement the incrementally-read-loop from
   // the fetch spec.
   RefPtr<ReadRequest> readRequest = new FetchReadRequest(this);
   ReadableStreamDefaultReaderRead(aes.cx(), MOZ_KnownLive(mReader), readRequest,
                                   rv);
 
   if (NS_WARN_IF(rv.Failed())) {
     // Let's close the stream.
     CloseAndRelease(aes.cx(), NS_ERROR_DOM_INVALID_STATE_ERR);
     return NS_ERROR_FAILURE;
   }
 
-#else
-  JS::Rooted<JSObject*> reader(aes.cx(), mReader);
-  JS::Rooted<JSObject*> promise(
-      aes.cx(), JS::ReadableStreamDefaultReaderRead(aes.cx(), reader));
-  if (NS_WARN_IF(!promise)) {
-    // Let's close the stream.
-    CloseAndRelease(aes.cx(), NS_ERROR_DOM_INVALID_STATE_ERR);
-    return NS_ERROR_FAILURE;
-  }
-
-  RefPtr<Promise> domPromise = Promise::CreateFromExisting(mGlobal, promise);
-  if (NS_WARN_IF(!domPromise)) {
-    // Let's close the stream.
-    CloseAndRelease(aes.cx(), NS_ERROR_DOM_INVALID_STATE_ERR);
-    return NS_ERROR_FAILURE;
-  }
-
-  // Let's wait.
-  domPromise->AppendNativeHandler(this);
-#endif
-
   return NS_OK;
 }
 
-#ifdef MOZ_DOM_STREAMS
 void FetchStreamReader::ChunkSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk,
                                    ErrorResult& aRv) {
   // This roughly implements the chunk steps from
   // https://fetch.spec.whatwg.org/#incrementally-read-loop.
 
   RootedSpiderMonkeyInterface<Uint8Array> chunk(aCx);
   if (!aChunk.isObject() || !chunk.Init(&aChunk.toObject())) {
     CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR);
@@ -389,82 +308,16 @@ void FetchStreamReader::ChunkSteps(JSCon
 }
 
 void FetchStreamReader::ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> aError,
                                    ErrorResult& aRv) {
   ReportErrorToConsole(aCx, aError);
   CloseAndRelease(aCx, NS_ERROR_FAILURE);
 }
 
-#else
-
-void FetchStreamReader::ResolvedCallback(JSContext* aCx,
-                                         JS::Handle<JS::Value> aValue,
-                                         ErrorResult& aRv) {
-  if (mStreamClosed) {
-    return;
-  }
-
-  // This promise should be resolved with { done: boolean, value: something },
-  // "value" is interesting only if done is false.
-
-  // We don't want to play with JS api, let's WebIDL bindings doing it for us.
-  // FetchReadableStreamReadDataDone is a dictionary with just a boolean, if the
-  // parsing succeeded, we can proceed with the parsing of the "value", which it
-  // must be a Uint8Array.
-  FetchReadableStreamReadDataDone valueDone;
-  if (!valueDone.Init(aCx, aValue)) {
-    JS_ClearPendingException(aCx);
-    CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR);
-    return;
-  }
-
-  if (valueDone.mDone) {
-    // Stream is completed.
-    CloseAndRelease(aCx, NS_BASE_STREAM_CLOSED);
-    return;
-  }
-
-  RootedDictionary<FetchReadableStreamReadDataArray> value(aCx);
-  if (!value.Init(aCx, aValue) || !value.mValue.WasPassed()) {
-    JS_ClearPendingException(aCx);
-    CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR);
-    return;
-  }
-
-  Uint8Array& array = value.mValue.Value();
-  array.ComputeState();
-  uint32_t len = array.Length();
-
-  if (len == 0) {
-    // If there is nothing to read, let's do another reading.
-    OnOutputStreamReady(mPipeOut);
-    return;
-  }
-
-  MOZ_DIAGNOSTIC_ASSERT(mBuffer.IsEmpty());
-
-  // Let's take a copy of the data.
-  if (!mBuffer.AppendElements(array.Data(), len, fallible)) {
-    CloseAndRelease(aCx, NS_ERROR_OUT_OF_MEMORY);
-    return;
-  }
-
-  mBufferOffset = 0;
-  mBufferRemaining = len;
-
-  nsresult rv = WriteBuffer();
-  if (NS_FAILED(rv)) {
-    // DOMException only understands errors from domerr.msg, so we normalize to
-    // identifying an abort if the write fails.
-    CloseAndRelease(aCx, NS_ERROR_DOM_ABORT_ERR);
-  }
-}
-#endif
-
 nsresult FetchStreamReader::WriteBuffer() {
   MOZ_ASSERT(!mBuffer.IsEmpty());
 
   char* data = reinterpret_cast<char*>(mBuffer.Elements());
 
   while (1) {
     uint32_t written = 0;
     nsresult rv =
@@ -491,25 +344,16 @@ nsresult FetchStreamReader::WriteBuffer(
   nsresult rv = mPipeOut->AsyncWait(this, 0, 0, mOwningEventTarget);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
-#ifndef MOZ_DOM_STREAMS
-void FetchStreamReader::RejectedCallback(JSContext* aCx,
-                                         JS::Handle<JS::Value> aValue,
-                                         ErrorResult& aRv) {
-  ReportErrorToConsole(aCx, aValue);
-  CloseAndRelease(aCx, NS_ERROR_FAILURE);
-}
-#endif
-
 void FetchStreamReader::ReportErrorToConsole(JSContext* aCx,
                                              JS::Handle<JS::Value> aValue) {
   nsCString sourceSpec;
   uint32_t line = 0;
   uint32_t column = 0;
   nsString valueString;
 
   nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column,
--- a/dom/fetch/FetchStreamReader.h
+++ b/dom/fetch/FetchStreamReader.h
@@ -17,86 +17,64 @@
 
 namespace mozilla {
 namespace dom {
 
 class ReadableStream;
 class ReadableStreamDefaultReader;
 class WeakWorkerRef;
 
-class FetchStreamReader final : public nsIOutputStreamCallback
-#ifndef MOZ_DOM_STREAMS
-    ,
-                                public PromiseNativeHandler
-#endif
-{
+class FetchStreamReader final : public nsIOutputStreamCallback {
  public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS(
       FetchStreamReader, nsIOutputStreamCallback)
   NS_DECL_NSIOUTPUTSTREAMCALLBACK
 
   // This creates a nsIInputStream able to retrieve data from the ReadableStream
   // object. The reading starts when StartConsuming() is called.
   static nsresult Create(JSContext* aCx, nsIGlobalObject* aGlobal,
                          FetchStreamReader** aStreamReader,
                          nsIInputStream** aInputStream);
 
-#ifdef MOZ_DOM_STREAMS
   void ChunkSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk,
                   ErrorResult& aRv);
   void ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> aError,
                   ErrorResult& aRv);
-#else
-  void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
-                        ErrorResult& aRv) override;
-
-  void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
-                        ErrorResult& aRv) override;
-#endif
 
   // Idempotently close the output stream and null out all state. If aCx is
   // provided, the reader will also be canceled.  aStatus must be a DOM error
   // as understood by DOMException because it will be provided as the
   // cancellation reason.
   //
   // This is a script boundary minimize annotation changes required while
   // we figure out how to handle some more tricky annotation cases (for
   // example, the destructor of this class. Tracking under Bug 1750656)
   MOZ_CAN_RUN_SCRIPT_BOUNDARY
   void CloseAndRelease(JSContext* aCx, nsresult aStatus);
 
-#ifdef MOZ_DOM_STREAMS
   void StartConsuming(JSContext* aCx, ReadableStream* aStream,
                       ReadableStreamDefaultReader** aReader, ErrorResult& aRv);
-#else
-  void StartConsuming(JSContext* aCx, JS::HandleObject aStream,
-                      JS::MutableHandle<JSObject*> aReader, ErrorResult& aRv);
-#endif
 
  private:
   explicit FetchStreamReader(nsIGlobalObject* aGlobal);
   ~FetchStreamReader();
 
   nsresult WriteBuffer();
 
   void ReportErrorToConsole(JSContext* aCx, JS::Handle<JS::Value> aValue);
 
   nsCOMPtr<nsIGlobalObject> mGlobal;
   nsCOMPtr<nsIEventTarget> mOwningEventTarget;
 
   nsCOMPtr<nsIAsyncOutputStream> mPipeOut;
 
   RefPtr<WeakWorkerRef> mWorkerRef;
 
-#ifdef MOZ_DOM_STREAMS
   RefPtr<ReadableStreamDefaultReader> mReader;
-#else
-  JS::Heap<JSObject*> mReader;
-#endif
 
   nsTArray<uint8_t> mBuffer;
   uint32_t mBufferRemaining;
   uint32_t mBufferOffset;
 
   bool mStreamClosed;
 };
 
--- a/dom/fetch/Request.cpp
+++ b/dom/fetch/Request.cpp
@@ -18,54 +18,43 @@
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/URL.h"
 #include "mozilla/dom/WorkerPrivate.h"
 #include "mozilla/dom/WorkerRunnable.h"
 #include "mozilla/dom/WindowContext.h"
 #include "mozilla/ipc/PBackgroundSharedTypes.h"
 #include "mozilla/Unused.h"
 
-#ifdef MOZ_DOM_STREAMS
-#  include "mozilla/dom/ReadableStreamDefaultReader.h"
-#endif
+#include "mozilla/dom/ReadableStreamDefaultReader.h"
 
 namespace mozilla::dom {
 
 NS_IMPL_ADDREF_INHERITED(Request, FetchBody<Request>)
 NS_IMPL_RELEASE_INHERITED(Request, FetchBody<Request>)
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(Request)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Request, FetchBody<Request>)
-#ifdef MOZ_DOM_STREAMS
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadableStreamBody)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadableStreamReader)
-#endif
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mHeaders)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSignal)
   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Request, FetchBody<Request>)
-#ifdef MOZ_DOM_STREAMS
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReadableStreamBody)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReadableStreamReader)
-#endif
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHeaders)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSignal)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(Request, FetchBody<Request>)
-#ifndef MOZ_DOM_STREAMS
-  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReadableStreamBody)
-  MOZ_DIAGNOSTIC_ASSERT(!tmp->mReadableStreamReader);
-  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReadableStreamReader)
-#endif
   NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
 NS_IMPL_CYCLE_COLLECTION_TRACE_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Request)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
 NS_INTERFACE_MAP_END_INHERITING(FetchBody<Request>)
 
 Request::Request(nsIGlobalObject* aOwner, SafeRefPtr<InternalRequest> aRequest,
--- a/dom/fetch/Response.cpp
+++ b/dom/fetch/Response.cpp
@@ -23,58 +23,45 @@
 #include "mozilla/dom/WorkerPrivate.h"
 
 #include "nsDOMString.h"
 
 #include "BodyExtractor.h"
 #include "FetchStreamReader.h"
 #include "InternalResponse.h"
 
-#ifdef MOZ_DOM_STREAMS
-#  include "mozilla/dom/ReadableStreamDefaultReader.h"
-#endif
+#include "mozilla/dom/ReadableStreamDefaultReader.h"
 
 namespace mozilla::dom {
 
 NS_IMPL_ADDREF_INHERITED(Response, FetchBody<Response>)
 NS_IMPL_RELEASE_INHERITED(Response, FetchBody<Response>)
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(Response)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Response, FetchBody<Response>)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mHeaders)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSignalImpl)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mFetchStreamReader)
-#ifdef MOZ_DOM_STREAMS
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadableStreamBody)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadableStreamReader)
-#else
-  tmp->mReadableStreamBody = nullptr;
-  tmp->mReadableStreamReader = nullptr;
-#endif
   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Response, FetchBody<Response>)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHeaders)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSignalImpl)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFetchStreamReader)
-#ifdef MOZ_DOM_STREAMS
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReadableStreamBody)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReadableStreamReader)
-#endif
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(Response, FetchBody<Response>)
-#ifndef MOZ_DOM_STREAMS
-  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReadableStreamBody)
-  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReadableStreamReader)
-#endif
   NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
 NS_IMPL_CYCLE_COLLECTION_TRACE_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Response)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
 NS_INTERFACE_MAP_END_INHERITING(FetchBody<Response>)
 
 Response::Response(nsIGlobalObject* aGlobal,
@@ -288,17 +275,16 @@ already_AddRefed<Response> Response::Con
 
     nsCString contentTypeWithCharset;
     nsCOMPtr<nsIInputStream> bodyStream;
     int64_t bodySize = InternalResponse::UNKNOWN_BODY_SIZE;
 
     const fetch::ResponseBodyInit& body = aBody.Value();
     if (body.IsReadableStream()) {
       JSContext* cx = aGlobal.Context();
-#ifdef MOZ_DOM_STREAMS
       aRv.MightThrowJSException();
 
       ReadableStream& readableStream = body.GetAsReadableStream();
 
       if (readableStream.Locked() || readableStream.Disturbed()) {
         aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
         return nullptr;
       }
@@ -313,68 +299,16 @@ already_AddRefed<Response> Response::Con
         MOZ_ASSERT(underlyingSource);
 
         aRv = BodyStream::RetrieveInputStream(underlyingSource,
                                               getter_AddRefs(bodyStream));
 
         if (NS_WARN_IF(aRv.Failed())) {
           return nullptr;
         }
-#else
-      aRv.MightThrowJSException();
-
-      const ReadableStream& readableStream = body.GetAsReadableStream();
-
-      JS::Rooted<JSObject*> readableStreamObj(cx, readableStream.Obj());
-
-      bool disturbed;
-      bool locked;
-      if (!JS::ReadableStreamIsDisturbed(cx, readableStreamObj, &disturbed) ||
-          !JS::ReadableStreamIsLocked(cx, readableStreamObj, &locked)) {
-        aRv.StealExceptionFromJSContext(cx);
-        return nullptr;
-      }
-      if (disturbed || locked) {
-        aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
-        return nullptr;
-      }
-
-      r->SetReadableStreamBody(cx, readableStreamObj);
-
-      JS::ReadableStreamMode streamMode;
-      if (!JS::ReadableStreamGetMode(cx, readableStreamObj, &streamMode)) {
-        aRv.StealExceptionFromJSContext(cx);
-        return nullptr;
-      }
-      if (streamMode == JS::ReadableStreamMode::ExternalSource) {
-        // If this is a DOM generated ReadableStream, we can extract the
-        // inputStream directly.
-        JS::ReadableStreamUnderlyingSource* underlyingSource = nullptr;
-        if (!JS::ReadableStreamGetExternalUnderlyingSource(
-                cx, readableStreamObj, &underlyingSource)) {
-          aRv.StealExceptionFromJSContext(cx);
-          return nullptr;
-        }
-
-        MOZ_ASSERT(underlyingSource);
-
-        aRv = BodyStream::RetrieveInputStream(underlyingSource,
-                                              getter_AddRefs(bodyStream));
-
-        // The releasing of the external source is needed in order to avoid an
-        // extra stream lock.
-        if (!JS::ReadableStreamReleaseExternalUnderlyingSource(
-                cx, readableStreamObj)) {
-          aRv.StealExceptionFromJSContext(cx);
-          return nullptr;
-        }
-        if (NS_WARN_IF(aRv.Failed())) {
-          return nullptr;
-        }
-#endif
       } else {
         // If this is a JS-created ReadableStream, let's create a
         // FetchStreamReader.
         aRv = FetchStreamReader::Create(aGlobal.Context(), global,
                                         getter_AddRefs(r->mFetchStreamReader),
                                         getter_AddRefs(bodyStream));
         if (NS_WARN_IF(aRv.Failed())) {
           return nullptr;
@@ -412,58 +346,32 @@ already_AddRefed<Response> Response::Con
 
 already_AddRefed<Response> Response::Clone(JSContext* aCx, ErrorResult& aRv) {
   bool bodyUsed = GetBodyUsed(aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   if (!bodyUsed && mReadableStreamBody) {
-#ifdef MOZ_DOM_STREAMS
     bool locked = mReadableStreamBody->Locked();
-#else
-    aRv.MightThrowJSException();
-
-    AutoJSAPI jsapi;
-    if (!jsapi.Init(mOwner)) {
-      aRv.Throw(NS_ERROR_FAILURE);
-      return nullptr;
-    }
-
-    JSContext* cx = jsapi.cx();
-    JS::Rooted<JSObject*> body(cx, mReadableStreamBody);
-    bool locked;
-    // We just need to check the 'locked' state because GetBodyUsed() already
-    // checked the 'disturbed' state.
-    if (!JS::ReadableStreamIsLocked(cx, body, &locked)) {
-      aRv.StealExceptionFromJSContext(cx);
-      return nullptr;
-    }
-#endif
     bodyUsed = locked;
   }
 
   if (bodyUsed) {
     aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
     return nullptr;
   }
 
   RefPtr<FetchStreamReader> streamReader;
   nsCOMPtr<nsIInputStream> inputStream;
 
-#ifdef MOZ_DOM_STREAMS
   RefPtr<ReadableStream> body;
   MaybeTeeReadableStreamBody(aCx, getter_AddRefs(body),
                              getter_AddRefs(streamReader),
                              getter_AddRefs(inputStream), aRv);
-#else
-  JS::Rooted<JSObject*> body(aCx);
-  MaybeTeeReadableStreamBody(aCx, &body, getter_AddRefs(streamReader),
-                             getter_AddRefs(inputStream), aRv);
-#endif
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   MOZ_ASSERT_IF(body, streamReader);
   MOZ_ASSERT_IF(body, inputStream);
 
   SafeRefPtr<InternalResponse> ir =
@@ -491,26 +399,20 @@ already_AddRefed<Response> Response::Clo
   if (GetBodyUsed(aRv)) {
     aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
     return nullptr;
   }
 
   RefPtr<FetchStreamReader> streamReader;
   nsCOMPtr<nsIInputStream> inputStream;
 
-#ifdef MOZ_DOM_STREAMS
   RefPtr<ReadableStream> body;
   MaybeTeeReadableStreamBody(aCx, getter_AddRefs(body),
                              getter_AddRefs(streamReader),
                              getter_AddRefs(inputStream), aRv);
-#else
-  JS::Rooted<JSObject*> body(aCx);
-  MaybeTeeReadableStreamBody(aCx, &body, getter_AddRefs(streamReader),
-                             getter_AddRefs(inputStream), aRv);
-#endif
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   MOZ_ASSERT_IF(body, streamReader);
   MOZ_ASSERT_IF(body, inputStream);
 
   SafeRefPtr<InternalResponse> clone =
--- a/dom/file/Blob.cpp
+++ b/dom/file/Blob.cpp
@@ -5,19 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "Blob.h"
 #include "EmptyBlobImpl.h"
 #include "File.h"
 #include "MemoryBlobImpl.h"
 #include "mozilla/dom/BlobBinding.h"
 #include "mozilla/dom/BodyStream.h"
-#ifdef MOZ_DOM_STREAMS
-#  include "mozilla/dom/ReadableStream.h"
-#endif
+#include "mozilla/dom/ReadableStream.h"
 #include "mozilla/dom/WorkerCommon.h"
 #include "mozilla/dom/WorkerPrivate.h"
 #include "mozilla/HoldDropJSObjects.h"
 #include "MultipartBlobImpl.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsIGlobalObject.h"
 #include "nsIInputStream.h"
 #include "nsPIDOMWindow.h"
@@ -309,73 +307,53 @@ class BlobBodyStreamHolder final : publi
                                                          BodyStreamHolder)
 
   BlobBodyStreamHolder() { mozilla::HoldJSObjects(this); }
 
   void NullifyStream() override { mozilla::DropJSObjects(this); }
 
   void MarkAsRead() override {}
 
-#ifdef MOZ_DOM_STREAMS
   void SetReadableStreamBody(ReadableStream* aBody) override {
     mStream = aBody;
   }
   ReadableStream* GetReadableStreamBody() override { return mStream; }
 
  private:
   RefPtr<ReadableStream> mStream;
-#else
-  void SetReadableStreamBody(JSObject* aBody) override {
-    MOZ_ASSERT(aBody);
-    mStream = aBody;
-  }
-
-  JSObject* GetReadableStreamBody() override { return mStream; }
-
-  // Public to make trace happy.
-  JS::Heap<JSObject*> mStream;
-#endif
 
  protected:
   virtual ~BlobBodyStreamHolder() { NullifyStream(); }
 };
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(BlobBodyStreamHolder)
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(BlobBodyStreamHolder,
                                                BodyStreamHolder)
-#ifndef MOZ_DOM_STREAMS
-  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mStream)
-#endif
 NS_IMPL_CYCLE_COLLECTION_TRACE_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(BlobBodyStreamHolder,
                                                   BodyStreamHolder)
-#ifdef MOZ_DOM_STREAMS
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStream)
-#endif
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(BlobBodyStreamHolder,
                                                 BodyStreamHolder)
   tmp->NullifyStream();
-#ifdef MOZ_DOM_STREAMS
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mStream)
-#endif
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_ADDREF_INHERITED(BlobBodyStreamHolder, BodyStreamHolder)
 NS_IMPL_RELEASE_INHERITED(BlobBodyStreamHolder, BodyStreamHolder)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(BlobBodyStreamHolder)
 NS_INTERFACE_MAP_END_INHERITING(BodyStreamHolder)
 
 }  // anonymous namespace
 
-#ifdef MOZ_DOM_STREAMS
 already_AddRefed<ReadableStream> Blob::Stream(JSContext* aCx,
                                               ErrorResult& aRv) {
   nsCOMPtr<nsIInputStream> stream;
   CreateInputStream(getter_AddRefs(stream), aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
@@ -389,34 +367,10 @@ already_AddRefed<ReadableStream> Blob::S
   BodyStream::Create(aCx, holder, mGlobal, stream, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   RefPtr<ReadableStream> rStream = holder->GetReadableStreamBody();
   return rStream.forget();
 }
-#else
-void Blob::Stream(JSContext* aCx, JS::MutableHandle<JSObject*> aStream,
-                  ErrorResult& aRv) {
-  nsCOMPtr<nsIInputStream> stream;
-  CreateInputStream(getter_AddRefs(stream), aRv);
-  if (NS_WARN_IF(aRv.Failed())) {
-    return;
-  }
-
-  if (NS_WARN_IF(!mGlobal)) {
-    aRv.Throw(NS_ERROR_FAILURE);
-    return;
-  }
-
-  RefPtr<BlobBodyStreamHolder> holder = new BlobBodyStreamHolder();
-
-  BodyStream::Create(aCx, holder, mGlobal, stream, aRv);
-  if (NS_WARN_IF(aRv.Failed())) {
-    return;
-  }
-
-  aStream.set(holder->GetReadableStreamBody());
-}
-#endif
 
 }  // namespace mozilla::dom
--- a/dom/file/Blob.h
+++ b/dom/file/Blob.h
@@ -23,19 +23,17 @@ namespace dom {
 
 struct BlobPropertyBag;
 class BlobImpl;
 class File;
 class GlobalObject;
 class OwningArrayBufferViewOrArrayBufferOrBlobOrUSVString;
 class Promise;
 
-#ifdef MOZ_DOM_STREAMS
 class ReadableStream;
-#endif
 
 #define NS_DOM_BLOB_IID                              \
   {                                                  \
     0x648c2a83, 0xbdb1, 0x4a7d, {                    \
       0xb5, 0x0a, 0xca, 0xcd, 0x92, 0x87, 0x45, 0xc2 \
     }                                                \
   }
 
@@ -117,22 +115,17 @@ class Blob : public nsSupportsWeakRefere
                                const Optional<nsAString>& aContentType,
                                ErrorResult& aRv);
 
   size_t GetAllocationSize() const;
 
   nsresult GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength,
                        nsACString& aContentType, nsACString& aCharset) const;
 
-#ifdef MOZ_DOM_STREAMS
   already_AddRefed<ReadableStream> Stream(JSContext* aCx, ErrorResult& aRv);
-#else
-  void Stream(JSContext* aCx, JS::MutableHandle<JSObject*> aStream,
-              ErrorResult& aRv);
-#endif
   already_AddRefed<Promise> Text(ErrorResult& aRv);
   already_AddRefed<Promise> ArrayBuffer(ErrorResult& aRv);
 
  protected:
   // File constructor should never be used directly. Use Blob::Create instead.
   Blob(nsIGlobalObject* aGlobal, BlobImpl* aImpl);
   virtual ~Blob();
 
--- a/dom/fs/FileSystemFileHandle.cpp
+++ b/dom/fs/FileSystemFileHandle.cpp
@@ -38,31 +38,29 @@ already_AddRefed<Promise> FileSystemFile
     return nullptr;
   }
 
   promise->MaybeReject(NS_ERROR_NOT_IMPLEMENTED);
 
   return promise.forget();
 }
 
-#ifdef MOZ_DOM_STREAMS
 already_AddRefed<Promise> FileSystemFileHandle::CreateWritable(
     const FileSystemCreateWritableOptions& aOptions) {
   IgnoredErrorResult rv;
 
   RefPtr<Promise> promise = Promise::Create(GetParentObject(), rv);
   if (rv.Failed()) {
     return nullptr;
   }
 
   promise->MaybeReject(NS_ERROR_NOT_IMPLEMENTED);
 
   return promise.forget();
 }
-#endif
 
 already_AddRefed<Promise> FileSystemFileHandle::CreateSyncAccessHandle() {
   IgnoredErrorResult rv;
 
   RefPtr<Promise> promise = Promise::Create(GetParentObject(), rv);
   if (rv.Failed()) {
     return nullptr;
   }
--- a/dom/fs/FileSystemFileHandle.h
+++ b/dom/fs/FileSystemFileHandle.h
@@ -23,20 +23,18 @@ class FileSystemFileHandle final : publi
   JSObject* WrapObject(JSContext* aCx,
                        JS::Handle<JSObject*> aGivenProto) override;
 
   // WebIDL interface
   FileSystemHandleKind Kind() override;
 
   already_AddRefed<Promise> GetFile();
 
-#ifdef MOZ_DOM_STREAMS
   already_AddRefed<Promise> CreateWritable(
       const FileSystemCreateWritableOptions& aOptions);
-#endif
 
   already_AddRefed<Promise> CreateSyncAccessHandle();
 
  private:
   ~FileSystemFileHandle() = default;
 };
 
 }  // namespace mozilla::dom
--- a/dom/fs/moz.build
+++ b/dom/fs/moz.build
@@ -5,29 +5,23 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 EXPORTS.mozilla.dom += [
     "FileSystemDirectoryHandle.h",
     "FileSystemDirectoryIterator.h",
     "FileSystemFileHandle.h",
     "FileSystemHandle.h",
     "FileSystemSyncAccessHandle.h",
+    "FileSystemWritableFileStream.h",
 ]
 
 UNIFIED_SOURCES += [
     "FileSystemDirectoryHandle.cpp",
     "FileSystemDirectoryIterator.cpp",
     "FileSystemFileHandle.cpp",
     "FileSystemHandle.cpp",
     "FileSystemSyncAccessHandle.cpp",
+    "FileSystemWritableFileStream.cpp",
 ]
 
-if CONFIG["MOZ_DOM_STREAMS"]:
-    EXPORTS.mozilla.dom += [
-        "FileSystemWritableFileStream.h",
-    ]
-    UNIFIED_SOURCES += [
-        "FileSystemWritableFileStream.cpp",
-    ]
-
 include("/ipc/chromium/chromium-config.mozbuild")
 
 FINAL_LIBRARY = "xul"
--- a/dom/html/ImageDocument.cpp
+++ b/dom/html/ImageDocument.cpp
@@ -231,18 +231,16 @@ void ImageDocument::SetScriptGlobalObjec
     target->AddEventListener(u"resize"_ns, this, false);
     target->AddEventListener(u"keypress"_ns, this, false);
 
     if (!InitialSetupHasBeenDone()) {
       LinkStylesheet(u"resource://content-accessible/ImageDocument.css"_ns);
       if (!nsContentUtils::IsChildOfSameType(this)) {
         LinkStylesheet(nsLiteralString(
             u"resource://content-accessible/TopLevelImageDocument.css"));
-        LinkStylesheet(nsLiteralString(
-            u"chrome://global/skin/media/TopLevelImageDocument.css"));
       }
       InitialSetupDone();
     }
   }
 }
 
 void ImageDocument::OnPageShow(bool aPersisted,
                                EventTarget* aDispatchStartTarget,
--- a/dom/html/VideoDocument.cpp
+++ b/dom/html/VideoDocument.cpp
@@ -82,18 +82,16 @@ void VideoDocument::SetScriptGlobalObjec
 
   if (aScriptGlobalObject && !InitialSetupHasBeenDone()) {
     DebugOnly<nsresult> rv = CreateSyntheticDocument();
     NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create synthetic video document");
 
     if (!nsContentUtils::IsChildOfSameType(this)) {
       LinkStylesheet(nsLiteralString(
           u"resource://content-accessible/TopLevelVideoDocument.css"));
-      LinkStylesheet(nsLiteralString(
-          u"chrome://global/skin/media/TopLevelVideoDocument.css"));
       LinkScript(u"chrome://global/content/TopLevelVideoDocument.js"_ns);
     }
     InitialSetupDone();
   }
 }
 
 nsresult VideoDocument::CreateVideoElement() {
   RefPtr<Element> body = GetBodyElement();
--- a/dom/media/ChannelMediaDecoder.cpp
+++ b/dom/media/ChannelMediaDecoder.cpp
@@ -134,18 +134,17 @@ void ChannelMediaDecoder::NotifyPrincipa
   if (!mInitialChannelPrincipalKnown) {
     // We'll receive one notification when the channel's initial principal
     // is known, after all HTTP redirects have resolved. This isn't really a
     // principal change, so return here to avoid the mSameOriginMedia check
     // below.
     mInitialChannelPrincipalKnown = true;
     return;
   }
-  if (!mSameOriginMedia &&
-      Preferences::GetBool("media.block-midflight-redirects", true)) {
+  if (!mSameOriginMedia) {
     // Block mid-flight redirects to non CORS same origin destinations.
     // See bugs 1441153, 1443942.
     LOG("ChannnelMediaDecoder prohibited cross origin redirect blocked.");
     NetworkError(MediaResult(NS_ERROR_DOM_BAD_URI,
                              "Prohibited cross origin redirect blocked"));
   }
 }
 
--- a/dom/media/test/test_midflight_redirect_blocked.html
+++ b/dom/media/test/test_midflight_redirect_blocked.html
@@ -51,39 +51,30 @@
           document.body.appendChild(element);
           element.load()
         });
       }
 
       let v = document.createElement("video");
       const testCases = gSmallTests.filter(t => v.canPlayType(t.type));
 
-      async function testMediaLoad(expectedToLoad, message, useCors) {
+      async function testMediaLoad(message, {useCors}) {
         for (let test of testCases) {
           let loaded = await testIfLoadsToMetadata(test, useCors);
-          is(loaded, expectedToLoad, test.name + " " + message);
+          is(loaded, useCors, test.name + " " + message);
         }
       }
 
       async function runTest() {
         try {
-          SimpleTest.info("Allowing midflight redirects...");
-          await SpecialPowers.pushPrefEnv({'set': [["media.block-midflight-redirects", false]]});
-
-          SimpleTest.info("Test that all media plays...");
-          await testMediaLoad(true, "expected to load", false);
-
-          SimpleTest.info("Blocking midflight redirects...");
-          await SpecialPowers.pushPrefEnv({'set': [["media.block-midflight-redirects", true]]});
-
-          SimpleTest.info("Test that all media no longer play...");
-          await testMediaLoad(false, "expected to be blocked", false);
+          SimpleTest.info("Test that all media do not play without CORS...");
+          await testMediaLoad("expected to be blocked", {useCors: false});
 
           SimpleTest.info("Test that all media play if CORS used...");
-          await testMediaLoad(true, "expected to play with CORS", true);
+          await testMediaLoad("expected to play with CORS", {useCors: true});
         } catch (e) {
           info("Exception " + e.message);
           ok(false, "Threw exception " + e.message);
         }
         SimpleTest.finish();
       }
 
       SimpleTest.waitForExplicitFinish();
--- a/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
+++ b/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
@@ -4,17 +4,16 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "MediaEngineWebRTCAudio.h"
 
 #include <stdio.h>
 #include <algorithm>
 
 #include "AudioConverter.h"
-#include "DeviceInputTrack.h"
 #include "MediaManager.h"
 #include "MediaTrackGraphImpl.h"
 #include "MediaTrackConstraints.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/ErrorNames.h"
 #include "nsIDUtils.h"
 #include "transport/runnable_utils.h"
 #include "Tracing.h"
@@ -1334,37 +1333,37 @@ nsresult AudioProcessingTrack::ConnectDe
   mDeviceId.emplace(aId);
 
   auto r = NativeInputTrack::OpenAudio(GraphImpl(), aId, aPrincipal,
                                        mInputListener.get());
   if (r.isErr()) {
     NS_WARNING("Failed to open audio device.");
     return r.unwrapErr();
   }
-  RefPtr<NativeInputTrack> input = r.unwrap();
-  MOZ_ASSERT(input);
-  LOG("Open device %p (InputTrack=%p) for Mic source %p", aId, input.get(),
-      this);
-  mPort = AllocateInputPort(input.get());
+  mDeviceInputTrack = r.unwrap();
+  MOZ_ASSERT(mDeviceInputTrack);
+  LOG("Open device %p (InputTrack=%p) for Mic source %p", aId,
+      mDeviceInputTrack.get(), this);
+  mPort = AllocateInputPort(mDeviceInputTrack.get());
   return NS_OK;
 }
 
 void AudioProcessingTrack::DisconnectDeviceInput() {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(GraphImpl());
   if (!mInputListener) {
     return;
   }
   MOZ_ASSERT(mPort);
   MOZ_ASSERT(mDeviceId.isSome());
-  RefPtr<NativeInputTrack> input(mPort->GetSource()->AsNativeInputTrack());
   LOG("Close device %p (InputTrack=%p) for Mic source %p ", *mDeviceId,
-      input.get(), this);
+      mDeviceInputTrack.get(), this);
   mPort->Destroy();
-  NativeInputTrack::CloseAudio(std::move(input), mInputListener.get());
+  NativeInputTrack::CloseAudio(std::move(mDeviceInputTrack),
+                               mInputListener.get());
   mInputListener = nullptr;
   mDeviceId = Nothing();
 }
 
 Maybe<CubebUtils::AudioDeviceID> AudioProcessingTrack::DeviceId() const {
   MOZ_ASSERT(NS_IsMainThread());
   return mDeviceId;
 }
--- a/dom/media/webrtc/MediaEngineWebRTCAudio.h
+++ b/dom/media/webrtc/MediaEngineWebRTCAudio.h
@@ -4,16 +4,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef MediaEngineWebRTCAudio_h
 #define MediaEngineWebRTCAudio_h
 
 #include "AudioPacketizer.h"
 #include "AudioSegment.h"
 #include "AudioDeviceInfo.h"
+#include "DeviceInputTrack.h"
 #include "MediaEngineWebRTC.h"
 #include "MediaTrackListener.h"
 #include "modules/audio_processing/include/audio_processing.h"
 
 namespace mozilla {
 
 class AudioInputProcessing;
 class AudioProcessingTrack;
@@ -220,16 +221,20 @@ class AudioInputProcessing : public Audi
 class AudioProcessingTrack : public ProcessedMediaTrack {
   // Only accessed on the graph thread.
   RefPtr<AudioInputProcessing> mInputProcessing;
 
   // Only accessed on the main thread. Link to the track producing raw audio
   // input data. Graph thread should use mInputs to get the source
   RefPtr<MediaInputPort> mPort;
 
+  // Only accessed on the main thread. This is the track producing raw audio
+  // input data. Graph thread should MediaInputPort::GetSource() to get this
+  RefPtr<NativeInputTrack> mDeviceInputTrack;
+
   // Only accessed on the main thread. Used for bookkeeping on main thread, such
   // that DisconnectDeviceInput can be idempotent.
   // XXX Should really be a CubebUtils::AudioDeviceID, but they aren't
   // copyable (opaque pointers)
   RefPtr<AudioDataListener> mInputListener;
 
   // Only accessed on the main thread.
   Maybe<CubebUtils::AudioDeviceID> mDeviceId;
--- a/dom/moz.build
+++ b/dom/moz.build
@@ -79,16 +79,17 @@ DIRS += [
     "system",
     "ipc",
     "workers",
     "audiochannel",
     "broadcastchannel",
     "messagechannel",
     "promise",
     "smil",
+    "streams",
     "url",
     "webauthn",
     "webidl",
     "webshare",
     "xml",
     "xslt",
     "xul",
     "manifest",
@@ -107,18 +108,16 @@ DIRS += [
     "simpledb",
     "reporting",
     "localstorage",
     "prio",
     "l10n",
     "origin-trials",
 ]
 
-if CONFIG["MOZ_DOM_STREAMS"]:
-    DIRS += ["streams"]
 
 TEST_DIRS += [
     "tests",
     "imptests",
 ]
 
 if CONFIG["MOZ_WIDGET_TOOLKIT"] in ("gtk", "cocoa", "windows"):
     TEST_DIRS += ["plugins/test"]
--- a/dom/prototype/PrototypeDocumentContentSink.cpp
+++ b/dom/prototype/PrototypeDocumentContentSink.cpp
@@ -666,17 +666,18 @@ nsresult PrototypeDocumentContentSink::D
     mScriptLoader->DeferCheckpointReached();
   }
 
   StartLayout();
 
   if (IsChromeURI(mDocumentURI) &&
       nsXULPrototypeCache::GetInstance()->IsEnabled()) {
     bool isCachedOnDisk;
-    nsXULPrototypeCache::GetInstance()->HasData(mDocumentURI, &isCachedOnDisk);
+    nsXULPrototypeCache::GetInstance()->HasPrototype(mDocumentURI,
+                                                     &isCachedOnDisk);
     if (!isCachedOnDisk) {
       nsXULPrototypeCache::GetInstance()->WritePrototype(mCurrentPrototype);
     }
   }
 
   mDocument->SetDelayFrameLoaderInitialization(false);
   mDocument->MaybeInitializeFinalizeFrameLoaders();
 
--- a/dom/streams/ReadableStream.h
+++ b/dom/streams/ReadableStream.h
@@ -14,20 +14,16 @@
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/QueuingStrategyBinding.h"
 #include "mozilla/dom/ReadableStreamController.h"
 #include "mozilla/dom/ReadableStreamDefaultController.h"
 #include "mozilla/dom/UnderlyingSourceCallbackHelpers.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsWrapperCache.h"
 
-#ifndef MOZ_DOM_STREAMS
-#  error "Shouldn't be compiling with this header without MOZ_DOM_STREAMS set"
-#endif
-
 namespace mozilla::dom {
 
 class Promise;
 class ReadableStreamGenericReader;
 class ReadableStreamDefaultReader;
 class ReadableStreamGenericReader;
 struct ReadableStreamGetReaderOptions;
 struct ReadIntoRequest;
--- a/dom/streams/TransformStream.h
+++ b/dom/streams/TransformStream.h
@@ -10,20 +10,16 @@
 #include "TransformStreamDefaultController.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/QueuingStrategyBinding.h"
 
 #include "mozilla/dom/TransformerBinding.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsWrapperCache.h"
 
-#ifndef MOZ_DOM_STREAMS
-#  error "Shouldn't be compiling with this header without MOZ_DOM_STREAMS set"
-#endif
-
 namespace mozilla::dom {
 
 class WritableStream;
 class ReadableStream;
 
 class TransformStream final : public nsISupports, public nsWrapperCache {
  public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
--- a/dom/streams/TransformStreamDefaultController.h
+++ b/dom/streams/TransformStreamDefaultController.h
@@ -11,20 +11,16 @@
 #include "mozilla/AlreadyAddRefed.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/QueuingStrategyBinding.h"
 
 #include "mozilla/dom/TransformerBinding.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsWrapperCache.h"
 
-#ifndef MOZ_DOM_STREAMS
-#  error "Shouldn't be compiling with this header without MOZ_DOM_STREAMS set"
-#endif
-
 namespace mozilla::dom {
 
 class TransformStream;
 class TransformerAlgorithms;
 
 class TransformStreamDefaultController final : public nsISupports,
                                                public nsWrapperCache {
  public:
--- a/dom/streams/WritableStream.h
+++ b/dom/streams/WritableStream.h
@@ -14,20 +14,16 @@
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/QueuingStrategyBinding.h"
 #include "mozilla/dom/WritableStreamDefaultController.h"
 #include "mozilla/dom/WritableStreamDefaultWriter.h"
 
 #include "nsCycleCollectionParticipant.h"
 #include "nsWrapperCache.h"
 
-#ifndef MOZ_DOM_STREAMS
-#  error "Shouldn't be compiling with this header without MOZ_DOM_STREAMS set"
-#endif
-
 namespace mozilla::dom {
 
 class Promise;
 class WritableStreamDefaultController;
 class WritableStreamDefaultWriter;
 class UnderlyingSinkAlgorithmsBase;
 
 class WritableStream : public nsISupports, public nsWrapperCache {
--- a/dom/streams/WritableStreamDefaultWriter.h
+++ b/dom/streams/WritableStreamDefaultWriter.h
@@ -12,20 +12,16 @@
 #include "mozilla/Attributes.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/QueuingStrategyBinding.h"
 
 #include "nsCycleCollectionParticipant.h"
 #include "nsWrapperCache.h"
 
-#ifndef MOZ_DOM_STREAMS
-#  error "Shouldn't be compiling with this header without MOZ_DOM_STREAMS set"
-#endif
-
 namespace mozilla::dom {
 
 class Promise;
 class WritableStream;
 
 class WritableStreamDefaultWriter final : public nsISupports,
                                           public nsWrapperCache {
  public:
--- a/dom/webidl/FileSystemFileHandle.webidl
+++ b/dom/webidl/FileSystemFileHandle.webidl
@@ -1,22 +1,19 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-#ifdef MOZ_DOM_STREAMS
 dictionary FileSystemCreateWritableOptions {
   boolean keepExistingData = false;
 };
-#endif
 
 // TODO: Add Serializable
 [Exposed=(Window,Worker), SecureContext, Pref="dom.fs.enabled"]
 interface FileSystemFileHandle : FileSystemHandle {
   Promise<File> getFile();
-#ifdef MOZ_DOM_STREAMS
+
   Promise<FileSystemWritableFileStream> createWritable(optional FileSystemCreateWritableOptions options = {});
-#endif
 
   [Exposed=DedicatedWorker]
   Promise<FileSystemSyncAccessHandle> createSyncAccessHandle();
 };
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -386,17 +386,16 @@ with Files("Extension*"):
     BUG_COMPONENT = ("WebExtensions", "General")
 
 GENERATED_WEBIDL_FILES = [
     "CSS2Properties.webidl",
 ]
 
 PREPROCESSED_WEBIDL_FILES = [
     "Animation.webidl",
-    "FileSystemFileHandle.webidl",
     "Node.webidl",
     "Window.webidl",
 ]
 
 WEBIDL_FILES = [
     "AbortController.webidl",
     "AbortSignal.webidl",
     "AbstractRange.webidl",
@@ -550,16 +549,17 @@ WEBIDL_FILES = [
     "FileReaderSync.webidl",
     "FileSystem.webidl",
     "FileSystemDirectoryEntry.webidl",
     "FileSystemDirectoryHandle.webidl",
     "FileSystemDirectoryIterator.webidl",
     "FileSystemDirectoryReader.webidl",
     "FileSystemEntry.webidl",
     "FileSystemFileEntry.webidl",
+    "FileSystemFileHandle.webidl",
     "FileSystemHandle.webidl",
     "FileSystemSyncAccessHandle.webidl",
     "FinalizationRegistry.webidl",
     "FocusEvent.webidl",
     "FontFace.webidl",
     "FontFaceSet.webidl",
     "FontFaceSource.webidl",
     "FormData.webidl",
--- a/dom/xul/nsXULElement.cpp
+++ b/dom/xul/nsXULElement.cpp
@@ -1665,34 +1665,34 @@ nsresult nsXULPrototypeScript::Serialize
     return NS_ERROR_NOT_IMPLEMENTED;
 
   nsXULPrototypeCache* cache = nsXULPrototypeCache::GetInstance();
   if (!cache) return NS_ERROR_OUT_OF_MEMORY;
 
   NS_ASSERTION(cache->IsEnabled(),
                "writing to the cache file, but the XUL cache is off?");
   bool exists;
-  cache->HasData(mSrcURI, &exists);
+  cache->HasScript(mSrcURI, &exists);
 
   /* return will be NS_OK from GetAsciiSpec.
    * that makes no sense.
    * nor does returning NS_OK from HasMuxedDocument.
    * XXX return something meaningful.
    */
   if (exists) return NS_OK;
 
   nsCOMPtr<nsIObjectOutputStream> oos;
-  nsresult rv = cache->GetOutputStream(mSrcURI, getter_AddRefs(oos));
+  nsresult rv = cache->GetScriptOutputStream(mSrcURI, getter_AddRefs(oos));
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsresult tmp = Serialize(oos, aProtoDoc, nullptr);
   if (NS_FAILED(tmp)) {
     rv = tmp;
   }
-  tmp = cache->FinishOutputStream(mSrcURI);
+  tmp = cache->FinishScriptOutputStream(mSrcURI);
   if (NS_FAILED(tmp)) {
     rv = tmp;
   }
 
   if (NS_FAILED(rv)) cache->AbortCaching();
   return rv;
 }
 
@@ -1749,17 +1749,17 @@ nsresult nsXULPrototypeScript::Deseriali
         if (newStencil) {
           Set(newStencil);
         }
       }
     }
 
     if (!mStencil) {
       if (mSrcURI) {
-        rv = cache->GetInputStream(mSrcURI, getter_AddRefs(objectInput));
+        rv = cache->GetScriptInputStream(mSrcURI, getter_AddRefs(objectInput));
       }
       // If !mSrcURI, we have an inline script. We shouldn't have
       // to do anything else in that case, I think.
 
       // We do reflect errors into rv, but our caller may want to
       // ignore our return value, because mStencil will be null
       // after any error, and that suffices to cause the script to
       // be reloaded (from the src= URI, if any) and recompiled.
@@ -1767,17 +1767,17 @@ nsresult nsXULPrototypeScript::Deseriali
       // error.
       if (NS_SUCCEEDED(rv))
         rv = Deserialize(objectInput, aProtoDoc, nullptr, nullptr);
 
       if (NS_SUCCEEDED(rv)) {
         if (useXULCache && mSrcURI && mSrcURI->SchemeIs("chrome")) {
           cache->PutStencil(mSrcURI, GetStencil());
         }
-        cache->FinishInputStream(mSrcURI);
+        cache->FinishScriptInputStream(mSrcURI);
       } else {
         // If mSrcURI is not in the cache,
         // rv will be NS_ERROR_NOT_AVAILABLE and we'll try to
         // update the cache file to hold a serialization of
         // this script, once it has finished loading.
         if (rv != NS_ERROR_NOT_AVAILABLE) cache->AbortCaching();
       }
     }
--- a/dom/xul/nsXULPrototypeCache.cpp
+++ b/dom/xul/nsXULPrototypeCache.cpp
@@ -32,17 +32,17 @@
 #include "mozilla/RefPtr.h"
 #include "mozilla/intl/LocaleService.h"
 
 using namespace mozilla;
 using namespace mozilla::scache;
 using mozilla::intl::LocaleService;
 
 static const char kXULCacheInfoKey[] = "nsXULPrototypeCache.startupCache";
-static const char kXULCachePrefix[] = "xulcache";
+#define CACHE_PREFIX(aCompilationTarget) "xulcache/" aCompilationTarget
 
 static void DisableXULCacheChangedCallback(const char* aPref, void* aClosure) {
   if (nsXULPrototypeCache* cache = nsXULPrototypeCache::GetInstance()) {
     if (!cache->IsEnabled()) {
       // AbortCaching() calls Flush() for us.
       cache->AbortCaching();
     }
   }
@@ -105,17 +105,17 @@ nsXULPrototypeDocument* nsXULPrototypeCa
     return protoDoc;
   }
 
   nsresult rv = BeginCaching(aURI);
   if (NS_FAILED(rv)) return nullptr;
 
   // No prototype in XUL memory cache. Spin up the cache Service.
   nsCOMPtr<nsIObjectInputStream> ois;
-  rv = GetInputStream(aURI, getter_AddRefs(ois));
+  rv = GetPrototypeInputStream(aURI, getter_AddRefs(ois));
   if (NS_FAILED(rv)) {
     return nullptr;
   }
 
   RefPtr<nsXULPrototypeDocument> newProto;
   rv = NS_NewXULPrototypeDocument(getter_AddRefs(newProto));
   if (NS_FAILED(rv)) return nullptr;
 
@@ -192,29 +192,41 @@ nsresult nsXULPrototypeCache::WriteProto
     nsXULPrototypeDocument* aPrototypeDocument) {
   nsresult rv = NS_OK, rv2 = NS_OK;
 
   if (!StartupCache::GetSingleton()) return NS_OK;
 
   nsCOMPtr<nsIURI> protoURI = aPrototypeDocument->GetURI();
 
   nsCOMPtr<nsIObjectOutputStream> oos;
-  rv = GetOutputStream(protoURI, getter_AddRefs(oos));
+  rv = GetPrototypeOutputStream(protoURI, getter_AddRefs(oos));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = aPrototypeDocument->Write(oos);
   NS_ENSURE_SUCCESS(rv, rv);
-  FinishOutputStream(protoURI);
+  FinishPrototypeOutputStream(protoURI);
   return NS_FAILED(rv) ? rv : rv2;
 }
 
-nsresult nsXULPrototypeCache::GetInputStream(nsIURI* uri,
+static nsresult PathifyURIForType(nsXULPrototypeCache::CacheType cacheType,
+                                  nsIURI* in, nsACString& out) {
+  switch (cacheType) {
+    case nsXULPrototypeCache::CacheType::Prototype:
+      return PathifyURI(CACHE_PREFIX("proto"), in, out);
+    case nsXULPrototypeCache::CacheType::Script:
+      return PathifyURI(CACHE_PREFIX("script"), in, out);
+  }
+  MOZ_ASSERT_UNREACHABLE("unknown cache type?");
+  return NS_ERROR_UNEXPECTED;
+}
+
+nsresult nsXULPrototypeCache::GetInputStream(CacheType cacheType, nsIURI* uri,
                                              nsIObjectInputStream** stream) {
-  nsAutoCString spec(kXULCachePrefix);
-  nsresult rv = PathifyURI(uri, spec);
+  nsAutoCString spec;
+  nsresult rv = PathifyURIForType(cacheType, uri, spec);
   if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE;
 
   const char* buf;
   uint32_t len;
   nsCOMPtr<nsIObjectInputStream> ois;
   StartupCache* sc = StartupCache::GetSingleton();
   if (!sc) return NS_ERROR_NOT_AVAILABLE;
 
@@ -256,55 +268,57 @@ nsresult nsXULPrototypeCache::GetOutputS
         getter_AddRefs(objectOutput), getter_AddRefs(storageStream), false);
     NS_ENSURE_SUCCESS(rv, rv);
     mOutputStreamTable.InsertOrUpdate(uri, storageStream);
   }
   objectOutput.forget(stream);
   return NS_OK;
 }
 
-nsresult nsXULPrototypeCache::FinishOutputStream(nsIURI* uri) {
+nsresult nsXULPrototypeCache::FinishOutputStream(CacheType cacheType,
+                                                 nsIURI* uri) {
   nsresult rv;
   StartupCache* sc = StartupCache::GetSingleton();
   if (!sc) return NS_ERROR_NOT_AVAILABLE;
 
   nsCOMPtr<nsIStorageStream> storageStream;
   bool found = mOutputStreamTable.Get(uri, getter_AddRefs(storageStream));
   if (!found) return NS_ERROR_UNEXPECTED;
   nsCOMPtr<nsIOutputStream> outputStream = do_QueryInterface(storageStream);
   outputStream->Close();
 
   UniquePtr<char[]> buf;
   uint32_t len;
   rv = NewBufferFromStorageStream(storageStream, &buf, &len);
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (!mStartupCacheURITable.GetEntry(uri)) {
-    nsAutoCString spec(kXULCachePrefix);
-    rv = PathifyURI(uri, spec);
+    nsAutoCString spec;
+    rv = PathifyURIForType(cacheType, uri, spec);
     if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE;
     rv = sc->PutBuffer(spec.get(), std::move(buf), len);
     if (NS_SUCCEEDED(rv)) {
       mOutputStreamTable.Remove(uri);
       mStartupCacheURITable.PutEntry(uri);
     }
   }
 
   return rv;
 }
 
 // We have data if we're in the middle of writing it or we already
 // have it in the cache.
-nsresult nsXULPrototypeCache::HasData(nsIURI* uri, bool* exists) {
+nsresult nsXULPrototypeCache::HasData(CacheType cacheType, nsIURI* uri,
+                                      bool* exists) {
   if (mOutputStreamTable.Get(uri, nullptr)) {
     *exists = true;
     return NS_OK;
   }
-  nsAutoCString spec(kXULCachePrefix);
-  nsresult rv = PathifyURI(uri, spec);
+  nsAutoCString spec;
+  nsresult rv = PathifyURIForType(cacheType, uri, spec);
   if (NS_FAILED(rv)) {
     *exists = false;
     return NS_OK;
   }
   UniquePtr<char[]> buf;
   StartupCache* sc = StartupCache::GetSingleton();
   if (sc) {
     *exists = sc->HasEntry(spec.get());
--- a/dom/xul/nsXULPrototypeCache.h
+++ b/dom/xul/nsXULPrototypeCache.h
@@ -29,16 +29,18 @@ class StyleSheet;
  * XUL documents, style sheets, XBL, and scripts.
  *
  * The cache has two levels:
  *  1. In-memory hashtables
  *  2. The on-disk cache file.
  */
 class nsXULPrototypeCache : public nsIObserver {
  public:
+  enum class CacheType { Prototype, Script };
+
   // nsISupports
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIOBSERVER
 
   bool IsCached(nsIURI* aURI) { return GetPrototype(aURI) != nullptr; }
   void AbortCaching();
 
   /**
@@ -66,22 +68,62 @@ class nsXULPrototypeCache : public nsIOb
    * fully loaded.
    */
   nsresult WritePrototype(nsXULPrototypeDocument* aPrototypeDocument);
 
   /**
    * This interface allows partial reads and writes from the buffers in the
    * startupCache.
    */
-  nsresult GetInputStream(nsIURI* aURI, nsIObjectInputStream** objectInput);
+
+  inline nsresult GetPrototypeInputStream(nsIURI* aURI,
+                                          nsIObjectInputStream** objectInput) {
+    return GetInputStream(CacheType::Prototype, aURI, objectInput);
+  }
+  inline nsresult GetScriptInputStream(nsIURI* aURI,
+                                       nsIObjectInputStream** objectInput) {
+    return GetInputStream(CacheType::Script, aURI, objectInput);
+  }
+  inline nsresult FinishScriptInputStream(nsIURI* aURI) {
+    return FinishInputStream(aURI);
+  }
+
+  inline nsresult GetPrototypeOutputStream(
+      nsIURI* aURI, nsIObjectOutputStream** objectOutput) {
+    return GetOutputStream(aURI, objectOutput);
+  }
+  inline nsresult GetScriptOutputStream(nsIURI* aURI,
+                                        nsIObjectOutputStream** objectOutput) {
+    return GetOutputStream(aURI, objectOutput);
+  }
+
+  inline nsresult FinishPrototypeOutputStream(nsIURI* aURI) {
+    return FinishOutputStream(CacheType::Prototype, aURI);
+  }
+  inline nsresult FinishScriptOutputStream(nsIURI* aURI) {
+    return FinishOutputStream(CacheType::Script, aURI);
+  }
+
+  inline nsresult HasPrototype(nsIURI* aURI, bool* exists) {
+    return HasData(CacheType::Prototype, aURI, exists);
+  }
+  inline nsresult HasScript(nsIURI* aURI, bool* exists) {
+    return HasData(CacheType::Script, aURI, exists);
+  }
+
+ private:
+  nsresult GetInputStream(CacheType cacheType, nsIURI* uri,
+                          nsIObjectInputStream** stream);
   nsresult FinishInputStream(nsIURI* aURI);
+
   nsresult GetOutputStream(nsIURI* aURI, nsIObjectOutputStream** objectOutput);
-  nsresult FinishOutputStream(nsIURI* aURI);
-  nsresult HasData(nsIURI* aURI, bool* exists);
+  nsresult FinishOutputStream(CacheType cacheType, nsIURI* aURI);
+  nsresult HasData(CacheType cacheType, nsIURI* aURI, bool* exists);
 
+ public:
   static nsXULPrototypeCache* GetInstance();
   static nsXULPrototypeCache* MaybeGetInstance() { return sInstance; }
 
   static void ReleaseGlobals() { NS_IF_RELEASE(sInstance); }
 
   void MarkInCCGeneration(uint32_t aGeneration);
 
   static void CollectMemoryReports(nsIHandleReportCallback* aHandleReport,
--- a/ipc/chromium/src/mojo/core/ports/port.h
+++ b/ipc/chromium/src/mojo/core/ports/port.h
@@ -8,26 +8,64 @@
 #include <memory>
 #include <queue>
 #include <utility>
 #include <vector>
 
 #include "mojo/core/ports/event.h"
 #include "mojo/core/ports/message_queue.h"
 #include "mojo/core/ports/user_data.h"
-#include "mozilla/Mutex.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/PlatformMutex.h"
 #include "mozilla/RefPtr.h"
 #include "nsISupportsImpl.h"
 
 namespace mojo {
 namespace core {
 namespace ports {
 
 class PortLocker;
 
+namespace detail {
+
+// Ports cannot use mozilla::Mutex, as the acquires-before relationships handled
+// by PortLocker can overload the debug-only deadlock detector.
+class CAPABILITY PortMutex : private ::mozilla::detail::MutexImpl {
+ public:
+  void AssertCurrentThreadOwns() const ASSERT_CAPABILITY(this) {
+#ifdef DEBUG
+    MOZ_ASSERT(mOwningThread == PR_GetCurrentThread());
+#endif
+  }
+
+ private:
+  // PortMutex should only be locked/unlocked via PortLocker
+  friend class ::mojo::core::ports::PortLocker;
+
+  void Lock() CAPABILITY_ACQUIRE() {
+    ::mozilla::detail::MutexImpl::lock();
+#ifdef DEBUG
+    mOwningThread = PR_GetCurrentThread();
+#endif
+  }
+  void Unlock() CAPABILITY_RELEASE() {
+#ifdef DEBUG
+    MOZ_ASSERT(mOwningThread == PR_GetCurrentThread());
+    mOwningThread = nullptr;
+#endif
+    ::mozilla::detail::MutexImpl::unlock();
+  }
+
+#ifdef DEBUG
+  mozilla::Atomic<PRThread*, mozilla::Relaxed> mOwningThread;
+#endif
+};
+
+}  // namespace detail
+
 // A Port is essentially a node in a circular list of addresses. For the sake of
 // this documentation such a list will henceforth be referred to as a "route."
 // Routes are the fundamental medium upon which all Node event circulation takes
 // place and are thus the backbone of all Mojo message passing.
 //
 // Each Port is identified by a 128-bit address within a Node (see node.h). A
 // Port doesn't really *do* anything per se: it's a named collection of state,
 // and its owning Node manages all event production, transmission, routing, and
@@ -186,16 +224,16 @@ class Port {
  private:
   friend class PortLocker;
 
   ~Port();
 
   // This lock guards all fields in Port, but is locked in a unique way which is
   // unfortunately somewhat difficult to get to work with the thread-safety
   // analysis.
-  mozilla::Mutex lock_ MOZ_ANNOTATED{"Port State"};
+  detail::PortMutex lock_ MOZ_ANNOTATED;
 };
 
 }  // namespace ports
 }  // namespace core
 }  // namespace mojo
 
 #endif  // MOJO_CORE_PORTS_PORT_H_
--- a/js/src/gc/Marking.cpp
+++ b/js/src/gc/Marking.cpp
@@ -1747,17 +1747,17 @@ GCMarker::MarkQueueProgress GCMarker::pr
           abortLinearWeakMarking();
         }
       } else if (js::StringEqualsLiteral(str, "drain")) {
         auto unlimited = SliceBudget::unlimited();
         MOZ_RELEASE_ASSERT(
             markUntilBudgetExhausted(unlimited, DontReportMarkTime));
       } else if (js::StringEqualsLiteral(str, "set-color-gray")) {
         queueMarkColor = mozilla::Some(MarkColor::Gray);
-        if (gcrt.state() != State::Sweep) {
+        if (gcrt.state() != State::Sweep || hasBlackEntries()) {
           // Cannot mark gray yet, so continue with the GC.
           queuePos--;
           return QueueSuspended;
         }
         setMarkColor(MarkColor::Gray);
       } else if (js::StringEqualsLiteral(str, "set-color-black")) {
         queueMarkColor = mozilla::Some(MarkColor::Black);
         setMarkColor(MarkColor::Black);
@@ -1959,18 +1959,18 @@ scan_value_range:
       return;
     }
 
     const Value& v = base[index];
     index++;
 
     if (v.isString()) {
       markAndTraverseEdge(obj, v.toString());
-    } else if (v.isObject()) {
-      JSObject* obj2 = &v.toObject();
+    } else if (v.hasObjectPayload()) {
+      JSObject* obj2 = &v.getObjectPayload();
 #ifdef DEBUG
       if (!obj2) {
         fprintf(stderr,
                 "processMarkStackTop found ObjectValue(nullptr) "
                 "at %zu Values from end of range in object:\n",
                 size_t(end - (index - 1)));
         obj->dump();
       }
--- a/js/src/gc/Tenuring.cpp
+++ b/js/src/gc/Tenuring.cpp
@@ -28,16 +28,19 @@
 
 #include "gc/Heap-inl.h"
 #include "gc/Marking-inl.h"
 #include "gc/ObjectKind-inl.h"
 #include "gc/StoreBuffer-inl.h"
 #include "vm/JSContext-inl.h"
 #include "vm/JSObject-inl.h"
 #include "vm/PlainObject-inl.h"
+#ifdef ENABLE_RECORD_TUPLE
+#  include "vm/TupleType.h"
+#endif
 
 using namespace js;
 using namespace js::gc;
 
 using mozilla::PodCopy;
 
 constexpr size_t MAX_DEDUPLICATABLE_STRING_LENGTH = 500;
 
@@ -646,19 +649,19 @@ size_t js::TenuringTracer::moveElementsT
     AddCellMemory(dst, allocSize, MemoryUse::ObjectElements);
 
     return 0;
   }
 
   // Shifted elements are copied too.
   uint32_t numShifted = srcHeader->numShiftedElements();
 
-  /* Unlike other objects, Arrays can have fixed elements. */
-  if (src->is<ArrayObject>() && nslots <= GetGCKindSlots(dstKind)) {
-    dst->as<ArrayObject>().setFixedElements();
+  /* Unlike other objects, Arrays and Tuples can have fixed elements. */
+  if (src->canHaveFixedElements() && nslots <= GetGCKindSlots(dstKind)) {
+    dst->as<NativeObject>().setFixedElements();
     js_memcpy(dst->getElementsHeader(), srcAllocatedHeader, allocSize);
     dst->elements_ += numShifted;
     nursery().setElementsForwardingPointer(srcHeader, dst->getElementsHeader(),
                                            srcHeader->capacity);
     return allocSize;
   }
 
   MOZ_ASSERT(nslots >= 2);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/gc/bug-1762771.js
@@ -0,0 +1,9 @@
+if (this.enqueueMark) {
+    gczeal(0);
+    enqueueMark('set-color-gray');
+    enqueueMark('set-color-black');
+    enqueueMark(newGlobal());
+    enqueueMark('set-color-gray');
+    newGlobal();
+    startgc();
+}
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/record-tuple/compacting-gc-nested-tuples.js
@@ -0,0 +1,23 @@
+// |jit-test| skip-if: !this.hasOwnProperty("Tuple")
+gczeal(14); // Be sure to run compacting GC
+
+function f() {
+  assertEq(#[1, 2].flatMap(function(e) {
+    return #[e, e * 2];
+  }), #[1, 2, 2, 4]);
+
+  var result = #[1, 2, 3].flatMap(function(ele) {
+    return #[
+      #[ele * 2]
+    ];
+  });
+
+  assertEq(result.length, 3);
+  assertEq(result[0], #[2]);
+  assertEq(result[1], #[4]);
+  assertEq(result[2], #[6]);
+}
+
+for (i = 0; i < 20; i++) {
+    f();
+}
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/record-tuple/from.js
@@ -0,0 +1,13 @@
+// |jit-test| skip-if: !this.hasOwnProperty("Tuple")
+
+gczeal(10); // Run incremental GC in many slices
+
+var c = ["a", "b"];
+var t = Tuple.from(c);
+
+for (i = 0; i < 100; i++) {
+c = ["a", "b"];
+t = Tuple.from(c);
+c = null;
+gc();
+}
--- a/js/src/vm/JSObject.cpp
+++ b/js/src/vm/JSObject.cpp
@@ -3202,32 +3202,36 @@ JS_PUBLIC_API void js::DumpBacktrace(JSC
 }
 
 JS_PUBLIC_API void js::DumpBacktrace(JSContext* cx) {
   DumpBacktrace(cx, stdout);
 }
 
 /* * */
 
+bool JSObject::canHaveFixedElements() const {
+  return (is<ArrayObject>() || IF_RECORD_TUPLE(is<TupleType>(), false));
+}
+
 js::gc::AllocKind JSObject::allocKindForTenure(
     const js::Nursery& nursery) const {
   using namespace js::gc;
 
   MOZ_ASSERT(IsInsideNursery(this));
 
-  if (is<ArrayObject>()) {
-    const ArrayObject& aobj = as<ArrayObject>();
-    MOZ_ASSERT(aobj.numFixedSlots() == 0);
+  if (canHaveFixedElements()) {
+    const NativeObject& nobj = as<NativeObject>();
+    MOZ_ASSERT(nobj.numFixedSlots() == 0);
 
     /* Use minimal size object if we are just going to copy the pointer. */
-    if (!nursery.isInside(aobj.getElementsHeader())) {
+    if (!nursery.isInside(nobj.getElementsHeader())) {
       return gc::AllocKind::OBJECT0_BACKGROUND;
     }
 
-    size_t nelements = aobj.getDenseCapacity();
+    size_t nelements = nobj.getDenseCapacity();
     return ForegroundToBackgroundAllocKind(GetGCArrayKind(nelements));
   }
 
   if (is<JSFunction>()) {
     return as<JSFunction>().getAllocKind();
   }
 
   /*
--- a/js/src/vm/JSObject.h
+++ b/js/src/vm/JSObject.h
@@ -298,16 +298,18 @@ class JSObject
   static MOZ_ALWAYS_INLINE void postWriteBarrier(void* cellp, JSObject* prev,
                                                  JSObject* next) {
     js::gc::PostWriteBarrierImpl<JSObject>(cellp, prev, next);
   }
 
   /* Return the allocKind we would use if we were to tenure this object. */
   js::gc::AllocKind allocKindForTenure(const js::Nursery& nursery) const;
 
+  bool canHaveFixedElements() const;
+
   size_t tenuredSizeOfThis() const {
     MOZ_ASSERT(isTenured());
     return js::gc::Arena::thingSize(asTenured().getAllocKind());
   }
 
   void addSizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf,
                               JS::ClassInfo* info,
                               JS::RuntimeSizes* runtimeSizes);
--- a/js/src/vm/TupleType.cpp
+++ b/js/src/vm/TupleType.cpp
@@ -6,22 +6,19 @@
 
 #include "vm/TupleType.h"
 
 #include "mozilla/FloatingPoint.h"
 #include "mozilla/HashFunctions.h"
 
 #include "jsapi.h"
 
-#include "builtin/Array.h"  // IsArray()
 #include "builtin/TupleObject.h"
 #include "gc/Allocator.h"
 #include "gc/AllocKind.h"
-#include "gc/Nursery.h"
-#include "gc/Tracer.h"
 
 #include "js/TypeDecls.h"
 #include "js/Value.h"
 #include "util/StringBuffer.h"
 #include "vm/EqualityOperations.h"
 #include "vm/GlobalObject.h"
 #include "vm/JSContext.h"
 #include "vm/RecordType.h"
@@ -507,17 +504,20 @@ static bool ArrayToTuple(JSContext* cx, 
     return false;
   }
 
   args.rval().setExtendedPrimitive(*tup);
   return true;
 }
 
 // Takes an array as a single argument and returns a tuple of the
-// array elements, without copying the array
+// array elements. This method copies the array, because the callee
+// may still hold a pointer to it and it would break garbage collection
+// to change the type of the object from ArrayObject to TupleType (which
+// is the only way to re-use the same object if it has fixed elements.)
 // Should only be called from self-hosted tuple methods;
 // assumes all elements are non-objects and the array is packed
 bool js::tuple_construct(JSContext* cx, unsigned argc, Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   MOZ_ASSERT(args[0].toObject().is<ArrayObject>());
 
   args.rval().set(args[0]);
@@ -525,43 +525,23 @@ bool js::tuple_construct(JSContext* cx, 
 }
 
 bool js::tuple_is_tuple(JSContext* cx, unsigned argc, Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
   return IsTupleUnchecked(cx, args);
 }
 
 TupleType* TupleType::createUnchecked(JSContext* cx, HandleArrayObject aObj) {
-  gc::AllocKind allocKind = GuessArrayGCKind(aObj->getDenseInitializedLength());
-
-  RootedShape shape(cx, TupleType::getInitialShape(cx));
-  if (!shape) {
-    return nullptr;
-  }
-
-  JSObject* obj =
-      js::AllocateObject(cx, allocKind, 0, gc::DefaultHeap, &TupleType::class_);
-
-  if (!obj) {
-    return nullptr;
-  }
-
-  TupleType* tup = static_cast<TupleType*>(obj);
-  tup->initShape(shape);
-  tup->initEmptyDynamicSlots();
-  tup->setFixedElements(0);
-
+  size_t len = aObj->getDenseInitializedLength();
+  MOZ_ASSERT(aObj->getElementsHeader()->numShiftedElements() == 0);
+  TupleType* tup = createUninitialized(cx, len);
   if (!tup) {
     return nullptr;
   }
-
-  tup->elements_ = aObj->getElementsHeader()->elements();
-
-  aObj->shrinkCapacityToInitializedLength(cx);
-
+  tup->initDenseElements(aObj, 0, len);
   tup->finishInitialization(cx);
   return tup;
 }
 
 bool js::tuple_of(JSContext* cx, unsigned argc, Value* vp) {
   /* Step 1 */
   CallArgs args = CallArgsFromVp(argc, vp);
   size_t len = args.length();
--- a/js/xpconnect/loader/mozJSComponentLoader.cpp
+++ b/js/xpconnect/loader/mozJSComponentLoader.cpp
@@ -70,17 +70,18 @@
 #include "mozilla/Unused.h"
 
 using namespace mozilla;
 using namespace mozilla::scache;
 using namespace mozilla::loader;
 using namespace xpc;
 using namespace JS;
 
-#define JS_CACHE_PREFIX(aType) "jsloader/" aType
+#define JS_CACHE_PREFIX(aScopeType, aCompilationTarget) \
+  "jsloader/" aScopeType "/" aCompilationTarget
 
 /**
  * Buffer sizes for serialization and deserialization of scripts.
  * FIXME: bug #411579 (tune this macro!) Last updated: Jan 2008
  */
 #define XPC_SERIALIZATION_BUFFER_SIZE (64 * 1024)
 #define XPC_DESERIALIZATION_BUFFER_SIZE (12 * 8192)
 
@@ -742,18 +743,19 @@ nsresult mozJSComponentLoader::ObjectFor
   // errors and startupcache errors are not fatal to loading the script, since
   // we can always slow-load.
 
   bool storeIntoStartupCache = false;
   StartupCache* cache = StartupCache::GetSingleton();
 
   aInfo.EnsureResolvedURI();
 
-  nsAutoCString cachePath(JS_CACHE_PREFIX("non-syntactic"));
-  rv = PathifyURI(aInfo.ResolvedURI(), cachePath);
+  nsAutoCString cachePath;
+  rv = PathifyURI(JS_CACHE_PREFIX("non-syntactic", "script"),
+                  aInfo.ResolvedURI(), cachePath);
   NS_ENSURE_SUCCESS(rv, rv);
 
   JS::DecodeOptions decodeOptions;
   ScriptPreloader::FillDecodeOptionsForCachedStencil(decodeOptions);
 
   RefPtr<JS::Stencil> stencil =
       ScriptPreloader::GetSingleton().GetCachedStencil(cx, decodeOptions,
                                                        cachePath);
--- a/js/xpconnect/loader/mozJSSubScriptLoader.cpp
+++ b/js/xpconnect/loader/mozJSSubScriptLoader.cpp
@@ -76,29 +76,29 @@ class MOZ_STACK_CLASS LoadSubScriptOptio
 #define LOAD_ERROR_CONTENTTOOBIG "ContentLength is too large"
 
 mozJSSubScriptLoader::mozJSSubScriptLoader() = default;
 
 mozJSSubScriptLoader::~mozJSSubScriptLoader() = default;
 
 NS_IMPL_ISUPPORTS(mozJSSubScriptLoader, mozIJSSubScriptLoader)
 
-#define JSSUB_CACHE_PREFIX(aType) "jssubloader/" aType
+#define JSSUB_CACHE_PREFIX(aScopeType, aCompilationTarget) \
+  "jssubloader/" aScopeType "/" aCompilationTarget
 
 static void SubscriptCachePath(JSContext* cx, nsIURI* uri,
                                JS::HandleObject targetObj,
                                nsACString& cachePath) {
   // StartupCache must distinguish between non-syntactic vs global when
   // computing the cache key.
   if (!JS_IsGlobalObject(targetObj)) {
-    cachePath.AssignLiteral(JSSUB_CACHE_PREFIX("non-syntactic"));
+    PathifyURI(JSSUB_CACHE_PREFIX("non-syntactic", "script"), uri, cachePath);
   } else {
-    cachePath.AssignLiteral(JSSUB_CACHE_PREFIX("global"));
+    PathifyURI(JSSUB_CACHE_PREFIX("global", "script"), uri, cachePath);
   }
-  PathifyURI(uri, cachePath);
 }
 
 static void ReportError(JSContext* cx, const nsACString& msg) {
   NS_ConvertUTF8toUTF16 ucMsg(msg);
 
   RootedValue exn(cx);
   if (xpc::NonVoidStringToJsval(cx, ucMsg, &exn)) {
     JS_SetPendingException(cx, exn);
--- a/js/xpconnect/src/Sandbox.cpp
+++ b/js/xpconnect/src/Sandbox.cpp
@@ -63,19 +63,17 @@
 #include "mozilla/dom/NodeBinding.h"
 #include "mozilla/dom/NodeFilterBinding.h"
 #include "mozilla/dom/PathUtilsBinding.h"
 #include "mozilla/dom/PerformanceBinding.h"
 #include "mozilla/dom/PromiseBinding.h"
 #include "mozilla/dom/PromiseDebuggingBinding.h"
 #include "mozilla/dom/RangeBinding.h"
 #include "mozilla/dom/RequestBinding.h"
-#ifdef MOZ_DOM_STREAMS
-#  include "mozilla/dom/ReadableStreamBinding.h"
-#endif
+#include "mozilla/dom/ReadableStreamBinding.h"
 #include "mozilla/dom/ResponseBinding.h"
 #ifdef MOZ_WEBRTC
 #  include "mozilla/dom/RTCIdentityProviderRegistrar.h"
 #endif
 #include "mozilla/dom/FileReaderBinding.h"
 #include "mozilla/dom/ScriptLoader.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/SelectionBinding.h"
@@ -969,20 +967,18 @@ bool xpc::GlobalProperties::Parse(JSCont
     } else if (JS_LinearStringEqualsLiteral(nameStr, "XMLHttpRequest")) {
       XMLHttpRequest = true;
     } else if (JS_LinearStringEqualsLiteral(nameStr, "WebSocket")) {
       WebSocket = true;
     } else if (JS_LinearStringEqualsLiteral(nameStr, "Window")) {
       Window = true;
     } else if (JS_LinearStringEqualsLiteral(nameStr, "XMLSerializer")) {
       XMLSerializer = true;
-#ifdef MOZ_DOM_STREAMS
     } else if (JS_LinearStringEqualsLiteral(nameStr, "ReadableStream")) {
       ReadableStream = true;
-#endif
     } else if (JS_LinearStringEqualsLiteral(nameStr, "atob")) {
       atob = true;
     } else if (JS_LinearStringEqualsLiteral(nameStr, "btoa")) {
       btoa = true;
     } else if (JS_LinearStringEqualsLiteral(nameStr, "caches")) {
       caches = true;
     } else if (JS_LinearStringEqualsLiteral(nameStr, "crypto")) {
       crypto = true;
@@ -1135,20 +1131,18 @@ bool xpc::GlobalProperties::Define(JSCon
   if (WebSocket && !dom::WebSocket_Binding::GetConstructorObject(cx))
     return false;
 
   if (Window && !dom::Window_Binding::GetConstructorObject(cx)) return false;
 
   if (XMLSerializer && !dom::XMLSerializer_Binding::GetConstructorObject(cx))
     return false;
 
-#ifdef MOZ_DOM_STREAMS
   if (ReadableStream && !dom::ReadableStream_Binding::GetConstructorObject(cx))
     return false;
-#endif
 
   if (atob && !JS_DefineFunction(cx, obj, "atob", Atob, 1, 0)) return false;
 
   if (btoa && !JS_DefineFunction(cx, obj, "btoa", Btoa, 1, 0)) return false;
 
   if (caches && !dom::cache::CacheStorage::DefineCaches(cx, obj)) {
     return false;
   }
--- a/js/xpconnect/src/XPCJSContext.cpp
+++ b/js/xpconnect/src/XPCJSContext.cpp
@@ -792,18 +792,16 @@ static JS::WeakRefSpecifier GetWeakRefsE
 }
 
 void xpc::SetPrefableRealmOptions(JS::RealmOptions& options) {
   options.creationOptions()
       .setSharedMemoryAndAtomicsEnabled(sSharedMemoryEnabled)
       .setCoopAndCoepEnabled(
           StaticPrefs::browser_tabs_remote_useCrossOriginOpenerPolicy() &&
           StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy())
-      .setStreamsEnabled(
-          sStreamsEnabled)  // Note: Overridden by MOZ_DOM_STREAMS
       .setWritableStreamsEnabled(
           StaticPrefs::javascript_options_writable_streams())
       .setPropertyErrorMessageFixEnabled(sPropertyErrorMessageFixEnabled)
       .setWeakRefsEnabled(GetWeakRefsEnabled())
       .setIteratorHelpersEnabled(sIteratorHelpersEnabled)
 #ifdef NIGHTLY_BUILD
       .setArrayGroupingEnabled(sArrayGroupingEnabled)
 #endif
--- a/js/xpconnect/src/XPCJSRuntime.cpp
+++ b/js/xpconnect/src/XPCJSRuntime.cpp
@@ -3009,29 +3009,16 @@ void XPCJSRuntime::Initialize(JSContext*
   }
   js::SetPreserveWrapperCallbacks(cx, PreserveWrapper, HasReleasedWrapper);
   JS_InitReadPrincipalsCallback(cx, nsJSPrincipals::ReadPrincipals);
   JS_SetAccumulateTelemetryCallback(cx, AccumulateTelemetryCallback);
   JS_SetSetUseCounterCallback(cx, SetUseCounterCallback);
 
   js::SetWindowProxyClass(cx, &OuterWindowProxyClass);
 
-#ifndef MOZ_DOM_STREAMS
-  {
-    JS::AbortSignalIsAborted isAborted = [](JSObject* obj) {
-      dom::AbortSignal* domObj = dom::UnwrapDOMObject<dom::AbortSignal>(obj);
-      MOZ_ASSERT(domObj);
-      return domObj->Aborted();
-    };
-
-    JS::InitPipeToHandling(dom::AbortSignal_Binding::GetJSClass(), isAborted,
-                           cx);
-  }
-#endif
-
   JS::SetXrayJitInfo(&gXrayJitInfo);
   JS::SetProcessLargeAllocationFailureCallback(
       OnLargeAllocationFailureCallback);
 
   // The WasmAltDataType is build by the JS engine from the build id.
   JS::SetProcessBuildIdOp(GetBuildId);
   FetchUtil::InitWasmAltDataType();
 
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -2250,19 +2250,17 @@ struct GlobalProperties {
   bool TextDecoder : 1;
   bool TextEncoder : 1;
   bool URL : 1;
   bool URLSearchParams : 1;
   bool XMLHttpRequest : 1;
   bool WebSocket : 1;
   bool Window : 1;
   bool XMLSerializer : 1;
-#ifdef MOZ_DOM_STREAMS
   bool ReadableStream : 1;
-#endif
 
   // Ad-hoc property names we implement.
   bool atob : 1;
   bool btoa : 1;
   bool caches : 1;
   bool crypto : 1;
   bool fetch : 1;
   bool storage : 1;
--- a/layout/painting/HitTestInfo.cpp
+++ b/layout/painting/HitTestInfo.cpp
@@ -14,17 +14,17 @@
 
 using namespace mozilla::gfx;
 
 namespace mozilla {
 
 static StaticAutoPtr<const HitTestInfo> gEmptyHitTestInfo;
 
 const HitTestInfo& HitTestInfo::Empty() {
-  if (gEmptyHitTestInfo) {
+  if (!gEmptyHitTestInfo) {
     gEmptyHitTestInfo = new HitTestInfo();
   }
 
   return *gEmptyHitTestInfo;
 }
 
 void HitTestInfo::Shutdown() { gEmptyHitTestInfo = nullptr; }
 
--- a/layout/style/TopLevelImageDocument.css
+++ b/layout/style/TopLevelImageDocument.css
@@ -4,16 +4,30 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*
   This CSS stylesheet defines the rules to be applied to ImageDocuments that
   are top level (e.g. not iframes).
 */
 
 @media not print {
+  :root {
+    /* The colors here were chosen to be readable over the corresponding
+       backgrounds. This is important in case this ImageDocument is for an
+       image that happens to be corrupt, in which case we'll display a textual
+       error message over the background, instead of the image itself. */
+    color: #eee;
+    background-image: url("chrome://global/skin/media/imagedoc-darknoise.png");
+  }
+
+  img.transparent {
+    color: #222;
+    background: hsl(0,0%,90%) url("chrome://global/skin/media/imagedoc-lightnoise.png");
+  }
+
   img {
     text-align: center;
     position: absolute;
     inset: 0;
     margin: auto;
   }
 
   img.overflowingVertical {
--- a/layout/style/TopLevelVideoDocument.css
+++ b/layout/style/TopLevelVideoDocument.css
@@ -3,17 +3,18 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*
   This CSS stylesheet defines the rules to be applied to VideoDocuments that
   are top level (e.g. not iframes).
 */
 
-html {
+:root {
+  background-color: black;
   /* Fill the viewport height, so that our '-moz-user-focus' styling will
      disregard clicks in the whole background area (so the video element
      doesn't inadvertently lose focus from a stray click on the background). */
   height: 100%;
   -moz-user-focus: ignore;
 }
 
 video {
--- a/media/libcubeb/README.md
+++ b/media/libcubeb/README.md
@@ -1,7 +1,7 @@
 [![Build Status](https://github.com/mozilla/cubeb/actions/workflows/build.yml/badge.svg)](https://github.com/mozilla/cubeb/actions/workflows/build.yml)
 
 See INSTALL.md for build instructions.
 
-See [Backend Support](https://github.com/kinetiknz/cubeb/wiki/Backend-Support) in the wiki for the support level of each backend.
+See [Backend Support](https://github.com/mozilla/cubeb/wiki/Backend-Support) in the wiki for the support level of each backend.
 
 Licensed under an ISC-style license.  See LICENSE for details.
--- a/media/libcubeb/moz.yaml
+++ b/media/libcubeb/moz.yaml
@@ -4,18 +4,18 @@ bugzilla:
   product: Core
   component: "Audio/Video: cubeb"
 
 origin:
   name: cubeb
   description: "Cross platform audio library"
   url: https://github.com/mozilla/cubeb
   license: ISC
-  release: commit b62d61bc661b49c7a7f5d97f4657189c630ac7a5 (2022-03-30T05:12:01Z).
-  revision: b62d61bc661b49c7a7f5d97f4657189c630ac7a5
+  release: commit 708f52cccffe69ed1d65b52903237c990db860a9 (2022-04-13T15:02:09Z).
+  revision: 708f52cccffe69ed1d65b52903237c990db860a9
 
 vendoring:
   url: https://github.com/mozilla/cubeb
   source-hosting: github
   vendor-directory: media/libcubeb
   skip-vendoring-steps:
     - update-moz-build
   exclude:
--- a/media/libcubeb/src/cubeb_wasapi.cpp
+++ b/media/libcubeb/src/cubeb_wasapi.cpp
@@ -235,18 +235,19 @@ ERole
 pref_to_role(cubeb_stream_prefs param);
 int
 wasapi_create_device(cubeb * ctx, cubeb_device_info & ret,
                      IMMDeviceEnumerator * enumerator, IMMDevice * dev,
                      wasapi_default_devices * defaults);
 void
 wasapi_destroy_device(cubeb_device_info * device_info);
 static int
-wasapi_enumerate_devices(cubeb * context, cubeb_device_type type,
-                         cubeb_device_collection * out);
+wasapi_enumerate_devices_internal(cubeb * context, cubeb_device_type type,
+                                  cubeb_device_collection * out,
+                                  DWORD state_mask);
 static int
 wasapi_device_collection_destroy(cubeb * ctx,
                                  cubeb_device_collection * collection);
 static char const *
 wstr_to_utf8(wchar_t const * str);
 static std::unique_ptr<wchar_t const[]>
 utf8_to_wstr(char const * str);
 
@@ -404,22 +405,19 @@ struct cubeb_stream {
   size_t bytes_per_sample = 0;
   /* WAVEFORMATEXTENSIBLE sub-format: either PCM or float. */
   GUID waveformatextensible_sub_format = GUID_NULL;
   /* Stream volume.  Set via stream_set_volume and used to reset volume on
      device changes. */
   float volume = 1.0;
   /* True if the stream is draining. */
   bool draining = false;
-  /* True when we've destroyed the stream. This pointer is leaked on stream
-   * destruction if we could not join the thread. */
-  std::atomic<std::atomic<bool> *> emergency_bailout{nullptr};
-  /* Synchronizes render thread start to ensure safe access to
-   * emergency_bailout. */
-  HANDLE thread_ready_event = 0;
+  /* If the render thread fails to stop, this is set to true and ownership of
+   * the stm is "leaked" to the render thread for later cleanup. */
+  std::atomic<bool> emergency_bailout{false};
   /* This needs an active audio input stream to be known, and is updated in the
    * first audio input callback. */
   std::atomic<int64_t> input_latency_hns{LATENCY_NOT_AVAILABLE_YET};
 
   /* Those attributes count the number of frames requested (resp. received) by
   the OS, to be able to detect drifts. This is only used for logging for now. */
   size_t total_input_frames = 0;
   size_t total_output_frames = 0;
@@ -748,16 +746,37 @@ private:
   /* refcount for this instance, necessary to implement MSCOM semantics. */
   LONG ref_count;
   HANDLE reconfigure_event;
   ERole role;
 };
 
 namespace {
 
+long
+wasapi_data_callback(cubeb_stream * stm, void * user_ptr,
+                     void const * input_buffer, void * output_buffer,
+                     long nframes)
+{
+  if (stm->emergency_bailout) {
+    return CUBEB_ERROR;
+  }
+  return stm->data_callback(stm, user_ptr, input_buffer, output_buffer,
+                            nframes);
+}
+
+void
+wasapi_state_callback(cubeb_stream * stm, void * user_ptr, cubeb_state state)
+{
+  if (stm->emergency_bailout) {
+    return;
+  }
+  return stm->state_callback(stm, user_ptr, state);
+}
+
 char const *
 intern_device_id(cubeb * ctx, wchar_t const * id)
 {
   XASSERT(id);
 
   auto_lock lock(ctx->lock);
 
   char const * tmp = wstr_to_utf8(id);
@@ -870,17 +889,17 @@ refill(cubeb_stream * stm, void * input_
     }
   }
 
   long out_frames =
       cubeb_resampler_fill(stm->resampler.get(), input_buffer,
                            &input_frames_count, dest, output_frames_needed);
   if (out_frames < 0) {
     ALOGV("Callback refill error: %d", out_frames);
-    stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
+    wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
     return out_frames;
   }
 
   float volume = 1.0;
   {
     auto_lock lock(stm->stream_reset_lock);
     stm->frames_written += out_frames;
     volume = stm->volume;
@@ -989,17 +1008,17 @@ get_input_buffer(cubeb_stream * stm)
       // https://msdn.microsoft.com/en-us/library/windows/desktop/dd316605(v=vs.85).aspx
       LOG("Input device invalidated error");
       // No need to reset device if user asks to use particular device, or
       // switching is disabled.
       if (stm->input_device_id ||
           (stm->input_stream_params.prefs &
            CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING) ||
           !trigger_async_reconfigure(stm)) {
-        stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
+        wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
         return false;
       }
       return true;
     }
 
     if (FAILED(hr)) {
       LOG("cannot get next packet size: %lx", hr);
       return false;
@@ -1103,33 +1122,33 @@ get_output_buffer(cubeb_stream * stm, vo
     // https://msdn.microsoft.com/en-us/library/windows/desktop/dd316605(v=vs.85).aspx
     LOG("Output device invalidated error");
     // No need to reset device if user asks to use particular device, or
     // switching is disabled.
     if (stm->output_device_id ||
         (stm->output_stream_params.prefs &
          CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING) ||
         !trigger_async_reconfigure(stm)) {
-      stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
+      wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
       return false;
     }
     return true;
   }
 
   if (FAILED(hr)) {
     LOG("Failed to get padding: %lx", hr);
     return false;
   }
 
   XASSERT(padding_out <= stm->output_buffer_frame_count);
 
   if (stm->draining) {
     if (padding_out == 0) {
       LOG("Draining finished.");
-      stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
+      wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
       return false;
     }
     LOG("Draining.");
     return true;
   }
 
   frame_count = stm->output_buffer_frame_count - padding_out;
   BYTE * output_buffer;
@@ -1295,27 +1314,35 @@ refill_callback_output(cubeb_stream * st
   if (FAILED(hr)) {
     LOG("failed to release buffer: %lx", hr);
     return false;
   }
 
   return size_t(got) == output_frames || stm->draining;
 }
 
+void
+wasapi_stream_destroy(cubeb_stream * stm);
+
+static void
+handle_emergency_bailout(cubeb_stream * stm)
+{
+  if (stm->emergency_bailout) {
+    CloseHandle(stm->thread);
+    stm->thread = NULL;
+    CloseHandle(stm->shutdown_event);
+    stm->shutdown_event = 0;
+    wasapi_stream_destroy(stm);
+    _endthreadex(0);
+  }
+}
+
 static unsigned int __stdcall wasapi_stream_render_loop(LPVOID stream)
 {
   cubeb_stream * stm = static_cast<cubeb_stream *>(stream);
-  std::atomic<bool> * emergency_bailout = stm->emergency_bailout;
-
-  // Signal wasapi_stream_start that we've copied emergency_bailout.
-  BOOL ok = SetEvent(stm->thread_ready_event);
-  if (!ok) {
-    LOG("thread_ready SetEvent failed: %lx", GetLastError());
-    return 0;
-  }
 
   bool is_playing = true;
   HANDLE wait_array[4] = {stm->shutdown_event, stm->reconfigure_event,
                           stm->refill_event, stm->input_available_event};
   HANDLE mmcss_handle = NULL;
   HRESULT hr = 0;
   DWORD mmcss_task_index = 0;
   struct auto_com {
@@ -1338,40 +1365,30 @@ static unsigned int __stdcall wasapi_str
 
   /* WaitForMultipleObjects timeout can trigger in cases where we don't want to
      treat it as a timeout, such as across a system sleep/wake cycle.  Trigger
      the timeout error handling only when the timeout_limit is reached, which is
      reset on each successful loop. */
   unsigned timeout_count = 0;
   const unsigned timeout_limit = 3;
   while (is_playing) {
-    // We want to check the emergency bailout variable before a
-    // and after the WaitForMultipleObject, because the handles
-    // WaitForMultipleObjects is going to wait on might have been closed
-    // already.
-    if (*emergency_bailout) {
-      delete emergency_bailout;
-      return 0;
-    }
+    handle_emergency_bailout(stm);
     DWORD waitResult = WaitForMultipleObjects(ARRAY_LENGTH(wait_array),
                                               wait_array, FALSE, 1000);
-    if (*emergency_bailout) {
-      delete emergency_bailout;
-      return 0;
-    }
+    handle_emergency_bailout(stm);
     if (waitResult != WAIT_TIMEOUT) {
       timeout_count = 0;
     }
     switch (waitResult) {
     case WAIT_OBJECT_0: { /* shutdown */
       is_playing = false;
       /* We don't check if the drain is actually finished here, we just want to
          shutdown. */
       if (stm->draining) {
-        stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
+        wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
       }
       continue;
     }
     case WAIT_OBJECT_0 + 1: { /* reconfigure */
       XASSERT(stm->output_client || stm->input_client);
       LOG("Reconfiguring the stream");
       /* Close the stream */
       if (stm->output_client) {
@@ -1423,17 +1440,18 @@ static unsigned int __stdcall wasapi_str
     case WAIT_OBJECT_0 + 2: /* refill */
       XASSERT((has_input(stm) && has_output(stm)) ||
               (!has_input(stm) && has_output(stm)));
       is_playing = stm->refill_callback(stm);
       break;
     case WAIT_OBJECT_0 + 3: { /* input available */
       HRESULT rv = get_input_buffer(stm);
       if (FAILED(rv)) {
-        return rv;
+        is_playing = false;
+        continue;
       }
 
       if (!has_output(stm)) {
         is_playing = stm->refill_callback(stm);
       }
 
       break;
     }
@@ -1442,28 +1460,39 @@ static unsigned int __stdcall wasapi_str
       if (++timeout_count >= timeout_limit) {
         LOG("Render loop reached the timeout limit.");
         is_playing = false;
         hr = E_FAIL;
       }
       break;
     default:
       LOG("case %lu not handled in render loop.", waitResult);
-      abort();
+      XASSERT(false);
     }
   }
 
-  if (FAILED(hr)) {
-    stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
+  // Stop audio clients since this thread will no longer service
+  // the events.
+  if (stm->output_client) {
+    stm->output_client->Stop();
+  }
+  if (stm->input_client) {
+    stm->input_client->Stop();
   }
 
   if (mmcss_handle) {
     AvRevertMmThreadCharacteristics(mmcss_handle);
   }
 
+  handle_emergency_bailout(stm);
+
+  if (FAILED(hr)) {
+    wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
+  }
+
   return 0;
 }
 
 void
 wasapi_destroy(cubeb * context);
 
 HRESULT
 register_notification_client(cubeb_stream * stm)
@@ -1702,64 +1731,68 @@ wasapi_init(cubeb ** context, char const
 
   *context = ctx;
 
   return CUBEB_OK;
 }
 }
 
 namespace {
+enum ShutdownPhase { OnStop, OnDestroy };
+
 bool
-stop_and_join_render_thread(cubeb_stream * stm)
+stop_and_join_render_thread(cubeb_stream * stm, ShutdownPhase phase)
 {
-  bool rv = true;
-  LOG("Stop and join render thread.");
+  // Only safe to transfer `stm` ownership to the render thread when
+  // the stream is being destroyed by the caller.
+  bool bailout = phase == OnDestroy;
+
+  LOG("%p: Stop and join render thread: %p (%d), phase=%d", stm, stm->thread,
+      stm->emergency_bailout.load(), static_cast<int>(phase));
   if (!stm->thread) {
-    LOG("No thread present.");
     return true;
   }
 
-  // If we've already leaked the thread, just return,
-  // there is not much we can do.
-  if (!stm->emergency_bailout.load()) {
-    return false;
-  }
+  XASSERT(!stm->emergency_bailout);
 
   BOOL ok = SetEvent(stm->shutdown_event);
   if (!ok) {
-    LOG("Destroy SetEvent failed: %lx", GetLastError());
+    LOG("stop_and_join_render_thread: SetEvent failed: %lx", GetLastError());
+    stm->emergency_bailout = bailout;
+    return false;
   }
 
   /* Wait five seconds for the rendering thread to return. It's supposed to
-   * check its event loop very often, five seconds is rather conservative. */
-  DWORD r = WaitForSingleObject(stm->thread, 5000);
-  if (r != WAIT_OBJECT_0) {
-    /* Something weird happened, leak the thread and continue the shutdown
-     * process. */
-    *(stm->emergency_bailout) = true;
-    // We give the ownership to the rendering thread.
-    stm->emergency_bailout = nullptr;
-    LOG("Destroy WaitForSingleObject on thread failed: %lx, %lx", r,
-        GetLastError());
-    rv = false;
+   * check its event loop very often, five seconds is rather conservative.
+   * Note: 5*1s loop to work around timer sleep issues on pre-Windows 8. */
+  DWORD r;
+  for (int i = 0; i < 5; ++i) {
+    r = WaitForSingleObject(stm->thread, 1000);
+    if (r == WAIT_OBJECT_0) {
+      break;
+    }
   }
-
-  // Only attempts to close and null out the thread and event if the
-  // WaitForSingleObject above succeeded, so that calling this function again
-  // attemps to clean up the thread and event each time.
-  if (rv) {
-    LOG("Closing thread.");
-    CloseHandle(stm->thread);
-    stm->thread = NULL;
-
-    CloseHandle(stm->shutdown_event);
-    stm->shutdown_event = 0;
+  if (r != WAIT_OBJECT_0) {
+    LOG("stop_and_join_render_thread: WaitForSingleObject on thread failed: "
+        "%lx, %lx",
+        r, GetLastError());
+    stm->emergency_bailout = bailout;
+    return false;
   }
 
-  return rv;
+  // Only attempt to close and null out the thread and event if the
+  // WaitForSingleObject above succeeded.
+  LOG("stop_and_join_render_thread: Closing thread.");
+  CloseHandle(stm->thread);
+  stm->thread = NULL;
+
+  CloseHandle(stm->shutdown_event);
+  stm->shutdown_event = 0;
+
+  return true;
 }
 
 void
 wasapi_destroy(cubeb * context)
 {
   auto_lock lock(context->lock);
   XASSERT(!context->device_collection_enumerator &&
           !context->collection_notification_client);
@@ -1887,19 +1920,16 @@ wasapi_get_preferred_sample_rate(cubeb *
 
   *rate = mix_format->nSamplesPerSec;
 
   LOG("Preferred sample rate for output: %u", *rate);
 
   return CUBEB_OK;
 }
 
-void
-wasapi_stream_destroy(cubeb_stream * stm);
-
 static void
 waveformatex_update_derived_properties(WAVEFORMATEX * format)
 {
   format->nBlockAlign = format->wBitsPerSample * format->nChannels / 8;
   format->nAvgBytesPerSec = format->nSamplesPerSec * format->nBlockAlign;
   if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
     WAVEFORMATEXTENSIBLE * format_pcm =
         reinterpret_cast<WAVEFORMATEXTENSIBLE *>(format);
@@ -2265,18 +2295,22 @@ setup_wasapi_stream_one_side(cubeb_strea
   if (direction == eCapture) {
     stm->input_bluetooth_handsfree = false;
 
     wasapi_default_devices default_devices(stm->device_enumerator.get());
     cubeb_device_info device_info;
     if (wasapi_create_device(stm->context, device_info,
                              stm->device_enumerator.get(), device.get(),
                              &default_devices) == CUBEB_OK) {
+      if (device_info.latency_hi == 0) {
+        LOG("Input: could not query latency_hi to guess safe latency");
+        wasapi_destroy_device(&device_info);
+        return CUBEB_ERROR;
+      }
       // This multiplicator has been found empirically.
-      XASSERT(device_info.latency_hi > 0);
       uint32_t latency_frames = device_info.latency_hi * 8;
       LOG("Input: latency increased to %u frames from a default of %u",
           latency_frames, device_info.latency_hi);
       latency_hns = frames_to_hns(device_info.default_rate, latency_frames);
 
       const char * HANDSFREE_TAG = "BTHHFENUM";
       size_t len = sizeof(HANDSFREE_TAG);
       if (strlen(device_info.group_id) >= len &&
@@ -2368,20 +2402,20 @@ wasapi_find_bt_handsfree_output_device(c
   }
   com_heap_ptr<wchar_t> device_id(tmp);
   cubeb_devid input_device_id = reinterpret_cast<cubeb_devid>(
       intern_device_id(stm->context, device_id.get()));
   if (!input_device_id) {
     return nullptr;
   }
 
-  int rv = wasapi_enumerate_devices(
+  int rv = wasapi_enumerate_devices_internal(
       stm->context,
       (cubeb_device_type)(CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT),
-      &collection);
+      &collection, DEVICE_STATE_ACTIVE);
   if (rv != CUBEB_OK) {
     return nullptr;
   }
 
   // Find the input device, and then find the output device with the same group
   // id and the same rate.
   for (uint32_t i = 0; i < collection.count; i++) {
     if (collection.device[i].devid == input_device_id) {
@@ -2567,17 +2601,17 @@ setup_wasapi_stream(cubeb_stream * stm)
   cubeb_stream_params input_params = stm->input_mix_params;
   input_params.channels = stm->input_stream_params.channels;
   cubeb_stream_params output_params = stm->output_mix_params;
   output_params.channels = stm->output_stream_params.channels;
 
   stm->resampler.reset(cubeb_resampler_create(
       stm, has_input(stm) ? &input_params : nullptr,
       has_output(stm) && !stm->has_dummy_output ? &output_params : nullptr,
-      target_sample_rate, stm->data_callback, stm->user_ptr,
+      target_sample_rate, wasapi_data_callback, stm->user_ptr,
       stm->voice ? CUBEB_RESAMPLER_QUALITY_VOIP
                  : CUBEB_RESAMPLER_QUALITY_DESKTOP,
       CUBEB_RESAMPLER_RECLOCK_NONE));
   if (!stm->resampler) {
     LOG("Could not get a resampler");
     return CUBEB_ERROR;
   }
 
@@ -2800,43 +2834,35 @@ close_wasapi_stream(cubeb_stream * stm)
 }
 
 void
 wasapi_stream_destroy(cubeb_stream * stm)
 {
   XASSERT(stm);
   LOG("Stream destroy (%p)", stm);
 
-  // Only free stm->emergency_bailout if we could join the thread.
-  // If we could not join the thread, stm->emergency_bailout is true
-  // and is still alive until the thread wakes up and exits cleanly.
-  if (stop_and_join_render_thread(stm)) {
-    delete stm->emergency_bailout.load();
-    stm->emergency_bailout = nullptr;
+  if (!stop_and_join_render_thread(stm, OnDestroy)) {
+    // Emergency bailout: render thread becomes responsible for calling
+    // wasapi_stream_destroy.
+    return;
   }
 
   if (stm->notification_client) {
     unregister_notification_client(stm);
   }
 
-  CloseHandle(stm->reconfigure_event);
-  CloseHandle(stm->refill_event);
-  CloseHandle(stm->input_available_event);
-
-  // The variables intialized in wasapi_stream_init,
-  // must be destroyed in wasapi_stream_destroy.
-  stm->linear_input_buffer.reset();
-
-  stm->device_enumerator = nullptr;
-
   {
     auto_lock lock(stm->stream_reset_lock);
     close_wasapi_stream(stm);
   }
 
+  CloseHandle(stm->reconfigure_event);
+  CloseHandle(stm->refill_event);
+  CloseHandle(stm->input_available_event);
+
   delete stm;
 }
 
 enum StreamDirection { OUTPUT, INPUT };
 
 int
 stream_start_one_side(cubeb_stream * stm, StreamDirection dir)
 {
@@ -2881,18 +2907,16 @@ stream_start_one_side(cubeb_stream * stm
 int
 wasapi_stream_start(cubeb_stream * stm)
 {
   auto_lock lock(stm->stream_reset_lock);
 
   XASSERT(stm && !stm->thread && !stm->shutdown_event);
   XASSERT(stm->output_client || stm->input_client);
 
-  stm->emergency_bailout = new std::atomic<bool>(false);
-
   if (stm->output_client) {
     int rv = stream_start_one_side(stm, OUTPUT);
     if (rv != CUBEB_OK) {
       return rv;
     }
   }
 
   if (stm->input_client) {
@@ -2903,40 +2927,28 @@ wasapi_stream_start(cubeb_stream * stm)
   }
 
   stm->shutdown_event = CreateEvent(NULL, 0, 0, NULL);
   if (!stm->shutdown_event) {
     LOG("Can't create the shutdown event, error: %lx", GetLastError());
     return CUBEB_ERROR;
   }
 
-  stm->thread_ready_event = CreateEvent(NULL, 0, 0, NULL);
-  if (!stm->thread_ready_event) {
-    LOG("Can't create the thread_ready event, error: %lx", GetLastError());
-    return CUBEB_ERROR;
-  }
-
   cubeb_async_log_reset_threads();
   stm->thread =
       (HANDLE)_beginthreadex(NULL, 512 * 1024, wasapi_stream_render_loop, stm,
                              STACK_SIZE_PARAM_IS_A_RESERVATION, NULL);
   if (stm->thread == NULL) {
     LOG("could not create WASAPI render thread.");
+    CloseHandle(stm->shutdown_event);
+    stm->shutdown_event = 0;
     return CUBEB_ERROR;
   }
 
-  // Wait for wasapi_stream_render_loop to signal that emergency_bailout has
-  // been read, avoiding a bailout situation where we could free `stm`
-  // before wasapi_stream_render_loop had a chance to run.
-  HRESULT hr = WaitForSingleObject(stm->thread_ready_event, INFINITE);
-  XASSERT(hr == WAIT_OBJECT_0);
-  CloseHandle(stm->thread_ready_event);
-  stm->thread_ready_event = 0;
-
-  stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED);
+  wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED);
 
   return CUBEB_OK;
 }
 
 int
 wasapi_stream_stop(cubeb_stream * stm)
 {
   XASSERT(stm);
@@ -2956,25 +2968,22 @@ wasapi_stream_stop(cubeb_stream * stm)
     if (stm->input_client) {
       hr = stm->input_client->Stop();
       if (FAILED(hr)) {
         LOG("could not stop AudioClient (input)");
         return CUBEB_ERROR;
       }
     }
 
-    stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED);
+    wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED);
   }
 
-  if (stop_and_join_render_thread(stm)) {
-    delete stm->emergency_bailout.load();
-    stm->emergency_bailout = nullptr;
-  } else {
+  if (!stop_and_join_render_thread(stm, OnStop)) {
     // If we could not join the thread, put the stream in error.
-    stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
+    wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
     return CUBEB_ERROR;
   }
 
   return CUBEB_OK;
 }
 
 int
 wasapi_stream_get_position(cubeb_stream * stm, uint64_t * position)
@@ -3330,18 +3339,19 @@ wasapi_create_device(cubeb * ctx, cubeb_
 void
 wasapi_destroy_device(cubeb_device_info * device)
 {
   delete[] device->friendly_name;
   delete[] device->group_id;
 }
 
 static int
-wasapi_enumerate_devices(cubeb * context, cubeb_device_type type,
-                         cubeb_device_collection * out)
+wasapi_enumerate_devices_internal(cubeb * context, cubeb_device_type type,
+                                  cubeb_device_collection * out,
+                                  DWORD state_mask)
 {
   com_ptr<IMMDeviceEnumerator> enumerator;
   com_ptr<IMMDeviceCollection> collection;
   HRESULT hr;
   UINT cc, i;
   EDataFlow flow;
 
   hr =
@@ -3359,18 +3369,17 @@ wasapi_enumerate_devices(cubeb * context
   } else if (type == CUBEB_DEVICE_TYPE_INPUT) {
     flow = eCapture;
   } else if (type & (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)) {
     flow = eAll;
   } else {
     return CUBEB_ERROR;
   }
 
-  hr = enumerator->EnumAudioEndpoints(flow, DEVICE_STATEMASK_ALL,
-                                      collection.receive());
+  hr = enumerator->EnumAudioEndpoints(flow, state_mask, collection.receive());
   if (FAILED(hr)) {
     LOG("Could not enumerate audio endpoints: %lx", hr);
     return CUBEB_ERROR;
   }
 
   hr = collection->GetCount(&cc);
   if (FAILED(hr)) {
     LOG("IMMDeviceCollection::GetCount() failed: %lx", hr);
@@ -3395,16 +3404,25 @@ wasapi_enumerate_devices(cubeb * context
     }
   }
 
   out->device = devices;
   return CUBEB_OK;
 }
 
 static int
+wasapi_enumerate_devices(cubeb * context, cubeb_device_type type,
+                         cubeb_device_collection * out)
+{
+  return wasapi_enumerate_devices_internal(
+      context, type, out,
+      DEVICE_STATE_ACTIVE | DEVICE_STATE_DISABLED | DEVICE_STATE_UNPLUGGED);
+}
+
+static int
 wasapi_device_collection_destroy(cubeb * /*ctx*/,
                                  cubeb_device_collection * collection)
 {
   XASSERT(collection);
 
   for (size_t n = 0; n < collection->count; n++) {
     cubeb_device_info & dev = collection->device[n];
     wasapi_destroy_device(&dev);
new file mode 100644
--- /dev/null
+++ b/media/libpng/aarch64.patch
@@ -0,0 +1,66 @@
+diff --git a/arm/filter_neon_intrinsics.c b/arm/filter_neon_intrinsics.c
+--- a/arm/filter_neon_intrinsics.c
++++ b/arm/filter_neon_intrinsics.c
+@@ -13,17 +13,17 @@
+ 
+ #include "../pngpriv.h"
+ 
+ #ifdef PNG_READ_SUPPORTED
+ 
+ /* This code requires -mfpu=neon on the command line: */
+ #if PNG_ARM_NEON_IMPLEMENTATION == 1 /* intrinsics code from pngpriv.h */
+ 
+-#if defined(_MSC_VER) && defined(_M_ARM64)
++#if defined(_MSC_VER) && defined(_M_ARM64) && !defined(__clang__)
+ #  include <arm64_neon.h>
+ #else
+ #  include <arm_neon.h>
+ #endif
+ 
+ /* libpng row pointers are not necessarily aligned to any particular boundary,
+  * however this code will only work with appropriate alignment.  arm/arm_init.c
+  * checks for this (and will not compile unless it is done). This code uses
+diff --git a/media/libpng/arm/palette_neon_intrinsics.c b/media/libpng/arm/palette_neon_intrinsics.c
+--- a/arm/palette_neon_intrinsics.c
++++ b/arm/palette_neon_intrinsics.c
+@@ -9,17 +9,17 @@
+  * For conditions of distribution and use, see the disclaimer
+  * and license in png.h
+  */
+ 
+ #include "../pngpriv.h"
+ 
+ #if PNG_ARM_NEON_IMPLEMENTATION == 1
+ 
+-#if defined(_MSC_VER) && defined(_M_ARM64)
++#if defined(_MSC_VER) && defined(_M_ARM64) && !defined(__clang__)
+ #  include <arm64_neon.h>
+ #else
+ #  include <arm_neon.h>
+ #endif
+ 
+ /* Build an RGBA8 palette from the separate RGB and alpha palettes. */
+ void
+ png_riffle_palette_neon(png_structrp png_ptr)
+diff --git a/media/libpng/pngrtran.c b/media/libpng/pngrtran.c
+--- a/pngrtran.c
++++ b/pngrtran.c
+@@ -16,17 +16,17 @@
+  * in pngtrans.c.
+  */
+ 
+ #include "pngpriv.h"
+ 
+ #ifdef PNG_ARM_NEON_IMPLEMENTATION
+ #  if PNG_ARM_NEON_IMPLEMENTATION == 1
+ #    define PNG_ARM_NEON_INTRINSICS_AVAILABLE
+-#    if defined(_MSC_VER) && defined(_M_ARM64)
++#    if defined(_MSC_VER) && defined(_M_ARM64) && !defined(__clang__)
+ #      include <arm64_neon.h>
+ #    else
+ #      include <arm_neon.h>
+ #    endif
+ #  endif
+ #endif
+ 
+ #ifdef PNG_READ_SUPPORTED
--- a/media/libpng/moz.yaml
+++ b/media/libpng/moz.yaml
@@ -6,17 +6,17 @@ bugzilla:
 
 origin:
   name: "libpng"
   description: "PNG reference library"
 
   url: "http://www.libpng.org/pub/png/libpng.html"
   license: libpng
 
-  release: commit a40189cf881e9f0db80511c382292a5604c3c3d1 (2019-04-14T10:10:32:00Z).
+  release: commit v1.6.37 (2019-04-14T14:10:32-04:00).
 
   revision: "v1.6.37"
 
   license-file: LICENSE
 
 updatebot:
   maintainer-phab: aosmond
   maintainer-bz: aosmond@mozilla.com
@@ -41,22 +41,42 @@ vendoring:
     - powerpc
     - ANNOUNCE
     - AUTHORS
     - CHANGES
     - libpng-manual.txt
     - LICENSE
     - README
     - TRADEMARK
-    - "*.c"
-    - "*.h"
+    - png.c
+    - pngconf.h
+    - pngdebug.h
+    - pngerror.c
+    - pngget.c
+    - png.h
+    - pnginfo.h
+    - pngmem.c
+    - pngpread.c
+    - pngpriv.h
+    - pngread.c
+    - pngrio.c
+    - pngrtran.c
+    - pngrutil.c
+    - pngset.c
+    - pngstruct.h
+    - pngtrans.c
+    - pngwio.c
+    - pngwrite.c
+    - pngwtran.c
+    - pngwutil.c
 
   keep:
     - MOZCHANGES
     - crashtests
+    - pnglibconf.h
 
   update-actions:
     - action: copy-file
       from: 'contrib/arm-neon/linux.c'
       to: 'arm/linux.c'
     - action: delete-path
       path: 'contrib'
 
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
@@ -1146,9 +1146,9 @@ to allow adding gecko profiler markers.
 [65.19]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.LoadRequest.html#isRedirect
 [65.20]: {{javadoc_uri}}/GeckoSession.html#LOAD_FLAGS_BYPASS_CLASSIFIER
 [65.21]: {{javadoc_uri}}/GeckoSession.ContentDelegate.ContextElement.html
 [65.22]: {{javadoc_uri}}/GeckoSession.ContentDelegate.html#onContextMenu(org.mozilla.geckoview.GeckoSession,int,int,org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement)
 [65.23]: {{javadoc_uri}}/GeckoSession.FinderResult.html
 [65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport(android.content.Context,android.os.Bundle,java.lang.String)
 [65.25]: {{javadoc_uri}}/GeckoResult.html
 
-[api-version]: c9f91f7e909c6c71adca2ea333f92af6ff3af054
+[api-version]: 24b60ce96bd68c81a55110bd1a3c57442dd8c65f
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -4218,17 +4218,17 @@
 # Enable the "noreferrer" feature argument for window.open()
 - name: dom.window.open.noreferrer.enabled
   type: bool
   value: true
   mirror: always
 
 - name: dom.window.content.untrusted.enabled
   type: bool
-  value: @IS_NOT_EARLY_BETA_OR_EARLIER@
+  value: false
   mirror: always
 
 - name: dom.window.clientinformation.enabled
   type: bool
   value: true
   mirror: always
 
 - name: dom.window.sidebar.enabled
--- a/netwerk/base/LoadInfo.cpp
+++ b/netwerk/base/LoadInfo.cpp
@@ -42,18 +42,17 @@
 #include "nsMixedContentBlocker.h"
 #include "nsQueryObject.h"
 #include "nsRedirectHistoryEntry.h"
 #include "nsSandboxFlags.h"
 #include "nsICookieService.h"
 
 using namespace mozilla::dom;
 
-namespace mozilla {
-namespace net {
+namespace mozilla::net {
 
 static nsContentPolicyType InternalContentPolicyTypeForFrame(
     CanonicalBrowsingContext* aBrowsingContext) {
   const auto& maybeEmbedderElementType =
       aBrowsingContext->GetEmbedderElementType();
   MOZ_ASSERT(maybeEmbedderElementType.isSome());
   auto embedderElementType = maybeEmbedderElementType.value();
 
@@ -1373,16 +1372,129 @@ LoadInfo::SetInitialSecurityCheckDone(bo
 }
 
 NS_IMETHODIMP
 LoadInfo::GetInitialSecurityCheckDone(bool* aResult) {
   *aResult = mInitialSecurityCheckDone;
   return NS_OK;