Merge mozilla-inbound to mozilla-central. a=merge
authorDaniel Varga <dvarga@mozilla.com>
Wed, 03 Apr 2019 09:05:41 +0300
changeset 467673 6188f34970576cf032692bc8679eca89607dfe34
parent 467672 eb21297f31590fc47581036327df9779a534875c (current diff)
parent 467671 45808ab18609bfd3b69c6ae2d21e2b50f5177a02 (diff)
child 467674 7dd52a4bdab50a655e2f8dfe94b7a39ec63500c6
child 467715 92b682ee0acc9f903480910e1aced6b8baa73ed9
push id112638
push userdvarga@mozilla.com
push dateWed, 03 Apr 2019 06:18:49 +0000
treeherdermozilla-inbound@7dd52a4bdab5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone68.0a1
first release with
nightly linux32
6188f3497057 / 68.0a1 / 20190403060632 / files
nightly linux64
6188f3497057 / 68.0a1 / 20190403060632 / files
nightly mac
6188f3497057 / 68.0a1 / 20190403060632 / files
nightly win32
6188f3497057 / 68.0a1 / 20190403060632 / files
nightly win64
6188f3497057 / 68.0a1 / 20190403060632 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-inbound to mozilla-central. a=merge
devtools/client/debugger/new/src/actions/breakpoints/breakpointPositions.js
devtools/client/debugger/new/src/actions/breakpoints/modify.js
devtools/client/debugger/new/src/actions/breakpoints/syncBreakpoint.js
devtools/client/debugger/new/src/actions/breakpoints/tests/breakpointPositions.spec.js
devtools/client/debugger/new/src/actions/breakpoints/tests/breakpoints.spec.js
devtools/client/debugger/new/src/actions/pause/mapFrames.js
devtools/client/debugger/new/src/actions/pause/mapScopes.js
devtools/client/debugger/new/src/actions/pause/tests/pause.spec.js
devtools/client/debugger/new/src/actions/project-text-search.js
devtools/client/debugger/new/src/actions/sources/loadSourceText.js
devtools/client/debugger/new/src/actions/sources/newSources.js
devtools/client/debugger/new/src/actions/sources/prettyPrint.js
devtools/client/debugger/new/src/actions/sources/select.js
devtools/client/debugger/new/src/actions/sources/symbols.js
devtools/client/debugger/new/src/actions/sources/tests/loadSource.spec.js
devtools/client/debugger/new/src/actions/tests/ast.spec.js
devtools/client/debugger/new/src/actions/tests/expressions.spec.js
devtools/client/debugger/new/src/actions/tests/pending-breakpoints.spec.js
devtools/client/debugger/new/src/actions/tests/project-text-search.spec.js
devtools/client/debugger/new/src/components/Editor/index.js
devtools/client/debugger/new/src/reducers/sources.js
devtools/client/debugger/new/src/utils/moz.build
devtools/client/debugger/new/test/mochitest/helpers.js
dom/base/domerr.msg
dom/localstorage/ActorsParent.cpp
testing/web-platform/meta/webrtc/RTCPeerConnection-setLocalDescription-answer.html.ini
testing/web-platform/meta/xhr/historical.html.ini
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1043,17 +1043,17 @@ pref("security.sandbox.rdd.win32k-disabl
 pref("security.sandbox.gmp.win32k-disable", false);
 #endif
 
 #if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
 // Start the Mac sandbox early during child process startup instead
 // of when messaged by the parent after the message loop is running.
 pref("security.sandbox.content.mac.earlyinit", true);
 // Remove this pref once RDD early init is stable on Release.
-pref("security.sandbox.rdd.mac.earlyinit", true);
+pref("security.sandbox.rdd.mac.earlyinit", false);
 
 // This pref is discussed in bug 1083344, the naming is inspired from its
 // Windows counterpart, but on Mac it's an integer which means:
 // 0 -> "no sandbox" (nightly only)
 // 1 -> "preliminary content sandboxing enabled: write access to
 //       home directory is prevented"
 // 2 -> "preliminary content sandboxing enabled with profile protection:
 //       write access to home directory is prevented, read and write access
--- a/browser/components/search/content/searchbar.js
+++ b/browser/components/search/content/searchbar.js
@@ -111,19 +111,19 @@ class MozSearchbar extends MozXULElement
         // Set .textbox first, since the popup setter will cause
         // a _rebuild call that uses it.
         oneOffButtons.textbox = this.textbox;
         oneOffButtons.popup = this.textbox.popup;
       }
     }, { capture: true, once: true });
   }
 
-  get engines() {
+  async getEngines() {
     if (!this._engines)
-      this._engines = Services.search.getVisibleEngines();
+      this._engines = await Services.search.getVisibleEngines();
     return this._engines;
   }
 
   set currentEngine(val) {
     Services.search.defaultEngine = val;
     return val;
   }
 
@@ -202,27 +202,41 @@ class MozSearchbar extends MozXULElement
       // showHistoryPopup does a startSearch("") call, ensure the
       // controller handles the text from the input box instead:
       this._textbox.mController.handleText();
     } else if (aShowOnlySettingsIfEmpty) {
       this.setAttribute("showonlysettings", "true");
     }
   }
 
-  selectEngine(aEvent, isNextEngine) {
-    // Find the new index
-    let newIndex = this.engines.indexOf(this.currentEngine);
-    newIndex += isNextEngine ? 1 : -1;
+  async selectEngine(aEvent, isNextEngine) {
+    // Stop event bubbling now, because the rest of this method is async.
+    aEvent.preventDefault();
+    aEvent.stopPropagation();
 
-    if (newIndex >= 0 && newIndex < this.engines.length) {
-      this.currentEngine = this.engines[newIndex];
+    // Find the new index.
+    let engines = await this.getEngines();
+    let currentName = this.currentEngine.name;
+    let newIndex = -1;
+    let lastIndex = engines.length - 1;
+    for (let i = lastIndex; i >= 0; --i) {
+      if (engines[i].name == currentName) {
+        // Check bounds to cycle through the list of engines continuously.
+        if (!isNextEngine && i == 0) {
+          newIndex = lastIndex;
+        } else if (isNextEngine && i == lastIndex) {
+          newIndex = 0;
+        } else {
+          newIndex = i + (isNextEngine ? 1 : -1);
+        }
+        break;
+      }
     }
 
-    aEvent.preventDefault();
-    aEvent.stopPropagation();
+    this.currentEngine = engines[newIndex];
 
     this.openSuggestionsPanel();
   }
 
   handleSearchCommand(aEvent, aEngine, aForceNewTab) {
     let where = "current";
     let params;
 
--- a/browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js
+++ b/browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js
@@ -1,10 +1,11 @@
 // Tests that keyboard navigation in the search panel works as designed.
 
+const {SearchTestUtils} = ChromeUtils.import("resource://testing-common/SearchTestUtils.jsm");
 const searchPopup = document.getElementById("PopupSearchAutoComplete");
 const oneOffsContainer = searchPopup.searchOneOffsContainer;
 
 const kValues = ["foo1", "foo2", "foo3"];
 const kUserValue = "foo";
 
 function getOpenSearchItems() {
   let os = [];
@@ -301,16 +302,60 @@ add_task(async function test_alt_up() {
   // Cleanup for the next test.
   EventUtils.synthesizeKey("KEY_ArrowDown");
   ok(textbox.selectedButton.classList.contains("search-setting-button"),
      "the settings item should be selected");
   EventUtils.synthesizeKey("KEY_ArrowDown");
   ok(!textbox.selectedButton, "no one-off should be selected anymore");
 });
 
+add_task(async function test_accel_down() {
+  // Pressing accel+down should select the next visible search engine, without
+  // selecting suggestions.
+  let engines = await Services.search.getVisibleEngines();
+  let current = Services.search.defaultEngine;
+  let currIdx = -1;
+  for (let i = 0, l = engines.length; i < l; ++i) {
+    if (engines[i].name == current.name) {
+      currIdx = i;
+      break;
+    }
+  }
+  for (let i = 0, l = engines.length; i < l; ++i) {
+    EventUtils.synthesizeKey("KEY_ArrowDown", {accelKey: true});
+    await SearchTestUtils.promiseSearchNotification("engine-default", "browser-search-engine-modified");
+    let expected = engines[++currIdx % engines.length];
+    is(Services.search.defaultEngine.name, expected.name, "Default engine should have changed");
+    is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+  }
+  Services.search.defaultEngine = current;
+});
+
+add_task(async function test_accel_up() {
+  // Pressing accel+down should select the previous visible search engine, without
+  // selecting suggestions.
+  let engines = await Services.search.getVisibleEngines();
+  let current = Services.search.defaultEngine;
+  let currIdx = -1;
+  for (let i = 0, l = engines.length; i < l; ++i) {
+    if (engines[i].name == current.name) {
+      currIdx = i;
+      break;
+    }
+  }
+  for (let i = 0, l = engines.length; i < l; ++i) {
+    EventUtils.synthesizeKey("KEY_ArrowUp", {accelKey: true});
+    await SearchTestUtils.promiseSearchNotification("engine-default", "browser-search-engine-modified");
+    let expected = engines[--currIdx < 0 ? currIdx = engines.length - 1 : currIdx];
+    is(Services.search.defaultEngine.name, expected.name, "Default engine should have changed");
+    is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+  }
+  Services.search.defaultEngine = current;
+});
+
 add_task(async function test_tab_and_arrows() {
   // Check the initial state is as expected.
   ok(!textbox.selectedButton, "no one-off button should be selected");
   is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
   is(textbox.value, kUserValue, "the textfield value should be unmodified");
 
   // After pressing down, the first sugggestion should be selected.
   EventUtils.synthesizeKey("KEY_ArrowDown");
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -3813,18 +3813,24 @@ var SessionStoreInternal = {
    */
   prepareConnectionToHost(tab, url) {
     if (!url.startsWith("about:")) {
       let principal = Services.scriptSecurityManager.createNullPrincipal({
         userContextId: tab.userContextId,
       });
       let sc = Services.io.QueryInterface(Ci.nsISpeculativeConnect);
       let uri = Services.io.newURI(url);
-      sc.speculativeConnect(uri, principal, null);
-      return true;
+      try {
+        sc.speculativeConnect(uri, principal, null);
+        return true;
+      } catch (error) {
+         // Can't setup speculative connection for this url.
+         Cu.reportError(error);
+        return false;
+      }
     }
     return false;
   },
 
   /**
    * Make a connection to a host when users hover mouse on a tab.
    * This will also set a flag in the tab to prevent us from speculatively
    * connecting a second time.
--- a/browser/components/urlbar/.eslintrc.js
+++ b/browser/components/urlbar/.eslintrc.js
@@ -22,10 +22,11 @@ module.exports = {
         String: "string",
         Object: "object",
         bool: "boolean",
       },
       requireParamDescription: false,
       requireReturn: false,
       requireReturnDescription: false,
     }],
+    "no-unused-expressions": "error",
   }
 };
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -154,17 +154,18 @@ class UrlbarInput {
     this.addEventListener("mousedown", this);
     this.view.panel.addEventListener("popupshowing", this);
     this.view.panel.addEventListener("popuphidden", this);
 
     this.inputField.controllers.insertControllerAt(0, new CopyCutController(this));
     this._initPasteAndGo();
 
     // Tracks IME composition.
-    this._compositionState == UrlbarUtils.COMPOSITION.NONE;
+    this._compositionState = UrlbarUtils.COMPOSITION.NONE;
+    this._compositionClosedPopup = false;
   }
 
   /**
    * Uninitializes this input object, detaching it from the inputField.
    */
   uninit() {
     for (let name of this._inputFieldEvents) {
       this.inputField.removeEventListener(name, this);
@@ -201,16 +202,21 @@ class UrlbarInput {
 
   /**
    * Applies styling to the text in the urlbar input, depending on the text.
    */
   formatValue() {
     this.valueFormatter.update();
   }
 
+  /**
+   * This exists for legacy compatibility, and can be removed once the old
+   * urlbar code goes away, by changing callers. Internal consumers should use
+   * view.close().
+   */
   closePopup() {
     this.view.close();
   }
 
   focus() {
     this.inputField.focus();
   }
 
@@ -1047,17 +1053,17 @@ class UrlbarInput {
       if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
         this.handleRevert();
       }
     }
 
     // Ensure the start of the URL is visible for usability reasons.
     this.selectionStart = this.selectionEnd = 0;
 
-    this.closePopup();
+    this.view.close();
   }
 
   /**
    * Determines where a URL/page should be opened.
    *
    * @param {Event} event the event triggering the opening.
    * @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
    */
@@ -1207,16 +1213,25 @@ class UrlbarInput {
   }
 
   _on_input() {
     let value = this.textValue;
     this.valueIsTyped = true;
     this._untrimmedValue = value;
     this.window.gBrowser.userTypedValue = value;
 
+    let compositionState = this._compositionState;
+    let compositionClosedPopup = this._compositionClosedPopup;
+
+    // Clear composition values if we're no more composing.
+    if (this._compositionState != UrlbarUtils.COMPOSITION.COMPOSING) {
+      this._compositionState = UrlbarUtils.COMPOSITION.NONE;
+      this._compositionClosedPopup = false;
+    }
+
     if (value) {
       this.setAttribute("usertyping", "true");
     } else {
       this.removeAttribute("usertyping");
     }
     this.removeAttribute("actiontype");
 
     if (!value && this.view.isOpen) {
@@ -1227,42 +1242,39 @@ class UrlbarInput {
     this.view.removeAccessibleFocus();
 
     // During composition with an IME, the following events happen in order:
     // 1. a compositionstart event
     // 2. some input events
     // 3. a compositionend event
     // 4. an input event
 
-    // We should do nothing during composition.
-    if (this._compositionState == UrlbarUtils.COMPOSITION.COMPOSING) {
+    // We should do nothing during composition or if composition was canceled
+    // and we didn't close the popup on composition start.
+    if (compositionState == UrlbarUtils.COMPOSITION.COMPOSING ||
+        (compositionState == UrlbarUtils.COMPOSITION.CANCELED &&
+         !compositionClosedPopup)) {
       return;
     }
 
-    let handlingCompositionCommit =
-      this._compositionState == UrlbarUtils.COMPOSITION.COMMIT;
-    if (handlingCompositionCommit) {
-      this._compositionState = UrlbarUtils.COMPOSITION.NONE;
-    }
-
     let sameSearchStrings = value == this._lastSearchString;
 
     // TODO (bug 1524550): Properly detect autofill removal, rather than
     // guessing based on string prefixes.
     let deletedAutofilledSubstring =
       sameSearchStrings &&
       value.length < this._autofillPlaceholder.length &&
       this._autofillPlaceholder.startsWith(value);
 
     // Don't search again when the new search would produce the same results.
-    // If we're handling a composition commit, we must continue the search
+    // If we're handling a composition input, we must continue the search
     // because we canceled the previous search on composition start.
     if (sameSearchStrings &&
         !deletedAutofilledSubstring &&
-        !handlingCompositionCommit &&
+        compositionState == UrlbarUtils.COMPOSITION.NONE &&
         value.length > 0) {
       return;
     }
 
     let allowAutofill =
       this._maybeAutofillOnInput(value, deletedAutofilledSubstring);
 
     // TODO Bug 1524550: Fill in lastKey, and add anything else we need.
@@ -1371,27 +1383,33 @@ class UrlbarInput {
 
   _on_compositionstart(event) {
     if (this._compositionState == UrlbarUtils.COMPOSITION.COMPOSING) {
       throw new Error("Trying to start a nested composition?");
     }
     this._compositionState = UrlbarUtils.COMPOSITION.COMPOSING;
 
     // Close the view. This will also stop searching.
-    this.closePopup();
+    if (this.view.isOpen) {
+      this._compositionClosedPopup = true;
+      this.view.close();
+    } else {
+      this._compositionClosedPopup = false;
+    }
   }
 
   _on_compositionend(event) {
     if (this._compositionState != UrlbarUtils.COMPOSITION.COMPOSING) {
       throw new Error("Trying to stop a non existing composition?");
     }
 
     // We can't yet retrieve the committed value from the editor, since it isn't
     // completely committed yet. We'll handle it at the next input event.
-    this._compositionState = UrlbarUtils.COMPOSITION.COMMIT;
+    this._compositionState = event.data ? UrlbarUtils.COMPOSITION.COMMIT :
+                                          UrlbarUtils.COMPOSITION.CANCELED;
   }
 
   _on_popupshowing() {
     this.setAttribute("open", "true");
   }
 
   _on_popuphidden() {
     this.removeAttribute("open");
--- a/browser/components/urlbar/UrlbarUtils.jsm
+++ b/browser/components/urlbar/UrlbarUtils.jsm
@@ -110,16 +110,17 @@ var UrlbarUtils = {
     SEARCH_GLASS: "chrome://browser/skin/search-glass.svg",
   },
 
   // IME composition states.
   COMPOSITION: {
     NONE: 1,
     COMPOSING: 2,
     COMMIT: 3,
+    CANCELED: 4,
   },
 
   // This defines possible reasons for canceling a query.
   CANCEL_REASON: {
     // 1 is intentionally left in case we want a none/undefined/other later.
     BLUR: 2,
   },
 
--- a/browser/components/urlbar/UrlbarView.jsm
+++ b/browser/components/urlbar/UrlbarView.jsm
@@ -336,22 +336,16 @@ class UrlbarView {
     if (this.isOpen) {
       return;
     }
     this.controller.userSelectionBehavior = "none";
 
     this.panel.removeAttribute("hidden");
     this.panel.removeAttribute("actionoverride");
 
-    this._alignPanel();
-
-    this.panel.openPopup(this.input.textbox, "after_start");
-  }
-
-  _alignPanel() {
     // Make the panel span the width of the window.
     let documentRect =
       this._getBoundsWithoutFlushing(this.document.documentElement);
     let width = documentRect.right - documentRect.left;
     this.panel.setAttribute("width", width);
 
     // Subtract two pixels for left and right borders on the panel.
     let contentWidth = width - 2;
@@ -389,20 +383,22 @@ class UrlbarView {
       this.panel.style.removeProperty("--item-padding-start");
       this.panel.style.removeProperty("--item-padding-end");
     }
     this.panel.style.setProperty("--item-content-width", Math.round(contentWidth) + "px");
 
     // Align the panel with the input's parent toolbar.
     let toolbarRect =
       this._getBoundsWithoutFlushing(this.input.textbox.closest("toolbar"));
-    this.panel.style.marginInlineStart = this.window.RTL_UI ?
-      inputRect.right - documentRect.right + "px" :
-      documentRect.left - inputRect.left + "px";
-    this.panel.style.marginTop = inputRect.top - toolbarRect.top + "px";
+    let offsetX = Math.round(this.window.RTL_UI ?
+      inputRect.right - documentRect.right :
+      documentRect.left - inputRect.left);
+    let offsetY = Math.round(inputRect.top - toolbarRect.top);
+
+    this.panel.openPopup(this.input.textbox, "after_start", offsetX, offsetY);
   }
 
   _createRow() {
     let item = this._createElement("div");
     item.className = "urlbarView-row";
     item.setAttribute("role", "option");
     item._elements = new Map;
 
--- a/browser/components/urlbar/tests/browser/browser.ini
+++ b/browser/components/urlbar/tests/browser/browser.ini
@@ -29,16 +29,17 @@ skip-if = os != "mac" # Mac only feature
 [browser_autoFill_placeholder.js]
 [browser_autoFill_preserve.js]
 [browser_autoFill_trimURLs.js]
 [browser_autoFill_typed.js]
 [browser_canonizeURL.js]
 [browser_caret_navigation.js]
 [browser_dragdropURL.js]
 [browser_dropmarker.js]
+[browser_ime_composition.js]
 [browser_keepStateAcrossTabSwitches.js]
 [browser_keyword_override.js]
 [browser_keyword_select_and_type.js]
 [browser_keyword.js]
 support-files =
   print_postdata.sjs
 [browser_locationBarCommand.js]
 [browser_locationBarExternalLoad.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_ime_composition.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ // Tests ime composition handling.
+
+function synthesizeCompositionChange(string) {
+  EventUtils.synthesizeCompositionChange({
+    composition: {
+      string,
+      clauses: [
+        { length: string.length, attr: Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE },
+      ],
+    },
+    caret: { start: string.length, length: 0 },
+    key: { key: string ? string[string.length - 1] : "KEY_Backspace" },
+  });
+  // Modifying composition should not open the popup.
+  Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed");
+}
+
+add_task(async function test_composition() {
+  gURLBar.focus();
+  // Add at least one typed entry for the empty results set. Also clear history
+  // so that this can be over the autofill threshold.
+  await PlacesUtils.history.clear();
+  await PlacesTestUtils.addVisits({
+    uri: "http://mozilla.org/",
+    transition: PlacesUtils.history.TRANSITIONS.TYPED,
+  });
+
+  info("The popup should not be shown during composition but, after compositionend, it should be.");
+  Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed");
+  synthesizeCompositionChange("I");
+  Assert.equal(gURLBar.value, "I", "Check urlbar value");
+  synthesizeCompositionChange("In");
+  Assert.equal(gURLBar.value, "In", "Check urlbar value");
+  // Committing composition should open the popup.
+  await UrlbarTestUtils.promisePopupOpen(window, () => {
+    EventUtils.synthesizeComposition({
+      type: "compositioncommitasis",
+      key: { key: "KEY_Enter" },
+    });
+  });
+  Assert.equal(gURLBar.value, "In", "Check urlbar value");
+
+  info("If composition starts while the popup is shown, the compositionstart event should close the popup.");
+  Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+  synthesizeCompositionChange("t");
+  Assert.equal(gURLBar.value, "Int", "Check urlbar value");
+  synthesizeCompositionChange("te");
+  Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+  // Committing composition should open the popup.
+  await UrlbarTestUtils.promisePopupOpen(window, () => {
+    EventUtils.synthesizeComposition({
+      type: "compositioncommitasis",
+      key: { key: "KEY_Enter" },
+    });
+  });
+  Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+
+  info("If composition is cancelled, the value shouldn't be changed.");
+  Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+  synthesizeCompositionChange("r");
+  Assert.equal(gURLBar.value, "Inter", "Check urlbar value");
+  synthesizeCompositionChange("");
+  Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+  // Canceled compositionend should reopen the popup.
+  await UrlbarTestUtils.promisePopupOpen(window, () => {
+    EventUtils.synthesizeComposition({
+      type: "compositioncommit",
+      data: "",
+      key: { key: "KEY_Escape" },
+    });
+  });
+  Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+
+  info("If composition replaces some characters and canceled, the search string should be the latest value.");
+  Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+  EventUtils.synthesizeKey("VK_LEFT", { shiftKey: true });
+  EventUtils.synthesizeKey("VK_LEFT", { shiftKey: true });
+  synthesizeCompositionChange("t");
+  Assert.equal(gURLBar.value, "Int", "Check urlbar value");
+  synthesizeCompositionChange("te");
+  Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+  synthesizeCompositionChange("");
+  Assert.equal(gURLBar.value, "In", "Check urlbar value");
+  // Canceled compositionend should search the result with the latest value.
+  await UrlbarTestUtils.promisePopupOpen(window, () => {
+    EventUtils.synthesizeComposition({
+      type: "compositioncommitasis",
+      key: { key: "KEY_Escape" },
+    });
+  });
+  Assert.equal(gURLBar.value, "In", "Check urlbar value");
+
+  info("If all characters are removed, the popup should be closed.");
+  Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+  await UrlbarTestUtils.promisePopupClose(window, () => {
+    EventUtils.synthesizeKey("KEY_Backspace", {});
+    EventUtils.synthesizeKey("KEY_Backspace", {});
+  });
+  Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+  info("Composition which is canceled shouldn't cause opening the popup.");
+  Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed");
+  synthesizeCompositionChange("I");
+  Assert.equal(gURLBar.value, "I", "Check urlbar value");
+  synthesizeCompositionChange("In");
+  Assert.equal(gURLBar.value, "In", "Check urlbar value");
+  synthesizeCompositionChange("");
+  Assert.equal(gURLBar.value, "", "Check urlbar value");
+  // Canceled compositionend shouldn't open the popup if it was closed.
+  EventUtils.synthesizeComposition({
+    type: "compositioncommitasis",
+    key: { key: "KEY_Escape" },
+  });
+  Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed");
+  Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+  info("Down key should open the popup even if the editor is empty.");
+  await UrlbarTestUtils.promisePopupOpen(window, () => {
+    EventUtils.synthesizeKey("KEY_ArrowDown", {});
+  });
+  Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+  info("If popup is open at starting composition, the popup should be reopened after composition anyway.");
+  Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+  synthesizeCompositionChange("I");
+  Assert.equal(gURLBar.value, "I", "Check urlbar value");
+  synthesizeCompositionChange("In");
+  Assert.equal(gURLBar.value, "In", "Check urlbar value");
+  synthesizeCompositionChange("");
+  Assert.equal(gURLBar.value, "", "Check urlbar value");
+  // A canceled compositionend should open the popup if it was open.
+  await UrlbarTestUtils.promisePopupOpen(window, () => {
+    EventUtils.synthesizeComposition({
+      type: "compositioncommitasis",
+      key: { key: "KEY_Escape" },
+    });
+  });
+  Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+  info("Type normally, and hit escape, the popup should be closed.");
+  Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+  EventUtils.synthesizeKey("I", {});
+  EventUtils.synthesizeKey("n", {});
+  // The old urlbar may close/reopen the popup and then ESC wouldn't act as
+  // expected.
+  if (!UrlbarPrefs.get("quantumbar")) {
+    await UrlbarTestUtils.promiseSearchComplete(window);
+  }
+  await UrlbarTestUtils.promisePopupClose(window, () => {
+    EventUtils.synthesizeKey("KEY_Escape", {});
+  });
+  Assert.equal(gURLBar.value, "In", "Check urlbar value");
+  // Clear typed chars.
+  EventUtils.synthesizeKey("KEY_Backspace", {});
+  EventUtils.synthesizeKey("KEY_Backspace", {});
+  Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+  info("With autofill, compositionstart shouldn't open the popup");
+  Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed");
+  synthesizeCompositionChange("M");
+  Assert.equal(gURLBar.value, "M", "Check urlbar value");
+  synthesizeCompositionChange("Mo");
+  Assert.equal(gURLBar.value, "Mo", "Check urlbar value");
+  // Committing composition should open the popup.
+  await UrlbarTestUtils.promisePopupOpen(window, () => {
+    EventUtils.synthesizeComposition({
+      type: "compositioncommitasis",
+      key: { key: "KEY_Enter" },
+    });
+  });
+  Assert.equal(gURLBar.value, "Mozilla.org/", "Check urlbar value");
+});
+
--- a/browser/components/urlbar/tests/legacy/browser.ini
+++ b/browser/components/urlbar/tests/legacy/browser.ini
@@ -49,16 +49,17 @@ skip-if = (verify && !debug && (os == 'w
 skip-if = os != "mac" # Mac only feature
 [../browser/browser_autoFill_backspaced.js]
 [../browser/browser_autoFill_canonize.js]
 [../browser/browser_autoFill_preserve.js]
 [../browser/browser_autoFill_trimURLs.js]
 [../browser/browser_autocomplete_tag_star_visibility.js]
 [../browser/browser_canonizeURL.js]
 [../browser/browser_dragdropURL.js]
+[../browser/browser_ime_composition.js]
 [../browser/browser_keepStateAcrossTabSwitches.js]
 [../browser/browser_keyword_override.js]
 [../browser/browser_keyword_select_and_type.js]
 [../browser/browser_keyword.js]
 support-files =
   ../browser/print_postdata.sjs
 [../browser/browser_URLBarSetURI.js]
 skip-if = (os == "linux" || os == "mac") && debug # bug 970052, bug 970053
--- a/browser/installer/windows/msi/installer.wxs
+++ b/browser/installer/windows/msi/installer.wxs
@@ -55,23 +55,23 @@
        directory settings though, we can't put them on the command line with the
        default values those properties have, so we need a separate action for
        each possible configuration of those settings, and conditions to select
        the right action to use based on which properties are configured.
        WiX throws warning LGHT1076 complaining that these command strings are
        too long, but they actually work just fine, the warning is spurious. -->
   <CustomAction Id="RunInstallNoDir" Return="check" Execute="deferred"
                 HideTarget="no" Impersonate="no" BinaryKey="WrappedExe"
-                ExeCommand="/S /TaskbarShortcut=[TASKBAR_SHORTCUT] /DesktopShortcut=[DESKTOP_SHORTCUT] /StartMenuShortcut=[START_MENU_SHORTCUT] /MaintenanceService=[INSTALL_MAINTENANCE_SERVICE] /RemoveDistributionDir=[REMOTE_DISTRIBUTION_DIR] /PreventRebootRequired=[PREVENT_REBOOT_REQUIRED] /OptionalExtensions=[OPTIONAL_EXTENSIONS] /LaunchedFromMSI" />
+                ExeCommand="/S /TaskbarShortcut=[TASKBAR_SHORTCUT] /DesktopShortcut=[DESKTOP_SHORTCUT] /StartMenuShortcut=[START_MENU_SHORTCUT] /MaintenanceService=[INSTALL_MAINTENANCE_SERVICE] /RemoveDistributionDir=[REMOVE_DISTRIBUTION_DIR] /PreventRebootRequired=[PREVENT_REBOOT_REQUIRED] /OptionalExtensions=[OPTIONAL_EXTENSIONS] /LaunchedFromMSI" />
   <CustomAction Id="RunInstallDirPath" Return="check" Execute="deferred"
                 HideTarget="no" Impersonate="no" BinaryKey="WrappedExe"
-                ExeCommand="/S /InstallDirectoryPath=[INSTALL_DIRECTORY_PATH] /TaskbarShortcut=[TASKBAR_SHORTCUT] /DesktopShortcut=[DESKTOP_SHORTCUT] /StartMenuShortcut=[START_MENU_SHORTCUT] /MaintenanceService=[INSTALL_MAINTENANCE_SERVICE] /RemoveDistributionDir=[REMOTE_DISTRIBUTION_DIR] /PreventRebootRequired=[PREVENT_REBOOT_REQUIRED] /OptionalExtensions=[OPTIONAL_EXTENSIONS] /LaunchedFromMSI" />
+                ExeCommand="/S /InstallDirectoryPath=[INSTALL_DIRECTORY_PATH] /TaskbarShortcut=[TASKBAR_SHORTCUT] /DesktopShortcut=[DESKTOP_SHORTCUT] /StartMenuShortcut=[START_MENU_SHORTCUT] /MaintenanceService=[INSTALL_MAINTENANCE_SERVICE] /RemoveDistributionDir=[REMOVE_DISTRIBUTION_DIR] /PreventRebootRequired=[PREVENT_REBOOT_REQUIRED] /OptionalExtensions=[OPTIONAL_EXTENSIONS] /LaunchedFromMSI" />
   <CustomAction Id="RunInstallDirName" Return="check" Execute="deferred"
                 HideTarget="no" Impersonate="no" BinaryKey="WrappedExe"
-                ExeCommand="/S /InstallDirectoryName=[INSTALL_DIRECTORY_NAME] /TaskbarShortcut=[TASKBAR_SHORTCUT] /DesktopShortcut=[DESKTOP_SHORTCUT] /StartMenuShortcut=[START_MENU_SHORTCUT] /MaintenanceService=[INSTALL_MAINTENANCE_SERVICE] /RemoveDistributionDir=[REMOTE_DISTRIBUTION_DIR] /PreventRebootRequired=[PREVENT_REBOOT_REQUIRED] /OptionalExtensions=[OPTIONAL_EXTENSIONS] /LaunchedFromMSI" />
+                ExeCommand="/S /InstallDirectoryName=[INSTALL_DIRECTORY_NAME] /TaskbarShortcut=[TASKBAR_SHORTCUT] /DesktopShortcut=[DESKTOP_SHORTCUT] /StartMenuShortcut=[START_MENU_SHORTCUT] /MaintenanceService=[INSTALL_MAINTENANCE_SERVICE] /RemoveDistributionDir=[REMOVE_DISTRIBUTION_DIR] /PreventRebootRequired=[PREVENT_REBOOT_REQUIRED] /OptionalExtensions=[OPTIONAL_EXTENSIONS] /LaunchedFromMSI" />
   <CustomAction Id="RunExtractOnly" Return="check" Execute="deferred"
                 HideTarget="no" Impersonate="no" BinaryKey="WrappedExe"
                 ExeCommand="/ExtractDir=[EXTRACT_DIR]" />
 
   <!-- When we run the custom actions is kind of arbitrary; this sequencing gets
        us the least confusing message showing in the MSI progress dialog while
        the installer runs. Our actions don't need to be sequenced relative
 	   to one another because only one will ever run. -->
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -334,17 +334,18 @@ html|input.urlbar-input {
   --urlbar-popup-url-color: hsl(210, 77%, 47%);
   --urlbar-popup-action-color: hsl(178, 100%, 28%);
 }
 
 /* Give an extra margin top to align the top of the awesomebar with the
  * bottom of the nav bar, OSX calculates the panel position with an missing
  * 1px - https://bugzilla.mozilla.org/show_bug.cgi?id=1406353
  */
-#PopupAutoCompleteRichResult {
+#PopupAutoCompleteRichResult,
+#urlbar-results {
   margin-top: 1px;
 }
 
 #PopupAutoComplete > richlistbox > richlistitem[originaltype~="datalist-first"] {
   border-top: 1px solid #C7C7C7;
 }
 
 .ac-title,
--- a/config/external/nspr/pr/moz.build
+++ b/config/external/nspr/pr/moz.build
@@ -35,16 +35,19 @@ elif CONFIG['OS_TARGET'] in ('FreeBSD', 
         HAVE_BSD_FLOCK=True,
         HAVE_SOCKLEN_T=True,
         HAVE_POINTER_LOCALTIME_R=True,
     )
     DEFINES[CONFIG['OS_TARGET'].upper()] = True
     SOURCES += ['/nsprpub/pr/src/md/unix/%s.c' % CONFIG['OS_TARGET'].lower()]
 elif CONFIG['OS_TARGET'] == 'Darwin':
     OS_LIBS += ['-framework CoreServices']
+    # See also IncreaseDescriptorLimits in toolkit/xre/nsAppRunner.cpp
+    DEFINES['FD_SETSIZE'] = 4096
+    DEFINES['_DARWIN_UNLIMITED_SELECT'] = True
     if not CONFIG['HOST_MAJOR_VERSION']:
         DEFINES.update(
             HAS_CONNECTX=True,
         )
     elif CONFIG['HOST_MAJOR_VERSION'] >= '15':
         DEFINES.update(
             HAS_CONNECTX=True,
         )
--- a/devtools/client/debugger/new/bin/module-manifest.json
+++ b/devtools/client/debugger/new/bin/module-manifest.json
@@ -10,17 +10,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../react-aria-components/src/tabs/tab.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -31,17 +31,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../react-aria-components/src/tabs/tab-list.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -52,17 +52,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../devtools-contextmenu/menu.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -73,17 +73,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../../packages/devtools-components/src/tree.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -94,17 +94,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../../packages/devtools-reps/src/object-inspector/components/ObjectInspector.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -115,17 +115,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../../packages/devtools-reps/src/reps/reps.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -136,17 +136,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "modules": {
     "byIdentifier": {
       "external \"devtools/client/shared/vendor/react-prop-types\"": 0,
       "external \"devtools/client/shared/vendor/react-dom-factories\"": 1,
--- a/devtools/client/debugger/new/index.html
+++ b/devtools/client/debugger/new/index.html
@@ -38,19 +38,25 @@
 
   <body>
     <div id="mount"></div>
     <script
       type="application/javascript"
       src="chrome://devtools/content/shared/theme-switching.js"
     ></script>
     <script type="text/javascript">
-      const { BrowserLoader } = ChromeUtils.import(
-        "resource://devtools/client/shared/browser-loader.js"
-      );
-      const { require } = BrowserLoader({
-        baseURI: "resource://devtools/client/debugger/new",
-        window
-      });
-      Debugger = require("devtools/client/debugger/new/src/main");
+      try {
+        const { BrowserLoader } = ChromeUtils.import(
+          "resource://devtools/client/shared/browser-loader.js"
+        );
+        const { require } = BrowserLoader({
+          baseURI: "resource://devtools/client/debugger/new",
+          window
+        });
+        Debugger = require("devtools/client/debugger/new/src/main");
+      } catch (e) {
+        dump("Exception happened while loading the debugger:\n");
+        dump(e + "\n");
+        dump(e.stack + "\n");
+      }
     </script>
   </body>
 </html>
--- a/devtools/client/debugger/new/packages/devtools-source-map/src/utils/index.js
+++ b/devtools/client/debugger/new/packages/devtools-source-map/src/utils/index.js
@@ -1,18 +1,22 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 
 const md5 = require("md5");
 
-function originalToGeneratedId(originalId: string) {
-  const match = originalId.match(/(.*)\/originalSource/);
+function originalToGeneratedId(sourceId: string) {
+  if (isGeneratedId(sourceId)) {
+    return sourceId;
+  }
+
+  const match = sourceId.match(/(.*)\/originalSource/);
   return match ? match[1] : "";
 }
 
 function generatedToOriginalId(generatedId: string, url: string) {
   return `${generatedId}/originalSource-${md5(url)}`;
 }
 
 function isOriginalId(id: string) {
--- a/devtools/client/debugger/new/src/actions/breakpoints/breakpointPositions.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/breakpointPositions.js
@@ -5,26 +5,35 @@
 // @flow
 
 import { isOriginalId, originalToGeneratedId } from "devtools-source-map";
 import { uniqBy, zip } from "lodash";
 
 import {
   getSource,
   getSourceFromId,
+  getGeneratedSourceById,
   hasBreakpointPositions,
   getBreakpointPositionsForSource
 } from "../../selectors";
 
-import type { MappedLocation, SourceLocation } from "../../types";
-import type { ThunkArgs } from "../../actions/types";
+import type {
+  MappedLocation,
+  SourceLocation,
+  BreakpointPositions
+} from "../../types";
 import { makeBreakpointId } from "../../utils/breakpoint";
+import {
+  memoizeableAction,
+  type MemoizedAction
+} from "../../utils/memoizableAction";
+
 import typeof SourceMaps from "../../../packages/devtools-source-map/src";
 
-const requests = new Map();
+// const requests = new Map();
 
 async function mapLocations(
   generatedLocations: SourceLocation[],
   { sourceMaps }: { sourceMaps: SourceMaps }
 ) {
   const originalLocations = await sourceMaps.getOriginalLocations(
     generatedLocations
   );
@@ -108,60 +117,34 @@ async function _setBreakpointPositions(s
   positions = filterBySource(positions, sourceId);
   positions = filterByUniqLocation(positions);
 
   const source = getSource(getState(), sourceId);
   // NOTE: it's possible that the source was removed during a navigate
   if (!source) {
     return;
   }
+
   dispatch({
     type: "ADD_BREAKPOINT_POSITIONS",
     source: source,
     positions
   });
-}
 
-function buildCacheKey(sourceId: string, thunkArgs: ThunkArgs): string {
-  const generatedSource = getSource(
-    thunkArgs.getState(),
-    isOriginalId(sourceId) ? originalToGeneratedId(sourceId) : sourceId
-  );
-
-  let key = sourceId;
-
-  if (generatedSource) {
-    for (const actor of generatedSource.actors) {
-      key += `:${actor.actor}`;
-    }
-  }
-  return key;
+  return positions;
 }
 
-export function setBreakpointPositions(sourceId: string) {
-  return async (thunkArgs: ThunkArgs) => {
-    const { getState } = thunkArgs;
-    if (hasBreakpointPositions(getState(), sourceId)) {
-      return getBreakpointPositionsForSource(getState(), sourceId);
-    }
-
-    const cacheKey = buildCacheKey(sourceId, thunkArgs);
-
-    if (!requests.has(cacheKey)) {
-      requests.set(
-        cacheKey,
-        (async () => {
-          try {
-            await _setBreakpointPositions(sourceId, thunkArgs);
-          } catch (e) {
-            // TODO: Address exceptions originating from 1536618
-            // `Debugger.Source belongs to a different Debugger`
-          } finally {
-            requests.delete(cacheKey);
-          }
-        })()
-      );
-    }
-
-    await requests.get(cacheKey);
-    return getBreakpointPositionsForSource(getState(), sourceId);
-  };
-}
+export const setBreakpointPositions: MemoizedAction<
+  { sourceId: string },
+  ?BreakpointPositions
+> = memoizeableAction("setBreakpointPositions", {
+  hasValue: ({ sourceId }, { getState }) =>
+    hasBreakpointPositions(getState(), sourceId),
+  getValue: ({ sourceId }, { getState }) =>
+    getBreakpointPositionsForSource(getState(), sourceId),
+  createKey({ sourceId }, { getState }) {
+    const generatedSource = getGeneratedSourceById(getState(), sourceId);
+    const actors = generatedSource.actors.map(({ actor }) => actor);
+    return [sourceId, ...actors].join(":");
+  },
+  action: ({ sourceId }, thunkArgs) =>
+    _setBreakpointPositions(sourceId, thunkArgs)
+});
--- a/devtools/client/debugger/new/src/actions/breakpoints/modify.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/modify.js
@@ -1,42 +1,42 @@
 /* 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/>. */
 
 // @flow
 
-import type { ThunkArgs } from "../types";
-import type {
-  Breakpoint,
-  BreakpointOptions,
-  SourceLocation
-} from "../../types";
-
 import {
   makeBreakpointLocation,
   makeBreakpointId,
   getASTLocation
 } from "../../utils/breakpoint";
 
 import { getTextAtPosition } from "../../utils/source";
 
 import {
   getBreakpoint,
   getBreakpointPositionsForLocation,
   getFirstBreakpointPosition,
-  getSourceFromId,
   getSymbols
 } from "../../selectors";
 
-import { loadSourceText } from "../sources/loadSourceText";
+import { loadSourceById } from "../sources/loadSourceText";
 import { setBreakpointPositions } from "./breakpointPositions";
 
 import { recordEvent } from "../../utils/telemetry";
 
+import type { ThunkArgs } from "../types";
+import type {
+  Breakpoint,
+  BreakpointOptions,
+  BreakpointPosition,
+  SourceLocation
+} from "../../types";
+
 // This file has the primitive operations used to modify individual breakpoints
 // and keep them in sync with the breakpoints installed on server threads. These
 // are collected here to make it easier to preserve the following invariant:
 //
 // Breakpoints are included in reducer state iff they are disabled or requests
 // have been dispatched to set them in all server threads.
 //
 // To maintain this property, updates to the reducer and installed breakpoints
@@ -97,41 +97,33 @@ export function addBreakpoint(
   options: BreakpointOptions = {},
   disabled: boolean = false
 ) {
   return async ({ dispatch, getState, sourceMaps, client }: ThunkArgs) => {
     recordEvent("add_breakpoint");
 
     const { sourceId, column } = initialLocation;
 
-    await dispatch(setBreakpointPositions(sourceId));
+    await dispatch(setBreakpointPositions({ sourceId }));
 
-    const position = column
+    const position: ?BreakpointPosition = column
       ? getBreakpointPositionsForLocation(getState(), initialLocation)
       : getFirstBreakpointPosition(getState(), initialLocation);
 
     if (!position) {
       return;
     }
 
     const { location, generatedLocation } = position;
-
     // Both the original and generated sources must be loaded to get the
     // breakpoint's text.
-    await dispatch(
-      loadSourceText(getSourceFromId(getState(), location.sourceId))
-    );
-    await dispatch(
-      loadSourceText(getSourceFromId(getState(), generatedLocation.sourceId))
-    );
 
-    const source = getSourceFromId(getState(), location.sourceId);
-    const generatedSource = getSourceFromId(
-      getState(),
-      generatedLocation.sourceId
+    const source = await dispatch(loadSourceById(sourceId));
+    const generatedSource = await dispatch(
+      loadSourceById(generatedLocation.sourceId)
     );
 
     const symbols = getSymbols(getState(), source);
     const astLocation = await getASTLocation(source, symbols, location);
 
     const originalText = getTextAtPosition(source, location);
     const text = getTextAtPosition(generatedSource, generatedLocation);
 
@@ -151,26 +143,20 @@ export function addBreakpoint(
     // Because a generated location cannot map to multiple original locations,
     // the only breakpoints that can map to this generated location have the
     // new breakpoint's |location| or |generatedLocation| as their own
     // |location|. We will overwrite any breakpoint at |location| with the
     // SET_BREAKPOINT action below, but need to manually remove any breakpoint
     // at |generatedLocation|.
     const generatedId = makeBreakpointId(breakpoint.generatedLocation);
     if (id != generatedId && getBreakpoint(getState(), generatedLocation)) {
-      dispatch({
-        type: "REMOVE_BREAKPOINT",
-        location: generatedLocation
-      });
+      dispatch({ type: "REMOVE_BREAKPOINT", location: generatedLocation });
     }
 
-    dispatch({
-      type: "SET_BREAKPOINT",
-      breakpoint
-    });
+    dispatch({ type: "SET_BREAKPOINT", breakpoint });
 
     if (disabled) {
       // If we just clobbered an enabled breakpoint with a disabled one, we need
       // to remove any installed breakpoint in the server.
       return dispatch(clientRemoveBreakpoint(breakpoint));
     }
 
     return dispatch(clientSetBreakpoint(breakpoint));
--- a/devtools/client/debugger/new/src/actions/breakpoints/syncBreakpoint.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/syncBreakpoint.js
@@ -1,52 +1,62 @@
 /* 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/>. */
 
 // @flow
 
 import { setBreakpointPositions } from "./breakpointPositions";
+import { setSymbols } from "../sources/symbols";
 import {
   assertPendingBreakpoint,
   findFunctionByName,
   findPosition,
   makeBreakpointLocation
 } from "../../utils/breakpoint";
 
 import { comparePosition, createLocation } from "../../utils/location";
 
 import { originalToGeneratedId, isOriginalId } from "devtools-source-map";
 import { getSource, getBreakpoint } from "../../selectors";
 import { removeBreakpoint, addBreakpoint } from ".";
 
 import type { ThunkArgs } from "../types";
+import type { LoadedSymbols } from "../../reducers/types";
 
 import type {
   SourceLocation,
   ASTLocation,
   PendingBreakpoint,
-  SourceId
+  SourceId,
+  BreakpointPositions
 } from "../../types";
 
 async function findBreakpointPosition(
   { getState, dispatch },
   location: SourceLocation
 ) {
-  const positions = await dispatch(setBreakpointPositions(location.sourceId));
+  const positions: BreakpointPositions = await dispatch(
+    setBreakpointPositions({ sourceId: location.sourceId })
+  );
+
   const position = findPosition(positions, location);
   return position && position.generatedLocation;
 }
 
 async function findNewLocation(
   { name, offset, index }: ASTLocation,
   location: SourceLocation,
-  source
+  source,
+  thunkArgs
 ) {
-  const func = await findFunctionByName(source, name, index);
+  const symbols: LoadedSymbols = await thunkArgs.dispatch(
+    setSymbols({ source })
+  );
+  const func = findFunctionByName(symbols, name, index);
 
   // Fallback onto the location line, if we do not find a function is not found
   let line = location.line;
   if (func) {
     line = func.location.start.line + offset.line;
   }
 
   return {
@@ -128,17 +138,18 @@ export function syncBreakpoint(
       );
     }
 
     const previousLocation = { ...location, sourceId };
 
     const newLocation = await findNewLocation(
       astLocation,
       previousLocation,
-      source
+      source,
+      thunkArgs
     );
 
     const newGeneratedLocation = await findBreakpointPosition(
       thunkArgs,
       newLocation
     );
 
     if (!newGeneratedLocation) {
--- a/devtools/client/debugger/new/src/actions/breakpoints/tests/breakpointPositions.spec.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/tests/breakpointPositions.spec.js
@@ -16,17 +16,17 @@ describe("breakpointPositions", () => {
   it("fetches positions", async () => {
     const store = createStore({
       getBreakpointPositions: async () => ({ "9": [1] })
     });
 
     const { dispatch, getState } = store;
     await dispatch(actions.newSource(makeSource("foo")));
 
-    dispatch(actions.setBreakpointPositions("foo"));
+    dispatch(actions.setBreakpointPositions({ sourceId: "foo" }));
 
     await waitForState(store, state =>
       selectors.hasBreakpointPositions(state, "foo")
     );
 
     expect(
       selectors.getBreakpointPositionsForSource(getState(), "foo")
     ).toEqual([
@@ -56,18 +56,18 @@ describe("breakpointPositions", () => {
           count++;
           resolve = r;
         })
     });
 
     const { dispatch, getState } = store;
     await dispatch(actions.newSource(makeSource("foo")));
 
-    dispatch(actions.setBreakpointPositions("foo"));
-    dispatch(actions.setBreakpointPositions("foo"));
+    dispatch(actions.setBreakpointPositions({ sourceId: "foo" }));
+    dispatch(actions.setBreakpointPositions({ sourceId: "foo" }));
 
     resolve({ "9": [1] });
     await waitForState(store, state =>
       selectors.hasBreakpointPositions(state, "foo")
     );
 
     expect(
       selectors.getBreakpointPositionsForSource(getState(), "foo")
--- a/devtools/client/debugger/new/src/actions/breakpoints/tests/breakpoints.spec.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/tests/breakpoints.spec.js
@@ -28,17 +28,17 @@ describe("breakpoints", () => {
       sourceId: "a",
       line: 2,
       column: 1,
       sourceUrl: "http://localhost:8000/examples/a"
     };
 
     const source = makeSource("a");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
     await dispatch(
       actions.setSelectedLocation(source, {
         line: 1,
         column: 1,
         sourceId: source.id
       })
     );
 
@@ -58,17 +58,17 @@ describe("breakpoints", () => {
     const loc1 = {
       sourceId: "a",
       line: 5,
       column: 1,
       sourceUrl: "http://localhost:8000/examples/a"
     };
     const source = makeSource("a");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
     await dispatch(
       actions.setSelectedLocation(source, {
         line: 1,
         column: 1,
         sourceId: source.id
       })
     );
 
@@ -85,17 +85,17 @@ describe("breakpoints", () => {
     const loc1 = {
       sourceId: "a",
       line: 5,
       column: 1,
       sourceUrl: "http://localhost:8000/examples/a"
     };
     const source = makeSource("a");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
     await dispatch(
       actions.setSelectedLocation(source, {
         line: 1,
         column: 1,
         sourceId: source.id
       })
     );
 
@@ -119,17 +119,17 @@ describe("breakpoints", () => {
       sourceId: "a",
       line: 5,
       column: 1,
       sourceUrl: "http://localhost:8000/examples/a"
     };
 
     const source = makeSource("a");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
     await dispatch(
       actions.setSelectedLocation(source, {
         line: 1,
         column: 1,
         sourceId: source.id
       })
     );
 
@@ -158,21 +158,21 @@ describe("breakpoints", () => {
       sourceId: "b",
       line: 6,
       column: 2,
       sourceUrl: "http://localhost:8000/examples/b"
     };
 
     const aSource = makeSource("a");
     await dispatch(actions.newSource(aSource));
-    await dispatch(actions.loadSourceText(aSource));
+    await dispatch(actions.loadSourceText({ source: aSource }));
 
     const bSource = makeSource("b");
     await dispatch(actions.newSource(bSource));
-    await dispatch(actions.loadSourceText(bSource));
+    await dispatch(actions.loadSourceText({ source: bSource }));
 
     await dispatch(
       actions.setSelectedLocation(aSource, {
         line: 1,
         column: 1,
         sourceId: aSource.id
       })
     );
@@ -205,21 +205,21 @@ describe("breakpoints", () => {
       sourceId: "b",
       line: 6,
       column: 2,
       sourceUrl: "http://localhost:8000/examples/b"
     };
 
     const aSource = makeSource("a");
     await dispatch(actions.newSource(aSource));
-    await dispatch(actions.loadSourceText(aSource));
+    await dispatch(actions.loadSourceText({ source: aSource }));
 
     const bSource = makeSource("b");
     await dispatch(actions.newSource(bSource));
-    await dispatch(actions.loadSourceText(bSource));
+    await dispatch(actions.loadSourceText({ source: bSource }));
 
     await dispatch(actions.addBreakpoint(loc1));
     await dispatch(actions.addBreakpoint(loc2));
 
     const breakpoint = selectors.getBreakpoint(getState(), loc1);
     if (!breakpoint) {
       throw new Error("no breakpoint");
     }
@@ -238,17 +238,17 @@ describe("breakpoints", () => {
       sourceId: "a",
       line: 5,
       column: 1,
       sourceUrl: "http://localhost:8000/examples/a"
     };
 
     const aSource = makeSource("a");
     await dispatch(actions.newSource(aSource));
-    await dispatch(actions.loadSourceText(aSource));
+    await dispatch(actions.loadSourceText({ source: aSource }));
 
     await dispatch(actions.addBreakpoint(loc));
     let bp = selectors.getBreakpoint(getState(), loc);
     if (!bp) {
       throw new Error("no breakpoint");
     }
 
     await dispatch(actions.disableBreakpoint(bp));
@@ -282,21 +282,21 @@ describe("breakpoints", () => {
       sourceId: "b",
       line: 6,
       column: 2,
       sourceUrl: "http://localhost:8000/examples/b"
     };
 
     const aSource = makeSource("a");
     await dispatch(actions.newSource(aSource));
-    await dispatch(actions.loadSourceText(aSource));
+    await dispatch(actions.loadSourceText({ source: aSource }));
 
     const bSource = makeSource("b");
     await dispatch(actions.newSource(bSource));
-    await dispatch(actions.loadSourceText(bSource));
+    await dispatch(actions.loadSourceText({ source: bSource }));
 
     await dispatch(actions.addBreakpoint(loc1));
     await dispatch(actions.addBreakpoint(loc2));
 
     await dispatch(actions.toggleAllBreakpoints(true));
 
     let bp1 = selectors.getBreakpoint(getState(), loc1);
     let bp2 = selectors.getBreakpoint(getState(), loc2);
@@ -315,17 +315,17 @@ describe("breakpoints", () => {
   it("should toggle a breakpoint at a location", async () => {
     const loc = { sourceId: "foo1", line: 5, column: 1 };
     const getBp = () => selectors.getBreakpoint(getState(), loc);
 
     const { dispatch, getState } = createStore(mockClient({ "5": [1] }));
 
     const source = makeSource("foo1");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.selectLocation(loc));
 
     await dispatch(actions.toggleBreakpointAtLine(5));
     const bp = getBp();
     expect(bp && !bp.disabled).toBe(true);
 
     await dispatch(actions.toggleBreakpointAtLine(5));
@@ -335,17 +335,17 @@ describe("breakpoints", () => {
   it("should disable/enable a breakpoint at a location", async () => {
     const location = { sourceId: "foo1", line: 5, column: 1 };
     const getBp = () => selectors.getBreakpoint(getState(), location);
 
     const { dispatch, getState } = createStore(mockClient({ "5": [1] }));
 
     const source = makeSource("foo1");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.selectLocation({ sourceId: "foo1", line: 1 }));
 
     await dispatch(actions.toggleBreakpointAtLine(5));
     let bp = getBp();
     expect(bp && !bp.disabled).toBe(true);
     bp = getBp();
     if (!bp) {
@@ -363,17 +363,17 @@ describe("breakpoints", () => {
       sourceId: "a",
       line: 5,
       column: 1,
       sourceUrl: "http://localhost:8000/examples/a"
     };
 
     const source = makeSource("a");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.addBreakpoint(loc));
 
     let bp = selectors.getBreakpoint(getState(), loc);
     expect(bp && bp.options.condition).toBe(undefined);
 
     await dispatch(
       actions.setBreakpointOptions(loc, {
@@ -393,17 +393,17 @@ describe("breakpoints", () => {
       sourceId: "a",
       line: 5,
       column: 1,
       sourceUrl: "http://localhost:8000/examples/a"
     };
 
     const source = makeSource("a");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.addBreakpoint(loc));
     let bp = selectors.getBreakpoint(getState(), loc);
     if (!bp) {
       throw new Error("no breakpoint");
     }
 
     await dispatch(actions.disableBreakpoint(bp));
@@ -431,17 +431,17 @@ describe("breakpoints", () => {
       sourceId: "a.js",
       line: 1,
       column: 0,
       sourceUrl: "http://localhost:8000/examples/a.js"
     };
 
     const source = makeSource("a.js");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.addBreakpoint(loc));
     await dispatch(actions.togglePrettyPrint("a.js"));
 
     const breakpoint = selectors.getBreakpointsList(getState())[0];
 
     expect(
       breakpoint.location.sourceUrl &&
--- a/devtools/client/debugger/new/src/actions/pause/mapFrames.js
+++ b/devtools/client/debugger/new/src/actions/pause/mapFrames.js
@@ -3,21 +3,23 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 
 import {
   getFrames,
   getSymbols,
   getSource,
+  getSourceFromId,
   getSelectedFrame
 } from "../../selectors";
 
 import assert from "../../utils/assert";
 import { findClosestFunction } from "../../utils/ast";
+import { setSymbols } from "../sources/symbols";
 
 import type { Frame, ThreadId } from "../../types";
 import type { State } from "../../reducers/types";
 import type { ThunkArgs } from "../types";
 
 import { isGeneratedId } from "devtools-source-map";
 
 function isFrameBlackboxed(state, frame) {
@@ -62,16 +64,17 @@ function updateFrameLocations(
 export function mapDisplayNames(
   frames: Frame[],
   getState: () => State
 ): Frame[] {
   return frames.map(frame => {
     if (frame.isOriginal) {
       return frame;
     }
+
     const source = getSource(getState(), frame.location.sourceId);
 
     if (!source) {
       return frame;
     }
 
     const symbols = getSymbols(getState(), source);
 
@@ -147,33 +150,45 @@ async function expandFrames(
         generatedLocation: frame.generatedLocation,
         originalDisplayName: originalFrame.displayName
       });
     });
   }
   return result;
 }
 
+async function updateFrameSymbols(frames, { dispatch, getState }) {
+  await Promise.all(
+    frames.map(frame => {
+      const source = getSourceFromId(getState(), frame.location.sourceId);
+      return dispatch(setSymbols({ source }));
+    })
+  );
+}
+
 /**
  * Map call stack frame locations and display names to originals.
  * e.g.
  * 1. When the debuggee pauses
  * 2. When a source is pretty printed
  * 3. When symbols are loaded
  * @memberof actions/pause
  * @static
  */
 export function mapFrames(thread: ThreadId) {
-  return async function({ dispatch, getState, sourceMaps }: ThunkArgs) {
+  return async function(thunkArgs: ThunkArgs) {
+    const { dispatch, getState, sourceMaps } = thunkArgs;
     const frames = getFrames(getState(), thread);
     if (!frames) {
       return;
     }
 
     let mappedFrames = await updateFrameLocations(frames, sourceMaps);
+    await updateFrameSymbols(mappedFrames, thunkArgs);
+
     mappedFrames = await expandFrames(mappedFrames, sourceMaps, getState);
     mappedFrames = mapDisplayNames(mappedFrames, getState);
 
     const selectedFrameId = getSelectedFrameId(
       getState(),
       thread,
       mappedFrames
     );
--- a/devtools/client/debugger/new/src/actions/pause/mapScopes.js
+++ b/devtools/client/debugger/new/src/actions/pause/mapScopes.js
@@ -67,19 +67,19 @@ export function mapScopes(scopes: Promis
           !generatedSource ||
           generatedSource.isWasm ||
           source.isPrettyPrinted ||
           isGenerated(source)
         ) {
           return null;
         }
 
-        await dispatch(loadSourceText(source));
+        await dispatch(loadSourceText({ source }));
         if (isOriginal(source)) {
-          await dispatch(loadSourceText(generatedSource));
+          await dispatch(loadSourceText({ source: generatedSource }));
         }
 
         try {
           return await buildMappedScopes(
             source,
             frame,
             await scopes,
             sourceMaps,
--- a/devtools/client/debugger/new/src/actions/pause/tests/pause.spec.js
+++ b/devtools/client/debugger/new/src/actions/pause/tests/pause.spec.js
@@ -156,17 +156,16 @@ describe("pause", () => {
       const mockPauseInfo = createPauseInfo({
         sourceId: "await",
         line: 2,
         column: 0
       });
 
       const source = makeSource("await");
       await dispatch(actions.newSource(source));
-      await dispatch(actions.loadSourceText(source));
 
       await dispatch(actions.paused(mockPauseInfo));
       const getNextStepSpy = jest.spyOn(parser, "getNextStep");
       dispatch(actions.stepOver());
       expect(getNextStepSpy).toBeCalled();
       getNextStepSpy.mockRestore();
     });
 
@@ -176,17 +175,16 @@ describe("pause", () => {
       const mockPauseInfo = createPauseInfo({
         sourceId: "await",
         line: 2,
         column: 6
       });
 
       const source = makeSource("await");
       await dispatch(actions.newSource(source));
-      await dispatch(actions.loadSourceText(source));
 
       await dispatch(actions.paused(mockPauseInfo));
       const getNextStepSpy = jest.spyOn(parser, "getNextStep");
       dispatch(actions.stepOver());
       expect(getNextStepSpy).toBeCalled();
       getNextStepSpy.mockRestore();
     });
 
@@ -203,24 +201,24 @@ describe("pause", () => {
         scope: {
           bindings: { variables: { b: {} }, arguments: [{ a: {} }] }
         }
       });
 
       const source = makeSource("foo");
       await dispatch(actions.newSource(source));
       await dispatch(actions.newSource(makeOriginalSource("foo")));
-      await dispatch(actions.loadSourceText(source));
 
       await dispatch(actions.paused(mockPauseInfo));
       expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
         {
           generatedLocation: { column: 0, line: 1, sourceId: "foo" },
           id: mockFrameId,
           location: { column: 0, line: 1, sourceId: "foo" },
+          originalDisplayName: "foo",
           scope: {
             bindings: { arguments: [{ a: {} }], variables: { b: {} } }
           },
           thread: "FakeThread"
         }
       ]);
 
       expect(selectors.getFrameScopes(getState(), "FakeThread")).toEqual({
@@ -267,19 +265,16 @@ describe("pause", () => {
       const store = createStore(mockThreadClient, {}, sourceMapsMock);
       const { dispatch, getState } = store;
       const mockPauseInfo = createPauseInfo(generatedLocation);
 
       const fooSource = makeSource("foo");
       const fooOriginalSource = makeSource("foo-original");
       await dispatch(actions.newSource(fooSource));
       await dispatch(actions.newSource(fooOriginalSource));
-      await dispatch(actions.loadSourceText(fooSource));
-      await dispatch(actions.loadSourceText(fooOriginalSource));
-      await dispatch(actions.setSymbols("foo-original"));
 
       await dispatch(actions.paused(mockPauseInfo));
       expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
         {
           generatedLocation: { column: 0, line: 1, sourceId: "foo" },
           id: mockFrameId,
           location: { column: 0, line: 3, sourceId: "foo-original" },
           originalDisplayName: "fooOriginal",
@@ -334,18 +329,16 @@ describe("pause", () => {
       const { dispatch, getState } = store;
       const mockPauseInfo = createPauseInfo(generatedLocation);
 
       const source = makeSource("foo-wasm", { isWasm: true });
       const originalSource = makeOriginalSource("foo-wasm");
 
       await dispatch(actions.newSource(source));
       await dispatch(actions.newSource(originalSource));
-      await dispatch(actions.loadSourceText(source));
-      await dispatch(actions.loadSourceText(originalSource));
 
       await dispatch(actions.paused(mockPauseInfo));
       expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
         {
           displayName: "fooBar",
           generatedLocation: { column: 0, line: 1, sourceId: "foo-wasm" },
           id: mockFrameId,
           isOriginal: true,
--- a/devtools/client/debugger/new/src/actions/project-text-search.js
+++ b/devtools/client/debugger/new/src/actions/project-text-search.js
@@ -83,17 +83,17 @@ export function searchSources(query: str
     dispatch(updateSearchStatus(statusType.fetching));
     const validSources = getSourceList(getState()).filter(
       source => !hasPrettySource(getState(), source.id) && !isThirdParty(source)
     );
     for (const source of validSources) {
       if (cancelled) {
         return;
       }
-      await dispatch(loadSourceText(source));
+      await dispatch(loadSourceText({ source }));
       await dispatch(searchSource(source.id, query));
     }
     dispatch(updateSearchStatus(statusType.done));
   };
 
   search.cancel = () => {
     cancelled = true;
   };
--- a/devtools/client/debugger/new/src/actions/sources/loadSourceText.js
+++ b/devtools/client/debugger/new/src/actions/sources/loadSourceText.js
@@ -2,33 +2,37 @@
  * 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/>. */
 
 // @flow
 
 import { PROMISE } from "../utils/middleware/promise";
 import {
   getSource,
+  getSourceFromId,
   getGeneratedSource,
   getSourcesEpoch
 } from "../../selectors";
 import { setBreakpointPositions } from "../breakpoints";
 
 import { prettyPrintSource } from "./prettyPrint";
 
 import * as parser from "../../workers/parser";
 import { isLoaded, isOriginal, isPretty } from "../../utils/source";
+import {
+  memoizeableAction,
+  type MemoizedAction
+} from "../../utils/memoizableAction";
+
 import { Telemetry } from "devtools-modules";
 
 import type { ThunkArgs } from "../types";
 
 import type { Source } from "../../types";
 
-const requests = new Map();
-
 // Measures the time it takes for a source to load
 const loadSourceHistogram = "DEVTOOLS_DEBUGGER_LOAD_SOURCE_MS";
 const telemetry = new Telemetry();
 
 async function loadSource(
   state,
   source: Source,
   { sourceMaps, client }
@@ -64,70 +68,52 @@ async function loadSource(
   return {
     text: response.source,
     contentType: response.contentType || "text/javascript"
   };
 }
 
 async function loadSourceTextPromise(
   source: Source,
-  epoch: number,
   { dispatch, getState, client, sourceMaps }: ThunkArgs
 ): Promise<?Source> {
-  if (isLoaded(source)) {
-    return source;
-  }
-
+  const epoch = getSourcesEpoch(getState());
   await dispatch({
     type: "LOAD_SOURCE_TEXT",
     sourceId: source.id,
     epoch,
     [PROMISE]: loadSource(getState(), source, { sourceMaps, client })
   });
 
   const newSource = getSource(getState(), source.id);
+
   if (!newSource) {
     return;
   }
 
   if (!newSource.isWasm && isLoaded(newSource)) {
     parser.setSource(newSource);
-    dispatch(setBreakpointPositions(newSource.id));
+    dispatch(setBreakpointPositions({ sourceId: newSource.id }));
   }
 
   return newSource;
 }
 
-/**
- * @memberof actions/sources
- * @static
- */
-export function loadSourceText(inputSource: ?Source) {
-  return async (thunkArgs: ThunkArgs) => {
-    if (!inputSource) {
-      return;
-    }
-
-    // This ensures that the falsy check above is preserved into the IIFE
-    // below in a way that Flow is happy with.
-    const source = inputSource;
-
-    const epoch = getSourcesEpoch(thunkArgs.getState());
-
-    const id = `${epoch}:${source.id}`;
-    let promise = requests.get(id);
-    if (!promise) {
-      promise = (async () => {
-        try {
-          return await loadSourceTextPromise(source, epoch, thunkArgs);
-        } catch (e) {
-          // TODO: This swallows errors for now. Ideally we would get rid of
-          // this once we have a better handle on our async state management.
-        } finally {
-          requests.delete(id);
-        }
-      })();
-      requests.set(id, promise);
-    }
-
-    return promise;
+export function loadSourceById(sourceId: string) {
+  return ({ getState, dispatch }: ThunkArgs) => {
+    const source = getSourceFromId(getState(), sourceId);
+    return dispatch(loadSourceText({ source }));
   };
 }
+
+export const loadSourceText: MemoizedAction<
+  { source: Source },
+  ?Source
+> = memoizeableAction("loadSourceText", {
+  exitEarly: ({ source }) => !source,
+  hasValue: ({ source }, { getState }) => isLoaded(source),
+  getValue: ({ source }, { getState }) => getSource(getState(), source.id),
+  createKey: ({ source }, { getState }) => {
+    const epoch = getSourcesEpoch(getState());
+    return `${epoch}:${source.id}`;
+  },
+  action: ({ source }, thunkArgs) => loadSourceTextPromise(source, thunkArgs)
+});
--- a/devtools/client/debugger/new/src/actions/sources/newSources.js
+++ b/devtools/client/debugger/new/src/actions/sources/newSources.js
@@ -187,17 +187,17 @@ function checkPendingBreakpoints(sourceI
       source
     );
 
     if (pendingBreakpoints.length === 0) {
       return;
     }
 
     // load the source text if there is a pending breakpoint for it
-    await dispatch(loadSourceText(source));
+    await dispatch(loadSourceText({ source }));
 
     await Promise.all(
       pendingBreakpoints.map(bp => {
         return dispatch(syncBreakpoint(sourceId, bp));
       })
     );
   };
 }
@@ -243,16 +243,16 @@ export function newSources(sources: Sour
       dispatch(checkSelectedSource(source.id));
     }
 
     // Adding new sources may have cleared this file's breakpoint positions
     // in cases where a new <script> loaded in the HTML, so we manually
     // re-request new breakpoint positions.
     for (const source of sourcesNeedingPositions) {
       if (!hasBreakpointPositions(getState(), source.id)) {
-        dispatch(setBreakpointPositions(source.id));
+        dispatch(setBreakpointPositions({ sourceId: source.id }));
       }
     }
 
     dispatch(restoreBlackBoxedSources(_newSources));
     dispatch(loadSourceMaps(_newSources));
   };
 }
--- a/devtools/client/debugger/new/src/actions/sources/prettyPrint.js
+++ b/devtools/client/debugger/new/src/actions/sources/prettyPrint.js
@@ -74,16 +74,31 @@ export function createPrettySource(sourc
 
     dispatch(({ type: "ADD_SOURCE", source: prettySource }: Action));
     await dispatch(selectSource(prettySource.id));
 
     return prettySource;
   };
 }
 
+function selectPrettyLocation(prettySource: Source) {
+  return async ({ dispatch, sourceMaps, getState }: ThunkArgs) => {
+    let location = getSelectedLocation(getState());
+
+    if (location) {
+      location = await sourceMaps.getOriginalLocation(location);
+      return dispatch(
+        selectSpecificLocation({ ...location, sourceId: prettySource.id })
+      );
+    }
+
+    return dispatch(selectSource(prettySource.id));
+  };
+}
+
 /**
  * Toggle the pretty printing of a source's text. All subsequent calls to
  * |getText| will return the pretty-toggled text. Nothing will happen for
  * non-javascript files.
  *
  * @memberof actions/sources
  * @static
  * @param string id The source form from the RDP.
@@ -98,51 +113,36 @@ export function togglePrettyPrint(source
       return {};
     }
 
     if (!source.isPrettyPrinted) {
       recordEvent("pretty_print");
     }
 
     if (!isLoaded(source)) {
-      await dispatch(loadSourceText(source));
+      await dispatch(loadSourceText({ source }));
     }
 
     assert(
       sourceMaps.isGeneratedId(sourceId),
       "Pretty-printing only allowed on generated sources"
     );
 
-    const selectedLocation = getSelectedLocation(getState());
     const url = getPrettySourceURL(source.url);
     const prettySource = getSourceByURL(getState(), url);
 
-    const options = {};
-    if (selectedLocation) {
-      options.location = await sourceMaps.getOriginalLocation(selectedLocation);
-    }
-
     if (prettySource) {
-      const _sourceId = prettySource.id;
-      return dispatch(
-        selectSpecificLocation({ ...options.location, sourceId: _sourceId })
-      );
+      return dispatch(selectPrettyLocation(prettySource));
     }
 
     const newPrettySource = await dispatch(createPrettySource(sourceId));
+    await dispatch(selectPrettyLocation(newPrettySource));
 
     await dispatch(remapBreakpoints(sourceId));
 
     const threads = getSourceThreads(getState(), source);
     await Promise.all(threads.map(thread => dispatch(mapFrames(thread))));
 
-    await dispatch(setSymbols(newPrettySource.id));
-
-    dispatch(
-      selectSpecificLocation({
-        ...options.location,
-        sourceId: newPrettySource.id
-      })
-    );
+    await dispatch(setSymbols({ source: newPrettySource }));
 
     return newPrettySource;
   };
 }
--- a/devtools/client/debugger/new/src/actions/sources/select.js
+++ b/devtools/client/debugger/new/src/actions/sources/select.js
@@ -140,17 +140,17 @@ export function selectLocation(
 
     const tabSources = getSourcesForTabs(getState());
     if (!tabSources.includes(source)) {
       dispatch(addTab(source));
     }
 
     dispatch(setSelectedLocation(source, location));
 
-    await dispatch(loadSourceText(source));
+    await dispatch(loadSourceText({ source }));
     const loadedSource = getSource(getState(), source.id);
 
     if (!loadedSource) {
       // If there was a navigation while we were loading the loadedSource
       return;
     }
 
     if (
@@ -159,17 +159,17 @@ export function selectLocation(
       !getPrettySource(getState(), loadedSource.id) &&
       shouldPrettyPrint(loadedSource) &&
       isMinified(loadedSource)
     ) {
       await dispatch(togglePrettyPrint(loadedSource.id));
       dispatch(closeTab(loadedSource));
     }
 
-    dispatch(setSymbols(loadedSource.id));
+    dispatch(setSymbols({ source: loadedSource }));
     dispatch(setOutOfScopeLocations());
 
     // If a new source is selected update the file search results
     const newSource = getSelectedSource(getState());
     if (currentSource && currentSource !== newSource) {
       dispatch(updateActiveFileSearch());
     }
   };
--- a/devtools/client/debugger/new/src/actions/sources/symbols.js
+++ b/devtools/client/debugger/new/src/actions/sources/symbols.js
@@ -1,39 +1,56 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
-import { getSourceFromId, getSourceThreads, getSymbols } from "../../selectors";
+
+// @flow
+
+import { hasSymbols, getSymbols } from "../../selectors";
 
 import { PROMISE } from "../utils/middleware/promise";
-import { mapFrames } from "../pause";
 import { updateTab } from "../tabs";
+import { loadSourceText } from "./loadSourceText";
 
 import * as parser from "../../workers/parser";
 
 import { isLoaded } from "../../utils/source";
+import {
+  memoizeableAction,
+  type MemoizedAction
+} from "../../utils/memoizableAction";
 
-import type { SourceId } from "../../types";
-import type { ThunkArgs } from "../types";
+import type { Source } from "../../types";
+import type { Symbols } from "../../reducers/types";
 
-export function setSymbols(sourceId: SourceId) {
-  return async ({ dispatch, getState, sourceMaps }: ThunkArgs) => {
-    const source = getSourceFromId(getState(), sourceId);
+async function doSetSymbols(source, { dispatch, getState }) {
+  const sourceId = source.id;
 
-    if (source.isWasm || getSymbols(getState(), source) || !isLoaded(source)) {
-      return;
-    }
+  if (!isLoaded(source)) {
+    await dispatch(loadSourceText({ source }));
+  }
+
+  await dispatch({
+    type: "SET_SYMBOLS",
+    sourceId,
+    [PROMISE]: parser.getSymbols(sourceId)
+  });
 
-    await dispatch({
-      type: "SET_SYMBOLS",
-      sourceId,
-      [PROMISE]: parser.getSymbols(sourceId)
-    });
+  const symbols = getSymbols(getState(), source);
+  if (symbols && symbols.framework) {
+    dispatch(updateTab(source, symbols.framework));
+  }
+
+  return symbols;
+}
+
+type Args = { source: Source };
 
-    const threads = getSourceThreads(getState(), source);
-    await Promise.all(threads.map(thread => dispatch(mapFrames(thread))));
-
-    const symbols = getSymbols(getState(), source);
-    if (symbols.framework) {
-      dispatch(updateTab(source, symbols.framework));
-    }
-  };
-}
+export const setSymbols: MemoizedAction<Args, ?Symbols> = memoizeableAction(
+  "setSymbols",
+  {
+    exitEarly: ({ source }) => source.isWasm,
+    hasValue: ({ source }, { getState }) => hasSymbols(getState(), source),
+    getValue: ({ source }, { getState }) => getSymbols(getState(), source),
+    createKey: ({ source }) => source.id,
+    action: ({ source }, thunkArgs) => doSetSymbols(source, thunkArgs)
+  }
+);
--- a/devtools/client/debugger/new/src/actions/sources/tests/loadSource.spec.js
+++ b/devtools/client/debugger/new/src/actions/sources/tests/loadSource.spec.js
@@ -20,27 +20,27 @@ import { getBreakpointsList } from "../.
 
 describe("loadSourceText", () => {
   it("should load source text", async () => {
     const store = createStore(sourceThreadClient);
     const { dispatch, getState } = store;
 
     const foo1Source = makeSource("foo1");
     await dispatch(actions.newSource(foo1Source));
-    await dispatch(actions.loadSourceText(foo1Source));
+    await dispatch(actions.loadSourceText({ source: foo1Source }));
     const fooSource = selectors.getSource(getState(), "foo1");
 
     if (!fooSource || typeof fooSource.text != "string") {
       throw new Error("bad fooSource");
     }
     expect(fooSource.text.indexOf("return foo1")).not.toBe(-1);
 
     const baseFoo2Source = makeSource("foo2");
     await dispatch(actions.newSource(baseFoo2Source));
-    await dispatch(actions.loadSourceText(baseFoo2Source));
+    await dispatch(actions.loadSourceText({ source: baseFoo2Source }));
     const foo2Source = selectors.getSource(getState(), "foo2");
 
     if (!foo2Source || typeof foo2Source.text != "string") {
       throw new Error("bad fooSource");
     }
     expect(foo2Source.text.indexOf("return foo2")).not.toBe(-1);
   });
 
@@ -79,31 +79,22 @@ describe("loadSourceText", () => {
     await dispatch(actions.newSource(fooGenSource));
 
     const location = {
       sourceId: fooOrigSource.id,
       line: 1,
       column: 0
     };
     await dispatch(actions.addBreakpoint(location, {}));
-    const breakpoint = selectors.getBreakpoint(getState(), location);
-    if (!breakpoint) {
-      throw new Error("no breakpoint");
-    }
-
-    expect(breakpoint.text).toBe("var fooGen = 42;");
-    expect(breakpoint.originalText).toBe("var fooOrig = 42;");
-
-    await dispatch(actions.loadSourceText(fooOrigSource));
 
     const breakpoint1 = getBreakpointsList(getState())[0];
     expect(breakpoint1.text).toBe("var fooGen = 42;");
     expect(breakpoint1.originalText).toBe("var fooOrig = 42;");
 
-    await dispatch(actions.loadSourceText(fooGenSource));
+    await dispatch(actions.loadSourceText({ source: fooGenSource }));
 
     const breakpoint2 = getBreakpointsList(getState())[0];
     expect(breakpoint2.text).toBe("var fooGen = 42;");
     expect(breakpoint2.originalText).toBe("var fooOrig = 42;");
   });
 
   it("loads two sources w/ one request", async () => {
     let resolve;
@@ -116,21 +107,21 @@ describe("loadSourceText", () => {
         }),
       getBreakpointPositions: async () => ({})
     });
     const id = "foo";
     const baseSource = makeSource(id, { loadedState: "unloaded" });
 
     await dispatch(actions.newSource(baseSource));
 
-    let source = selectors.getSource(getState(), id);
-    dispatch(actions.loadSourceText(source));
+    let source = selectors.getSourceFromId(getState(), id);
+    dispatch(actions.loadSourceText({ source }));
 
-    source = selectors.getSource(getState(), id);
-    const loading = dispatch(actions.loadSourceText(source));
+    source = selectors.getSourceFromId(getState(), id);
+    const loading = dispatch(actions.loadSourceText({ source }));
 
     if (!resolve) {
       throw new Error("no resolve");
     }
     resolve({ source: "yay", contentType: "text/javascript" });
     await loading;
     expect(count).toEqual(1);
 
@@ -148,41 +139,42 @@ describe("loadSourceText", () => {
           resolve = r;
         }),
       getBreakpointPositions: async () => ({})
     });
     const id = "foo";
     const baseSource = makeSource(id, { loadedState: "unloaded" });
 
     await dispatch(actions.newSource(baseSource));
-    let source = selectors.getSource(getState(), id);
-    const loading = dispatch(actions.loadSourceText(source));
+    let source = selectors.getSourceFromId(getState(), id);
+    const loading = dispatch(actions.loadSourceText({ source }));
 
     if (!resolve) {
       throw new Error("no resolve");
     }
     resolve({ source: "yay", contentType: "text/javascript" });
     await loading;
 
-    source = selectors.getSource(getState(), id);
-    await dispatch(actions.loadSourceText(source));
+    source = selectors.getSourceFromId(getState(), id);
+    await dispatch(actions.loadSourceText({ source }));
     expect(count).toEqual(1);
 
     source = selectors.getSource(getState(), id);
     expect(source && source.text).toEqual("yay");
   });
 
   it("should cache subsequent source text loads", async () => {
     const { dispatch, getState } = createStore(sourceThreadClient);
 
     const source = makeSource("foo1");
-    await dispatch(actions.loadSourceText(source));
-    const prevSource = selectors.getSource(getState(), "foo1");
+    dispatch(actions.newSource(source));
+    await dispatch(actions.loadSourceText({ source }));
+    const prevSource = selectors.getSourceFromId(getState(), "foo1");
 
-    await dispatch(actions.loadSourceText(prevSource));
+    await dispatch(actions.loadSourceText({ source: prevSource }));
     const curSource = selectors.getSource(getState(), "foo1");
 
     expect(prevSource === curSource).toBeTruthy();
   });
 
   it("should indicate a loading source", async () => {
     const store = createStore(sourceThreadClient);
     const { dispatch } = store;
@@ -190,27 +182,27 @@ describe("loadSourceText", () => {
     const source = makeSource("foo2");
     await dispatch(actions.newSource(source));
 
     const wasLoading = watchForState(store, state => {
       const fooSource = selectors.getSource(state, "foo2");
       return fooSource && fooSource.loadedState === "loading";
     });
 
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     expect(wasLoading()).toBe(true);
   });
 
   it("should indicate an errored source text", async () => {
     const { dispatch, getState } = createStore(sourceThreadClient);
 
     const source = makeSource("bad-id");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
     const badSource = selectors.getSource(getState(), "bad-id");
 
     if (!badSource || !badSource.error) {
       throw new Error("bad badSource");
     }
     expect(badSource.error.indexOf("unknown source")).not.toBe(-1);
   });
 });
--- a/devtools/client/debugger/new/src/actions/tests/ast.spec.js
+++ b/devtools/client/debugger/new/src/actions/tests/ast.spec.js
@@ -12,17 +12,16 @@ import {
   makeSource,
   makeOriginalSource,
   makeFrame,
   waitForState
 } from "../../utils/test-head";
 
 import readFixture from "./helpers/readFixture";
 const {
-  getSource,
   getSymbols,
   getOutOfScopeLocations,
   getInScopeLines,
   isSymbolsLoading,
   getFramework
 } = selectors;
 
 import { prefs } from "../../utils/prefs";
@@ -65,18 +64,20 @@ const evaluationResult = {
 describe("ast", () => {
   describe("setSymbols", () => {
     describe("when the source is loaded", () => {
       it("should be able to set symbols", async () => {
         const store = createStore(threadClient);
         const { dispatch, getState } = store;
         const base = makeSource("base.js");
         await dispatch(actions.newSource(base));
-        await dispatch(actions.loadSourceText(base));
-        await dispatch(actions.setSymbols("base.js"));
+        await dispatch(actions.loadSourceText({ source: base }));
+
+        const loadedSource = selectors.getSourceFromId(getState(), base.id);
+        await dispatch(actions.setSymbols({ source: loadedSource }));
         await waitForState(store, state => !isSymbolsLoading(state, base));
 
         const baseSymbols = getSymbols(getState(), base);
         expect(baseSymbols).toMatchSnapshot();
       });
     });
 
     describe("when the source is not loaded", () => {
@@ -103,31 +104,30 @@ describe("ast", () => {
         const store = createStore(threadClient, {}, sourceMaps);
         const { dispatch, getState } = store;
         const source = makeOriginalSource("reactComponent.js");
 
         await dispatch(actions.newSource(makeSource("reactComponent.js")));
 
         await dispatch(actions.newSource(source));
 
-        await dispatch(
-          actions.loadSourceText(getSource(getState(), source.id))
-        );
-        await dispatch(actions.setSymbols(source.id));
+        await dispatch(actions.loadSourceText({ source }));
+        const loadedSource = selectors.getSourceFromId(getState(), source.id);
+        await dispatch(actions.setSymbols({ source: loadedSource }));
 
         expect(getFramework(getState(), source)).toBe("React");
       });
 
       it("should not give false positive on non react components", async () => {
         const store = createStore(threadClient);
         const { dispatch, getState } = store;
         const base = makeSource("base.js");
         await dispatch(actions.newSource(base));
-        await dispatch(actions.loadSourceText(base));
-        await dispatch(actions.setSymbols("base.js"));
+        await dispatch(actions.loadSourceText({ source: base }));
+        await dispatch(actions.setSymbols({ source: base }));
 
         expect(getFramework(getState(), base)).toBe(undefined);
       });
     });
   });
 
   describe("getOutOfScopeLocations", () => {
     beforeEach(async () => {
--- a/devtools/client/debugger/new/src/actions/tests/expressions.spec.js
+++ b/devtools/client/debugger/new/src/actions/tests/expressions.spec.js
@@ -32,16 +32,17 @@ const mockThreadClient = {
             } else {
               resolve("boo");
             }
           })
       )
     ),
   getFrameScopes: async () => {},
   sourceContents: () => ({ source: "", contentType: "text/javascript" }),
+  getBreakpointPositions: async () => [],
   autocomplete: () => {
     return new Promise(resolve => {
       resolve({
         from: "foo",
         matches: ["toLocaleString", "toSource", "toString", "toolbar", "top"],
         matchProp: "to"
       });
     });
@@ -56,17 +57,16 @@ describe("expressions", () => {
     expect(selectors.getExpressions(getState()).size).toBe(1);
   });
 
   it("should not add empty expressions", () => {
     const { dispatch, getState } = createStore(mockThreadClient);
 
     dispatch(actions.addExpression((undefined: any)));
     dispatch(actions.addExpression(""));
-
     expect(selectors.getExpressions(getState()).size).toBe(0);
   });
 
   it("should not add invalid expressions", async () => {
     const { dispatch, getState } = createStore(mockThreadClient);
     await dispatch(actions.addExpression("foo#"));
     const state = getState();
     expect(selectors.getExpressions(state).size).toBe(0);
@@ -92,72 +92,64 @@ describe("expressions", () => {
     expect(selectors.getExpression(getState(), "bar")).toBeUndefined();
   });
 
   it("should delete an expression", async () => {
     const { dispatch, getState } = createStore(mockThreadClient);
 
     await dispatch(actions.addExpression("foo"));
     await dispatch(actions.addExpression("bar"));
-
     expect(selectors.getExpressions(getState()).size).toBe(2);
 
     const expression = selectors.getExpression(getState(), "foo");
     dispatch(actions.deleteExpression(expression));
-
     expect(selectors.getExpressions(getState()).size).toBe(1);
     expect(selectors.getExpression(getState(), "bar").input).toBe("bar");
   });
 
   it("should evaluate expressions global scope", async () => {
     const { dispatch, getState } = createStore(mockThreadClient);
 
     await dispatch(actions.addExpression("foo"));
     await dispatch(actions.addExpression("bar"));
-
     expect(selectors.getExpression(getState(), "foo").value).toBe("bla");
     expect(selectors.getExpression(getState(), "bar").value).toBe("bla");
 
     await dispatch(actions.evaluateExpressions());
-
     expect(selectors.getExpression(getState(), "foo").value).toBe("bla");
     expect(selectors.getExpression(getState(), "bar").value).toBe("bla");
   });
 
   it("should evaluate expressions in specific scope", async () => {
     const { dispatch, getState } = createStore(mockThreadClient);
     await createFrames(dispatch);
-
     await dispatch(actions.newSource(makeSource("source")));
-
     await dispatch(actions.addExpression("foo"));
     await dispatch(actions.addExpression("bar"));
 
     expect(selectors.getExpression(getState(), "foo").value).toBe("boo");
     expect(selectors.getExpression(getState(), "bar").value).toBe("boo");
 
     await dispatch(actions.evaluateExpressions());
 
     expect(selectors.getExpression(getState(), "foo").value).toBe("boo");
     expect(selectors.getExpression(getState(), "bar").value).toBe("boo");
   });
 
   it("should get the autocomplete matches for the input", async () => {
     const { dispatch, getState } = createStore(mockThreadClient);
-
     await dispatch(actions.autocomplete("to", 2));
-
     expect(selectors.getAutocompleteMatchset(getState())).toMatchSnapshot();
   });
 });
 
 async function createFrames(dispatch) {
   const frame = makeMockFrame();
-
   await dispatch(actions.newSource(makeSource("example.js")));
+  await dispatch(actions.newSource(makeSource("source")));
 
   await dispatch(
     actions.paused({
       thread: "UnknownThread",
       frame,
       frames: [frame],
       why: { type: "just because" }
     })
--- a/devtools/client/debugger/new/src/actions/tests/pending-breakpoints.spec.js
+++ b/devtools/client/debugger/new/src/actions/tests/pending-breakpoints.spec.js
@@ -73,17 +73,17 @@ describe("when adding breakpoints", () =
       mockClient({ "5": [1] }),
       loadInitialState(),
       mockSourceMaps()
     );
 
     const source = makeSource("foo.js");
     await dispatch(actions.newSource(source));
     await dispatch(actions.newSource(makeSource("foo.js")));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     const bp = generateBreakpoint("foo.js", 5, 1);
     const id = makePendingLocationId(bp.location);
 
     await dispatch(actions.addBreakpoint(bp.location));
     const pendingBps = selectors.getPendingBreakpoints(getState());
 
     expect(selectors.getPendingBreakpointList(getState())).toHaveLength(2);
@@ -114,18 +114,18 @@ describe("when adding breakpoints", () =
       const source2 = makeSource("foo2");
 
       await dispatch(actions.newSource(makeSource("foo")));
       await dispatch(actions.newSource(makeSource("foo2")));
 
       await dispatch(actions.newSource(source1));
       await dispatch(actions.newSource(source2));
 
-      await dispatch(actions.loadSourceText(source1));
-      await dispatch(actions.loadSourceText(source2));
+      await dispatch(actions.loadSourceText({ source: source1 }));
+      await dispatch(actions.loadSourceText({ source: source2 }));
 
       await dispatch(actions.addBreakpoint(breakpoint1.location));
       await dispatch(actions.addBreakpoint(breakpoint2.location));
 
       const pendingBps = selectors.getPendingBreakpoints(getState());
 
       // NOTE the sourceId should be `foo2/originalSource`, but is `foo2`
       // because we do not have a real source map for `getOriginalLocation`
@@ -139,17 +139,17 @@ describe("when adding breakpoints", () =
         mockClient({ "5": [0] }),
         loadInitialState(),
         mockSourceMaps()
       );
 
       const source = makeSource("foo");
       await dispatch(actions.newSource(makeSource("foo")));
       await dispatch(actions.newSource(source));
-      await dispatch(actions.loadSourceText(source));
+      await dispatch(actions.loadSourceText({ source }));
 
       await dispatch(
         actions.addBreakpoint(breakpoint1.location, { hidden: true })
       );
       const pendingBps = selectors.getPendingBreakpoints(getState());
 
       expect(pendingBps[breakpointLocationId1]).toBeUndefined();
     });
@@ -165,18 +165,18 @@ describe("when adding breakpoints", () =
       await dispatch(actions.newSource(makeSource("foo2")));
 
       const source1 = makeSource("foo");
       const source2 = makeSource("foo2");
 
       await dispatch(actions.newSource(source1));
       await dispatch(actions.newSource(source2));
 
-      await dispatch(actions.loadSourceText(source1));
-      await dispatch(actions.loadSourceText(source2));
+      await dispatch(actions.loadSourceText({ source: source1 }));
+      await dispatch(actions.loadSourceText({ source: source2 }));
 
       await dispatch(actions.addBreakpoint(breakpoint1.location));
       await dispatch(actions.addBreakpoint(breakpoint2.location));
       await dispatch(actions.removeBreakpoint(breakpoint1));
 
       const pendingBps = selectors.getPendingBreakpoints(getState());
       expect(pendingBps.hasOwnProperty(breakpointLocationId1)).toBe(false);
       expect(pendingBps.hasOwnProperty(breakpointLocationId2)).toBe(true);
@@ -192,17 +192,17 @@ describe("when changing an existing brea
       mockSourceMaps()
     );
     const bp = generateBreakpoint("foo");
     const id = makePendingLocationId(bp.location);
 
     const source = makeSource("foo");
     await dispatch(actions.newSource(source));
     await dispatch(actions.newSource(makeSource("foo")));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.addBreakpoint(bp.location));
     await dispatch(
       actions.setBreakpointOptions(bp.location, { condition: "2" })
     );
     const bps = selectors.getPendingBreakpoints(getState());
     const breakpoint = bps[id];
     expect(breakpoint.options.condition).toBe("2");
@@ -216,17 +216,17 @@ describe("when changing an existing brea
     );
     const bp = generateBreakpoint("foo");
     const id = makePendingLocationId(bp.location);
 
     await dispatch(actions.newSource(makeSource("foo")));
 
     const source = makeSource("foo");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.addBreakpoint(bp.location));
     await dispatch(actions.disableBreakpoint(bp));
     const bps = selectors.getPendingBreakpoints(getState());
     const breakpoint = bps[id];
     expect(breakpoint.disabled).toBe(true);
   });
 
@@ -236,17 +236,17 @@ describe("when changing an existing brea
       loadInitialState(),
       mockSourceMaps()
     );
     const bp = generateBreakpoint("foo.js");
 
     const source = makeSource("foo.js");
     await dispatch(actions.newSource(source));
     await dispatch(actions.newSource(makeSource("foo.js")));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     const id = makePendingLocationId(bp.location);
 
     await dispatch(actions.addBreakpoint(bp.location));
     await dispatch(
       actions.setBreakpointOptions(bp.location, { condition: "2" })
     );
     const bps = selectors.getPendingBreakpoints(getState());
@@ -273,17 +273,17 @@ describe("initializing when pending brea
       mockSourceMaps()
     );
     const bar = generateBreakpoint("bar.js", 5, 1);
 
     await dispatch(actions.newSource(makeSource("bar.js")));
 
     const source = makeSource("bar.js");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
     await dispatch(actions.addBreakpoint(bar.location));
 
     const bps = selectors.getPendingBreakpointList(getState());
     expect(bps).toHaveLength(2);
   });
 
   it("adding bps doesn't remove existing pending breakpoints", async () => {
     const { dispatch, getState } = createStore(
@@ -291,17 +291,17 @@ describe("initializing when pending brea
       loadInitialState(),
       mockSourceMaps()
     );
     const bp = generateBreakpoint("foo.js");
 
     const source = makeSource("foo.js");
     await dispatch(actions.newSource(source));
     await dispatch(actions.newSource(makeSource("foo.js")));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.addBreakpoint(bp.location));
 
     const bps = selectors.getPendingBreakpointList(getState());
     expect(bps).toHaveLength(2);
   });
 });
 
@@ -313,17 +313,17 @@ describe("initializing with disabled pen
       mockSourceMaps()
     );
 
     const { getState, dispatch } = store;
     const source = makeSource("bar.js");
 
     await dispatch(actions.newSource(makeSource("bar.js")));
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await waitForState(store, state => {
       const bps = selectors.getBreakpointsForSource(state, source.id);
       return bps && Object.values(bps).length > 0;
     });
 
     const bp = selectors.getBreakpointForLocation(getState(), {
       line: 5,
@@ -349,17 +349,17 @@ describe("adding sources", () => {
     const { getState, dispatch } = store;
 
     expect(selectors.getBreakpointCount(getState())).toEqual(0);
 
     const source = makeSource("bar.js");
 
     await dispatch(actions.newSource(makeSource("bar.js")));
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await waitForState(store, state => selectors.getBreakpointCount(state) > 0);
 
     expect(selectors.getBreakpointCount(getState())).toEqual(1);
   });
 
   it("corresponding breakpoints are added to the original source", async () => {
     const source = makeSource("bar.js", { sourceMapURL: "foo" });
@@ -400,15 +400,15 @@ describe("adding sources", () => {
 
     expect(selectors.getBreakpointCount(getState())).toEqual(0);
 
     const source1 = makeSource("bar.js");
     const source2 = makeSource("foo.js");
     await dispatch(actions.newSource(makeSource("bar.js")));
     await dispatch(actions.newSource(makeSource("foo.js")));
     await dispatch(actions.newSources([source1, source2]));
-    await dispatch(actions.loadSourceText(source1));
-    await dispatch(actions.loadSourceText(source2));
+    await dispatch(actions.loadSourceText({ source: source1 }));
+    await dispatch(actions.loadSourceText({ source: source2 }));
 
     await waitForState(store, state => selectors.getBreakpointCount(state) > 0);
     expect(selectors.getBreakpointCount(getState())).toEqual(1);
   });
 });
--- a/devtools/client/debugger/new/src/actions/tests/project-text-search.spec.js
+++ b/devtools/client/debugger/new/src/actions/tests/project-text-search.spec.js
@@ -63,24 +63,30 @@ describe("project text search", () => {
 
     await dispatch(actions.searchSources(mockQuery));
 
     const results = getTextSearchResults(getState());
     expect(results).toMatchSnapshot();
   });
 
   it("should ignore sources with minified versions", async () => {
-    const source1 = makeSource("bar", { sourceMapURL: "bar:formatted" });
+    const source1 = makeSource("bar", {
+      sourceMapURL: "bar:formatted",
+      loadedState: "loaded",
+      source: "function bla(x, y) { const bar = 4; return 2;}",
+      contentType: "text/javascript"
+    });
     const source2 = makeSource("bar:formatted");
 
     const mockMaps = {
       getOriginalSourceText: async () => ({
         source: "function bla(x, y) {\n const bar = 4; return 2;\n}",
         contentType: "text/javascript"
       }),
+      applySourceMap: async () => {},
       getOriginalURLs: async () => [source2.url],
       getGeneratedRangesForOriginal: async () => [],
       getOriginalLocations: async items => items
     };
 
     const { dispatch, getState } = createStore(threadClient, {}, mockMaps);
     const mockQuery = "bla";
 
@@ -93,17 +99,17 @@ describe("project text search", () => {
     expect(results).toMatchSnapshot();
   });
 
   it("should search a specific source", async () => {
     const { dispatch, getState } = createStore(threadClient);
 
     const source = makeSource("bar");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     dispatch(actions.addSearchQuery("bla"));
 
     const barSource = getSource(getState(), "bar");
     if (!barSource) {
       throw new Error("no barSource");
     }
     const sourceId = barSource.id;
--- a/devtools/client/debugger/new/src/components/Editor/DebugLine.js
+++ b/devtools/client/debugger/new/src/components/Editor/DebugLine.js
@@ -37,35 +37,35 @@ type TextClasses = {
 
 function isDocumentReady(source, frame) {
   return frame && isLoaded(source) && hasDocument(frame.location.sourceId);
 }
 
 export class DebugLine extends Component<Props> {
   debugExpression: null;
 
-  componentDidUpdate(prevProps: Props) {
-    const { why, frame, source } = this.props;
-
-    startOperation();
-    this.clearDebugLine(prevProps.why, prevProps.frame, prevProps.source);
-    this.setDebugLine(why, frame, source);
-    endOperation();
-  }
-
   componentDidMount() {
     const { why, frame, source } = this.props;
     this.setDebugLine(why, frame, source);
   }
 
   componentWillUnmount() {
     const { why, frame, source } = this.props;
     this.clearDebugLine(why, frame, source);
   }
 
+  componentDidUpdate(prevProps: Props) {
+    const { why, frame, source } = this.props;
+
+    startOperation();
+    this.clearDebugLine(prevProps.why, prevProps.frame, prevProps.source);
+    this.setDebugLine(why, frame, source);
+    endOperation();
+  }
+
   setDebugLine(why: Why, frame: Frame, source: Source) {
     if (!isDocumentReady(source, frame)) {
       return;
     }
     const sourceId = frame.location.sourceId;
     const doc = getDocument(sourceId);
 
     let { line, column } = toEditorPosition(frame.location);
--- a/devtools/client/debugger/new/src/components/Editor/index.js
+++ b/devtools/client/debugger/new/src/components/Editor/index.js
@@ -49,17 +49,16 @@ import ColumnBreakpoints from "./ColumnB
 import DebugLine from "./DebugLine";
 import HighlightLine from "./HighlightLine";
 import EmptyLines from "./EmptyLines";
 import EditorMenu from "./EditorMenu";
 import ConditionalPanel from "./ConditionalPanel";
 
 import {
   showSourceText,
-  updateDocument,
   showLoading,
   showErrorMessage,
   getEditor,
   clearEditor,
   getCursorLine,
   lineAtHeight,
   toSourceLine,
   getDocument,
@@ -122,35 +121,35 @@ class Editor extends PureComponent<Props
 
     this.state = {
       highlightedLineRange: null,
       editor: (null: any),
       contextMenu: null
     };
   }
 
-  componentWillReceiveProps(nextProps) {
-    if (!this.state.editor) {
-      return;
+  componentWillReceiveProps(nextProps: Props) {
+    let editor = this.state.editor;
+
+    if (!this.state.editor && nextProps.selectedSource) {
+      editor = this.setupEditor();
     }
 
     startOperation();
-    resizeBreakpointGutter(this.state.editor.codeMirror);
-    resizeToggleButton(this.state.editor.codeMirror);
-    endOperation();
-  }
+    this.setText(nextProps, editor);
+    this.setSize(nextProps, editor);
+    this.scrollToLocation(nextProps, editor);
 
-  componentWillUpdate(nextProps) {
-    if (!this.state.editor) {
-      return;
+    if (this.props.selectedSource != nextProps.selectedSource) {
+      this.props.updateViewport();
+      resizeBreakpointGutter(editor.codeMirror);
+      resizeToggleButton(editor.codeMirror);
     }
 
-    this.setText(nextProps);
-    this.setSize(nextProps);
-    this.scrollToLocation(nextProps);
+    endOperation();
   }
 
   setupEditor() {
     const editor = getEditor();
 
     // disables the default search shortcuts
     // $FlowIgnore
     editor._initShortcuts = () => {};
@@ -158,21 +157,16 @@ class Editor extends PureComponent<Props
     const node = ReactDOM.findDOMNode(this);
     if (node instanceof HTMLElement) {
       editor.appendToLocalElement(node.querySelector(".editor-mount"));
     }
 
     const { codeMirror } = editor;
     const codeMirrorWrapper = codeMirror.getWrapperElement();
 
-    startOperation();
-    resizeBreakpointGutter(codeMirror);
-    resizeToggleButton(codeMirror);
-    endOperation();
-
     codeMirror.on("gutterClick", this.onGutterClick);
 
     // Set code editor wrapper to be focusable
     codeMirrorWrapper.tabIndex = 0;
     codeMirrorWrapper.addEventListener("keydown", e => this.onKeyDown(e));
     codeMirrorWrapper.addEventListener("click", e => this.onClick(e));
     codeMirrorWrapper.addEventListener("mouseover", onMouseOver(codeMirror));
 
@@ -254,36 +248,16 @@ class Editor extends PureComponent<Props
     shortcuts.off(L10N.getStr("sourceTabs.closeTab.key"));
     shortcuts.off(L10N.getStr("toggleBreakpoint.key"));
     shortcuts.off(L10N.getStr("toggleCondPanel.breakpoint.key"));
     shortcuts.off(L10N.getStr("toggleCondPanel.logPoint.key"));
     shortcuts.off(searchAgainPrevKey);
     shortcuts.off(searchAgainKey);
   }
 
-  componentDidUpdate(prevProps, prevState) {
-    const { selectedSource } = this.props;
-    // NOTE: when devtools are opened, the editor is not set when
-    // the source loads so we need to wait until the editor is
-    // set to update the text and size.
-    if (!prevState.editor && selectedSource) {
-      if (!this.state.editor) {
-        const editor = this.setupEditor();
-        updateDocument(editor, selectedSource);
-      } else {
-        this.setText(this.props);
-        this.setSize(this.props);
-      }
-    }
-
-    if (prevProps.selectedSource != selectedSource) {
-      this.props.updateViewport();
-    }
-  }
-
   getCurrentLine() {
     const { codeMirror } = this.state.editor;
     const { selectedSource } = this.props;
     if (!selectedSource) {
       return;
     }
 
     const line = getCursorLine(codeMirror);
@@ -482,20 +456,18 @@ class Editor extends PureComponent<Props
         line: line,
         sourceId: selectedSource.id,
         sourceUrl: selectedSource.url
       },
       log
     );
   };
 
-  shouldScrollToLocation(nextProps) {
+  shouldScrollToLocation(nextProps, editor) {
     const { selectedLocation, selectedSource } = this.props;
-    const { editor } = this.state;
-
     if (
       !editor ||
       !nextProps.selectedSource ||
       !nextProps.selectedLocation ||
       !nextProps.selectedLocation.line ||
       !isLoaded(nextProps.selectedSource)
     ) {
       return false;
@@ -505,67 +477,67 @@ class Editor extends PureComponent<Props
       (!selectedSource || !isLoaded(selectedSource)) &&
       isLoaded(nextProps.selectedSource);
     const locationChanged = selectedLocation !== nextProps.selectedLocation;
     const symbolsChanged = nextProps.symbols != this.props.symbols;
 
     return isFirstLoad || locationChanged || symbolsChanged;
   }
 
-  scrollToLocation(nextProps) {
-    const { editor } = this.state;
+  scrollToLocation(nextProps, editor) {
     const { selectedLocation, selectedSource } = nextProps;
 
-    if (selectedLocation && this.shouldScrollToLocation(nextProps)) {
+    if (selectedLocation && this.shouldScrollToLocation(nextProps, editor)) {
       let { line, column } = toEditorPosition(selectedLocation);
 
       if (selectedSource && hasDocument(selectedSource.id)) {
         const doc = getDocument(selectedSource.id);
         const lineText: ?string = doc.getLine(line);
         column = Math.max(column, getIndentation(lineText));
       }
+
       scrollToColumn(editor.codeMirror, line, column);
     }
   }
 
-  setSize(nextProps) {
-    if (!this.state.editor) {
+  setSize(nextProps, editor) {
+    if (!editor) {
       return;
     }
 
     if (
       nextProps.startPanelSize !== this.props.startPanelSize ||
       nextProps.endPanelSize !== this.props.endPanelSize
     ) {
-      this.state.editor.codeMirror.setSize();
+      editor.codeMirror.setSize();
     }
   }
 
-  setText(props) {
+  setText(props, editor) {
     const { selectedSource, symbols } = props;
 
-    if (!this.state.editor) {
+    if (!editor) {
       return;
     }
 
     // check if we previously had a selected source
     if (!selectedSource) {
       return this.clearEditor();
     }
 
     if (!isLoaded(selectedSource)) {
-      return showLoading(this.state.editor);
+      return showLoading(editor);
     }
 
     if (selectedSource.error) {
       return this.showErrorMessage(selectedSource.error);
     }
 
     if (selectedSource) {
-      return showSourceText(this.state.editor, selectedSource, symbols);
+      return showSourceText(editor, selectedSource, symbols);
     }
   }
 
   clearEditor() {
     const { editor } = this.state;
     if (!editor) {
       return;
     }
--- a/devtools/client/debugger/new/src/reducers/ast.js
+++ b/devtools/client/debugger/new/src/reducers/ast.js
@@ -11,17 +11,19 @@
 
 import type { AstLocation, SymbolDeclarations } from "../workers/parser";
 
 import type { Source } from "../types";
 import type { Action, DonePromiseAction } from "../actions/types";
 
 type EmptyLinesType = number[];
 
-export type Symbols = SymbolDeclarations | {| loading: true |};
+export type LoadedSymbols = SymbolDeclarations;
+export type Symbols = LoadedSymbols | {| loading: true |};
+
 export type EmptyLinesMap = { [k: string]: EmptyLinesType };
 export type SymbolsMap = { [k: string]: Symbols };
 
 export type SourceMetaDataType = {
   framework: ?string
 };
 
 export type SourceMetaDataMap = { [k: string]: SourceMetaDataType };
--- a/devtools/client/debugger/new/src/reducers/sources.js
+++ b/devtools/client/debugger/new/src/reducers/sources.js
@@ -498,16 +498,24 @@ export function getGeneratedSource(
 
   if (isGenerated(source)) {
     return source;
   }
 
   return getSourceFromId(state, originalToGeneratedId(source.id));
 }
 
+export function getGeneratedSourceById(
+  state: OuterState,
+  sourceId: string
+): Source {
+  const generatedSourceId = originalToGeneratedId(sourceId);
+  return getSourceFromId(state, generatedSourceId);
+}
+
 export function getPendingSelectedLocation(state: OuterState) {
   return state.sources.pendingSelectedLocation;
 }
 
 export function getPrettySource(state: OuterState, id: ?string) {
   if (!id) {
     return;
   }
--- a/devtools/client/debugger/new/src/reducers/types.js
+++ b/devtools/client/debugger/new/src/reducers/types.js
@@ -45,9 +45,9 @@ export type PendingSelectedLocation = {
   line?: number,
   column?: number
 };
 
 export type { SourcesMap, SourcesMapByThread } from "./sources";
 export type { ActiveSearchType, OrientationType } from "./ui";
 export type { BreakpointsMap, XHRBreakpointsList } from "./breakpoints";
 export type { Command } from "./pause";
-export type { Symbols } from "./ast";
+export type { LoadedSymbols, Symbols } from "./ast";
--- a/devtools/client/debugger/new/src/selectors/visibleColumnBreakpoints.js
+++ b/devtools/client/debugger/new/src/selectors/visibleColumnBreakpoints.js
@@ -1,12 +1,12 @@
-// @flow
 /* 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/>. */
+// @flow
 
 import { groupBy } from "lodash";
 import { createSelector } from "reselect";
 
 import {
   getViewport,
   getSource,
   getSelectedSource,
@@ -159,17 +159,17 @@ export const visibleColumnBreakpoints: S
   getViewport,
   getSelectedSource,
   getColumnBreakpoints
 );
 
 export function getFirstBreakpointPosition(
   state: State,
   { line, sourceId }: SourceLocation
-) {
+): ?BreakpointPosition {
   const positions = getBreakpointPositionsForSource(state, sourceId);
   const source = getSource(state, sourceId);
 
   if (!source || !positions) {
     return;
   }
 
   return sortSelectedLocations(positions, source).find(
--- a/devtools/client/debugger/new/src/utils/breakpoint/astBreakpointLocation.js
+++ b/devtools/client/debugger/new/src/utils/breakpoint/astBreakpointLocation.js
@@ -1,15 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 
-import { getSymbols } from "../../workers/parser";
 import { findClosestFunction } from "../ast";
 
 import type { SourceLocation, Source, ASTLocation } from "../../types";
 import type { Symbols } from "../../reducers/ast";
 
 export function getASTLocation(
   source: Source,
   symbols: ?Symbols,
@@ -28,18 +27,20 @@ export function getASTLocation(
       name: scope.name,
       offset: { line, column: undefined },
       index: scope.index
     };
   }
   return { name: undefined, offset: location, index: 0 };
 }
 
-export async function findFunctionByName(
-  source: Source,
+export function findFunctionByName(
+  symbols: Symbols,
   name: ?string,
   index: number
 ) {
-  const symbols = await getSymbols(source.id);
+  if (symbols.loading) {
+    return null;
+  }
+
   const functions = symbols.functions;
-
   return functions.find(node => node.name === name && node.index === index);
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/src/utils/memoizableAction.js
@@ -0,0 +1,84 @@
+/* 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/>. */
+
+// @flow
+
+import type { ThunkArgs } from "../actions/types";
+
+export type MemoizedAction<
+  Args,
+  Result
+> = Args => ThunkArgs => Promise<?Result>;
+type MemoizableActionParams<Args, Result> = {
+  exitEarly?: (args: Args, thunkArgs: ThunkArgs) => boolean,
+  hasValue: (args: Args, thunkArgs: ThunkArgs) => boolean,
+  getValue: (args: Args, thunkArgs: ThunkArgs) => Result,
+  createKey: (args: Args, thunkArgs: ThunkArgs) => string,
+  action: (args: Args, thunkArgs: ThunkArgs) => Promise<Result>
+};
+
+/*
+ * memoizableActon is a utility for actions that should only be performed
+ * once per key. It is useful for loading sources, parsing symbols ...
+ *
+ * @exitEarly - if true, do not attempt to perform the action
+ * @hasValue - checks to see if the result is in the redux store
+ * @getValue - gets the result from the redux store
+ * @createKey - creates a key for the requests map
+ * @action - kicks off the async work for the action
+ *
+ *
+ * For Example
+ *
+ * export const setItem = memoizeableAction(
+ *   "setItem",
+ *   {
+ *     hasValue: ({ a }, { getState }) => hasItem(getState(), a),
+ *     getValue: ({ a }, { getState }) => getItem(getState(), a),
+ *     createKey: ({ a }) => a,
+ *     action: ({ a }, thunkArgs) => doSetItem(a, thunkArgs)
+ *   }
+ * );
+ * 
+ */
+export function memoizeableAction<Args, Result>(
+  name: string,
+  {
+    hasValue,
+    getValue,
+    createKey,
+    action,
+    exitEarly
+  }: MemoizableActionParams<Args, Result>
+): MemoizedAction<Args, Result> {
+  const requests = new Map();
+  return args => async (thunkArgs: ThunkArgs) => {
+    if (exitEarly && exitEarly(args, thunkArgs)) {
+      return;
+    }
+
+    if (hasValue(args, thunkArgs)) {
+      return getValue(args, thunkArgs);
+    }
+
+    const key = createKey(args, thunkArgs);
+    if (!requests.has(key)) {
+      requests.set(
+        key,
+        (async () => {
+          try {
+            await action(args, thunkArgs);
+          } catch (e) {
+            console.warn(`Action ${name} had an exception:`, e);
+          } finally {
+            requests.delete(key);
+          }
+        })()
+      );
+    }
+
+    await requests.get(key);
+    return getValue(args, thunkArgs);
+  };
+}
--- a/devtools/client/debugger/new/src/utils/moz.build
+++ b/devtools/client/debugger/new/src/utils/moz.build
@@ -26,16 +26,17 @@ CompiledModules(
     'fromJS.js',
     'function.js',
     'indentation.js',
     'isMinified.js',
     'location.js',
     'log.js',
     'makeRecord.js',
     'memoize.js',
+    'memoizableAction.js',
     'path.js',
     'prefs.js',
     'preview.js',
     'project-search.js',
     'quick-open.js',
     'result-list.js',
     'source-maps.js',
     'source-queue.js',
--- a/devtools/client/debugger/new/test/mochitest/browser.ini
+++ b/devtools/client/debugger/new/test/mochitest/browser.ini
@@ -631,16 +631,17 @@ support-files =
   examples/doc-sourcemaps3.html
   examples/doc-sourcemap-bogus.html
   examples/doc-sources.html
   examples/doc-sources-querystring.html
   examples/doc-strict.html
   examples/doc-pause-points.html
   examples/doc-return-values.html
   examples/doc-wasm-sourcemaps.html
+  examples/doc-audiocontext.html
   examples/asm.js
   examples/async.js
   examples/bogus-map.js
   examples/entry.js
   examples/exceptions.js
   examples/long.js
   examples/math.min.js
   examples/nested/nested-source.js
@@ -660,16 +661,17 @@ support-files =
   examples/doc-windowless-workers.html
   examples/doc-windowless-workers-early-breakpoint.html
   examples/simple-worker.js
   examples/doc-event-handler.html
   examples/doc-eval-throw.html
   examples/doc-sourceURL-breakpoint.html
 
 [browser_dbg-asm.js]
+[browser_dbg-audiocontext.js]
 [browser_dbg-async-stepping.js]
 [browser_dbg-sourcemapped-breakpoint-console.js]
 skip-if = (os == "win" && ccov) # Bug 1453549
 [browser_dbg-xhr-breakpoints.js]
 [browser_dbg-xhr-run-to-completion.js]
 [browser_dbg-scroll-run-to-completion.js]
 [browser_dbg-sourcemapped-scopes.js]
 skip-if = ccov || debug || (verify && debug && (os == 'linux')) # Bug 1441545, 1536253 - very slow on debug
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-audiocontext.js
@@ -0,0 +1,18 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the AudioContext are paused and resume appropriately when using the
+// debugger.
+
+add_task(async function() {
+  const dbg = await initDebugger("doc-audiocontext.html");
+
+  invokeInTab("myFunction");
+  invokeInTab("suspendAC");
+  invokeInTab("debuggerStatement");
+  await waitForPaused(dbg);
+  invokeInTab("checkACState");
+  ok(true, "No AudioContext state transition are caused by the debugger")
+});
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-debugger-buttons.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-debugger-buttons.js
@@ -1,10 +1,11 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* 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/>. */
 
 function clickButton(dbg, button) {
   const resumeFired = waitForDispatch(dbg, "COMMAND");
   clickElement(dbg, button);
   return resumeFired;
 }
 
 async function clickStepOver(dbg) {
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-pretty-print-paused.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-pretty-print-paused.js
@@ -1,21 +1,28 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* 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/>. */
 
 // Tests pretty-printing a source that is currently paused.
 
 add_task(async function() {
   const dbg = await initDebugger("doc-minified.html", "math.min.js");
+  const thread = dbg.selectors.getCurrentThread(dbg.getState());
 
   await selectSource(dbg, "math.min.js");
   await addBreakpoint(dbg, "math.min.js", 2);
 
   invokeInTab("arithmetic");
   await waitForPaused(dbg);
   assertPausedLocation(dbg);
 
   clickElement(dbg, "prettyPrintButton");
   await waitForSelectedSource(dbg, "math.min.js:formatted");
+  await waitForState(
+    dbg,
+    state => dbg.selectors.getSelectedFrame(state, thread).location.line == 18
+  );
   assertPausedLocation(dbg);
+  await assertEditorBreakpoint(dbg, 18, true);
 
   await resume(dbg);
 });
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-xhr-run-to-completion.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-xhr-run-to-completion.js
@@ -2,17 +2,19 @@
  * 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/>. */
 
 // Test that XHR handlers are not called when pausing in the debugger.
 add_task(async function() {
   const dbg = await initDebugger("doc-xhr-run-to-completion.html");
   invokeInTab("singleRequest", "doc-xhr-run-to-completion.html");
   await waitForPaused(dbg);
+  await waitForSelectedLocation(dbg, 23);
   assertPausedLocation(dbg);
+
   resume(dbg);
   await once(Services.ppmm, "test passed");
 });
 
 // Test that XHR handlers are not called when pausing in the debugger,
 // including when there are multiple XHRs and multiple times we pause before
 // they can be processed.
 add_task(async function() {
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc-audiocontext.html
@@ -0,0 +1,48 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+<head>
+<meta charset="utf-8"/>
+<title>Debugger test page</title>
+</head>
+
+<body>
+<button id="start" onclick="myFunction()">start ac</button>
+<button id="suspend" onclick="suspendAC()">suspend ac</button>
+<button id="break" onclick="debuggerStatement()">break</button>
+<button id="check" onclick="checkACState()">check ac state</button>
+<script type="text/javascript">
+var ac = null;
+var suspend_called = false;
+function suspendAC() {
+  suspend_called = true;
+  ac.suspend();
+}
+
+function debuggerStatement() {
+  debugger;
+}
+
+function checkACState() {
+  if (ac.state != "suspended") {
+    throw "AudioContext should be suspended.";
+  }
+}
+
+function myFunction() {
+  ac = new AudioContext();
+  function statechange_suspend() {
+    ac.onstatechange = statechange_fail;
+  }
+  function statechange_fail() {
+    throw "No state change should occur when paused in the debugger.";
+  }
+  ac.onstatechange = function() {
+    ac.onstatechange = statechange_suspend;
+  }
+}
+</script>
+</body>
+</html>
--- a/devtools/client/debugger/new/test/mochitest/helpers.js
+++ b/devtools/client/debugger/new/test/mochitest/helpers.js
@@ -202,16 +202,26 @@ async function waitForElement(dbg, name,
   return findElement(dbg, name, ...args);
 }
 
 async function waitForElementWithSelector(dbg, selector) {
   await waitUntil(() => findElementWithSelector(dbg, selector));
   return findElementWithSelector(dbg, selector);
 }
 
+function waitForSelectedLocation(dbg, line ) {
+  return waitForState(
+    dbg,
+    state => {
+      const location = dbg.selectors.getSelectedLocation(state)
+      return location && location.line == line
+    }
+  );
+}
+
 function waitForSelectedSource(dbg, url) {
   const {
     getSelectedSource,
     hasSymbols,
     hasBreakpointPositions
   } = dbg.selectors;
 
   return waitForState(
--- a/devtools/client/inspector/fonts/components/FontEditor.js
+++ b/devtools/client/inspector/fonts/components/FontEditor.js
@@ -8,16 +8,17 @@ const { createFactory, PureComponent } =
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const FontAxis = createFactory(require("./FontAxis"));
 const FontName = createFactory(require("./FontName"));
 const FontSize = createFactory(require("./FontSize"));
 const FontStyle = createFactory(require("./FontStyle"));
 const FontWeight = createFactory(require("./FontWeight"));
+const LetterSpacing = createFactory(require("./LetterSpacing"));
 const LineHeight = createFactory(require("./LineHeight"));
 
 const { getStr } = require("../utils/l10n");
 const Types = require("../types");
 
 // Maximum number of font families to be shown by default. Any others will be hidden
 // under a collapsed <details> element with a toggle to reveal them.
 const MAX_FONTS = 3;
@@ -156,16 +157,25 @@ class FontEditor extends PureComponent {
     return value !== null && LineHeight({
       key: `${this.props.fontEditor.id}:line-height`,
       disabled: this.props.fontEditor.disabled,
       onChange: this.props.onPropertyChange,
       value,
     });
   }
 
+  renderLetterSpacing(value) {
+    return value !== null && LetterSpacing({
+      key: `${this.props.fontEditor.id}:letter-spacing`,
+      disabled: this.props.fontEditor.disabled,
+      onChange: this.props.onPropertyChange,
+      value,
+    });
+  }
+
   renderFontStyle(value) {
     return value && FontStyle({
       onChange: this.props.onPropertyChange,
       disabled: this.props.fontEditor.disabled,
       value,
     });
   }
 
@@ -278,16 +288,18 @@ class FontEditor extends PureComponent {
       // Always render UI for used fonts.
       this.renderUsedFonts(fonts),
       // Render UI for font variation instances if they are defined.
       hasFontInstances && this.renderInstances(font.variationInstances, instance),
       // Always render UI for font size.
       this.renderFontSize(properties["font-size"]),
       // Always render UI for line height.
       this.renderLineHeight(properties["line-height"]),
+      // Always render UI for letter spacing.
+      this.renderLetterSpacing(properties["letter-spacing"]),
       // Render UI for font weight if no "wght" registered axis is defined.
       !hasWeightAxis && this.renderFontWeight(properties["font-weight"]),
       // Render UI for font style if no "slnt" or "ital" registered axis is defined.
       !hasSlantOrItalicAxis && this.renderFontStyle(properties["font-style"]),
       // Render UI for each variable font axis if any are defined.
       hasFontAxes && this.renderAxes(font.variationAxes, axes)
     );
   }
--- a/devtools/client/inspector/fonts/components/FontPropertyValue.js
+++ b/devtools/client/inspector/fonts/components/FontPropertyValue.js
@@ -14,45 +14,55 @@ const PropTypes = require("devtools/clie
 
 const { toFixed } = require("../utils/font-utils");
 
 class FontPropertyValue extends PureComponent {
   static get propTypes() {
     return {
       // Whether to allow input values above the value defined by the `max` prop.
       allowOverflow: PropTypes.bool,
+      // Whether to allow input values below the value defined by the `min` prop.
+      allowUnderflow: PropTypes.bool,
       className: PropTypes.string,
       defaultValue: PropTypes.number,
       disabled: PropTypes.bool.isRequired,
       label: PropTypes.string.isRequired,
       min: PropTypes.number.isRequired,
       // Whether to show the `min` prop value as a label.
       minLabel: PropTypes.bool,
       max: PropTypes.number.isRequired,
       // Whether to show the `max` prop value as a label.
       maxLabel: PropTypes.bool,
       name: PropTypes.string.isRequired,
       // Whether to show the `name` prop value as an extra label (used to show axis tags).
       nameLabel: PropTypes.bool,
       onChange: PropTypes.func.isRequired,
       step: PropTypes.number,
+      // Whether to show the value input field.
+      showInput: PropTypes.bool,
+      // Whether to show the unit select dropdown.
+      showUnit: PropTypes.bool,
       unit: PropTypes.string,
       unitOptions: PropTypes.array,
       value: PropTypes.number,
+      valueLabel: PropTypes.string,
     };
   }
 
   static get defaultProps() {
     return {
       allowOverflow: false,
+      allowUnderflow: false,
       className: "",
       minLabel: false,
       maxLabel: false,
       nameLabel: false,
       step: 1,
+      showInput: true,
+      showUnit: true,
       unit: null,
       unitOptions: [],
     };
   }
 
   constructor(props) {
     super(props);
     this.state = {
@@ -88,36 +98,37 @@ class FontPropertyValue extends PureComp
     const decimals = Math.abs(Math.log10(this.props.step));
 
     return label ? toFixed(this.props[prop], decimals) : null;
   }
 
   /**
    * Check if the given value is valid according to the constraints of this component.
    * Ensure it is a number and that it does not go outside the min/max limits, unless
-   * allowed by the `allowOverflow` props flag.
+   * allowed by the `allowOverflow` and `allowUnderflow` props.
    *
    * @param  {Number} value
    *         Numeric value
    * @return {Boolean}
    *         Whether the value conforms to the components contraints.
    */
   isValueValid(value) {
-    const { allowOverflow, min, max } = this.props;
+    const { allowOverflow, allowUnderflow, min, max } = this.props;
 
     if (typeof value !== "number" || isNaN(value)) {
       return false;
     }
 
-    if (min !== undefined && value < min) {
+    // Ensure it does not go below minimum value, unless underflow is allowed.
+    if (min !== undefined && value < min && !allowUnderflow) {
       return false;
     }
 
-    // Ensure it does not exceed maximum value, unless overflow is allowed.
-    if (max !== undefined && value > this.props.max && !allowOverflow) {
+    // Ensure it does not go over maximum value, unless overflow is allowed.
+    if (max !== undefined && value > max && !allowOverflow) {
       return false;
     }
 
     return true;
   }
 
   /**
    * Handler for "blur" events from the range and number input fields.
@@ -323,16 +334,24 @@ class FontPropertyValue extends PureComp
         this.getPropLabel("name")
       )
       :
       null;
 
     return createElement(Fragment, null, labelEl, detailEl);
   }
 
+  renderValueLabel() {
+    if (!this.props.valueLabel) {
+      return null;
+    }
+
+    return dom.div({ className: "font-value-label" }, this.props.valueLabel);
+  }
+
   render() {
     // Guard against bad axis data.
     if (this.props.min === this.props.max) {
       return null;
     }
 
     const propsValue = this.props.value !== null
       ? this.props.value
@@ -361,16 +380,18 @@ class FontPropertyValue extends PureComp
         title: this.props.label,
         type: "range",
       }
     );
 
     const input = dom.input(
       {
         ...defaults,
+        // Remove lower limit from number input if it is allowed to underflow.
+        min: this.props.allowUnderflow ? null : this.props.min,
         // Remove upper limit from number input if it is allowed to overflow.
         max: this.props.allowOverflow ? null : this.props.max,
         name: this.props.name,
         className: "font-value-input",
         disabled: this.props.disabled,
         type: "number",
       }
     );
@@ -394,16 +415,17 @@ class FontPropertyValue extends PureComp
         dom.div(
           {
             className: "font-value-slider-container",
             "data-min": this.getPropLabel("min"),
             "data-max": this.getPropLabel("max"),
           },
           range
         ),
-        input,
-        this.renderUnitSelect()
+        this.renderValueLabel(),
+        this.props.showInput && input,
+        this.props.showUnit && this.renderUnitSelect()
       )
     );
   }
 }
 
 module.exports = FontPropertyValue;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/components/LetterSpacing.js
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+const FontPropertyValue = createFactory(require("./FontPropertyValue"));
+
+const { getStr } = require("../utils/l10n");
+const { getUnitFromValue, getStepForUnit } = require("../utils/font-utils");
+
+class LineHeight extends PureComponent {
+  static get propTypes() {
+    return {
+      disabled: PropTypes.bool.isRequired,
+      onChange: PropTypes.func.isRequired,
+      value: PropTypes.string.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+    // Local state for min/max bounds indexed by unit to allow user input that
+    // goes out-of-bounds while still providing a meaningful default range. The indexing
+    // by unit is needed to account for unit conversion (ex: em to px) where the operation
+    // may result in out-of-bounds values. Avoiding React's state and setState() because
+    // `value` is a prop coming from the Redux store while min/max are local. Reconciling
+    // value/unit changes is needlessly complicated and adds unnecessary re-renders.
+    this.historicMin = {};
+    this.historicMax = {};
+  }
+
+  getDefaultMinMax(unit) {
+    let min;
+    let max;
+    switch (unit) {
+      case "px":
+        min = -10;
+        max = 10;
+        break;
+      default:
+        min = -0.2;
+        max = 0.6;
+        break;
+    }
+
+    return { min, max };
+  }
+
+  render() {
+    // For a unitless or a NaN value, default unit to "em".
+    const unit = getUnitFromValue(this.props.value) || "em";
+    // When the initial value of "letter-spacing" is "normal", the parsed value
+    // is not a number (NaN). Guard by setting the default value to 0.
+    let value = parseFloat(this.props.value);
+    const hasKeywordValue = isNaN(value);
+    value = isNaN(value) ? 0 : value;
+
+    let { min, max } = this.getDefaultMinMax(unit);
+    min = Math.min(min, value);
+    max = Math.max(max, value);
+    // Allow lower and upper bounds to move to accomodate the incoming value.
+    this.historicMin[unit] = this.historicMin[unit]
+      ? Math.min(this.historicMin[unit], min)
+      : min;
+    this.historicMax[unit] = this.historicMax[unit]
+      ? Math.max(this.historicMax[unit], max)
+      : max;
+
+    return FontPropertyValue({
+      allowOverflow: true,
+      allowUnderflow: true,
+      disabled: this.props.disabled,
+      label: getStr("fontinspector.letterSpacingLabel"),
+      min: this.historicMin[unit],
+      max: this.historicMax[unit],
+      name: "letter-spacing",
+      onChange: this.props.onChange,
+      // Increase the increment granularity because letter spacing is very sensitive.
+      step: getStepForUnit(unit) / 100,
+      // Show the value input and unit only when the value is not a keyword.
+      showInput: !hasKeywordValue,
+      showUnit: !hasKeywordValue,
+      unit,
+      unitOptions: ["em", "rem", "px"],
+      value,
+      // Show the value as a read-only label if it's a keyword.
+      valueLabel: hasKeywordValue ? this.props.value : null,
+    });
+  }
+}
+
+module.exports = LineHeight;
--- a/devtools/client/inspector/fonts/components/moz.build
+++ b/devtools/client/inspector/fonts/components/moz.build
@@ -14,10 +14,11 @@ DevToolsModules(
     'FontOverview.js',
     'FontPreview.js',
     'FontPreviewInput.js',
     'FontPropertyValue.js',
     'FontsApp.js',
     'FontSize.js',
     'FontStyle.js',
     'FontWeight.js',
+    'LetterSpacing.js',
     'LineHeight.js',
 )
--- a/devtools/client/inspector/fonts/fonts.js
+++ b/devtools/client/inspector/fonts/fonts.js
@@ -37,16 +37,17 @@ const CUSTOM_INSTANCE_NAME = getStr("fon
 const FONT_PROPERTIES = [
   "font-family",
   "font-optical-sizing",
   "font-size",
   "font-stretch",
   "font-style",
   "font-variation-settings",
   "font-weight",
+  "letter-spacing",
   "line-height",
 ];
 const REGISTERED_AXES_TO_FONT_PROPERTIES = {
   "ital": "font-style",
   "opsz": "font-optical-sizing",
   "slnt": "font-style",
   "wdth": "font-stretch",
   "wght": "font-weight",
@@ -139,16 +140,17 @@ class FontInspector {
    * @return {Number}
    *         Converted numeric value.
    */
   async convertUnits(property, value, fromUnit, toUnit) {
     if (value !== parseFloat(value)) {
       throw TypeError(`Invalid value for conversion. Expected Number, got ${value}`);
     }
 
+    // Early return with the same value if conversion is not required.
     if (fromUnit === toUnit || value === 0) {
       return value;
     }
 
     // Special case for line-height. Consider em and untiless to be equivalent.
     if (property === "line-height" &&
        (fromUnit === "" && toUnit === "em") || (fromUnit === "em" && toUnit === "")) {
       return value;
@@ -160,25 +162,19 @@ class FontInspector {
       value = await this.convertUnits(property, value, fromUnit, "px");
       fromUnit = "px";
     }
 
     // Whether the conversion is done from pixels.
     const fromPx = fromUnit === "px";
     // Determine the target CSS unit for conversion.
     const unit = toUnit === "px" ? fromUnit : toUnit;
-    // NodeFront instance of selected/target element.
-    const node = this.node;
-    // Reference node based on which to convert relative sizes like "em" and "%".
-    const referenceNode = (property === "line-height") ? node : node.parentNode();
     // Default output value to input value for a 1-to-1 conversion as a guard against
     // unrecognized CSS units. It will not be correct, but it will also not break.
     let out = value;
-    // Computed style for reference node used for conversion of "em", "rem", "%".
-    let computedStyle;
 
     if (unit === "in") {
       out = fromPx
         ? value / 96
         : value * 96;
     }
 
     if (unit === "cm") {
@@ -201,94 +197,80 @@ class FontInspector {
 
     if (unit === "pc") {
       out = fromPx
         ? value * 0.0625
         : value / 0.0625;
     }
 
     if (unit === "%") {
-      computedStyle =
-        await this.pageStyle.getComputed(referenceNode).catch(console.error);
-
-      if (!computedStyle) {
-        return value;
-      }
-
+      const fontSize = await this.getReferenceFontSize(property, unit);
       out = fromPx
-        ? value * 100 / parseFloat(computedStyle["font-size"].value)
-        : value / 100 * parseFloat(computedStyle["font-size"].value);
+        ? value * 100 / parseFloat(fontSize)
+        : value / 100 * parseFloat(fontSize);
     }
 
     // Special handling for unitless line-height.
     if (unit === "em" || (unit === "" && property === "line-height")) {
-      computedStyle =
-        await this.pageStyle.getComputed(referenceNode).catch(console.error);
-
-      if (!computedStyle) {
-        return value;
-      }
-
+      const fontSize = await this.getReferenceFontSize(property, unit);
       out = fromPx
-        ? value / parseFloat(computedStyle["font-size"].value)
-        : value * parseFloat(computedStyle["font-size"].value);
+        ? value / parseFloat(fontSize)
+        : value * parseFloat(fontSize);
     }
 
     if (unit === "rem") {
-      const document = await this.inspector.walker.documentElement();
-      computedStyle = await this.pageStyle.getComputed(document).catch(console.error);
+      const fontSize = await this.getReferenceFontSize(property, unit);
+      out = fromPx
+        ? value / parseFloat(fontSize)
+        : value * parseFloat(fontSize);
+    }
 
-      if (!computedStyle) {
-        return value;
-      }
-
+    if (unit === "vh") {
+      const { height } = await this.getReferenceBox(property, unit);
       out = fromPx
-        ? value / parseFloat(computedStyle["font-size"].value)
-        : value * parseFloat(computedStyle["font-size"].value);
+        ? value * 100 / height
+        : value / 100 * height;
     }
 
-    if (unit === "vh" || unit === "vw" || unit === "vmin" || unit === "vmax") {
-      const dim = await node.getOwnerGlobalDimensions();
+    if (unit === "vw") {
+      const { width } = await this.getReferenceBox(property, unit);
+      out = fromPx
+        ? value * 100 / width
+        : value / 100 * width;
+    }
 
-      // The getOwnerGlobalDimensions() method does not exist on the NodeFront API spec
-      // prior to Firefox 63. In that case, return a 1-to-1 conversion which isn't a
-      // correct conversion, but doesn't break the font editor either.
-      if (!dim || !dim.innerWidth || !dim.innerHeight) {
-        out = value;
-      } else if (unit === "vh") {
-        out = fromPx
-          ? value * 100 / dim.innerHeight
-          : value / 100 * dim.innerHeight;
-      } else if (unit === "vw") {
-        out = fromPx
-          ? value * 100 / dim.innerWidth
-          : value / 100 * dim.innerWidth;
-      } else if (unit === "vmin") {
-        out = fromPx
-          ? value * 100 / Math.min(dim.innerWidth, dim.innerHeight)
-          : value / 100 * Math.min(dim.innerWidth, dim.innerHeight);
-      } else if (unit === "vmax") {
-        out = fromPx
-          ? value * 100 / Math.max(dim.innerWidth, dim.innerHeight)
-          : value / 100 * Math.max(dim.innerWidth, dim.innerHeight);
-      }
+    if (unit === "vmin") {
+      const { width, height } = await this.getReferenceBox(property, unit);
+      out = fromPx
+        ? value * 100 / Math.min(width, height)
+        : value / 100 * Math.min(width, height);
+    }
+
+    if (unit === "vmax") {
+      const { width, height } = await this.getReferenceBox(property, unit);
+      out = fromPx
+        ? value * 100 / Math.max(width, height)
+        : value / 100 * Math.max(width, height);
     }
 
     // Catch any NaN or Infinity as result of dividing by zero in any
     // of the relative unit conversions which rely on external values.
     if (isNaN(out) || Math.abs(out) === Infinity) {
       out = 0;
     }
 
-    // Return rounded pixel values. Limit other values to 3 decimals.
-    if (fromPx) {
+    // Return values limited to 3 decimals when:
+    // - the unit is converted from pixels to something else
+    // - the value is for letter spacing, regardless of unit (allow sub-pixel precision)
+    if (fromPx || property === "letter-spacing") {
       // Round values like 1.000 to 1
       return out === Math.round(out) ? Math.round(out) : out.toFixed(3);
     }
 
+    // Round pixel values.
     return Math.round(out);
   }
 
   /**
    * Destruction function called when the inspector is destroyed. Removes event listeners
    * and cleans up references.
    */
   destroy() {
@@ -355,28 +337,30 @@ class FontInspector {
     return properties;
   }
 
   /**
    * Get an array of keyword values supported by the following CSS properties:
    * - font-size
    * - font-weight
    * - font-stretch
+   * - letter-spacing
    * - line-height
    *
    * This list is used to filter out values when reading CSS font properties from rules.
    * Computed styles will be used instead of any of these values.
    *
    * @return {Array}
    */
   getFontPropertyValueKeywords() {
     return [
       "font-size",
       "font-weight",
       "font-stretch",
+      "letter-spacing",
       "line-height",
     ].reduce((acc, property) => {
       return acc.concat(this.cssProperties.getValues(property));
     }, []);
   }
 
   async getFontsForNode(node, options) {
     // In case we've been destroyed in the meantime
@@ -403,16 +387,119 @@ class FontInspector {
     if (!allFonts) {
       allFonts = [];
     }
 
     return allFonts;
   }
 
   /**
+   * Get the box dimensions used for unit conversion according to the CSS property and
+   * target CSS unit.
+   *
+   * @param  {String} property
+   *         CSS property
+   * @param  {String} unit
+   *         Target CSS unit
+   * @return {Promise}
+   *         Promise that resolves with an object with box dimensions in pixels.
+   */
+  async getReferenceBox(property, unit) {
+    const box = { width: 0, height: 0 };
+    const node = await this.getReferenceNode(property, unit).catch(console.error);
+
+    if (!node) {
+      return box;
+    }
+
+    switch (unit) {
+      case "vh":
+      case "vw":
+      case "vmin":
+      case "vmax":
+        const dim = await node.getOwnerGlobalDimensions().catch(console.error);
+        if (dim) {
+          box.width = dim.innerWidth;
+          box.height = dim.innerHeight;
+        }
+        break;
+
+      case "%":
+        const style = await this.pageStyle.getComputed(node).catch(console.error);
+        if (style) {
+          box.width = style.width.value;
+          box.height = style.height.value;
+        }
+        break;
+    }
+
+    return box;
+  }
+
+  /**
+   * Get the refernece font size value used for unit conversion according to the
+   * CSS property and target CSS unit.
+   *
+   * @param {String} property
+   *        CSS property
+   * @param {String} unit
+   *        Target CSS unit
+   * @return {Promise}
+   *         Promise that resolves with the reference font size value or null if there
+   *         was an error getting that value.
+   */
+  async getReferenceFontSize(property, unit) {
+    const node = await this.getReferenceNode(property, unit).catch(console.error);
+    if (!node) {
+      return null;
+    }
+
+    const style = await this.pageStyle.getComputed(node).catch(console.error);
+    if (!style) {
+      return null;
+    }
+
+    return style["font-size"].value;
+  }
+
+  /**
+   * Get the reference node used in measurements for unit conversion according to the
+   * the CSS property and target CSS unit type.
+   *
+   * @param  {String} property
+   *         CSS property
+   * @param  {String} unit
+   *         Target CSS unit
+   * @return {Promise}
+   *          Promise that resolves with the reference node used in measurements for unit
+   *          conversion.
+   */
+  async getReferenceNode(property, unit) {
+    let node;
+
+    switch (property) {
+      case "line-height":
+      case "letter-spacing":
+        node = this.node;
+        break;
+      default:
+        node = this.node.parentNode();
+    }
+
+    switch (unit) {
+      case "rem":
+        // Regardless of CSS property, always use the root document element for "rem".
+        node = await this.inspector.walker.documentElement();
+        break;
+    }
+
+    return node;
+  }
+
+  /**
    * Get a reference to a TextProperty instance from the current selected rule for a
    * given property name.
    *
    * @param {String} name
    *        CSS property name
    * @return {TextProperty|null}
    */
   getTextProperty(name) {
--- a/devtools/client/inspector/fonts/test/browser.ini
+++ b/devtools/client/inspector/fonts/test/browser.ini
@@ -17,17 +17,18 @@ support-files =
 
 [browser_fontinspector.js]
 [browser_fontinspector_copy-URL.js]
 skip-if = !e10s # too slow on !e10s, logging fully serialized actors (Bug 1446595)
 subsuite = clipboard
 [browser_fontinspector_all-fonts.js]
 [browser_fontinspector_edit-previews.js]
 [browser_fontinspector_editor-font-size-conversion.js]
+[browser_fontinspector_editor-keywords.js]
+[browser_fontinspector_editor-letter-spacing-conversion.js]
 [browser_fontinspector_editor-values.js]
-[browser_fontinspector_editor-keywords.js]
 [browser_fontinspector_expand-css-code.js]
 [browser_fontinspector_font-type-telemetry.js]
 [browser_fontinspector_input-element-used-font.js]
 [browser_fontinspector_no-fonts.js]
 [browser_fontinspector_reveal-in-page.js]
 [browser_fontinspector_text-node.js]
 [browser_fontinspector_theme-change.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/test/browser_fontinspector_editor-letter-spacing-conversion.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* global getPropertyValue */
+
+"use strict";
+
+// Unit test for math behind conversion of units for letter-spacing.
+
+const TEST_URI = `
+  <style type='text/css'>
+    body {
+      /* Set root font-size to equivalent of 32px (2*16px) */
+      font-size: 200%;
+    }
+    div {
+      letter-spacing: 1em;
+    }
+  </style>
+  <div>LETTER SPACING</div>
+`;
+
+add_task(async function() {
+  const URI = "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI);
+  const { inspector, view } = await openFontInspectorForURL(URI);
+  const viewDoc = view.document;
+  const property = "letter-spacing";
+  const UNITS = {
+    "px": 32,
+    "rem": 2,
+    "em": 1,
+  };
+
+  await selectNode("div", inspector);
+
+  info("Check that font editor shows letter-spacing value in original units");
+  const letterSpacing = getPropertyValue(viewDoc, property);
+  is(letterSpacing.value + letterSpacing.unit, "1em", "Original letter spacing is 1em");
+
+  // Starting value and unit for conversion.
+  let prevValue = letterSpacing.value;
+  let prevUnit = letterSpacing.unit;
+
+  for (const unit in UNITS) {
+    const value = UNITS[unit];
+
+    info(`Convert letter-spacing from ${prevValue}${prevUnit} to ${unit}`);
+    const convertedValue = await view.convertUnits(property, prevValue, prevUnit, unit);
+    is(convertedValue, value, `Converting to ${unit} returns transformed value.`);
+
+    // Store current unit and value to use in conversion on the next iteration.
+    prevUnit = unit;
+    prevValue = value;
+  }
+
+  info(`Check that conversion to fake unit returns 1-to-1 mapping`);
+  const valueToFakeUnit = await view.convertUnits(property, 1, "px", "fake");
+  is(valueToFakeUnit, 1, `Converting to fake unit returns same value.`);
+});
--- a/devtools/client/inspector/fonts/utils/font-utils.js
+++ b/devtools/client/inspector/fonts/utils/font-utils.js
@@ -38,17 +38,17 @@ module.exports = {
    * Returns null for non-string input or unitless values.
    *
    * @param {String} value
    *        CSS value string.
    * @return {String|null}
    *         CSS unit type, like "px", "em", "rem", etc or null.
    */
   getUnitFromValue(value) {
-    if (typeof value !== "string") {
+    if (typeof value !== "string" || isNaN(parseFloat(value))) {
       return null;
     }
 
     const match = value.match(/\D+?$/);
     return match && match.length ? match[0] : null;
   },
 
   /**
--- a/devtools/client/locales/en-US/font-inspector.properties
+++ b/devtools/client/locales/en-US/font-inspector.properties
@@ -44,16 +44,20 @@ fontinspector.fontWeightLabel=Weight
 fontinspector.fontItalicLabel=Italic
 
 # LOCALIZATION NOTE (fontinspector.showMore): Label for a collapsed list of fonts.
 fontinspector.showMore=Show more
 
 # LOCALIZATION NOTE (fontinspector.showLess): Label for an expanded list of fonts.
 fontinspector.showLess=Show less
 
+# LOCALIZATION NOTE (fontinspector.letterSpacingLabel): Label for the UI to change the
+# letter spacing in the font editor.
+fontinspector.letterSpacingLabel=Spacing
+
 # LOCALIZATION NOTE (fontinspector.lineHeightLabelCapitalized): Label for the UI to change the line height in the font editor.
 fontinspector.lineHeightLabelCapitalized=Line Height
 
 # LOCALIZATION NOTE (fontinspector.allFontsOnPageHeader): Header for the section listing
 # all the fonts on the current page.
 fontinspector.allFontsOnPageHeader=All fonts on page
 
 # LOCALIZATION NOTE (fontinspector.fontsUsedLabel): Label for the Font Editor section
--- a/devtools/client/netmonitor/src/assets/styles/CustomRequestPanel.css
+++ b/devtools/client/netmonitor/src/assets/styles/CustomRequestPanel.css
@@ -28,16 +28,17 @@
 .network-monitor .tabpanel-summary-container.custom-method-and-url {
   display: grid;
   grid-template-columns: auto 1fr;
 }
 
 .network-monitor .custom-method-and-url input {
   font-weight: 400;
   margin-top: 4px;
+  min-width: 9ch;
   padding: 2px 3px;
 }
 
 .network-monitor .custom-request-panel textarea {
   font-weight: 400;
   margin-top: 4px;
   padding: 8px;
 }
@@ -53,38 +54,59 @@
 
 .network-monitor .custom-request-panel > div:not(.custom-request) {
   margin-bottom: 12px;
   padding-left: 16px;
   padding-right: 16px;
 }
 
 .network-monitor .custom-request {
-  flex-direction: row-reverse;
-  flex-wrap: wrap;
+  display: block;
   padding: 0;
 }
 
+.network-monitor .custom-request .custom-request-button-container {
+  display: flex;
+  flex-wrap: wrap-reverse;
+  margin-left: 12px;
+}
+
 .network-monitor .custom-request-panel .custom-request-label {
   font-weight: 400;
+  white-space: nowrap;
 }
 
 .network-monitor .custom-request button {
-  align-self: flex-end;
   height: 24px;
+  margin-bottom: 4px;
   padding-left: 8px;
   padding-right: 8px;
   width: auto;
 }
 
+.network-monitor .custom-request button:focus {
+  box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff,
+  0 0 0 4px rgba(10,132,255,0.3)
+}
+
 .network-monitor .custom-request #custom-request-send-button {
   background-color: var(--blue-60);
   color: white;
-  margin-left: 4px;
-  margin-right: 16px;
+}
+
+.network-monitor .custom-request #custom-request-send-button:active {
+  background-color: var(--blue-80);
+}
+
+.network-monitor .custom-request #custom-request-send-button:hover {
+  background-color: var(--blue-70);
+}
+
+.network-monitor .custom-request #custom-request-close-button {
+  margin-right: 4px;
 }
 
 .network-monitor .custom-request .custom-header {
   border-bottom-width: 1px;
   border-style: solid;
   border-width: 0;
   flex: 1 0 100%;
   height: 24px;
@@ -96,16 +118,24 @@
   background-color: var(--grey-85);
 }
 
 :root.theme-dark .network-monitor #custom-request-close-button {
   background-color: var(--toolbarbutton-background);
   border: 1px solid var(--theme-splitter-color);
 }
 
+:root.theme-dark .network-monitor #custom-request-close-button:hover:active {
+  background-color: var(--theme-selection-background-hover);
+}
+
+:root.theme-dark .network-monitor #custom-request-close-button:focus {
+  background-color: var(--theme-selection-focus-background);
+}
+
 :root.theme-dark .network-monitor .custom-request-label.custom-header {
   background-color: var(--grey-80);
   border-bottom: 1px solid var(--theme-splitter-color);
 }
 
 :root.theme-dark .network-details-panel .custom-request-panel input,
 :root.theme-dark .network-details-panel .custom-request-panel textarea {
   background-color: var(--grey-70);
@@ -119,16 +149,29 @@
 
 :root.theme-light .network-details-panel .custom-request-label.custom-header {
   background-color: var(--grey-10);
   border-bottom: 1px solid var(--grey-25);
 }
 
 :root.theme-light .network-monitor #custom-request-close-button {
   background-color: var(--grey-20);
+  border: var(--theme-splitter-color);
+}
+
+:root.theme-light .network-monitor #custom-request-close-button:hover:active {
+  background-color: var(--theme-selection-background-hover);
+}
+
+:root.theme-light .network-monitor #custom-request-close-button:focus {
+  outline: 2px solid var(--blue-50);
+  outline-offset: -2px;
+  box-shadow: 0 0 0 2px rgba(10, 132, 255, 0.3);
+  border-radius: 2px;
+  -moz-outline-radius: 2px;
 }
 
 :root.theme-light .network-details-panel .custom-request-panel input,
 :root.theme-light .network-details-panel .custom-request-panel textarea {
   background-color: white;
   border: 1px solid var(--grey-25);
   color: var(--grey-90);
 }
--- a/devtools/client/netmonitor/src/components/CustomRequestPanel.js
+++ b/devtools/client/netmonitor/src/components/CustomRequestPanel.js
@@ -193,29 +193,31 @@ class CustomRequestPanel extends Compone
       requestPostData.postData.text : "";
 
     return (
       div({ className: "custom-request-panel" },
         div({ className: "tabpanel-summary-container custom-request" },
           div({ className: "custom-request-label custom-header" },
             CUSTOM_NEW_REQUEST,
           ),
-          button({
-            className: "devtools-button",
-            id: "custom-request-send-button",
-            onClick: sendCustomRequest,
-          },
-            CUSTOM_SEND,
-          ),
-          button({
-            className: "devtools-button",
-            id: "custom-request-close-button",
-            onClick: removeSelectedCustomRequest,
-          },
-            CUSTOM_CANCEL,
+          div({ className: "custom-request-button-container" },
+            button({
+              className: "devtools-button",
+              id: "custom-request-close-button",
+              onClick: removeSelectedCustomRequest,
+            },
+              CUSTOM_CANCEL,
+            ),
+            button({
+              className: "devtools-button",
+              id: "custom-request-send-button",
+              onClick: sendCustomRequest,
+            },
+              CUSTOM_SEND,
+            ),
           ),
         ),
         div({
           className: "tabpanel-summary-container custom-method-and-url",
           id: "custom-method-and-url",
         },
           label({
             className: "custom-method-value-label custom-request-label",
--- a/devtools/client/netmonitor/src/components/StatusBar.js
+++ b/devtools/client/netmonitor/src/components/StatusBar.js
@@ -1,57 +1,90 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const { Component } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const { connect } = require("devtools/client/shared/redux/visibility-handler-connect");
 const { PluralForm } = require("devtools/shared/plural-form");
 const Actions = require("../actions/index");
 const {
   getDisplayedRequestsSummary,
   getDisplayedTimingMarker,
 } = require("../selectors/index");
 const {
   getFormattedSize,
   getFormattedTime,
 } = require("../utils/format-utils");
 const { L10N } = require("../utils/l10n");
+const { propertiesEqual } = require("../utils/request-utils");
 
 const { button, div } = dom;
 
 const REQUESTS_COUNT_EMPTY = L10N.getStr("networkMenu.summary.requestsCountEmpty");
 const TOOLTIP_PERF = L10N.getStr("networkMenu.summary.tooltip.perf");
 const TOOLTIP_REQUESTS_COUNT = L10N.getStr("networkMenu.summary.tooltip.requestsCount");
 const TOOLTIP_TRANSFERRED = L10N.getStr("networkMenu.summary.tooltip.transferred");
 const TOOLTIP_FINISH = L10N.getStr("networkMenu.summary.tooltip.finish");
 const TOOLTIP_DOM_CONTENT_LOADED =
         L10N.getStr("networkMenu.summary.tooltip.domContentLoaded");
 const TOOLTIP_LOAD = L10N.getStr("networkMenu.summary.tooltip.load");
 
-function StatusBar({ summary, openStatistics, timingMarkers }) {
-  const { count, contentSize, transferredSize, millis } = summary;
-  const {
-    DOMContentLoaded,
-    load,
-  } = timingMarkers;
+const UPDATED_SUMMARY_PROPS = [
+  "count",
+  "contentSize",
+  "transferredSize",
+  "millis",
+];
+
+const UPDATED_TIMING_PROPS = [
+  "DOMContentLoaded",
+  "load",
+];
 
-  const countText = count === 0 ? REQUESTS_COUNT_EMPTY :
-    PluralForm.get(count,
-      L10N.getStr("networkMenu.summary.requestsCount2")).replace("#1", count);
-  const transferText = L10N.getFormatStrWithNumbers("networkMenu.summary.transferred",
-    getFormattedSize(contentSize), getFormattedSize(transferredSize));
-  const finishText = L10N.getFormatStrWithNumbers("networkMenu.summary.finish",
-    getFormattedTime(millis));
+/**
+ * Status Bar component
+ * Displays the summary of total size and transferred size by all requests
+ * Also displays different timing markers
+ */
+class StatusBar extends Component {
+  static get propTypes() {
+    return {
+      connector: PropTypes.object.isRequired,
+      openStatistics: PropTypes.func.isRequired,
+      summary: PropTypes.object.isRequired,
+      timingMarkers: PropTypes.object.isRequired,
+    };
+  }
 
-  return (
-    div({ className: "devtools-toolbar devtools-toolbar-bottom" },
+  shouldComponentUpdate(nextProps) {
+    const { summary, timingMarkers } = this.props;
+    return !propertiesEqual(UPDATED_SUMMARY_PROPS, summary, nextProps.summary) ||
+      !propertiesEqual(UPDATED_TIMING_PROPS, timingMarkers, nextProps.timingMarkers);
+  }
+
+  render() {
+    const { openStatistics, summary, timingMarkers } = this.props;
+    const { count, contentSize, transferredSize, millis } = summary;
+    const { DOMContentLoaded, load } = timingMarkers;
+
+    const countText = count === 0 ? REQUESTS_COUNT_EMPTY :
+      PluralForm.get(count,
+        L10N.getStr("networkMenu.summary.requestsCount2")).replace("#1", count);
+    const transferText = L10N.getFormatStrWithNumbers("networkMenu.summary.transferred",
+      getFormattedSize(contentSize), getFormattedSize(transferredSize));
+    const finishText = L10N.getFormatStrWithNumbers("networkMenu.summary.finish",
+      getFormattedTime(millis));
+
+    return (
+      div({ className: "devtools-toolbar devtools-toolbar-bottom" },
       button({
         className: "devtools-button requests-list-network-summary-button",
         title: TOOLTIP_PERF,
         onClick: openStatistics,
       },
         div({ className: "summary-info-icon" }),
       ),
       div({
@@ -73,29 +106,21 @@ function StatusBar({ summary, openStatis
           className: "status-bar-label dom-content-loaded",
           title: TOOLTIP_DOM_CONTENT_LOADED,
         }, `DOMContentLoaded: ${getFormattedTime(DOMContentLoaded)}`),
       load > -1 &&
         div({
           className: "status-bar-label load",
           title: TOOLTIP_LOAD,
         }, `load: ${getFormattedTime(load)}`),
-    )
-  );
+      )
+    );
+  }
 }
 
-StatusBar.displayName = "StatusBar";
-
-StatusBar.propTypes = {
-  connector: PropTypes.object.isRequired,
-  openStatistics: PropTypes.func.isRequired,
-  summary: PropTypes.object.isRequired,
-  timingMarkers: PropTypes.object.isRequired,
-};
-
 module.exports = connect(
   (state) => ({
     summary: getDisplayedRequestsSummary(state),
     timingMarkers: {
       DOMContentLoaded:
         getDisplayedTimingMarker(state, "firstDocumentDOMContentLoadedTimestamp"),
       load: getDisplayedTimingMarker(state, "firstDocumentLoadTimestamp"),
     },
--- a/devtools/client/shared/source-map/index.js
+++ b/devtools/client/shared/source-map/index.js
@@ -746,18 +746,22 @@ module.exports = __webpack_require__(182
 
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const md5 = __webpack_require__(105);
 
-function originalToGeneratedId(originalId) {
-  const match = originalId.match(/(.*)\/originalSource/);
+function originalToGeneratedId(sourceId) {
+  if (isGeneratedId(sourceId)) {
+    return sourceId;
+  }
+
+  const match = sourceId.match(/(.*)\/originalSource/);
   return match ? match[1] : "";
 }
 
 function generatedToOriginalId(generatedId, url) {
   return `${generatedId}/originalSource-${md5(url)}`;
 }
 
 function isOriginalId(id) {
--- a/devtools/client/shared/source-map/worker.js
+++ b/devtools/client/shared/source-map/worker.js
@@ -13215,18 +13215,22 @@ module.exports = {
 
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const md5 = __webpack_require__(105);
 
-function originalToGeneratedId(originalId) {
-  const match = originalId.match(/(.*)\/originalSource/);
+function originalToGeneratedId(sourceId) {
+  if (isGeneratedId(sourceId)) {
+    return sourceId;
+  }
+
+  const match = sourceId.match(/(.*)\/originalSource/);
   return match ? match[1] : "";
 }
 
 function generatedToOriginalId(generatedId, url) {
   return `${generatedId}/originalSource-${md5(url)}`;
 }
 
 function isOriginalId(id) {
--- a/devtools/client/themes/fonts.css
+++ b/devtools/client/themes/fonts.css
@@ -122,25 +122,31 @@
   margin-bottom: 0.6em;
   font-size: 1em;
   color: var(--grey-50);
 }
 
 .font-family-name {
   margin-bottom: 0.2em;
   font-size: 1.2em;
+  padding: 0 3px;
 }
 
 .font-group {
   margin-bottom: .5em;
 }
 
 .font-group .font-name {
   white-space: unset;
-  margin-right: .5em;
+  padding: 3px;
+  border-radius: 3px;
+}
+
+.font-group .font-name:hover {
+  background-color: var(--theme-selection-background-hover);
 }
 
 .font-group .font-name::after {
   content: ",";
 }
 
 .font-group .font-name:nth-last-child(1)::after {
   content: "";
@@ -280,16 +286,24 @@
 /* Do not show number stepper for line height and font-size */
 .font-value-input[name=line-height],
 .font-value-input[name=font-size] {
   -moz-appearance: textfield;
   padding-right: 5px;
   border-right: none;
 }
 
+.font-value-label {
+  /* Combined width of .font-value-input and .font-value-select */
+  width: calc(60px + 3.8em);
+  margin-left: 10px;
+  padding-top: 2px;
+  padding-bottom: 4px;
+}
+
 /* Mock separator because inputs don't have distinguishable borders in dark theme */
 .theme-dark .font-value-input + .font-value-select {
   margin-left: 2px;
 }
 
 /* Custom styles for <select> elements within the font editor. */
 .font-value-select {
   background-image: url(chrome://devtools/skin/images/select-arrow.svg);
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -259,20 +259,22 @@ class JSTerm extends Component {
             "Cmd-Up": onArrowUp,
 
             "Down": onArrowDown,
             "Cmd-Down": onArrowDown,
 
             "Left": onArrowLeft,
             "Ctrl-Left": onArrowLeft,
             "Cmd-Left": onArrowLeft,
+            "Alt-Left": onArrowLeft,
 
             "Right": onArrowRight,
             "Ctrl-Right": onArrowRight,
             "Cmd-Right": onArrowRight,
+            "Alt-Right": onArrowRight,
 
             "Ctrl-N": () => {
               // Control-N differs from down arrow: it ignores autocomplete state.
               // Note that we preserve the default 'down' navigation within
               // multiline text.
               if (
                 Services.appinfo.OS === "Darwin"
                 && this.canCaretGoNext()
@@ -903,16 +905,17 @@ class JSTerm extends Component {
       ) {
         this.clearCompletion();
       }
 
       // We only want to complete on Right arrow if the completion text is displayed.
       if (event.keyCode === KeyCodes.DOM_VK_RIGHT) {
         if (this.getAutoCompletionText()) {
           this.acceptProposedCompletion();
+          event.preventDefault();
         }
         this.clearCompletion();
         event.preventDefault();
       }
 
       return;
     } else if (event.keyCode == KeyCodes.DOM_VK_RETURN) {
       if (!this.autocompletePopup.isOpen && (
--- a/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_arrow_keys.js
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_arrow_keys.js
@@ -6,17 +6,18 @@
 "use strict";
 
 const TEST_URI = `data:text/html;charset=utf-8,<head><script>
     /* Create a prototype-less object so popup does not contain native
      * Object prototype properties.
      */
     window.foo = Object.create(null, Object.getOwnPropertyDescriptors({
       aa: "a",
-      bb: "b",
+      bbb: "b",
+      bbbb: "b",
     }));
   </script></head><body>Autocomplete text navigation key usage test</body>`;
 
 add_task(async function() {
   // Run test with legacy JsTerm
   await pushPref("devtools.webconsole.jsterm.codeMirror", false);
   await performTests();
   // And then run it with the CodeMirror-powered one.
@@ -24,75 +25,157 @@ add_task(async function() {
   await performTests();
 });
 
 async function performTests() {
   const hud = await openNewTabAndConsole(TEST_URI);
   const { jsterm } = hud;
   const { autocompletePopup: popup } = jsterm;
 
-  const checkInput = (expected, assertionInfo) =>
-    checkInputValueAndCursorPosition(hud, expected, assertionInfo);
-
-  let onPopUpOpen = popup.once("popup-opened");
-  setInputValue(hud, "window.foo");
-  EventUtils.sendString(".");
-  await onPopUpOpen;
-
-  info("Trigger autocomplete popup opening");
-  // checkInput is asserting the cursor position with the "|" char.
-  checkInput("window.foo.|");
-  is(popup.isOpen, true, "popup is open");
-  checkInputCompletionValue(hud, "           aa", "completeNode has expected value");
-
-  info("Test that arrow left closes the popup and clears complete node");
-  let onPopUpClose = popup.once("popup-closed");
-  EventUtils.synthesizeKey("KEY_ArrowLeft");
-  await onPopUpClose;
-  checkInput("window.foo|.");
-  is(popup.isOpen, false, "popup is closed");
-  checkInputCompletionValue(hud, "", "completeNode is empty");
-
-  info("Trigger autocomplete popup opening again");
-  onPopUpOpen = popup.once("popup-opened");
-  setInputValue(hud, "window.foo");
-  EventUtils.sendString(".");
-  await onPopUpOpen;
-
-  checkInput("window.foo.|");
-  is(popup.isOpen, true, "popup is open");
-  checkInputCompletionValue(hud, "           aa", "completeNode has expected value");
-
-  info("Test that arrow right selects selected autocomplete item");
-  onPopUpClose = popup.once("popup-closed");
-  EventUtils.synthesizeKey("KEY_ArrowRight");
-  await onPopUpClose;
-  checkInput("window.foo.aa|");
-  is(popup.isOpen, false, "popup is closed");
-  checkInputCompletionValue(hud, "", "completeNode is empty");
-
-  info("Test that Ctrl/Cmd + Left removes complete node");
-  await setInputValueForAutocompletion(hud, "window.foo.a");
-  const prefix = getInputValue(hud).replace(/[\S]/g, " ");
-  checkInputCompletionValue(hud, prefix + "a", "completeNode has expected value");
-
-  const isOSX = Services.appinfo.OS == "Darwin";
-  EventUtils.synthesizeKey("KEY_ArrowLeft", {
-    [isOSX ? "metaKey" : "ctrlKey"]: true,
-  });
-  checkInputCompletionValue(hud, "",
-    "completeNode was cleared after Ctrl/Cmd + left");
+  await checkArrowLeftDismissPopup(hud);
+  await checkArrowLeftDismissCompletion(hud);
+  await checkArrowRightAcceptCompletion(hud);
 
   info("Test that Ctrl/Cmd + Right closes the popup if there's text after cursor");
   setInputValue(hud, ".");
   EventUtils.synthesizeKey("KEY_ArrowLeft");
-  onPopUpOpen = popup.once("popup-opened");
+  const onPopUpOpen = popup.once("popup-opened");
   EventUtils.sendString("win");
   await onPopUpOpen;
   ok(popup.isOpen, "popup is open");
 
-  onPopUpClose = popup.once("popup-closed");
+  const isOSX = Services.appinfo.OS == "Darwin";
+  const onPopUpClose = popup.once("popup-closed");
   EventUtils.synthesizeKey("KEY_ArrowRight", {
     [isOSX ? "metaKey" : "ctrlKey"]: true,
   });
   await onPopUpClose;
   is(getInputValue(hud), "win.", "input value wasn't modified");
 }
+
+async function checkArrowLeftDismissPopup(hud) {
+  const popup = hud.jsterm.autocompletePopup;
+  let tests;
+  if (Services.appinfo.OS == "Darwin") {
+    tests = [{
+      keyOption: null,
+      expectedInput: "window.foo.b|b",
+    }, {
+      keyOption: {metaKey: true},
+      expectedInput: "|window.foo.bb",
+    }, {
+      keyOption: {altKey: true},
+      expectedInput: "window.foo.|bb",
+    }];
+  } else {
+    tests = [{
+      keyOption: null,
+      expectedInput: "window.foo.b|b",
+    }, {
+      keyOption: {ctrlKey: true},
+      expectedInput: "window.foo.|bb",
+    }];
+  }
+
+  for (const test of tests) {
+    info("Trigger autocomplete popup opening");
+    const onPopUpOpen = popup.once("popup-opened");
+    await setInputValueForAutocompletion(hud, "window.foo.bb");
+    await onPopUpOpen;
+
+    // checkInput is asserting the cursor position with the "|" char.
+    checkInputValueAndCursorPosition(hud, "window.foo.bb|");
+    is(popup.isOpen, true, "popup is open");
+    checkInputCompletionValue(hud, "             b", "completeNode has expected value");
+
+    const {keyOption, expectedInput} = test;
+    info(`Test that arrow left closes the popup and clears complete node`);
+    const onPopUpClose = popup.once("popup-closed");
+    EventUtils.synthesizeKey("KEY_ArrowLeft", keyOption);
+    await onPopUpClose;
+
+    checkInputValueAndCursorPosition(hud, expectedInput);
+    is(popup.isOpen, false, "popup is closed");
+    checkInputCompletionValue(hud, "", "completeNode is empty");
+  }
+  setInputValue(hud, "");
+}
+
+async function checkArrowLeftDismissCompletion(hud) {
+  let tests;
+  if (Services.appinfo.OS == "Darwin") {
+    tests = [{
+      keyOption: null,
+      expectedInput: "window.foo.|a",
+    }, {
+      keyOption: {metaKey: true},
+      expectedInput: "|window.foo.a",
+    }, {
+      keyOption: {altKey: true},
+      expectedInput: "window.foo.|a",
+    }];
+  } else {
+    tests = [{
+      keyOption: null,
+      expectedInput: "window.foo.|a",
+    }, {
+      keyOption: {ctrlKey: true},
+      expectedInput: "window.foo.|a",
+    }];
+  }
+
+  for (const test of tests) {
+    await setInputValueForAutocompletion(hud, "window.foo.a");
+    const prefix = getInputValue(hud).replace(/[\S]/g, " ");
+    checkInputCompletionValue(hud, prefix + "a", "completeNode has expected value");
+
+    info(`Test that arrow left dismiss the completion text`);
+    const {keyOption, expectedInput} = test;
+    EventUtils.synthesizeKey("KEY_ArrowLeft", keyOption);
+
+    checkInputValueAndCursorPosition(hud, expectedInput);
+    checkInputCompletionValue(hud, "", "completeNode is empty");
+  }
+  setInputValue(hud, "");
+}
+
+async function checkArrowRightAcceptCompletion(hud) {
+  const popup = hud.jsterm.autocompletePopup;
+  let tests;
+  if (Services.appinfo.OS == "Darwin") {
+    tests = [{
+      keyOption: null,
+    }, {
+      keyOption: {metaKey: true},
+    }, {
+      keyOption: {altKey: true},
+    }];
+  } else {
+    tests = [{
+      keyOption: null,
+    }, {
+      keyOption: {ctrlKey: true},
+    }];
+  }
+
+  for (const test of tests) {
+    info("Trigger autocomplete popup opening");
+    const onPopUpOpen = popup.once("popup-opened");
+    await setInputValueForAutocompletion(hud, `window.foo.bb`);
+    await onPopUpOpen;
+
+    // checkInput is asserting the cursor position with the "|" char.
+    checkInputValueAndCursorPosition(hud, `window.foo.bb|`);
+    is(popup.isOpen, true, "popup is open");
+    checkInputCompletionValue(hud, "             b", "completeNode has expected value");
+
+    const {keyOption} = test;
+    info(`Test that arrow right closes the popup and accepts the completion`);
+    const onPopUpClose = popup.once("popup-closed");
+    EventUtils.synthesizeKey("KEY_ArrowRight", keyOption);
+    await onPopUpClose;
+
+    checkInputValueAndCursorPosition(hud, "window.foo.bbb|");
+    is(popup.isOpen, false, "popup is closed");
+    checkInputCompletionValue(hud, "", "completeNode is empty");
+  }
+  setInputValue(hud, "");
+}
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_warning_groups.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_warning_groups.js
@@ -11,29 +11,28 @@ const TEST_FILE =
   "browser/devtools/client/webconsole/test/mochitest/test-warning-groups.html";
 const TEST_URI = "http://example.com/" + TEST_FILE;
 
 const TRACKER_URL = "http://tracking.example.org/";
 const BLOCKED_URL = TRACKER_URL +
   "browser/devtools/client/webconsole/test/mochitest/test-image.png";
 
 const {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm");
+UrlClassifierTestUtils.addTestTrackers();
 registerCleanupFunction(function() {
   UrlClassifierTestUtils.cleanupTestTrackers();
 });
 
+pushPref("privacy.trackingprotection.enabled", true);
+pushPref("devtools.webconsole.groupWarningMessages", true);
+
 add_task(async function testContentBlockingMessage() {
   const CONTENT_BLOCKING_GROUP_LABEL = "Content blocked messages";
 
-  // Tracking protection preferences
-  await UrlClassifierTestUtils.addTestTrackers();
-  await pushPref("privacy.trackingprotection.enabled", true);
-
   // Enable groupWarning and persist log
-  await pushPref("devtools.webconsole.groupWarningMessages", true);
   await pushPref("devtools.webconsole.persistlog", true);
 
   const hud = await openNewTabAndConsole(TEST_URI);
 
   info("Log a tracking protection message to check a single message isn't grouped");
   let onContentBlockingWarningMessage = waitForMessage(hud, BLOCKED_URL, ".warn");
   emitStorageAccessBlockedMessage(hud);
   let {node} = await onContentBlockingWarningMessage;
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -22,18 +22,18 @@
   letter-spacing: initial;
   word-spacing: initial;
   color: initial;
   direction: initial;
   writing-mode: initial;
 }
 
 :-moz-native-anonymous .highlighter-container {
-  --highlighter-guide-color: #08c;
-  --highlighter-content-color: #87ceeb;
+  --highlighter-guide-color: hsl(200, 100%, 40%);
+  --highlighter-content-color: hsl(197, 71%, 73%);
   --highlighter-bubble-text-color: hsl(216, 33%, 97%);
   --highlighter-bubble-background-color: hsl(214, 13%, 24%);
   --highlighter-bubble-border-color: rgba(255, 255, 255, 0.2);
   --highlighter-bubble-arrow-size: 8px;
   --highlighter-font-family: message-box;
   --highlighter-font-size: 11px;
   --highlighter-infobar-color: hsl(210, 30%, 85%);
   --highlighter-marker-color: #000;
--- a/devtools/server/actors/highlighters/fonts.js
+++ b/devtools/server/actors/highlighters/fonts.js
@@ -1,20 +1,30 @@
  /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const InspectorUtils = require("InspectorUtils");
+loader.lazyRequireGetter(this, "loadSheet", "devtools/shared/layout/utils", true);
+loader.lazyRequireGetter(this, "removeSheet", "devtools/shared/layout/utils", true);
 
 // How many text runs are we highlighting at a time. There may be many text runs, and we
 // want to prevent performance problems.
 const MAX_TEXT_RANGES = 100;
 
+// This stylesheet is inserted into the page to customize the color of the selected text
+// runs.
+// Note that this color is defined as --highlighter-content-color in the highlighters.css
+// file, and corresponds to the box-model content color. We want to give it an opacity of
+// 0.6 here.
+const STYLESHEET_URI = "data:text/css," +
+  encodeURIComponent("::selection{background-color:hsl(197,71%,73%,.6)!important;}");
+
 /**
  * This highlighter highlights runs of text in the page that have been rendered given a
  * certain font. The highlighting is done with window selection ranges, so no extra
  * markup is being inserted into the content page.
  */
 class FontsHighlighter {
   constructor(highlighterEnv) {
     this.env = highlighterEnv;
@@ -56,16 +66,20 @@ class FontsHighlighter {
 
     // Find the ones we want, based on the provided option.
     const matchingFonts = fonts.filter(f => f.CSSFamilyName === options.CSSFamilyName &&
                                           f.name === options.name);
     if (!matchingFonts.length) {
       return;
     }
 
+    // Load the stylesheet that will customize the color of the highlighter (using a
+    // ::selection rule).
+    loadSheet(this.env.window, STYLESHEET_URI);
+
     // Create a multi-selection in the page to highlight the text runs.
     const selection = doc.defaultView.getSelection();
     selection.removeAllRanges();
 
     for (const matchingFont of matchingFonts) {
       for (const range of matchingFont.ranges) {
         selection.addRange(range);
       }
@@ -73,16 +87,22 @@ class FontsHighlighter {
   }
 
   hide() {
     // No node was highlighted before, don't need to continue any further.
     if (!this.currentNode) {
       return;
     }
 
+    try {
+      removeSheet(this.env.window, STYLESHEET_URI);
+    } catch (e) {
+      // Silently fail here as we might not have inserted the stylesheet at all.
+    }
+
     // Simply remove all current ranges in the seletion.
     const doc = this.currentNodeDocument;
     const selection = doc.defaultView.getSelection();
     selection.removeAllRanges();
   }
 }
 
 exports.FontsHighlighter = FontsHighlighter;
--- a/devtools/shared/heapsnapshot/HeapSnapshot.cpp
+++ b/devtools/shared/heapsnapshot/HeapSnapshot.cpp
@@ -131,28 +131,28 @@ static bool parseMessage(ZeroCopyInputSt
 
 template <typename CharT, typename InternedStringSet>
 struct GetOrInternStringMatcher {
   InternedStringSet& internedStrings;
 
   explicit GetOrInternStringMatcher(InternedStringSet& strings)
       : internedStrings(strings) {}
 
-  const CharT* match(const std::string* str) {
+  const CharT* operator()(const std::string* str) {
     MOZ_ASSERT(str);
     size_t length = str->length() / sizeof(CharT);
     auto tempString = reinterpret_cast<const CharT*>(str->data());
 
     UniqueFreePtr<CharT[]> owned(NS_xstrndup(tempString, length));
     if (!internedStrings.append(std::move(owned))) return nullptr;
 
     return internedStrings.back().get();
   }
 
-  const CharT* match(uint64_t ref) {
+  const CharT* operator()(uint64_t ref) {
     if (MOZ_LIKELY(ref < internedStrings.length())) {
       auto& string = internedStrings[ref];
       MOZ_ASSERT(string);
       return string.get();
     }
 
     return nullptr;
   }
@@ -782,63 +782,34 @@ static bool EstablishBoundaries(JSContex
 }
 
 // A variant covering all the various two-byte strings that we can get from the
 // ubi::Node API.
 class TwoByteString
     : public Variant<JSAtom*, const char16_t*, JS::ubi::EdgeName> {
   using Base = Variant<JSAtom*, const char16_t*, JS::ubi::EdgeName>;
 
-  struct AsTwoByteStringMatcher {
-    TwoByteString match(JSAtom* atom) { return TwoByteString(atom); }
-
-    TwoByteString match(const char16_t* chars) { return TwoByteString(chars); }
-  };
-
-  struct IsNonNullMatcher {
-    template <typename T>
-    bool match(const T& t) {
-      return t != nullptr;
-    }
-  };
-
-  struct LengthMatcher {
-    size_t match(JSAtom* atom) {
-      MOZ_ASSERT(atom);
-      JS::ubi::AtomOrTwoByteChars s(atom);
-      return s.length();
-    }
-
-    size_t match(const char16_t* chars) {
-      MOZ_ASSERT(chars);
-      return NS_strlen(chars);
-    }
-
-    size_t match(const JS::ubi::EdgeName& ptr) {
-      MOZ_ASSERT(ptr);
-      return NS_strlen(ptr.get());
-    }
-  };
-
   struct CopyToBufferMatcher {
     RangedPtr<char16_t> destination;
     size_t maxLength;
 
     CopyToBufferMatcher(RangedPtr<char16_t> destination, size_t maxLength)
         : destination(destination), maxLength(maxLength) {}
 
-    size_t match(JS::ubi::EdgeName& ptr) { return ptr ? match(ptr.get()) : 0; }
+    size_t operator()(JS::ubi::EdgeName& ptr) {
+      return ptr ? operator()(ptr.get()) : 0;
+    }
 
-    size_t match(JSAtom* atom) {
+    size_t operator()(JSAtom* atom) {
       MOZ_ASSERT(atom);
       JS::ubi::AtomOrTwoByteChars s(atom);
       return s.copyToBuffer(destination, maxLength);
     }
 
-    size_t match(const char16_t* chars) {
+    size_t operator()(const char16_t* chars) {
       MOZ_ASSERT(chars);
       JS::ubi::AtomOrTwoByteChars s(chars);
       return s.copyToBuffer(destination, maxLength);
     }
   };
 
  public:
   template <typename T>
@@ -852,30 +823,40 @@ class TwoByteString
     return *this;
   }
 
   TwoByteString(const TwoByteString&) = delete;
   TwoByteString& operator=(const TwoByteString&) = delete;
 
   // Rewrap the inner value of a JS::ubi::AtomOrTwoByteChars as a TwoByteString.
   static TwoByteString from(JS::ubi::AtomOrTwoByteChars&& s) {
-    AsTwoByteStringMatcher m;
-    return s.match(m);
+    return s.match([](auto* a) { return TwoByteString(a); });
   }
 
   // Returns true if the given TwoByteString is non-null, false otherwise.
   bool isNonNull() const {
-    IsNonNullMatcher m;
-    return match(m);
+    return match([](auto& t) { return t != nullptr; });
   }
 
   // Return the length of the string, 0 if it is null.
   size_t length() const {
-    LengthMatcher m;
-    return match(m);
+    return match(
+        [](JSAtom* atom) -> size_t {
+          MOZ_ASSERT(atom);
+          JS::ubi::AtomOrTwoByteChars s(atom);
+          return s.length();
+        },
+        [](const char16_t* chars) -> size_t {
+          MOZ_ASSERT(chars);
+          return NS_strlen(chars);
+        },
+        [](const JS::ubi::EdgeName& ptr) -> size_t {
+          MOZ_ASSERT(ptr);
+          return NS_strlen(ptr.get());
+        });
   }
 
   // Copy the contents of a TwoByteString into the provided buffer. The buffer
   // is NOT null terminated. The number of characters written is returned.
   size_t copyToBuffer(RangedPtr<char16_t> destination, size_t maxLength) {
     CopyToBufferMatcher m(destination, maxLength);
     return match(m);
   }
@@ -890,47 +871,43 @@ class TwoByteString
 // strings. In practice, we expect the amount of this duplication to be very low
 // because each type is generally a different semantic thing in addition to
 // having a slightly different representation. For example, the set of edge
 // names and the set stack frames' source names naturally tend not to overlap
 // very much if at all.
 struct TwoByteString::HashPolicy {
   using Lookup = TwoByteString;
 
-  struct HashingMatcher {
-    js::HashNumber match(const JSAtom* atom) {
-      return js::DefaultHasher<const JSAtom*>::hash(atom);
-    }
-
-    js::HashNumber match(const char16_t* chars) {
-      MOZ_ASSERT(chars);
-      auto length = NS_strlen(chars);
-      return HashString(chars, length);
-    }
-
-    js::HashNumber match(const JS::ubi::EdgeName& ptr) {
-      MOZ_ASSERT(ptr);
-      return match(ptr.get());
-    }
-  };
-
   static js::HashNumber hash(const Lookup& l) {
-    HashingMatcher hasher;
-    return l.match(hasher);
+    return l.match(
+        [](const JSAtom* atom) {
+          return js::DefaultHasher<const JSAtom*>::hash(atom);
+        },
+        [](const char16_t* chars) {
+          MOZ_ASSERT(chars);
+          auto length = NS_strlen(chars);
+          return HashString(chars, length);
+        },
+        [](const JS::ubi::EdgeName& ptr) {
+          const char16_t* chars = ptr.get();
+          MOZ_ASSERT(chars);
+          auto length = NS_strlen(chars);
+          return HashString(chars, length);
+        });
   }
 
   struct EqualityMatcher {
     const TwoByteString& rhs;
     explicit EqualityMatcher(const TwoByteString& rhs) : rhs(rhs) {}
 
-    bool match(const JSAtom* atom) {
+    bool operator()(const JSAtom* atom) {
       return rhs.is<JSAtom*>() && rhs.as<JSAtom*>() == atom;
     }
 
-    bool match(const char16_t* chars) {
+    bool operator()(const char16_t* chars) {
       MOZ_ASSERT(chars);
 
       const char16_t* rhsChars = nullptr;
       if (rhs.is<const char16_t*>())
         rhsChars = rhs.as<const char16_t*>();
       else if (rhs.is<JS::ubi::EdgeName>())
         rhsChars = rhs.as<JS::ubi::EdgeName>().get();
       else
@@ -938,19 +915,19 @@ struct TwoByteString::HashPolicy {
       MOZ_ASSERT(rhsChars);
 
       auto length = NS_strlen(chars);
       if (NS_strlen(rhsChars) != length) return false;
 
       return memcmp(chars, rhsChars, length * sizeof(char16_t)) == 0;
     }
 
-    bool match(const JS::ubi::EdgeName& ptr) {
+    bool operator()(const JS::ubi::EdgeName& ptr) {
       MOZ_ASSERT(ptr);
-      return match(ptr.get());
+      return operator()(ptr.get());
     }
   };
 
   static bool match(const TwoByteString& k, const Lookup& l) {
     EqualityMatcher eq(l);
     return k.match(eq);
   }
 
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -1863,16 +1863,17 @@ class Element : public FragmentOrElement
   /**
    * Handle status bar updates before they can be cancelled.
    */
   void GetEventTargetParentForLinks(EventChainPreVisitor& aVisitor);
 
   /**
    * Handle default actions for link event if the event isn't consumed yet.
    */
+  MOZ_CAN_RUN_SCRIPT
   nsresult PostHandleEventForLinks(EventChainPostVisitor& aVisitor);
 
   /**
    * Get the target of this link element. Consumers should established that
    * this element is a link (probably using IsLink) before calling this
    * function (or else why call it?)
    *
    * Note: for HTML this gets the value of the 'target' attribute; for XLink
--- a/dom/base/Selection.h
+++ b/dom/base/Selection.h
@@ -905,14 +905,15 @@ inline RawSelectionType ToRawSelectionTy
 inline RawSelectionType ToRawSelectionType(TextRangeType aTextRangeType) {
   return ToRawSelectionType(ToSelectionType(aTextRangeType));
 }
 
 inline SelectionTypeMask ToSelectionTypeMask(SelectionType aSelectionType) {
   MOZ_ASSERT(aSelectionType != SelectionType::eInvalid);
   return aSelectionType == SelectionType::eNone
              ? 0
-             : (1 << (static_cast<uint8_t>(aSelectionType) - 1));
+             : static_cast<SelectionTypeMask>(
+                   1 << (static_cast<uint8_t>(aSelectionType) - 1));
 }
 
 }  // namespace mozilla
 
 #endif  // mozilla_Selection_h__
--- a/dom/base/domerr.msg
+++ b/dom/base/domerr.msg
@@ -157,15 +157,14 @@ DOM4_MSG_DEF(SyntaxError, "Invalid heade
 
 /* XMLHttpRequest errors. */
 DOM4_MSG_DEF(InvalidStateError, "XMLHttpRequest has an invalid context.", NS_ERROR_DOM_INVALID_STATE_XHR_HAS_INVALID_CONTEXT)
 DOM4_MSG_DEF(InvalidStateError, "XMLHttpRequest state must be OPENED.", NS_ERROR_DOM_INVALID_STATE_XHR_MUST_BE_OPENED)
 DOM4_MSG_DEF(InvalidStateError, "XMLHttpRequest must not be sending.", NS_ERROR_DOM_INVALID_STATE_XHR_MUST_NOT_BE_SENDING)
 DOM4_MSG_DEF(InvalidStateError, "XMLHttpRequest state must not be LOADING or DONE.", NS_ERROR_DOM_INVALID_STATE_XHR_MUST_NOT_BE_LOADING_OR_DONE)
 DOM4_MSG_DEF(InvalidStateError, "responseXML is only available if responseType is '' or 'document'.", NS_ERROR_DOM_INVALID_STATE_XHR_HAS_WRONG_RESPONSETYPE_FOR_RESPONSEXML)
 DOM4_MSG_DEF(InvalidStateError, "responseText is only available if responseType is '' or 'text'.", NS_ERROR_DOM_INVALID_STATE_XHR_HAS_WRONG_RESPONSETYPE_FOR_RESPONSETEXT)
-DOM4_MSG_DEF(InvalidStateError, "synchronous XMLHttpRequests do not support 'moz-chunked-arraybuffer' responseType.", NS_ERROR_DOM_INVALID_STATE_XHR_CHUNKED_RESPONSETYPES_UNSUPPORTED_FOR_SYNC)
 DOM4_MSG_DEF(InvalidAccessError, "synchronous XMLHttpRequests do not support timeout and responseType.", NS_ERROR_DOM_INVALID_ACCESS_XHR_TIMEOUT_AND_RESPONSETYPE_UNSUPPORTED_FOR_SYNC)
 
 /* Image decode errors. */
 DOM4_MSG_DEF(EncodingError, "Node bound to inactive document.", NS_ERROR_DOM_IMAGE_INACTIVE_DOCUMENT)
 DOM4_MSG_DEF(EncodingError, "Invalid image request.", NS_ERROR_DOM_IMAGE_INVALID_REQUEST)
 DOM4_MSG_DEF(EncodingError, "Invalid encoded image data.", NS_ERROR_DOM_IMAGE_BROKEN)
--- a/dom/base/nsINode.h
+++ b/dom/base/nsINode.h
@@ -943,16 +943,17 @@ class nsINode : public mozilla::dom::Eve
   virtual nsPIDOMWindowOuter* GetOwnerGlobalForBindingsInternal() override;
   virtual nsIGlobalObject* GetOwnerGlobal() const override;
 
   using mozilla::dom::EventTarget::DispatchEvent;
   bool DispatchEvent(mozilla::dom::Event& aEvent,
                      mozilla::dom::CallerType aCallerType,
                      mozilla::ErrorResult& aRv) override;
 
+  MOZ_CAN_RUN_SCRIPT
   nsresult PostHandleEvent(mozilla::EventChainPostVisitor& aVisitor) override;
 
   /**
    * Adds a mutation observer to be notified when this node, or any of its
    * descendants, are modified. The node will hold a weak reference to the
    * observer, which means that it is the responsibility of the observer to
    * remove itself in case it dies before the node.  If an observer is added
    * while observers are being notified, it may also be notified.  In general,
--- a/dom/base/test/file_bug1008126_worker.js
+++ b/dom/base/test/file_bug1008126_worker.js
@@ -50,42 +50,16 @@ self.onmessage = function onmessage(even
       ok(false, "Error: " + e.error + "\n");
     };
     xhr.onprogress = null;
     xhr.onreadystatechange = null;
     xhr.onload = null;
     xhr.onloadend = null;
   }
 
-  function test_chunked_arraybuffer() {
-    ok(true, "Test chunked arraybuffer");
-
-    var lastIndex = 0;
-    xhr.onprogress = function(event) {
-      if (xhr.response) {
-        var buf = new Uint8Array(xhr.response);
-        var allMatched = true;
-        // The content of data cycles from 0 to 9 (i.e. 01234567890123......).
-        for (var i = 0; i < buf.length; i++) {
-          if (String.fromCharCode(buf[i]) != lastIndex % 10) {
-            allMatched = false;
-            break;
-          }
-          lastIndex++;
-        }
-        ok(allMatched, "Data chunk is correct.  Loaded " +
-                        event.loaded + "/" + event.total + " bytes.");
-      }
-    };
-    xhr.onload = runTests;
-    xhr.open("GET", makeJarURL(gEntry3), true);
-    xhr.responseType = "moz-chunked-arraybuffer";
-    xhr.send();
-  }
-
   var readystatechangeCount = 0;
   var loadCount = 0;
   var loadendCount = 0;
 
   function checkEventCount(cb) {
     ok(readystatechangeCount == 1 && loadCount == 1 && loadendCount == 1,
        "Saw all expected events");
     cb();
@@ -147,17 +121,16 @@ self.onmessage = function onmessage(even
       checkData(xhr, gData2, false, runTests);
     };
     xhr.open("GET", makeJarURL(gEntry2), true);
     xhr.responseType = "arraybuffer";
     xhr.send();
   }
 
   var tests = [
-    test_chunked_arraybuffer,
     test_multiple_events,
     test_sync_xhr_data1,
     test_sync_xhr_data2,
     test_async_xhr_data1,
     test_async_xhr_data2
   ];
 
   function runTests() {
--- a/dom/canvas/CanvasRenderingContext2D.cpp
+++ b/dom/canvas/CanvasRenderingContext2D.cpp
@@ -887,33 +887,17 @@ NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_B
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CanvasRenderingContext2D)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY(nsICanvasRenderingContextInternal)
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
-CanvasRenderingContext2D::ContextState::ContextState()
-    : textAlign(TextAlign::START),
-      textBaseline(TextBaseline::ALPHABETIC),
-      shadowColor(0),
-      lineWidth(1.0f),
-      miterLimit(10.0f),
-      globalAlpha(1.0f),
-      shadowBlur(0.0),
-      dashOffset(0.0f),
-      op(mozilla::gfx::CompositionOp::OP_OVER),
-      fillRule(mozilla::gfx::FillRule::FILL_WINDING),
-      lineCap(mozilla::gfx::CapStyle::BUTT),
-      lineJoin(mozilla::gfx::JoinStyle::MITER_OR_BEVEL),
-      filterString(u"none"),
-      filterSourceGraphicTainted(false),
-      imageSmoothingEnabled(true),
-      fontExplicitLanguage(false) {}
+CanvasRenderingContext2D::ContextState::ContextState() =  default;
 
 CanvasRenderingContext2D::ContextState::ContextState(const ContextState& aOther)
     : fontGroup(aOther.fontGroup),
       fontLanguage(aOther.fontLanguage),
       fontFont(aOther.fontFont),
       gradientStyles(aOther.gradientStyles),
       patternStyles(aOther.patternStyles),
       colorStyles(aOther.colorStyles),
--- a/dom/canvas/CanvasRenderingContext2D.h
+++ b/dom/canvas/CanvasRenderingContext2D.h
@@ -940,36 +940,37 @@ class CanvasRenderingContext2D final : p
     RefPtr<nsAtom> fontLanguage;
     nsFont fontFont;
 
     EnumeratedArray<Style, Style::MAX, RefPtr<CanvasGradient>> gradientStyles;
     EnumeratedArray<Style, Style::MAX, RefPtr<CanvasPattern>> patternStyles;
     EnumeratedArray<Style, Style::MAX, nscolor> colorStyles;
 
     nsString font;
-    TextAlign textAlign;
-    TextBaseline textBaseline;
+    TextAlign textAlign = TextAlign::START;
+    TextBaseline textBaseline = TextBaseline::ALPHABETIC;
 
-    nscolor shadowColor;
+    nscolor shadowColor = 0;
 
     mozilla::gfx::Matrix transform;
     mozilla::gfx::Point shadowOffset;
-    mozilla::gfx::Float lineWidth;
-    mozilla::gfx::Float miterLimit;
-    mozilla::gfx::Float globalAlpha;
-    mozilla::gfx::Float shadowBlur;
-    nsTArray<mozilla::gfx::Float> dash;
-    mozilla::gfx::Float dashOffset;
+    mozilla::gfx::Float lineWidth = 1.0f;
+    mozilla::gfx::Float miterLimit = 10.0f;
+    mozilla::gfx::Float globalAlpha = 1.0f;
+    mozilla::gfx::Float shadowBlur = 0.0f;
 
-    mozilla::gfx::CompositionOp op;
-    mozilla::gfx::FillRule fillRule;
-    mozilla::gfx::CapStyle lineCap;
-    mozilla::gfx::JoinStyle lineJoin;
+    nsTArray<mozilla::gfx::Float> dash;
+    mozilla::gfx::Float dashOffset = 0.0f;
 
-    nsString filterString;
+    mozilla::gfx::CompositionOp op = mozilla::gfx::CompositionOp::OP_OVER;
+    mozilla::gfx::FillRule fillRule = mozilla::gfx::FillRule::FILL_WINDING;
+    mozilla::gfx::CapStyle lineCap = mozilla::gfx::CapStyle::BUTT;
+    mozilla::gfx::JoinStyle lineJoin = mozilla::gfx::JoinStyle::MITER_OR_BEVEL;
+
+    nsString filterString = nsString(u"none");
     nsTArray<nsStyleFilter> filterChain;
     // RAII object that we obtain when we start to observer SVG filter elements
     // for rendering changes.  When released we stop observing the SVG elements.
     nsCOMPtr<nsISupports> autoSVGFiltersObserver;
     mozilla::gfx::FilterDescription filter;
     nsTArray<RefPtr<mozilla::gfx::SourceSurface>> filterAdditionalImages;
 
     // This keeps track of whether the canvas was "tainted" or not when
@@ -978,20 +979,20 @@ class CanvasRenderingContext2D final : p
     // This is to stop bad actors from reading back data they shouldn't have
     // access to.
     //
     // This also limits what filters we can apply to the context; in particular
     // feDisplacementMap is restricted.
     //
     // We keep track of this to ensure that if this gets out of sync with the
     // tainted state of the canvas itself, we update our filters accordingly.
-    bool filterSourceGraphicTainted;
+    bool filterSourceGraphicTainted = false;
 
-    bool imageSmoothingEnabled;
-    bool fontExplicitLanguage;
+    bool imageSmoothingEnabled = true;
+    bool fontExplicitLanguage = false;
   };
 
   AutoTArray<ContextState, 3> mStyleStack;
 
   inline ContextState& CurrentState() {
     return mStyleStack[mStyleStack.Length() - 1];
   }
 
--- a/dom/canvas/ImageBitmapColorUtils.cpp
+++ b/dom/canvas/ImageBitmapColorUtils.cpp
@@ -14,19 +14,19 @@ namespace dom {
  * Utility function form libyuv source files.
  */
 static __inline int32_t clamp0(int32_t v) { return ((-(v) >> 31) & (v)); }
 
 static __inline int32_t clamp255(int32_t v) {
   return (((255 - (v)) >> 31) | (v)) & 255;
 }
 
-static __inline uint32_t Clamp(int32_t val) {
-  int v = clamp0(val);
-  return (uint32_t)(clamp255(v));
+static __inline uint8_t Clamp(int32_t val) {
+  const auto v = clamp0(val);
+  return uint8_t(clamp255(v));
 }
 
 #define YG 74 /* (int8_t)(1.164 * 64 + 0.5) */
 
 #define UB 127 /* min(63,(int8_t)(2.018 * 64)) */
 #define UG -25 /* (int8_t)(-0.391 * 64 - 0.5) */
 #define UR 0
 
@@ -42,26 +42,26 @@ static __inline uint32_t Clamp(int32_t v
 static __inline void YuvPixel(uint8_t y, uint8_t u, uint8_t v, uint8_t* b,
                               uint8_t* g, uint8_t* r) {
   int32_t y1 = ((int32_t)(y)-16) * YG;
   *b = Clamp((int32_t)((u * UB + v * VB) - (BB) + y1) >> 6);
   *g = Clamp((int32_t)((u * UG + v * VG) - (BG) + y1) >> 6);
   *r = Clamp((int32_t)((u * UR + v * VR) - (BR) + y1) >> 6);
 }
 
-static __inline int RGBToY(uint8_t r, uint8_t g, uint8_t b) {
-  return (66 * r + 129 * g + 25 * b + 0x1080) >> 8;
+static __inline uint8_t RGBToY(uint8_t r, uint8_t g, uint8_t b) {
+  return uint8_t((66 * r + 129 * g + 25 * b + 0x1080) >> 8);
 }
 
-static __inline int RGBToU(uint8_t r, uint8_t g, uint8_t b) {
-  return (112 * b - 74 * g - 38 * r + 0x8080) >> 8;
+static __inline uint8_t RGBToU(uint8_t r, uint8_t g, uint8_t b) {
+  return uint8_t((112 * b - 74 * g - 38 * r + 0x8080) >> 8);
 }
 
-static __inline int RGBToV(uint8_t r, uint8_t g, uint8_t b) {
-  return (112 * r - 94 * g - 18 * b + 0x8080) >> 8;
+static __inline uint8_t RGBToV(uint8_t r, uint8_t g, uint8_t b) {
+  return uint8_t((112 * r - 94 * g - 18 * b + 0x8080) >> 8);
 }
 
 /*
  * Generic functions.
  */
 template <int aSrcRIndex, int aSrcGIndex, int aSrcBIndex, int aDstRIndex,
           int aDstGIndex, int aDstBIndex, int aDstAIndex>
 static int RGBFamilyToRGBAFamily(const uint8_t* aSrcBuffer, int aSrcStride,
--- a/dom/canvas/ImageBitmapUtils.cpp
+++ b/dom/canvas/ImageBitmapUtils.cpp
@@ -186,17 +186,17 @@ class Utils {
   // Check whether or not the current ImageBitmapFormat can be converted from
   // the given ImageBitmapFormat.
   virtual bool CanConvertFrom(ImageBitmapFormat aSrcFormat) = 0;
 
   // Get the number of channels.
   uint8_t GetChannelCount() const { return mChannels; }
 
  protected:
-  Utils(uint32_t aChannels, ChannelPixelLayoutDataType aDataType)
+  Utils(uint8_t aChannels, ChannelPixelLayoutDataType aDataType)
       : mChannels(aChannels),
         mBytesPerPixelValue(GetBytesPerPixelValue(aDataType)),
         mDataType(aDataType) {}
 
   virtual ~Utils() {}
 
   const uint8_t mChannels;
   const int mBytesPerPixelValue;
--- a/dom/canvas/WebGLContext.cpp
+++ b/dom/canvas/WebGLContext.cpp
@@ -123,17 +123,17 @@ WebGLContext::WebGLContext()
       mDataAllocGLCallCount(0),
       mBypassShaderValidation(false),
       mEmptyTFO(0),
       mContextLossHandler(this),
       mNeedsFakeNoAlpha(false),
       mNeedsFakeNoDepth(false),
       mNeedsFakeNoStencil(false),
       mAllowFBInvalidation(gfxPrefs::WebGLFBInvalidation()),
-      mMsaaSamples(gfxPrefs::WebGLMsaaSamples()) {
+      mMsaaSamples((uint8_t)gfxPrefs::WebGLMsaaSamples()) {
   mGeneration = 0;
   mInvalidated = false;
   mCapturedFrameInvalidated = false;
   mShouldPresent = true;
   mResetLayer = true;
   mOptionsFrozen = false;
   mDisableExtensions = false;
   mIsMesa = false;
--- a/dom/canvas/WebGLContextFramebufferOperations.cpp
+++ b/dom/canvas/WebGLContextFramebufferOperations.cpp
@@ -107,18 +107,18 @@ void WebGLContext::ClearStencil(GLint v)
   gl->fClearStencil(v);
 }
 
 void WebGLContext::ColorMask(WebGLboolean r, WebGLboolean g, WebGLboolean b,
                              WebGLboolean a) {
   const FuncScope funcScope(*this, "colorMask");
   if (IsContextLost()) return;
 
-  mColorWriteMask = uint8_t(bool(r)) << 0 | uint8_t(bool(g)) << 1 |
-                    uint8_t(bool(b)) << 2 | uint8_t(bool(a)) << 3;
+  mColorWriteMask = uint8_t(bool(r) << 0) | uint8_t(bool(g) << 1) |
+                    uint8_t(bool(b) << 2) | uint8_t(bool(a) << 3);
 }
 
 void WebGLContext::DepthMask(WebGLboolean b) {
   const FuncScope funcScope(*this, "depthMask");
   if (IsContextLost()) return;
 
   mDepthWriteMask = b;
   gl->fDepthMask(b);
--- a/dom/canvas/WebGLContextVertices.cpp
+++ b/dom/canvas/WebGLContextVertices.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "WebGLContext.h"
 
 #include "GLContext.h"
+#include "mozilla/Casting.h"
 #include "mozilla/CheckedInt.h"
 #include "WebGLBuffer.h"
 #include "WebGLFramebuffer.h"
 #include "WebGLProgram.h"
 #include "WebGLRenderbuffer.h"
 #include "WebGLShader.h"
 #include "WebGLTexture.h"
 #include "WebGLVertexArray.h"
@@ -396,17 +397,17 @@ void WebGLContext::VertexAttribAnyPointe
     gl->fVertexAttribIPointer(index, size, type, stride,
                               reinterpret_cast<void*>(byteOffset));
   } else {
     gl->fVertexAttribPointer(index, size, type, normalized, stride,
                              reinterpret_cast<void*>(byteOffset));
   }
 
   WebGLVertexAttribData& vd = mBoundVertexArray->mAttribs[index];
-  vd.VertexAttribPointer(isFuncInt, buffer, size, type, normalized, stride,
+  vd.VertexAttribPointer(isFuncInt, buffer, AutoAssertCast(size), type, normalized, stride,
                          byteOffset);
   mBoundVertexArray->InvalidateCaches();
 }
 
 ////////////////////////////////////////
 
 void WebGLContext::VertexAttribDivisor(GLuint index, GLuint divisor) {
   const FuncScope funcScope(*this, "vertexAttribDivisor");
--- a/dom/canvas/WebGLTexelConversions.h
+++ b/dom/canvas/WebGLTexelConversions.h
@@ -516,19 +516,19 @@ MOZ_ALWAYS_INLINE void unpack<WebGLTexel
 // 3-channel formats
 template <>
 MOZ_ALWAYS_INLINE void unpack<WebGLTexelFormat::RGB565, uint16_t, uint8_t>(
     const uint16_t* __restrict src, uint8_t* __restrict dst) {
   uint16_t packedValue = src[0];
   uint8_t r = (packedValue >> 11) & 0x1F;
   uint8_t g = (packedValue >> 5) & 0x3F;
   uint8_t b = packedValue & 0x1F;
-  dst[0] = (r << 3) | (r & 0x7);
-  dst[1] = (g << 2) | (g & 0x3);
-  dst[2] = (b << 3) | (b & 0x7);
+  dst[0] = uint8_t(r << 3) | (r & 0x7);
+  dst[1] = uint8_t(g << 2) | (g & 0x3);
+  dst[2] = uint8_t(b << 3) | (b & 0x7);
   dst[3] = 0xFF;
 }
 
 template <>
 MOZ_ALWAYS_INLINE void unpack<WebGLTexelFormat::RGB8, uint8_t, uint8_t>(
     const uint8_t* __restrict src, uint8_t* __restrict dst) {
   dst[0] = src[0];
   dst[1] = src[1];
@@ -559,32 +559,32 @@ MOZ_ALWAYS_INLINE void unpack<WebGLTexel
 template <>
 MOZ_ALWAYS_INLINE void unpack<WebGLTexelFormat::RGBA4444, uint16_t, uint8_t>(
     const uint16_t* __restrict src, uint8_t* __restrict dst) {
   uint16_t packedValue = src[0];
   uint8_t r = (packedValue >> 12) & 0x0F;
   uint8_t g = (packedValue >> 8) & 0x0F;
   uint8_t b = (packedValue >> 4) & 0x0F;
   uint8_t a = packedValue & 0x0F;
-  dst[0] = (r << 4) | r;
-  dst[1] = (g << 4) | g;
-  dst[2] = (b << 4) | b;
-  dst[3] = (a << 4) | a;
+  dst[0] = uint8_t(r << 4) | r;
+  dst[1] = uint8_t(g << 4) | g;
+  dst[2] = uint8_t(b << 4) | b;
+  dst[3] = uint8_t(a << 4) | a;
 }
 
 template <>
 MOZ_ALWAYS_INLINE void unpack<WebGLTexelFormat::RGBA5551, uint16_t, uint8_t>(
     const uint16_t* __restrict src, uint8_t* __restrict dst) {
   uint16_t packedValue = src[0];
   uint8_t r = (packedValue >> 11) & 0x1F;
   uint8_t g = (packedValue >> 6) & 0x1F;
   uint8_t b = (packedValue >> 1) & 0x1F;
-  dst[0] = (r << 3) | (r & 0x7);
-  dst[1] = (g << 3) | (g & 0x7);
-  dst[2] = (b << 3) | (b & 0x7);
+  dst[0] = uint8_t(r << 3) | (r & 0x7);
+  dst[1] = uint8_t(g << 3) | (g & 0x7);
+  dst[2] = uint8_t(b << 3) | (b & 0x7);
   dst[3] = (packedValue & 0x1) ? 0xFF : 0;
 }
 
 template <>
 MOZ_ALWAYS_INLINE void unpack<WebGLTexelFormat::RGBA8, uint8_t, uint8_t>(
     const uint8_t* __restrict src, uint8_t* __restrict dst) {
   dst[0] = src[0];
   dst[1] = src[1];
@@ -956,43 +956,43 @@ pack<WebGLTexelFormat::RG32F, WebGLTexel
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // 3-channel formats
 template <>
 MOZ_ALWAYS_INLINE void
 pack<WebGLTexelFormat::RGB565, WebGLTexelPremultiplicationOp::None, uint8_t,
      uint16_t>(const uint8_t* __restrict src, uint16_t* __restrict dst) {
-  *dst = (((src[0] & 0xF8) << 8) | ((src[1] & 0xFC) << 3) |
+  *dst = uint16_t(((src[0] & 0xF8) << 8) | ((src[1] & 0xFC) << 3) |
           ((src[2] & 0xF8) >> 3));
 }
 
 template <>
 MOZ_ALWAYS_INLINE void
 pack<WebGLTexelFormat::RGB565, WebGLTexelPremultiplicationOp::Premultiply,
      uint8_t, uint16_t>(const uint8_t* __restrict src,
                         uint16_t* __restrict dst) {
   float scaleFactor = src[3] / 255.0f;
   uint8_t srcR = static_cast<uint8_t>(src[0] * scaleFactor);
   uint8_t srcG = static_cast<uint8_t>(src[1] * scaleFactor);
   uint8_t srcB = static_cast<uint8_t>(src[2] * scaleFactor);
-  *dst = (((srcR & 0xF8) << 8) | ((srcG & 0xFC) << 3) | ((srcB & 0xF8) >> 3));
+  *dst = uint16_t(((srcR & 0xF8) << 8) | ((srcG & 0xFC) << 3) | ((srcB & 0xF8) >> 3));
 }
 
 // FIXME: this routine is lossy and must be removed.
 template <>
 MOZ_ALWAYS_INLINE void
 pack<WebGLTexelFormat::RGB565, WebGLTexelPremultiplicationOp::Unpremultiply,
      uint8_t, uint16_t>(const uint8_t* __restrict src,
                         uint16_t* __restrict dst) {
   float scaleFactor = src[3] ? 255.0f / src[3] : 1.0f;
   uint8_t srcR = static_cast<uint8_t>(src[0] * scaleFactor);
   uint8_t srcG = static_cast<uint8_t>(src[1] * scaleFactor);
   uint8_t srcB = static_cast<uint8_t>(src[2] * scaleFactor);
-  *dst = (((srcR & 0xF8) << 8) | ((srcG & 0xFC) << 3) | ((srcB & 0xF8) >> 3));
+  *dst = uint16_t(((srcR & 0xF8) << 8) | ((srcG & 0xFC) << 3) | ((srcB & 0xF8) >> 3));
 }
 
 template <>
 MOZ_ALWAYS_INLINE void
 pack<WebGLTexelFormat::RGB8, WebGLTexelPremultiplicationOp::None, uint8_t,
      uint8_t>(const uint8_t* __restrict src, uint8_t* __restrict dst) {
   dst[0] = src[0];
   dst[1] = src[1];
@@ -1116,79 +1116,79 @@ pack<WebGLTexelFormat::RGB32F, WebGLTexe
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // 4-channel formats
 template <>
 MOZ_ALWAYS_INLINE void
 pack<WebGLTexelFormat::RGBA4444, WebGLTexelPremultiplicationOp::None, uint8_t,
      uint16_t>(const uint8_t* __restrict src, uint16_t* __restrict dst) {
-  *dst = (((src[0] & 0xF0) << 8) | ((src[1] & 0xF0) << 4) | (src[2] & 0xF0) |
+  *dst = uint16_t(((src[0] & 0xF0) << 8) | ((src[1] & 0xF0) << 4) | (src[2] & 0xF0) |
           (src[3] >> 4));
 }
 
 template <>
 MOZ_ALWAYS_INLINE void
 pack<WebGLTexelFormat::RGBA4444, WebGLTexelPremultiplicationOp::Premultiply,
      uint8_t, uint16_t>(const uint8_t* __restrict src,
                         uint16_t* __restrict dst) {
   float scaleFactor = src[3] / 255.0f;
   uint8_t srcR = static_cast<uint8_t>(src[0] * scaleFactor);
   uint8_t srcG = static_cast<uint8_t>(src[1] * scaleFactor);
   uint8_t srcB = static_cast<uint8_t>(src[2] * scaleFactor);
-  *dst = (((srcR & 0xF0) << 8) | ((srcG & 0xF0) << 4) | (srcB & 0xF0) |
+  *dst = uint16_t(((srcR & 0xF0) << 8) | ((srcG & 0xF0) << 4) | (srcB & 0xF0) |
           (src[3] >> 4));
 }
 
 // FIXME: this routine is lossy and must be removed.
 template <>
 MOZ_ALWAYS_INLINE void
 pack<WebGLTexelFormat::RGBA4444, WebGLTexelPremultiplicationOp::Unpremultiply,
      uint8_t, uint16_t>(const uint8_t* __restrict src,
                         uint16_t* __restrict dst) {
   float scaleFactor = src[3] ? 255.0f / src[3] : 1.0f;
   uint8_t srcR = static_cast<uint8_t>(src[0] * scaleFactor);
   uint8_t srcG = static_cast<uint8_t>(src[1] * scaleFactor);
   uint8_t srcB = static_cast<uint8_t>(src[2] * scaleFactor);
-  *dst = (((srcR & 0xF0) << 8) | ((srcG & 0xF0) << 4) | (srcB & 0xF0) |
+  *dst = uint16_t(((srcR & 0xF0) << 8) | ((srcG & 0xF0) << 4) | (srcB & 0xF0) |
           (src[3] >> 4));
 }
 
 template <>
 MOZ_ALWAYS_INLINE void
 pack<WebGLTexelFormat::RGBA5551, WebGLTexelPremultiplicationOp::None, uint8_t,
      uint16_t>(const uint8_t* __restrict src, uint16_t* __restrict dst) {
-  *dst = (((src[0] & 0xF8) << 8) | ((src[1] & 0xF8) << 3) |
+  *dst = uint16_t(((src[0] & 0xF8) << 8) | ((src[1] & 0xF8) << 3) |
           ((src[2] & 0xF8) >> 2) | (src[3] >> 7));
 }
 
 template <>
 MOZ_ALWAYS_INLINE void
 pack<WebGLTexelFormat::RGBA5551, WebGLTexelPremultiplicationOp::Premultiply,
      uint8_t, uint16_t>(const uint8_t* __restrict src,
                         uint16_t* __restrict dst) {
   float scaleFactor = src[3] / 255.0f;
   uint8_t srcR = static_cast<uint8_t>(src[0] * scaleFactor);
   uint8_t srcG = static_cast<uint8_t>(src[1] * scaleFactor);
   uint8_t srcB = static_cast<uint8_t>(src[2] * scaleFactor);
-  *dst = (((srcR & 0xF8) << 8) | ((srcG & 0xF8) << 3) | ((srcB & 0xF8) >> 2) |
+  *dst = uint16_t(((srcR & 0xF8) << 8) | ((srcG & 0xF8) << 3) | ((srcB & 0xF8) >> 2) |
           (src[3] >> 7));
 }
 
 // FIXME: this routine is lossy and must be removed.
 template <>
 MOZ_ALWAYS_INLINE void
 pack<WebGLTexelFormat::RGBA5551, WebGLTexelPremultiplicationOp::Unpremultiply,
      uint8_t, uint16_t>(const uint8_t* __restrict src,
                         uint16_t* __restrict dst) {
   float scaleFactor = src[3] ? 255.0f / src[3] : 1.0f;
   uint8_t srcR = static_cast<uint8_t>(src[0] * scaleFactor);
   uint8_t srcG = static_cast<uint8_t>(src[1] * scaleFactor);
   uint8_t srcB = static_cast<uint8_t>(src[2] * scaleFactor);
-  *dst = (((srcR & 0xF8) << 8) | ((srcG & 0xF8) << 3) | ((srcB & 0xF8) >> 2) |
+  *dst = uint16_t(((srcR & 0xF8) << 8) | ((srcG & 0xF8) << 3) | ((srcB & 0xF8) >> 2) |
           (src[3] >> 7));
 }
 
 template <>
 MOZ_ALWAYS_INLINE void
 pack<WebGLTexelFormat::RGBA8, WebGLTexelPremultiplicationOp::None, uint8_t,
      uint8_t>(const uint8_t* __restrict src, uint8_t* __restrict dst) {
   dst[0] = src[0];
--- a/dom/canvas/WebGLTexture.cpp
+++ b/dom/canvas/WebGLTexture.cpp
@@ -2,16 +2,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "WebGLTexture.h"
 
 #include <algorithm>
 #include "GLContext.h"
+#include "mozilla/Casting.h"
 #include "mozilla/dom/WebGLRenderingContextBinding.h"
 #include "mozilla/gfx/Logging.h"
 #include "mozilla/MathAlgorithms.h"
 #include "mozilla/Scoped.h"
 #include "mozilla/Unused.h"
 #include "ScopedGLHelpers.h"
 #include "WebGLContext.h"
 #include "WebGLContextUtils.h"
@@ -255,17 +256,17 @@ Maybe<const WebGLTexture::CompletenessIn
   if (skipMips) return ret;
 
   if (!IsMipAndCubeComplete(maxLevel, ensureInit, &initFailed)) {
     if (initFailed) return {};
 
     ret->incompleteReason = "Bad mipmap dimension or format.";
     return ret;
   }
-  ret->levels = maxLevel - mBaseMipmapLevel + 1;
+  ret->levels = AutoAssertCast(maxLevel - mBaseMipmapLevel + 1);
   ret->mipmapComplete = true;
 
   // -
 
   return ret;
 }
 
 Maybe<const webgl::SampleableInfo> WebGLTexture::CalcSampleableInfo(
--- a/dom/canvas/WebGLTexture.h
+++ b/dom/canvas/WebGLTexture.h
@@ -7,16 +7,17 @@
 #define WEBGL_TEXTURE_H_
 
 #include <algorithm>
 #include <map>
 #include <set>
 #include <vector>
 
 #include "mozilla/Assertions.h"
+#include "mozilla/Casting.h"
 #include "mozilla/CheckedInt.h"
 #include "mozilla/dom/TypedArray.h"
 #include "mozilla/LinkedList.h"
 #include "nsWrapperCache.h"
 
 #include "CacheInvalidator.h"
 #include "WebGLObjectModel.h"
 #include "WebGLStrongTypes.h"
@@ -264,17 +265,17 @@ class WebGLTexture final : public nsWrap
     GLenum rawTexImageTarget = texImageTarget.get();
     switch (rawTexImageTarget) {
       case LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X:
       case LOCAL_GL_TEXTURE_CUBE_MAP_NEGATIVE_X:
       case LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_Y:
       case LOCAL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
       case LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_Z:
       case LOCAL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
-        return rawTexImageTarget - LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X;
+        return AutoAssertCast(rawTexImageTarget - LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X);
 
       default:
         return 0;
     }
   }
 
   auto& ImageInfoAtFace(uint8_t face, uint32_t level) {
     MOZ_ASSERT(face < mFaceCount);
--- a/dom/canvas/WebGLTextureUpload.cpp
+++ b/dom/canvas/WebGLTextureUpload.cpp
@@ -6,16 +6,17 @@
 #include "WebGLTexture.h"
 
 #include <algorithm>
 
 #include "CanvasUtils.h"
 #include "gfxPrefs.h"
 #include "GLBlitHelper.h"
 #include "GLContext.h"
+#include "mozilla/Casting.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/dom/HTMLCanvasElement.h"
 #include "mozilla/dom/HTMLVideoElement.h"
 #include "mozilla/dom/ImageBitmap.h"
 #include "mozilla/dom/ImageData.h"
 #include "mozilla/MathAlgorithms.h"
 #include "mozilla/Scoped.h"
 #include "mozilla/Unused.h"
@@ -1119,17 +1120,17 @@ void WebGLTexture::TexStorage(TexTarget 
 
     ImageInfoAtFace(0, 0) = newInfo;
     PopulateMipChain(levels - 1);
 
     mBaseMipmapLevel = base_level;
   }
 
   mImmutable = true;
-  mImmutableLevelCount = levels;
+  mImmutableLevelCount = AutoAssertCast(levels);
   ClampLevelBaseAndMax();
 }
 
 ////////////////////////////////////////
 // Tex(Sub)Image
 
 void WebGLTexture::TexImage(TexImageTarget target, GLint level,
                             GLenum internalFormat, const webgl::PackingInfo& pi,
--- a/dom/canvas/WebGLTypes.h
+++ b/dom/canvas/WebGLTypes.h
@@ -5,16 +5,17 @@
 
 #ifndef WEBGLTYPES_H_
 #define WEBGLTYPES_H_
 
 #include <limits>
 
 // Most WebIDL typedefs are identical to their OpenGL counterparts.
 #include "GLTypes.h"
+#include "mozilla/Casting.h"
 
 // Manual reflection of WebIDL typedefs that are different from their
 // OpenGL counterparts.
 typedef int64_t WebGLsizeiptr;
 typedef int64_t WebGLintptr;
 typedef bool WebGLboolean;
 
 // -
@@ -50,16 +51,40 @@ inline void* malloc(const ForbidNarrowin
   return ::malloc(size_t(s));
 }
 
 inline void* calloc(const ForbidNarrowing<size_t> n,
                     const ForbidNarrowing<size_t> size) {
   return ::calloc(size_t(n), size_t(size));
 }
 
+// -
+
+namespace detail {
+
+template<typename From>
+class AutoAssertCastT final {
+  const From mVal;
+
+public:
+  explicit AutoAssertCastT(const From val) : mVal(val) { }
+
+  template<typename To>
+  operator To() const {
+    return AssertedCast<To>(mVal);
+  }
+};
+
+}  // namespace detail
+
+template<typename From>
+inline auto AutoAssertCast(const From val) {
+  return detail::AutoAssertCastT<From>(val);
+}
+
 /*
  * Implementing WebGL (or OpenGL ES 2.0) on top of desktop OpenGL requires
  * emulating the vertex attrib 0 array when it's not enabled. Indeed,
  * OpenGL ES 2.0 allows drawing without vertex attrib 0 array enabled, but
  * desktop OpenGL does not allow that.
  */
 enum class WebGLVertexAttrib0Status : uint8_t {
   Default,                     // default status - no emulation needed
--- a/dom/canvas/moz.build
+++ b/dom/canvas/moz.build
@@ -212,8 +212,11 @@ LOCAL_INCLUDES += [
 
 CXXFLAGS += CONFIG['MOZ_CAIRO_CFLAGS']
 CXXFLAGS += CONFIG['TK_CFLAGS']
 
 LOCAL_INCLUDES += CONFIG['SKIA_INCLUDES']
 
 if CONFIG['CC_TYPE'] in ('clang', 'gcc'):
     CXXFLAGS += ['-Wno-error=shadow', '-Wno-missing-braces']
+
+if CONFIG['CC_TYPE'] in ('clang', 'clang-cl'):
+    CXXFLAGS += ['-Werror=implicit-int-conversion']
--- a/dom/events/EventDispatcher.cpp
+++ b/dom/events/EventDispatcher.cpp
@@ -123,34 +123,31 @@ static bool IsEventTargetChrome(EventTar
                  do_QueryInterface(aEventTarget->GetOwnerGlobal())) {
     isChrome = nsContentUtils::IsSystemPrincipal(sop->GetPrincipal());
   }
   return isChrome;
 }
 
 // EventTargetChainItem represents a single item in the event target chain.
 class EventTargetChainItem {
- private:
-  explicit EventTargetChainItem(EventTarget* aTarget);
-
  public:
-  EventTargetChainItem() : mItemFlags(0) {
+  explicit EventTargetChainItem(EventTarget* aTarget)
+      : mTarget(aTarget), mItemFlags(0) {
     MOZ_COUNT_CTOR(EventTargetChainItem);
   }
 
   ~EventTargetChainItem() { MOZ_COUNT_DTOR(EventTargetChainItem); }
 
   static EventTargetChainItem* Create(nsTArray<EventTargetChainItem>& aChain,
                                       EventTarget* aTarget,
                                       EventTargetChainItem* aChild = nullptr) {
     // The last item which can handle the event must be aChild.
     MOZ_ASSERT(GetLastCanHandleEventTarget(aChain) == aChild);
     MOZ_ASSERT(!aTarget || aTarget == aTarget->GetTargetForEventTargetChain());
-    EventTargetChainItem* etci = aChain.AppendElement();
-    etci->mTarget = aTarget;
+    EventTargetChainItem* etci = aChain.AppendElement(aTarget);
     return etci;
   }
 
   static void DestroyLast(nsTArray<EventTargetChainItem>& aChain,
                           EventTargetChainItem* aItem) {
     uint32_t lastIndex = aChain.Length() - 1;
     MOZ_ASSERT(&aChain[lastIndex] == aItem);
     aChain.RemoveElementAt(lastIndex);
@@ -299,16 +296,17 @@ class EventTargetChainItem {
   EventTarget* CurrentTarget() const { return mTarget; }
 
   /**
    * Dispatches event through the event target chain.
    * Handles capture, target and bubble phases both in default
    * and system event group and calls also PostHandleEvent for each
    * item in the chain.
    */
+  MOZ_CAN_RUN_SCRIPT
   static void HandleEventTargetChain(nsTArray<EventTargetChainItem>& aChain,
                                      EventChainPostVisitor& aVisitor,
                                      EventDispatchingCallback* aCallback,
                                      ELMCreationDetector& aCd);
 
   /**
    * Resets aVisitor object and calls GetEventTargetParent.
    * Copies mItemFlags and mItemData to the current EventTargetChainItem.
@@ -354,20 +352,20 @@ class EventTargetChainItem {
       NS_ASSERTION(aVisitor.mEvent->mCurrentTarget == nullptr,
                    "CurrentTarget should be null!");
     }
   }
 
   /**
    * Copies mItemFlags and mItemData to aVisitor and calls PostHandleEvent.
    */
-  void PostHandleEvent(EventChainPostVisitor& aVisitor);
+  MOZ_CAN_RUN_SCRIPT void PostHandleEvent(EventChainPostVisitor& aVisitor);
 
  private:
-  nsCOMPtr<EventTarget> mTarget;
+  const nsCOMPtr<EventTarget> mTarget;
   nsCOMPtr<EventTarget> mRetargetedRelatedTarget;
   Maybe<nsTArray<RefPtr<EventTarget>>> mRetargetedTouchTargets;
   Maybe<nsTArray<RefPtr<dom::Touch>>> mInitialTargetTouches;
 
   class EventTargetChainFlags {
    public:
     explicit EventTargetChainFlags() { SetRawFlags(0); }
     // Cached flags for each EventTargetChainItem which are set when calling
--- a/dom/events/EventDispatcher.h
+++ b/dom/events/EventDispatcher.h
@@ -327,16 +327,19 @@ class EventDispatcher {
    * nothing to do with the construction of the event target chain.
    * Neither aTarget nor aEvent is allowed to be nullptr.
    *
    * If aTargets is non-null, event target chain will be created, but
    * event won't be handled. In this case aEvent->mMessage should be
    * eVoidEvent.
    * @note Use this method when dispatching a WidgetEvent.
    */
+  // This should obviously be MOZ_CAN_RUN_SCRIPT, but that's a bit of
+  // a project.  See bug 1539884.
+  MOZ_CAN_RUN_SCRIPT_BOUNDARY
   static nsresult Dispatch(nsISupports* aTarget, nsPresContext* aPresContext,
                            WidgetEvent* aEvent, dom::Event* aDOMEvent = nullptr,
                            nsEventStatus* aEventStatus = nullptr,
                            EventDispatchingCallback* aCallback = nullptr,
                            nsTArray<dom::EventTarget*>* aTargets = nullptr);
 
   /**
    * Dispatches an event.
--- a/dom/events/EventTarget.h
+++ b/dom/events/EventTarget.h
@@ -243,16 +243,17 @@ class EventTarget : public nsISupports, 
   /**
    * Called after the bubble phase of the system event group.
    * The default handling of the event should happen here.
    * @param aVisitor the visitor object which is used during post handling.
    *
    * @see EventDispatcher.h for documentation about aVisitor.
    * @note Only EventDispatcher should call this method.
    */
+  MOZ_CAN_RUN_SCRIPT
   virtual nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) = 0;
 
  protected:
   EventHandlerNonNull* GetEventHandler(nsAtom* aType);
   void SetEventHandler(nsAtom* aType, EventHandlerNonNull* aHandler);
 
   /**
    * Hook for AddEventListener that allows it to compute the right
--- a/dom/html/HTMLAnchorElement.h
+++ b/dom/html/HTMLAnchorElement.h
@@ -46,17 +46,18 @@ class HTMLAnchorElement final : public n
   virtual nsresult BindToTree(Document* aDocument, nsIContent* aParent,
                               nsIContent* aBindingParent) override;
   virtual void UnbindFromTree(bool aDeep = true,
                               bool aNullParent = true) override;
   virtual bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
                                int32_t* aTabIndex) override;
 
   void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
-  virtual nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
+  MOZ_CAN_RUN_SCRIPT
+  nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
   virtual bool IsLink(nsIURI** aURI) const override;
   virtual void GetLinkTarget(nsAString& aTarget) override;
   virtual already_AddRefed<nsIURI> GetHrefURI() const override;
 
   virtual nsresult BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName,
                                  const nsAttrValueOrString* aValue,
                                  bool aNotify) override;
   virtual nsresult AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
--- a/dom/html/HTMLAreaElement.h
+++ b/dom/html/HTMLAreaElement.h
@@ -32,17 +32,18 @@ class HTMLAreaElement final : public nsG
 
   NS_DECL_ADDSIZEOFEXCLUDINGTHIS
 
   NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLAreaElement, area)
 
   virtual int32_t TabIndexDefault() override;
 
   void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
-  virtual nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
+  MOZ_CAN_RUN_SCRIPT
+  nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
   virtual bool IsLink(nsIURI** aURI) const override;
   virtual void GetLinkTarget(nsAString& aTarget) override;
   virtual already_AddRefed<nsIURI> GetHrefURI() const override;
 
   virtual nsresult BindToTree(Document* aDocument, nsIContent* aParent,
                               nsIContent* aBindingParent) override;
   virtual void UnbindFromTree(bool aDeep = true,
                               bool aNullParent = true) override;
--- a/dom/html/HTMLLinkElement.h
+++ b/dom/html/HTMLLinkElement.h
@@ -34,17 +34,18 @@ class HTMLLinkElement final : public nsG
   NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLLinkElement, link);
   NS_DECL_ADDSIZEOFEXCLUDINGTHIS
 
   void LinkAdded();
   void LinkRemoved();
 
   // EventTarget
   void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
-  virtual nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
+  MOZ_CAN_RUN_SCRIPT
+  nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
 
   // nsINode
   virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
   virtual JSObject* WrapNode(JSContext* aCx,
                              JS::Handle<JSObject*> aGivenProto) override;
 
   // nsIContent
   virtual nsresult BindToTree(Document* aDocument, nsIContent* aParent,
--- a/dom/html/HTMLSelectElement.h
+++ b/dom/html/HTMLSelectElement.h
@@ -191,17 +191,18 @@ class HTMLSelectElement final : public n
   using nsINode::Remove;
 
   // nsINode
   virtual JSObject* WrapNode(JSContext* aCx,
                              JS::Handle<JSObject*> aGivenProto) override;
 
   // nsIContent
   void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
-  virtual nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
+  MOZ_CAN_RUN_SCRIPT
+  nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
 
   virtual bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
                                int32_t* aTabIndex) override;
   virtual nsresult InsertChildBefore(nsIContent* aKid, nsIContent* aBeforeThis,
                                      bool aNotify) override;
   virtual void RemoveChildNode(nsIContent* aKid, bool aNotify) override;
 
   // Overriden nsIFormControl methods
--- a/dom/html/nsGenericHTMLElement.h
+++ b/dom/html/nsGenericHTMLElement.h
@@ -262,16 +262,17 @@ class nsGenericHTMLElement : public nsGe
 
   /**
    * Check if an event for an anchor can be handled
    * @return true if the event can be handled, false otherwise
    */
   bool CheckHandleEventForAnchorsPreconditions(
       mozilla::EventChainVisitor& aVisitor);
   void GetEventTargetParentForAnchors(mozilla::EventChainPreVisitor& aVisitor);
+  MOZ_CAN_RUN_SCRIPT
   nsresult PostHandleEventForAnchors(mozilla::EventChainPostVisitor& aVisitor);
   bool IsHTMLLink(nsIURI** aURI) const;
 
   // HTML element methods
   void Compact() { mAttrs.Compact(); }
 
   virtual void UpdateEditableState(bool aNotify) override;
 
--- a/dom/localstorage/ActorsParent.cpp
+++ b/dom/localstorage/ActorsParent.cpp
@@ -7254,85 +7254,85 @@ ArchivedOriginScope* ArchivedOriginScope
 
 void ArchivedOriginScope::GetBindingClause(nsACString& aBindingClause) const {
   struct Matcher {
     nsACString* mBindingClause;
 
     explicit Matcher(nsACString* aBindingClause)
         : mBindingClause(aBindingClause) {}
 
-    void match(const Origin& aOrigin) {
+    void operator()(const Origin& aOrigin) {
       *mBindingClause = NS_LITERAL_CSTRING(
           " WHERE originKey = :originKey "
           "AND originAttributes = :originAttributes");
     }
 
-    void match(const Prefix& aPrefix) {
+    void operator()(const Prefix& aPrefix) {
       *mBindingClause = NS_LITERAL_CSTRING(" WHERE originKey = :originKey");
     }
 
-    void match(const Pattern& aPattern) {
+    void operator()(const Pattern& aPattern) {
       *mBindingClause = NS_LITERAL_CSTRING(
           " WHERE originAttributes MATCH :originAttributesPattern");
     }
 
-    void match(const Null& aNull) { *mBindingClause = EmptyCString(); }
+    void operator()(const Null& aNull) { *mBindingClause = EmptyCString(); }
   };
 
   mData.match(Matcher(&aBindingClause));
 }
 
 nsresult ArchivedOriginScope::BindToStatement(
     mozIStorageStatement* aStmt) const {
   MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread());
   MOZ_ASSERT(aStmt);
 
   struct Matcher {
     mozIStorageStatement* mStmt;
 
     explicit Matcher(mozIStorageStatement* aStmt) : mStmt(aStmt) {}
 
-    nsresult match(const Origin& aOrigin) {
+    nsresult operator()(const Origin& aOrigin) {
       nsresult rv = mStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("originKey"),
                                                 aOrigin.OriginNoSuffix());
       if (NS_WARN_IF(NS_FAILED(rv))) {
         return rv;
       }
 
       rv = mStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("originAttributes"),
                                        aOrigin.OriginSuffix());
       if (NS_WARN_IF(NS_FAILED(rv))) {
         return rv;
       }
 
       return NS_OK;
     }
 
-    nsresult match(const Prefix& aPrefix) {
+    nsresult operator()(const Prefix& aPrefix) {
       nsresult rv = mStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("originKey"),
                                                 aPrefix.OriginNoSuffix());
       if (NS_WARN_IF(NS_FAILED(rv))) {
         return rv;
       }
 
       return NS_OK;
     }
 
-    nsresult match(const Pattern& aPattern) {
+    nsresult operator()(const Pattern& aPattern) {
       nsresult rv = mStmt->BindUTF8StringByName(
           NS_LITERAL_CSTRING("originAttributesPattern"),
           NS_LITERAL_CSTRING("pattern1"));
       if (NS_WARN_IF(NS_FAILED(rv))) {
         return rv;
       }
 
       return NS_OK;
     }
 
-    nsresult match(const Null& aNull) { return NS_OK; }
+    nsresult operator()(const Null& aNull) { return NS_OK; }
   };
 
   nsresult rv = mData.match(Matcher(aStmt));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
@@ -7344,95 +7344,95 @@ bool ArchivedOriginScope::HasMatches(
   MOZ_ASSERT(aHashtable);
 
   struct Matcher {
     ArchivedOriginHashtable* mHashtable;
 
     explicit Matcher(ArchivedOriginHashtable* aHashtable)
         : mHashtable(aHashtable) {}
 
-    bool match(const Origin& aOrigin) {
+    bool operator()(const Origin& aOrigin) {
       nsCString hashKey = GetArchivedOriginHashKey(aOrigin.OriginSuffix(),
                                                    aOrigin.OriginNoSuffix());
 
       ArchivedOriginInfo* archivedOriginInfo;
       return mHashtable->Get(hashKey, &archivedOriginInfo);
     }
 
-    bool match(const Prefix& aPrefix) {
+    bool operator()(const Prefix& aPrefix) {
       for (auto iter = mHashtable->ConstIter(); !iter.Done(); iter.Next()) {
         ArchivedOriginInfo* archivedOriginInfo = iter.Data();
 
         if (archivedOriginInfo->mOriginNoSuffix == aPrefix.OriginNoSuffix()) {
           return true;
         }
       }
 
       return false;
     }
 
-    bool match(const Pattern& aPattern) {
+    bool operator()(const Pattern& aPattern) {
       for (auto iter = mHashtable->ConstIter(); !iter.Done(); iter.Next()) {
         ArchivedOriginInfo* archivedOriginInfo = iter.Data();
 
         if (aPattern.GetPattern().Matches(
                 archivedOriginInfo->mOriginAttributes)) {
           return true;
         }
       }
 
       return false;
     }
 
-    bool match(const Null& aNull) { return mHashtable->Count(); }
+    bool operator()(const Null& aNull) { return mHashtable->Count(); }
   };
 
   return mData.match(Matcher(aHashtable));
 }
 
 void ArchivedOriginScope::RemoveMatches(
     ArchivedOriginHashtable* aHashtable) const {
   AssertIsOnIOThread();
   MOZ_ASSERT(aHashtable);
 
   struct Matcher {
     ArchivedOriginHashtable* mHashtable;
 
     explicit Matcher(ArchivedOriginHashtable* aHashtable)
         : mHashtable(aHashtable) {}
 
-    void match(const Origin& aOrigin) {
+    void operator()(const Origin& aOrigin) {
       nsCString hashKey = GetArchivedOriginHashKey(aOrigin.OriginSuffix(),
                                                    aOrigin.OriginNoSuffix());
 
       mHashtable->Remove(hashKey);
     }
 
-    void match(const Prefix& aPrefix) {
+    void operator()(const Prefix& aPrefix) {
       for (auto iter = mHashtable->Iter(); !iter.Done(); iter.Next()) {
         ArchivedOriginInfo* archivedOriginInfo = iter.Data();
 
         if (archivedOriginInfo->mOriginNoSuffix == aPrefix.OriginNoSuffix()) {
           iter.Remove();
         }
       }
     }
 
-    void match(const Pattern& aPattern) {
+    void operator()(const Pattern& aPattern) {
       for (auto iter = mHashtable->Iter(); !iter.Done(); iter.Next()) {
         ArchivedOriginInfo* archivedOriginInfo = iter.Data();
 
         if (aPattern.GetPattern().Matches(
                 archivedOriginInfo->mOriginAttributes)) {
           iter.Remove();
         }
       }
     }
 
-    void match(const Null& aNull) { mHashtable->Clear(); }
+    void operator()(const Null& aNull) { mHashtable->Clear(); }
   };
 
   mData.match(Matcher(aHashtable));
 }
 
 /*******************************************************************************
  * QuotaClient
  ******************************************************************************/
--- a/dom/mathml/nsMathMLElement.h
+++ b/dom/mathml/nsMathMLElement.h
@@ -60,18 +60,18 @@ class nsMathMLElement final : public nsM
 
   static bool ParseNumericValue(const nsString& aString, nsCSSValue& aCSSValue,
                                 uint32_t aFlags, Document* aDocument);
 
   static void MapMathMLAttributesInto(const nsMappedAttributes* aAttributes,
                                       mozilla::MappedDeclarations&);
 
   void GetEventTargetParent(mozilla::EventChainPreVisitor& aVisitor) override;
-  virtual nsresult PostHandleEvent(
-      mozilla::EventChainPostVisitor& aVisitor) override;
+  MOZ_CAN_RUN_SCRIPT
+  nsresult PostHandleEvent(mozilla::EventChainPostVisitor& aVisitor) override;
   nsresult Clone(mozilla::dom::NodeInfo*, nsINode** aResult) const override;
   virtual mozilla::EventStates IntrinsicState() const override;
   virtual bool IsNodeOfType(uint32_t aFlags) const override;
 
   // Set during reflow as necessary. Does a style change notification,
   // aNotify must be true.
   void SetIncrementScriptLevel(bool aIncrementScriptLevel, bool aNotify);
   bool GetIncrementScriptLevel() const { return mIncrementScriptLevel; }
--- a/dom/media/AudioSampleFormat.h
+++ b/dom/media/AudioSampleFormat.h
@@ -97,17 +97,17 @@ template <typename T>
 T UInt8bitToAudioSample(uint8_t aValue);
 
 template <>
 inline float UInt8bitToAudioSample<float>(uint8_t aValue) {
   return aValue * (static_cast<float>(2) / UINT8_MAX) - static_cast<float>(1);
 }
 template <>
 inline int16_t UInt8bitToAudioSample<int16_t>(uint8_t aValue) {
-  return (int16_t(aValue) << 8) + aValue + INT16_MIN;
+  return static_cast<int16_t>((aValue << 8) + aValue + INT16_MIN);
 }
 
 template <typename T>
 T IntegerToAudioSample(int16_t aValue);
 
 template <>
 inline float IntegerToAudioSample<float>(int16_t aValue) {
   return aValue / 32768.0f;
@@ -121,17 +121,17 @@ template <typename T>
 T Int24bitToAudioSample(int32_t aValue);
 
 template <>
 inline float Int24bitToAudioSample<float>(int32_t aValue) {
   return aValue / static_cast<float>(1 << 23);
 }
 template <>
 inline int16_t Int24bitToAudioSample<int16_t>(int32_t aValue) {
-  return aValue / 256;
+  return static_cast<int16_t>(aValue / 256);
 }
 
 template <typename SrcT, typename DstT>
 inline void ConvertAudioSample(SrcT aIn, DstT& aOut);
 
 template <>
 inline void ConvertAudioSample(int16_t aIn, int16_t& aOut) {
   aOut = aIn;
--- a/dom/media/GraphDriver.cpp
+++ b/dom/media/GraphDriver.cpp
@@ -456,20 +456,22 @@ AsyncCubebTask::Run() {
       MOZ_CRASH("Operation not implemented.");
   }
 
   // The thread will kill itself after a bit
   return NS_OK;
 }
 
 StreamAndPromiseForOperation::StreamAndPromiseForOperation(
-    MediaStream* aStream, void* aPromise, dom::AudioContextOperation aOperation)
-    : mStream(aStream), mPromise(aPromise), mOperation(aOperation) {
-  // MOZ_ASSERT(aPromise);
-}
+    MediaStream* aStream, void* aPromise, dom::AudioContextOperation aOperation,
+    dom::AudioContextOperationFlags aFlags)
+    : mStream(aStream),
+      mPromise(aPromise),
+      mOperation(aOperation),
+      mFlags(aFlags) {}
 
 AudioCallbackDriver::AudioCallbackDriver(MediaStreamGraphImpl* aGraphImpl,
                                          uint32_t aInputChannelCount)
     : GraphDriver(aGraphImpl),
       mOutputChannels(0),
       mSampleRate(0),
       mInputChannelCount(aInputChannelCount),
       mIterationDurationMS(MEDIA_GRAPH_TARGET_PERIOD_MS),
@@ -1037,22 +1039,26 @@ uint32_t AudioCallbackDriver::IterationD
   // The real fix would be to have an API in cubeb to give us the number. Short
   // of that, we approximate it here. bug 1019507
   return mIterationDurationMS;
 }
 
 bool AudioCallbackDriver::IsStarted() { return mStarted; }
 
 void AudioCallbackDriver::EnqueueStreamAndPromiseForOperation(
-    MediaStream* aStream, void* aPromise,
-    dom::AudioContextOperation aOperation) {
+    MediaStream* aStream, void* aPromise, dom::AudioContextOperation aOperation,
+    dom::AudioContextOperationFlags aFlags) {
   MOZ_ASSERT(OnGraphThread() || !ThreadRunning());
   MonitorAutoLock mon(mGraphImpl->GetMonitor());
-  mPromisesForOperation.AppendElement(
-      StreamAndPromiseForOperation(aStream, aPromise, aOperation));
+  MOZ_ASSERT((aFlags | dom::AudioContextOperationFlags::SendStateChange) ||
+             !aPromise);
+  if (aFlags == dom::AudioContextOperationFlags::SendStateChange) {
+    mPromisesForOperation.AppendElement(
+        StreamAndPromiseForOperation(aStream, aPromise, aOperation, aFlags));
+  }
 }
 
 void AudioCallbackDriver::CompleteAudioContextOperations(
     AsyncCubebOperation aOperation) {
   MOZ_ASSERT(OnCubebOperationThread());
   AutoTArray<StreamAndPromiseForOperation, 1> array;
 
   // We can't lock for the whole function because AudioContextOperationCompleted
@@ -1063,18 +1069,19 @@ void AudioCallbackDriver::CompleteAudioC
   }
 
   for (uint32_t i = 0; i < array.Length(); i++) {
     StreamAndPromiseForOperation& s = array[i];
     if ((aOperation == AsyncCubebOperation::INIT &&
          s.mOperation == dom::AudioContextOperation::Resume) ||
         (aOperation == AsyncCubebOperation::SHUTDOWN &&
          s.mOperation != dom::AudioContextOperation::Resume)) {
+      MOZ_ASSERT(s.mFlags == dom::AudioContextOperationFlags::SendStateChange);
       GraphImpl()->AudioContextOperationCompleted(s.mStream, s.mPromise,
-                                                  s.mOperation);
+                                                  s.mOperation, s.mFlags);
       array.RemoveElementAt(i);
       i--;
     }
   }
 
   if (!array.IsEmpty()) {
     MonitorAutoLock mon(GraphImpl()->GetMonitor());
     mPromisesForOperation.AppendElements(array);
--- a/dom/media/GraphDriver.h
+++ b/dom/media/GraphDriver.h
@@ -309,20 +309,22 @@ class OfflineClockDriver : public Thread
 
  private:
   // Time, in GraphTime, for each iteration
   GraphTime mSlice;
 };
 
 struct StreamAndPromiseForOperation {
   StreamAndPromiseForOperation(MediaStream* aStream, void* aPromise,
-                               dom::AudioContextOperation aOperation);
+                               dom::AudioContextOperation aOperation,
+                               dom::AudioContextOperationFlags aFlags);
   RefPtr<MediaStream> mStream;
   void* mPromise;
   dom::AudioContextOperation mOperation;
+  dom::AudioContextOperationFlags mFlags;
 };
 
 enum AsyncCubebOperation { INIT, SHUTDOWN };
 
 /**
  * This is a graph driver that is based on callback functions called by the
  * audio api. This ensures minimal audio latency, because it means there is no
  * buffering happening: the audio is generated inside the callback.
@@ -399,17 +401,18 @@ class AudioCallbackDriver : public Graph
   }
 
   uint32_t InputChannelCount() { return mInputChannelCount; }
 
   /* Enqueue a promise that is going to be resolved when a specific operation
    * occurs on the cubeb stream. */
   void EnqueueStreamAndPromiseForOperation(
       MediaStream* aStream, void* aPromise,
-      dom::AudioContextOperation aOperation);
+      dom::AudioContextOperation aOperation,
+      dom::AudioContextOperationFlags aFlags);
 
   std::thread::id ThreadId() { return mAudioThreadId.load(); }
 
   bool OnThread() override {
     return mAudioThreadId.load() == std::this_thread::get_id();
   }
 
   bool ThreadRunning() override { return mAudioThreadRunning; }
--- a/dom/media/MediaStreamGraph.cpp
+++ b/dom/media/MediaStreamGraph.cpp
@@ -3564,17 +3564,22 @@ void MediaStreamGraphImpl::SuspendOrResu
           mStreams[i] != mSuspendedStreams[j],
           "The suspended stream set and running stream set are not disjoint.");
     }
   }
 #endif
 }
 
 void MediaStreamGraphImpl::AudioContextOperationCompleted(
-    MediaStream* aStream, void* aPromise, AudioContextOperation aOperation) {
+    MediaStream* aStream, void* aPromise, AudioContextOperation aOperation,
+    AudioContextOperationFlags aFlags) {
+  if (aFlags != AudioContextOperationFlags::SendStateChange) {
+    MOZ_ASSERT(!aPromise);
+    return;
+  }
   // This can be called from the thread created to do cubeb operation, or the
   // MSG thread. The pointers passed back here are refcounted, so are still
   // alive.
   AudioContextState state;
   switch (aOperation) {
     case AudioContextOperation::Suspend:
       state = AudioContextState::Suspended;
       break;
@@ -3590,17 +3595,18 @@ void MediaStreamGraphImpl::AudioContextO
 
   nsCOMPtr<nsIRunnable> event =
       new dom::StateChangeTask(aStream->AsAudioNodeStream(), aPromise, state);
   mAbstractMainThread->Dispatch(event.forget());
 }
 
 void MediaStreamGraphImpl::ApplyAudioContextOperationImpl(
     MediaStream* aDestinationStream, const nsTArray<MediaStream*>& aStreams,
-    AudioContextOperation aOperation, void* aPromise) {
+    AudioContextOperation aOperation, void* aPromise,
+    AudioContextOperationFlags aFlags) {
   MOZ_ASSERT(OnGraphThread());
 
   SuspendOrResumeStreams(aOperation, aStreams);
 
   bool switching = false;
   GraphDriver* nextDriver = nullptr;
   {
     MonitorAutoLock lock(mMonitor);
@@ -3623,36 +3629,37 @@ void MediaStreamGraphImpl::ApplyAudioCon
         MOZ_ASSERT(nextDriver->AsAudioCallbackDriver());
         driver = nextDriver->AsAudioCallbackDriver();
       } else {
         driver = new AudioCallbackDriver(this, AudioInputChannelCount());
         MonitorAutoLock lock(mMonitor);
         CurrentDriver()->SwitchAtNextIteration(driver);
       }
       driver->EnqueueStreamAndPromiseForOperation(aDestinationStream, aPromise,
-                                                  aOperation);
+                                                  aOperation, aFlags);
     } else {
       // We are resuming a context, but we are already using an
       // AudioCallbackDriver, we can resolve the promise now.
-      AudioContextOperationCompleted(aDestinationStream, aPromise, aOperation);
+      AudioContextOperationCompleted(aDestinationStream, aPromise, aOperation,
+                                     aFlags);
     }
   }
   // Close, suspend: check if we are going to switch to a
   // SystemAudioCallbackDriver, and pass the promise to the AudioCallbackDriver
   // if that's the case, so it can notify the content.
   // This is the same logic as in UpdateStreamOrder, but it's simpler to have it
   // here as well so we don't have to store the Promise(s) on the Graph.
   if (aOperation != AudioContextOperation::Resume) {
     bool audioTrackPresent = AudioTrackPresent();
 
     if (!audioTrackPresent && CurrentDriver()->AsAudioCallbackDriver()) {
       CurrentDriver()
           ->AsAudioCallbackDriver()
           ->EnqueueStreamAndPromiseForOperation(aDestinationStream, aPromise,
-                                                aOperation);
+                                                aOperation, aFlags);
 
       SystemClockDriver* driver;
       if (nextDriver) {
         MOZ_ASSERT(!nextDriver->AsAudioCallbackDriver());
       } else {
         driver = new SystemClockDriver(this);
         MonitorAutoLock lock(mMonitor);
         CurrentDriver()->SwitchAtNextIteration(driver);
@@ -3661,70 +3668,75 @@ void MediaStreamGraphImpl::ApplyAudioCon
       // Queue the operation on the next driver so that the ordering is
       // preserved.
     } else if (!audioTrackPresent && switching) {
       MOZ_ASSERT(nextDriver->AsAudioCallbackDriver() ||
                  nextDriver->AsSystemClockDriver()->IsFallback());
       if (nextDriver->AsAudioCallbackDriver()) {
         nextDriver->AsAudioCallbackDriver()
             ->EnqueueStreamAndPromiseForOperation(aDestinationStream, aPromise,
-                                                  aOperation);
+                                                  aOperation, aFlags);
       } else {
         // If this is not an AudioCallbackDriver, this means we failed opening
         // an AudioCallbackDriver in the past, and we're constantly trying to
         // re-open an new audio stream, but are running this graph that has an
         // audio track off a SystemClockDriver for now to keep things moving.
         // This is the case where we're trying to switch an an system driver
         // (because suspend or close have been called on an AudioContext, or
         // we've closed the page), but we're already running one. We can just
         // resolve the promise now: we're already running off a system thread.
-        AudioContextOperationCompleted(aDestinationStream, aPromise,
-                                       aOperation);
+        AudioContextOperationCompleted(aDestinationStream, aPromise, aOperation,
+                                       aFlags);
       }
     } else {
       // We are closing or suspending an AudioContext, but something else is
       // using the audio stream, we can resolve the promise now.
-      AudioContextOperationCompleted(aDestinationStream, aPromise, aOperation);
+      AudioContextOperationCompleted(aDestinationStream, aPromise, aOperation,
+                                     aFlags);
     }
   }
 }
 
 void MediaStreamGraph::ApplyAudioContextOperation(
     MediaStream* aDestinationStream, const nsTArray<MediaStream*>& aStreams,
-    AudioContextOperation aOperation, void* aPromise) {
+    AudioContextOperation aOperation, void* aPromise,
+    AudioContextOperationFlags aFlags) {
   class AudioContextOperationControlMessage : public ControlMessage {
    public:
     AudioContextOperationControlMessage(MediaStream* aDestinationStream,
                                         const nsTArray<MediaStream*>& aStreams,
                                         AudioContextOperation aOperation,
-                                        void* aPromise)
+                                        void* aPromise,
+                                        AudioContextOperationFlags aFlags)
         : ControlMessage(aDestinationStream),
           mStreams(aStreams),
           mAudioContextOperation(aOperation),
-          mPromise(aPromise) {}
+          mPromise(aPromise),
+          mFlags(aFlags) {}
     void Run() override {
       mStream->GraphImpl()->ApplyAudioContextOperationImpl(
-          mStream, mStreams, mAudioContextOperation, mPromise);
+          mStream, mStreams, mAudioContextOperation, mPromise, mFlags);
     }
     void RunDuringShutdown() override {
       MOZ_ASSERT(mAudioContextOperation == AudioContextOperation::Close,
                  "We should be reviving the graph?");
     }
 
    private:
     // We don't need strong references here for the same reason ControlMessage
     // doesn't.
     nsTArray<MediaStream*> mStreams;
     AudioContextOperation mAudioContextOperation;
     void* mPromise;
+    AudioContextOperationFlags mFlags;
   };
 
   MediaStreamGraphImpl* graphImpl = static_cast<MediaStreamGraphImpl*>(this);
   graphImpl->AppendMessage(MakeUnique<AudioContextOperationControlMessage>(
-      aDestinationStream, aStreams, aOperation, aPromise));
+      aDestinationStream, aStreams, aOperation, aPromise, aFlags));
 }
 
 bool MediaStreamGraph::IsNonRealtime() const {
   return !static_cast<const MediaStreamGraphImpl*>(this)->mRealtime;
 }
 
 void MediaStreamGraph::StartNonRealtimeProcessing(uint32_t aTicksToProcess) {
   MOZ_ASSERT(NS_IsMainThread(), "main thread only");
--- a/dom/media/MediaStreamGraph.h
+++ b/dom/media/MediaStreamGraph.h
@@ -42,17 +42,18 @@ class nsAutoRefTraits<SpeexResamplerStat
 };
 
 namespace mozilla {
 
 extern LazyLogModule gMediaStreamGraphLog;
 
 namespace dom {
 enum class AudioContextOperation;
-}
+enum class AudioContextOperationFlags;
+}  // namespace dom
 
 /*
  * MediaStreamGraph is a framework for synchronized audio/video processing
  * and playback. It is designed to be used by other browser components such as
  * HTML media elements, media capture APIs, real-time media streaming APIs,
  * multitrack media APIs, and advanced audio APIs.
  *
  * The MediaStreamGraph uses a dedicated thread to process media --- the media
@@ -1244,17 +1245,18 @@ class MediaStreamGraph {
    * This can possibly pause the graph thread, releasing system resources, if
    * all streams have been suspended/closed.
    *
    * When the operation is complete, aPromise is resolved.
    */
   void ApplyAudioContextOperation(MediaStream* aDestinationStream,
                                   const nsTArray<MediaStream*>& aStreams,
                                   dom::AudioContextOperation aState,
-                                  void* aPromise);
+                                  void* aPromise,
+                                  dom::AudioContextOperationFlags aFlags);
 
   bool IsNonRealtime() const;
   /**
    * Start processing non-realtime for a specific number of ticks.
    */
   void StartNonRealtimeProcessing(uint32_t aTicksToProcess);
 
   /**
--- a/dom/media/MediaStreamGraphImpl.h
+++ b/dom/media/MediaStreamGraphImpl.h
@@ -284,26 +284,28 @@ class MediaStreamGraphImpl : public Medi
    */
   void RunMessageAfterProcessing(UniquePtr<ControlMessage> aMessage);
 
   /**
    * Called when a suspend/resume/close operation has been completed, on the
    * graph thread.
    */
   void AudioContextOperationCompleted(MediaStream* aStream, void* aPromise,
-                                      dom::AudioContextOperation aOperation);
+                                      dom::AudioContextOperation aOperation,
+                                      dom::AudioContextOperationFlags aFlags);
 
   /**
    * Apply and AudioContext operation (suspend/resume/closed), on the graph
    * thread.
    */
   void ApplyAudioContextOperationImpl(MediaStream* aDestinationStream,
                                       const nsTArray<MediaStream*>& aStreams,
                                       dom::AudioContextOperation aOperation,
-                                      void* aPromise);
+                                      void* aPromise,
+                                      dom::AudioContextOperationFlags aSource);
 
   /**
    * Increment suspend count on aStream and move it to mSuspendedStreams if
    * necessary.
    */
   void IncrementSuspendCount(MediaStream* aStream);
   /**
    * Increment suspend count on aStream and move it to mStreams if
--- a/dom/media/PeerConnection.jsm
+++ b/dom/media/PeerConnection.jsm
@@ -1624,81 +1624,65 @@ class PeerConnectionObserver {
   init(win) {
     this._win = win;
   }
 
   __init(dompc) {
     this._dompc = dompc._innerObject;
   }
 
-  newError(message, code) {
-    // These strings must match those defined in the WebRTC spec.
-    const reasonName = [
-      "",
-      "InternalError",
-      "InternalError",
-      "InvalidParameterError",
-      "InvalidStateError",
-      "InvalidSessionDescriptionError",
-      "IncompatibleSessionDescriptionError",
-      "InternalError",
-      "IncompatibleMediaStreamTrackError",
-      "InternalError",
-      "TypeError",
-      "OperationError",
-    ];
-    let name = reasonName[Math.min(code, reasonName.length - 1)];
+  newError({message, name}) {
     return new this._dompc._win.DOMException(message, name);
   }
 
   dispatchEvent(event) {
     this._dompc.dispatchEvent(event);
   }
 
   onCreateOfferSuccess(sdp) {
     this._dompc._onCreateOfferSuccess(sdp);
   }
 
-  onCreateOfferError(code, message) {
-    this._dompc._onCreateOfferFailure(this.newError(message, code));
+  onCreateOfferError(error) {
+    this._dompc._onCreateOfferFailure(this.newError(error));
   }
 
   onCreateAnswerSuccess(sdp) {
     this._dompc._onCreateAnswerSuccess(sdp);
   }
 
-  onCreateAnswerError(code, message) {
-    this._dompc._onCreateAnswerFailure(this.newError(message, code));
+  onCreateAnswerError(error) {
+    this._dompc._onCreateAnswerFailure(this.newError(error));
   }
 
   onSetLocalDescriptionSuccess() {
     this._dompc._onSetLocalDescriptionSuccess();
   }
 
   onSetRemoteDescriptionSuccess() {
     this._dompc._processTrackAdditionsAndRemovals();
     this._dompc._fireLegacyAddStreamEvents();
     this._dompc._transceivers = this._dompc._transceivers.filter(t => !t.shouldRemove);
     this._dompc._onSetRemoteDescriptionSuccess();
   }
 
-  onSetLocalDescriptionError(code, message) {
-    this._dompc._onSetLocalDescriptionFailure(this.newError(message, code));
+  onSetLocalDescriptionError(error) {
+    this._dompc._onSetLocalDescriptionFailure(this.newError(error));
   }
 
-  onSetRemoteDescriptionError(code, message) {
-    this._dompc._onSetRemoteDescriptionFailure(this.newError(message, code));
+  onSetRemoteDescriptionError(error) {
+    this._dompc._onSetRemoteDescriptionFailure(this.newError(error));
   }
 
   onAddIceCandidateSuccess() {
     this._dompc._onAddIceCandidateSuccess();
   }
 
-  onAddIceCandidateError(code, message) {
-    this._dompc._onAddIceCandidateError(this.newError(message, code));
+  onAddIceCandidateError(error) {
+    this._dompc._onAddIceCandidateError(this.newError(error));
   }
 
   onIceCandidate(sdpMLineIndex, sdpMid, candidate, usernameFragment) {
     let win = this._dompc._win;
     if (candidate || sdpMid || usernameFragment) {
       if (candidate.includes(" typ relay ")) {
         this._dompc._iceGatheredRelayCandidates = true;
       }
@@ -1831,18 +1815,18 @@ class PeerConnectionObserver {
   onGetStatsSuccess(dict) {
     let pc = this._dompc;
     let chromeobj = new RTCStatsReport(pc, dict);
     let webidlobj = pc._win.RTCStatsReport._create(pc._win, chromeobj);
     chromeobj.makeStatsPublic();
     pc._onGetStatsSuccess(webidlobj);
   }
 
-  onGetStatsError(code, message) {
-    this._dompc._onGetStatsFailure(this.newError(message, code));
+  onGetStatsError(message) {
+    this._dompc._onGetStatsFailure(this.newError({name: "OperationError", message}));
   }
 
   _getTransceiverWithRecvTrack(webrtcTrackId) {
     return this._dompc.getTransceivers().find(
         transceiver => transceiver.remoteTrackIdIs(webrtcTrackId));
   }
 
   onTransceiverNeeded(kind, transceiverImpl) {
--- a/dom/media/doctor/DDLogValue.cpp
+++ b/dom/media/doctor/DDLogValue.cpp
@@ -8,104 +8,106 @@
 
 #include "mozilla/JSONWriter.h"
 
 namespace mozilla {
 
 struct LogValueMatcher {
   nsCString& mString;
 
-  void match(const DDNoValue&) const {}
-  void match(const DDLogObject& a) const { a.AppendPrintf(mString); }
-  void match(const char* a) const { mString.AppendPrintf(R"("%s")", a); }
-  void match(const nsCString& a) const {
+  void operator()(const DDNoValue&) const {}
+  void operator()(const DDLogObject& a) const { a.AppendPrintf(mString); }
+  void operator()(const char* a) const { mString.AppendPrintf(R"("%s")", a); }
+  void operator()(const nsCString& a) const {
     mString.AppendPrintf(R"(nsCString("%s"))", a.Data());
   }
-  void match(bool a) const { mString.AppendPrintf(a ? "true" : "false"); }
-  void match(int8_t a) const { mString.AppendPrintf("int8_t(%" PRIi8 ")", a); }
-  void match(uint8_t a) const {
+  void operator()(bool a) const { mString.AppendPrintf(a ? "true" : "false"); }
+  void operator()(int8_t a) const {
+    mString.AppendPrintf("int8_t(%" PRIi8 ")", a);
+  }
+  void operator()(uint8_t a) const {
     mString.AppendPrintf("uint8_t(%" PRIu8 ")", a);
   }
-  void match(int16_t a) const {
+  void operator()(int16_t a) const {
     mString.AppendPrintf("int16_t(%" PRIi16 ")", a);
   }
-  void match(uint16_t a) const {
+  void operator()(uint16_t a) const {
     mString.AppendPrintf("uint16_t(%" PRIu16 ")", a);
   }
-  void match(int32_t a) const {
+  void operator()(int32_t a) const {
     mString.AppendPrintf("int32_t(%" PRIi32 ")", a);
   }
-  void match(uint32_t a) const {
+  void operator()(uint32_t a) const {
     mString.AppendPrintf("uint32_t(%" PRIu32 ")", a);
   }
-  void match(int64_t a) const {
+  void operator()(int64_t a) const {
     mString.AppendPrintf("int64_t(%" PRIi64 ")", a);
   }
-  void match(uint64_t a) const {
+  void operator()(uint64_t a) const {
     mString.AppendPrintf("uint64_t(%" PRIu64 ")", a);
   }
-  void match(double a) const { mString.AppendPrintf("double(%f)", a); }
-  void match(const DDRange& a) const {
+  void operator()(double a) const { mString.AppendPrintf("double(%f)", a); }
+  void operator()(const DDRange& a) const {
     mString.AppendPrintf("%" PRIi64 "<=(%" PRIi64 "B)<%" PRIi64 "", a.mOffset,
                          a.mBytes, a.mOffset + a.mBytes);
   }
-  void match(const nsresult& a) const {
+  void operator()(const nsresult& a) const {
     nsCString name;
     GetErrorName(a, name);
     mString.AppendPrintf("nsresult(%s =0x%08" PRIx32 ")", name.get(),
                          static_cast<uint32_t>(a));
   }
-  void match(const MediaResult& a) const {
+  void operator()(const MediaResult& a) const {
     nsCString name;
     GetErrorName(a.Code(), name);
     mString.AppendPrintf("MediaResult(%s =0x%08" PRIx32 ", \"%s\")", name.get(),
                          static_cast<uint32_t>(a.Code()), a.Message().get());
   }
 };
 
 void AppendToString(const DDLogValue& aValue, nsCString& aString) {
   aValue.match(LogValueMatcher{aString});
 }
 
 struct LogValueMatcherJson {
   JSONWriter& mJW;
   const char* mPropertyName;
 
-  void match(const DDNoValue&) const { mJW.NullProperty(mPropertyName); }
-  void match(const DDLogObject& a) const {
+  void operator()(const DDNoValue&) const { mJW.NullProperty(mPropertyName); }
+  void operator()(const DDLogObject& a) const {
     mJW.StringProperty(
         mPropertyName,
         nsPrintfCString(R"("%s[%p]")", a.TypeName(), a.Pointer()).get());
   }
-  void match(const char* a) const { mJW.StringProperty(mPropertyName, a); }
-  void match(const nsCString& a) const {
+  void operator()(const char* a) const { mJW.StringProperty(mPropertyName, a); }
+  void operator()(const nsCString& a) const {
     mJW.StringProperty(mPropertyName, a.Data());
   }
-  void match(bool a) const { mJW.BoolProperty(mPropertyName, a); }
-  void match(int8_t a) const { mJW.IntProperty(mPropertyName, a); }
-  void match(uint8_t a) const { mJW.IntProperty(mPropertyName, a); }
-  void match(int16_t a) const { mJW.IntProperty(mPropertyName, a); }
-  void match(uint16_t a) const { mJW.IntProperty(mPropertyName, a); }
-  void match(int32_t a) const { mJW.IntProperty(mPropertyName, a); }
-  void match(uint32_t a) const { mJW.IntProperty(mPropertyName, a); }
-  void match(int64_t a) const { mJW.IntProperty(mPropertyName, a); }
-  void match(uint64_t a) const { mJW.DoubleProperty(mPropertyName, a); }
-  void match(double a) const { mJW.DoubleProperty(mPropertyName, a); }
-  void match(const DDRange& a) const {
+  void operator()(bool a) const { mJW.BoolProperty(mPropertyName, a); }
+  void operator()(int8_t a) const { mJW.IntProperty(mPropertyName, a); }
+  void operator()(uint8_t a) const { mJW.IntProperty(mPropertyName, a); }
+  void operator()(int16_t a) const { mJW.IntProperty(mPropertyName, a); }
+  void operator()(uint16_t a) const { mJW.IntProperty(mPropertyName, a); }
+  void operator()(int32_t a) const { mJW.IntProperty(mPropertyName, a); }
+  void operator()(uint32_t a) const { mJW.IntProperty(mPropertyName, a); }
+  void operator()(int64_t a) const { mJW.IntProperty(mPropertyName, a); }
+  void operator()(uint64_t a) const { mJW.DoubleProperty(mPropertyName, a); }
+  void operator()(double a) const { mJW.DoubleProperty(mPropertyName, a); }
+  void operator()(const DDRange& a) const {
     mJW.StartArrayProperty(mPropertyName);
     mJW.IntElement(a.mOffset);
     mJW.IntElement(a.mOffset + a.mBytes);
     mJW.EndArray();
   }
-  void match(const nsresult& a) const {
+  void operator()(const nsresult& a) const {
     nsCString name;
     GetErrorName(a, name);
     mJW.StringProperty(mPropertyName, name.get());
   }
-  void match(const MediaResult& a) const {
+  void operator()(const MediaResult& a) const {
     nsCString name;
     GetErrorName(a.Code(), name);
     mJW.StringProperty(mPropertyName,
                        nsPrintfCString(R"lit("MediaResult(%s, %s)")lit",
                                        name.get(), a.Message().get())
                            .get());
   }
 };
--- a/dom/media/moz.build
+++ b/dom/media/moz.build
@@ -354,8 +354,12 @@ include('/ipc/chromium/chromium-config.m
 #    defined, which complains about an important MOZ_EXPORT for android::AString
 if CONFIG['CC_TYPE'] in ('clang', 'gcc'):
     CXXFLAGS += [
         '-Wno-error=attributes',
         '-Wno-error=shadow',
     ]
 
 FINAL_LIBRARY = 'xul'
+
+MARIONETTE_DOM_MEDIA_MANIFESTS += [
+ 'test/marionette/manifest.ini'
+]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/marionette/manifest.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+run-if = buildapp == 'browser'
+
+[test_youtube.py]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/marionette/test_youtube.py
@@ -0,0 +1,28 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+import sys
+import os
+
+sys.path.append(os.path.dirname(__file__))
+from yttest.support import VideoStreamTestCase
+
+
+class YoutubeTest(VideoStreamTestCase):
+
+    # bug 1513511
+    def test_stream_30_seconds(self):
+        # XXX use the VP9 video we will settle on.
+        with self.youtube_video("BZP1rYjoBgI") as page:
+            res = page.run_test()
+            self.assertTrue(res is not None, "We did not get back the results")
+            self.assertLess(res["droppedVideoFrames"], res["totalVideoFrames"] * 0.04)
+            # extracting in/out from the debugInfo
+            video_state = res["debugInfo"][7]
+            video_in = int(video_state.split(" ")[10].split("=")[-1])
+            video_out = int(video_state.split(" ")[11].split("=")[-1])
+            # what's the ratio ? we want 99%+
+            if video_out == video_in:
+                return
+            in_out_ratio = float(video_out) / float(video_in) * 100
+            self.assertMore(in_out_ratio, 99.0)
new file mode 100644
--- /dev/null
+++ b/dom/media/test/marionette/yttest/BZP1rYjoBgI.manifest
@@ -0,0 +1,10 @@
+[
+  {
+    "size": 20396656,
+    "visibility": "public",
+    "digest": "ccdecb515cadd243608898f38d74c23162fccb9246fee3084834c23d3a57710ed24c7c5dcc9b8bc6f5c3acb5fc0f3be144de08aa14d93e7dbbd372ec6166c138",
+    "algorithm": "sha512",
+    "filename": "BZP1rYjoBgI.tar.gz",
+    "unpack": true
+  }
+]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/marionette/yttest/__init__.py
@@ -0,0 +1,1 @@
+#
new file mode 100644
--- /dev/null
+++ b/dom/media/test/marionette/yttest/debug_info.js
@@ -0,0 +1,18 @@
+video.mozRequestDebugInfo().then(debugInfo => {
+  try {
+    debugInfo = debugInfo.replace(/\t/g, '').split(/\n/g);
+    var JSONDebugInfo = "{";
+      for(let g =0; g<debugInfo.length-1; g++){
+          var pair = debugInfo[g].split(": ");
+          JSONDebugInfo += '"' + pair[0] + '":"' + pair[1] + '",';
+      }
+      JSONDebugInfo = JSONDebugInfo.slice(0,JSONDebugInfo.length-1);
+      JSONDebugInfo += "}";
+      result["debugInfo"] = JSON.parse(JSONDebugInfo);
+  } catch (err) {
+    console.log(`Error '${err.toString()} in JSON.parse(${debugInfo})`);
+    result["debugInfo"] = debugInfo;
+  }
+  result["debugInfo"] = debugInfo;
+  resolve(result);
+});
new file mode 100644
--- /dev/null
+++ b/dom/media/test/marionette/yttest/download.py
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+import sys
+from pytube import YouTube
+
+
+def download_streams(video_id, output_path="data"):
+    yt = YouTube("https://youtube.com/watch?v=%s" % video_id)
+    for stream in yt.streams.all():
+        fn = "%s-%s-%s.%s" % (video_id, stream.itag, stream.type, stream.subtype)
+        stream.download(output_path="data", filename=fn)
+        print("%s downloaded" % fn)
+
+
+if __name__ == "__main__":
+    download_streams(sys.argv[-1])
new file mode 100644
--- /dev/null
+++ b/dom/media/test/marionette/yttest/duration_test.js
@@ -0,0 +1,21 @@
+%(force_hd)s
+
+const resolve = arguments[arguments.length - 1];
+
+// this script is injected by marionette to collect metrics
+var video = document.getElementsByTagName("video")[0];
+if (!video) {
+  return "Can't find the video tag";
+}
+
+video.addEventListener("timeupdate", () => {
+    if (video.currentTime >= %(duration)s) {
+      video.pause();
+      %(video_playback_quality)s
+      %(debug_info)s
+    }
+  }
+);
+
+video.play();
+
new file mode 100644
--- /dev/null
+++ b/dom/media/test/marionette/yttest/force_hd.js
@@ -0,0 +1,73 @@
+// This parts forces the highest definition
+// https://addons.mozilla.org/en-US/firefox/addon/youtube-auto-hd-lq/
+// licence: MPL 2.0
+var config = {
+  "HD": true,
+  "LQ": false,
+  "ID": "auto-hd-lq-for-ytb",
+  "type": function (t) {
+    config.HD = t === 'hd';
+    config.LQ = t === 'lq';
+  },
+  "quality": function () {
+    if (config.HD || config.LQ) {
+      var youtubePlayerListener = function (LQ, HD) {
+        return function youtubePlayerListener (e) {
+          if (e === 1) {
+            var player = document.getElementById('movie_player');
+            if (player) {
+              var levels = player.getAvailableQualityLevels();
+              if (levels.length) {
+                var q = (HD && levels[0]) ? levels[0] : ((LQ && levels[levels.length - 2]) ? levels[levels.length - 2] : null);
+                if (q) {
+                  player.setPlaybackQuality(q);
+                  player.setPlaybackQualityRange(q, q);
+                }
+              }
+            }
+          }
+        }
+      }
+      /*  */
+      var inject = function () {
+        var action = function () {
+          var player = document.getElementById('movie_player');
+          if (player && player.addEventListener && player.getPlayerState) {
+            player.addEventListener("onStateChange", "youtubePlayerListener");
+          } else window.setTimeout(action, 1000);
+        };
+        /*  */
+        action();
+      };
+      var script = document.getElementById(config.ID);
+      if (!script) {
+        script = document.createElement("script");
+        script.setAttribute("type", "text/javascript");
+        script.setAttribute("id", config.ID);
+        document.documentElement.appendChild(script);
+      }
+      /*  */
+      script.textContent = "var youtubePlayerListener = (" + youtubePlayerListener + ')(' + config.LQ + ',' + config.HD  + ');(' + inject + ')();';
+    }
+  }
+};
+
+  if (/^https?:\/\/www\.youtube.com\/watch\?/.test(document.location.href)) config.quality();
+  var content = document.getElementById('content');
+  if (content) {
+    var observer = new window.MutationObserver(function (e) {
+      e.forEach(function (m) {
+        if (m.addedNodes !== null) {
+          for (var i = 0; i < m.addedNodes.length; i++) {
+            if (m.addedNodes[i].id === 'movie_player') {
+              config.quality();
+              return;
+            }
+          }
+        }
+      });
+    });
+    /*  */
+    observer.observe(content, {"childList": true, "subtree": true});
+  }
+
new file mode 100644
--- /dev/null
+++ b/dom/media/test/marionette/yttest/playback.py
@@ -0,0 +1,648 @@
+# 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/.
+"""
+MITM Script used to play back media files when a YT video is played.
+
+This is a self-contained script that should not import anything else
+except modules from the standard library and mitmproxy modules.
+"""
+import os
+import sys
+import datetime
+import time
+
+
+itags = {
+    "5": {
+        "Extension": "flv",
+        "Resolution": "240p",
+        "VideoEncoding": "Sorenson H.283",
+        "AudioEncoding": "mp3",
+        "Itag": 5,
+        "AudioBitrate": 64,
+    },
+    "6": {
+        "Extension": "flv",
+        "Resolution": "270p",
+        "VideoEncoding": "Sorenson H.263",
+        "AudioEncoding": "mp3",
+        "Itag": 6,
+        "AudioBitrate": 64,
+    },
+    "13": {
+        "Extension": "3gp",
+        "Resolution": "",
+        "VideoEncoding": "MPEG-4 Visual",
+        "AudioEncoding": "aac",
+        "Itag": 13,
+        "AudioBitrate": 0,
+    },
+    "17": {
+        "Extension": "3gp",
+        "Resolution": "144p",
+        "VideoEncoding": "MPEG-4 Visual",
+        "AudioEncoding": "aac",
+        "Itag": 17,
+        "AudioBitrate": 24,
+    },
+    "18": {
+        "Extension": "mp4",
+        "Resolution": "360p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 18,
+        "AudioBitrate": 96,
+    },
+    "22": {
+        "Extension": "mp4",
+        "Resolution": "720p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 22,
+        "AudioBitrate": 192,
+    },
+    "34": {
+        "Extension": "flv",
+        "Resolution": "480p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 34,
+        "AudioBitrate": 128,
+    },
+    "35": {
+        "Extension": "flv",
+        "Resolution": "360p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 35,
+        "AudioBitrate": 128,
+    },
+    "36": {
+        "Extension": "3gp",
+        "Resolution": "240p",
+        "VideoEncoding": "MPEG-4 Visual",
+        "AudioEncoding": "aac",
+        "Itag": 36,
+        "AudioBitrate": 36,
+    },
+    "37": {
+        "Extension": "mp4",
+        "Resolution": "1080p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 37,
+        "AudioBitrate": 192,
+    },
+    "38": {
+        "Extension": "mp4",
+        "Resolution": "3072p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 38,
+        "AudioBitrate": 192,
+    },
+    "43": {
+        "Extension": "webm",
+        "Resolution": "360p",
+        "VideoEncoding": "VP8",
+        "AudioEncoding": "vorbis",
+        "Itag": 43,
+        "AudioBitrate": 128,
+    },
+    "44": {
+        "Extension": "webm",
+        "Resolution": "480p",
+        "VideoEncoding": "VP8",
+        "AudioEncoding": "vorbis",
+        "Itag": 44,
+        "AudioBitrate": 128,
+    },
+    "45": {
+        "Extension": "webm",
+        "Resolution": "720p",
+        "VideoEncoding": "VP8",
+        "AudioEncoding": "vorbis",
+        "Itag": 45,
+        "AudioBitrate": 192,
+    },
+    "46": {
+        "Extension": "webm",
+        "Resolution": "1080p",
+        "VideoEncoding": "VP8",
+        "AudioEncoding": "vorbis",
+        "Itag": 46,
+        "AudioBitrate": 192,
+    },
+    "82": {
+        "Extension": "mp4",
+        "Resolution": "360p",
+        "VideoEncoding": "H.264",
+        "Itag": 82,
+        "AudioBitrate": 96,
+    },
+    "83": {
+        "Extension": "mp4",
+        "Resolution": "240p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 83,
+        "AudioBitrate": 96,
+    },
+    "84": {
+        "Extension": "mp4",
+        "Resolution": "720p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 84,
+        "AudioBitrate": 192,
+    },
+    "85": {
+        "Extension": "mp4",
+        "Resolution": "1080p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 85,
+        "AudioBitrate": 192,
+    },
+    "100": {
+        "Extension": "webm",
+        "Resolution": "360p",
+        "VideoEncoding": "VP8",
+        "AudioEncoding": "vorbis",
+        "Itag": 100,
+        "AudioBitrate": 128,
+    },
+    "101": {
+        "Extension": "webm",
+        "Resolution": "360p",
+        "VideoEncoding": "VP8",
+        "AudioEncoding": "vorbis",
+        "Itag": 101,
+        "AudioBitrate": 192,
+    },
+    "102": {
+        "Extension": "webm",
+        "Resolution": "720p",
+        "VideoEncoding": "VP8",
+        "AudioEncoding": "vorbis",
+        "Itag": 102,
+        "AudioBitrate": 192,
+    },
+    "133": {
+        "Extension": "mp4",
+        "Resolution": "240p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "",
+        "Itag": 133,
+        "AudioBitrate": 0,
+    },
+    "134": {
+        "Extension": "mp4",
+        "Resolution": "360p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "",
+        "Itag": 134,
+        "AudioBitrate": 0,
+    },
+    "135": {
+        "Extension": "mp4",
+        "Resolution": "480p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "",
+        "Itag": 135,
+        "AudioBitrate": 0,
+    },
+    "136": {
+        "Extension": "mp4",
+        "Resolution": "720p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "",
+        "Itag": 136,
+        "AudioBitrate": 0,
+    },
+    "137": {
+        "Extension": "mp4",
+        "Resolution": "1080p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "",
+        "Itag": 137,
+        "AudioBitrate": 0,
+    },
+    "138": {
+        "Extension": "mp4",
+        "Resolution": "2160p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "",
+        "Itag": 138,
+        "AudioBitrate": 0,
+    },
+    "160": {
+        "Extension": "mp4",
+        "Resolution": "144p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "",
+        "Itag": 160,
+        "AudioBitrate": 0,
+    },
+    "242": {
+        "Extension": "webm",
+        "Resolution": "240p",
+        "VideoEncoding": "VP9",
+        "AudioEncoding": "",
+        "Itag": 242,
+        "AudioBitrate": 0,
+    },
+    "243": {
+        "Extension": "webm",
+        "Resolution": "360p",
+        "VideoEncoding": "VP9",
+        "AudioEncoding": "",
+        "Itag": 243,
+        "AudioBitrate": 0,
+    },
+    "244": {
+        "Extension": "webm",
+        "Resolution": "480p",
+        "VideoEncoding": "VP9",
+        "AudioEncoding": "",
+        "Itag": 244,
+        "AudioBitrate": 0,
+    },
+    "247": {
+        "Extension": "webm",
+        "Resolution": "720p",
+        "VideoEncoding": "VP9",
+        "AudioEncoding": "",
+        "Itag": 247,
+        "AudioBitrate": 0,
+    },
+    "248": {
+        "Extension": "webm",
+        "Resolution": "1080p",
+        "VideoEncoding": "VP9",
+        "AudioEncoding": "",
+        "Itag": 248,
+        "AudioBitrate": 9,
+    },
+    "264": {
+        "Extension": "mp4",
+        "Resolution": "1440p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "",
+        "Itag": 264,
+        "AudioBitrate": 0,
+    },
+    "266": {
+        "Extension": "mp4",
+        "Resolution": "2160p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "",
+        "Itag": 266,
+        "AudioBitrate": 0,
+    },
+    "271": {
+        "Extension": "webm",
+        "Resolution": "1440p",
+        "VideoEncoding": "VP9",
+        "AudioEncoding": "",
+        "Itag": 271,
+        "AudioBitrate": 0,
+    },
+    "272": {
+        "Extension": "webm",
+        "Resolution": "2160p",
+        "VideoEncoding": "VP9",
+        "AudioEncoding": "",
+        "Itag": 272,
+        "AudioBitrate": 0,
+    },
+    "278": {
+        "Extension": "webm",
+        "Resolution": "144p",
+        "VideoEncoding": "VP9",
+        "AudioEncoding": "",
+        "Itag": 278,
+        "AudioBitrate": 0,
+    },
+    "298": {
+        "Extension": "mp4",
+        "Resolution": "720p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "",
+        "Itag": 298,
+        "AudioBitrate": 0,
+    },
+    "299": {
+        "Extension": "mp4",
+        "Resolution": "1080p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "",
+        "Itag": 299,
+        "AudioBitrate": 0,
+    },
+    "302": {
+        "Extension": "webm",
+        "Resolution": "720p",
+        "VideoEncoding": "VP9",
+        "AudioEncoding": "",
+        "Itag": 302,
+        "AudioBitrate": 0,
+    },
+    "303": {
+        "Extension": "webm",
+        "Resolution": "1080p",
+        "VideoEncoding": "VP9",
+        "AudioEncoding": "",
+        "Itag": 303,
+        "AudioBitrate": 0,
+    },
+    "139": {
+        "Extension": "mp4",
+        "Resolution": "",
+        "VideoEncoding": "",
+        "AudioEncoding": "aac",
+        "Itag": 139,
+        "AudioBitrate": 48,
+    },
+    "140": {
+        "Extension": "mp4",
+        "Resolution": "",
+        "VideoEncoding": "",
+        "AudioEncoding": "aac",
+        "Itag": 140,
+        "AudioBitrate": 128,
+    },
+    "141": {
+        "Extension": "mp4",
+        "Resolution": "",
+        "VideoEncoding": "",
+        "AudioEncoding": "aac",
+        "Itag": 141,
+        "AudioBitrate": 256,
+    },
+    "171": {
+        "Extension": "webm",
+        "Resolution": "",
+        "VideoEncoding": "",
+        "AudioEncoding": "vorbis",
+        "Itag": 171,
+        "AudioBitrate": 128,
+    },
+    "172": {
+        "Extension": "webm",
+        "Resolution": "",
+        "VideoEncoding": "",
+        "AudioEncoding": "vorbis",
+        "Itag": 172,
+        "AudioBitrate": 192,
+    },
+    "249": {
+        "Extension": "webm",
+        "Resolution": "",
+        "VideoEncoding": "",
+        "AudioEncoding": "opus",
+        "Itag": 249,
+        "AudioBitrate": 50,
+    },
+    "250": {
+        "Extension": "webm",
+        "Resolution": "",
+        "VideoEncoding": "",
+        "AudioEncoding": "opus",
+        "Itag": 250,
+        "AudioBitrate": 70,
+    },
+    "251": {
+        "Extension": "webm",
+        "Resolution": "",
+        "VideoEncoding": "",
+        "AudioEncoding": "opus",
+        "Itag": 251,
+        "AudioBitrate": 160,
+    },
+    "92": {
+        "Extension": "ts",
+        "Resolution": "240p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 92,
+        "AudioBitrate": 48,
+    },
+    "93": {
+        "Extension": "ts",
+        "Resolution": "480p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 93,
+        "AudioBitrate": 128,
+    },
+    "94": {
+        "Extension": "ts",
+        "Resolution": "720p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 94,
+        "AudioBitrate": 128,
+    },
+    "95": {
+        "Extension": "ts",
+        "Resolution": "1080p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 95,
+        "AudioBitrate": 256,
+    },
+    "96": {
+        "Extension": "ts",
+        "Resolution": "720p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 96,
+        "AudioBitrate": 256,
+    },
+    "120": {
+        "Extension": "flv",
+        "Resolution": "720p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 120,
+        "AudioBitrate": 128,
+    },
+    "127": {
+        "Extension": "ts",
+        "Resolution": "",
+        "VideoEncoding": "",
+        "AudioEncoding": "aac",
+        "Itag": 127,
+        "AudioBitrate": 96,
+    },
+    "128": {
+        "Extension": "ts",
+        "Resolution": "",
+        "VideoEncoding": "",
+        "AudioEncoding": "aac",
+        "Itag": 128,
+        "AudioBitrate": 96,
+    },
+    "132": {
+        "Extension": "ts",
+        "Resolution": "240p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 132,
+        "AudioBitrate": 48,
+    },
+    "151": {
+        "Extension": "ts",
+        "Resolution": "720p",
+        "VideoEncoding": "H.264",
+        "AudioEncoding": "aac",
+        "Itag": 151,
+        "AudioBitrate": 24,
+    },
+}
+
+
+def repr_itag(itag):
+    itag_info = ["  %s: %s" % (k, v) for k, v in get_itag_info(itag).items()]
+    return "\n".join(itag_info)
+
+
+def get_itag_info(itag):
+    if itag not in itags:
+        # unknown itag...
+        # XXX this could be an issue
+        return {"Itag": itag, "Error": "Unknown"}
+    return itags[itag]
+
+
+def log(msg):
+    print(msg)
+
+
+_HERE = os.path.dirname(__file__)
+if "MOZPROXY_DIR" in os.environ:
+    _DEFAULT_DATA_DIR = os.environ["MOZPROXY_DIR"]
+else:
+    _DEFAULT_DATA_DIR = os.path.join(_HERE, "..", "data")
+
+_HEADERS = {
+    b"Last-Modified": b"Mon, 10 Dec 2018 19:39:24 GMT",
+    b"Content-Type": b"video/webm",
+    b"Date": b"Wed, 02 Jan 2019 15:14:06 GMT",
+    b"Expires": b"Wed, 02 Jan 2019 15:14:06 GMT",
+    b"Cache-Control": b"private, max-age=21292",
+    b"Accept-Ranges": b"bytes",
+    b"Content-Length": b"173448",
+    b"Connection": b"keep-alive",
+    b"Alt-Svc": b'quic=":443"; ma=2592000; v="44,43,39,35"',
+    b"Access-Control-Allow-Origin": b"https://www.youtube.com",
+    b"Access-Control-Allow-Credentials": b"true",
+    b"Timing-Allow-Origin": b"https://www.youtube.com",
+    b"Access-Control-Expose-Headers": (
+        b"Client-Protocol, Content-Length, "
+        b"Content-Type, X-Bandwidth-Est, "
+        b"X-Bandwidth-Est2, X-Bandwidth-Est3, "
+        b"X-Bandwidth-App-Limited, "
+        b"X-Bandwidth-Est-App-Limited, "
+        b"X-Bandwidth-Est-Comp, X-Bandwidth-Avg, "
+        b"X-Head-Time-Millis, X-Head-Time-Sec, "
+        b"X-Head-Seqnum, X-Response-Itag, "
+        b"X-Restrict-Formats-Hint, "
+        b"X-Sequence-Num, X-Segment-Lmt, "
+        b"X-Walltime-Ms"
+    ),
+    b"X-Restrict-Formats-Hint": b"None",
+    b"X-Content-Type-Options": b"nosniff",
+    b"Server": b"gvs 1.0",
+}
+
+
+def get_cached_data(request, datadir=_DEFAULT_DATA_DIR):
+    query_args = dict(request.query)
+    mime = query_args["mime"]
+    file_id = query_args["id"]
+    file_range = query_args["range"]
+    itag = query_args["itag"]
+    log("Request File %s - %s" % (file_id, mime))
+    log("Requested range %s" % file_range)
+    log("Requested quality\n%s" % repr_itag(itag))
+    frange = file_range.split("-")
+    range_start, range_end = int(frange[0]), int(frange[1])
+    video_id = sys.argv[-1].split(".")[0]
+    fn = "%s-%s-%s.%s" % (video_id, itag, mime.replace("/", ""), mime.split("/")[-1])
+    fn = os.path.join(datadir, fn)
+    if not os.path.exists(fn):
+        raise Exception("no file at %s" % fn)
+    with open(fn, "rb") as f:
+        data = f.read()
+    data = data[range_start : range_end + 1]  # noqa: E203
+    headers = dict(_HEADERS)
+    headers[b"Content-Type"] = bytes(mime, "utf8")
+    headers[b"Content-Length"] = bytes(str(len(data)), "utf8")
+    return headers.items(), data
+
+
+def OK(flow, code=204):
+    """ Sending back a dummy response.
+
+    204 is the default in most cases on YT requests.
+    """
+    from mitmproxy import http
+
+    flow.error = None
+    flow.response = http.HTTPResponse(b"HTTP/1.1", code, b"OK", {}, b"")
+
+
+def request(flow):
+    # All requests made for stats purposes can be discarded and
+    # a 204 sent back to the client.
+    if flow.request.url.startswith("https://www.youtube.com/ptracking"):
+        OK(flow)
+        return
+    if flow.request.url.startswith("https://www.youtube.com/api/stats/playback"):
+        OK(flow)
+        return
+    if flow.request.url.startswith("https://www.youtube.com/api/stats/watchtime"):
+        OK(flow)
+        return
+    # disable a few trackers, sniffers, etc
+    if "push.services.mozilla.com" in flow.request.url:
+        OK(flow, code=200)
+        return
+    if "gen_204" in flow.request.url:
+        OK(flow)
+        return
+
+    # we don't want to post back any data, discarding.
+    if flow.request.method == "POST":
+        OK(flow)
+        return
+    if "googlevideo.com/videoplayback" in flow.request.url:
+        from mitmproxy import http
+
+        query_args = dict(flow.request.query)
+        file_id = query_args["id"]
+        file_range = query_args["range"]
+        try:
+            headers, data = get_cached_data(flow.request)
+        except Exception:
+            OK(flow, code=404)
+            return
+        headers = list(headers)
+        flow.error = None
+        flow.response = http.HTTPResponse(b"HTTP/1.1", 200, b"OK", headers, data)
+        now = datetime.datetime.now()
+        then = now - datetime.timedelta(hours=1)
+        flow.response.timestamp_start = time.mktime(then.timetuple())
+        flow.response.refresh()
+        log("SENT FILE %s IN CACHE - range %s" % (file_id, file_range))
+
+
+def error(flow):
+    print("\n\n\n\nERROR %s\n\n\n\n" % flow.error.msg)
+
+
+def tcp_error(flow):
+    print("\n\n\n\nTCP ERROR %s\n\n\n\n" % flow.error.msg)
new file mode 100644
--- /dev/null
+++ b/dom/media/test/marionette/yttest/record.py
@@ -0,0 +1,34 @@
+"""
+MITM Script used to collect media files when a YT video is played.
+
+This is a self-contained script that should not import anything else
+except modules from the standard library and mitmproxy modules.
+"""
+import os
+
+
+_HERE = os.path.dirname(__file__)
+if "MOZPROXY_DIR" in os.environ:
+    _DEFAULT_DATA_DIR = os.environ["MOZPROXY_DIR"]
+else:
+    _DEFAULT_DATA_DIR = os.path.join(_HERE, "..", "data")
+
+
+def response(flow):
+    print(flow.request.url)
+    if "googlevideo.com/videoplayback" in flow.request.url:
+        itag = flow.request.query["itag"]
+        mime = flow.request.query["mime"].replace("/", "-")
+        query_args = dict(flow.request.query)
+        file_id = query_args["id"]
+        file_range = query_args["range"]
+        print("Writing %s:%s" % (file_id, file_range))
+        # changing the host so the MITM recording file
+        # does not rely on a specific YT server
+        flow.request.host = "googlevideo.com"
+        if len(flow.response.content) == 0:
+            return
+        path = "%s-%s-%s.%s" % (file_id, itag, file_range, mime)
+        path = os.path.join(_DEFAULT_DATA_DIR, path)
+        with open(path, "wb") as f:
+            f.write(flow.response.content)
new file mode 100644
--- /dev/null
+++ b/dom/media/test/marionette/yttest/support.py
@@ -0,0 +1,90 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+import sys
+import os
+from contextlib import contextmanager
+
+from mozproxy import get_playback
+import mozinfo
+import mozlog
+from marionette_harness.marionette_test import MarionetteTestCase
+from yttest.ytpage import using_page
+
+
+mozlog.commandline.setup_logging("mozproxy", {}, {"tbpl": sys.stdout})
+here = os.path.dirname(__file__)
+playback_script = os.path.join(here, "playback.py")
+
+
+class VideoStreamTestCase(MarionetteTestCase):
+    def setUp(self):
+        MarionetteTestCase.setUp(self)
+        if "MOZ_UPLOAD_DIR" not in os.environ:
+            os.environ["OBJ_PATH"] = "/tmp/"
+        self.marionette.set_pref("media.autoplay.default", 1)
+
+    @contextmanager
+    def using_proxy(self, video_id):
+        config = {}
+        config["binary"] = self.marionette.bin
+        config["app"] = "firefox"
+        config["platform"] = mozinfo.os
+        config["processor"] = mozinfo.processor
+        config["run_local"] = "MOZ_UPLOAD_DIR" not in os.environ
+
+        if "MOZ_UPLOAD_DIR" not in os.environ:
+            config["obj_path"] = os.environ["OBJ_PATH"]
+            playback_dir = os.path.join(config["obj_path"], "testing", "mozproxy")
+        else:
+            root_dir = os.path.dirname(os.path.dirname(os.environ["MOZ_UPLOAD_DIR"]))
+            playback_dir = os.path.join(root_dir, "testing", "mozproxy")
+
+        config["host"] = "localhost"
+        config["playback_tool"] = "mitmproxy"
+        config["playback_artifacts"] = os.path.join(here, "%s.manifest" % video_id)
+
+        # XXX once Bug 1540622 lands, we can use the version here
+        # config["playback_version"] = "4.0.4"
+        # and have playback_binary_manifest default to
+        # mitmproxy-rel-bin-{playback_version}-{platform}.manifest
+        # so we don't have to ask amozproxy tool user to provide this:
+        config[
+            "playback_binary_manifest"
+        ] = "mitmproxy-rel-bin-4.0.4-{platform}.manifest"
+
+        playback_file = os.path.join(playback_dir, "%s.playback" % video_id)
+
+        config["playback_tool_args"] = [
+            "--set",
+            "stream_large_bodies=30",
+            "--ssl-insecure",
+            "--server-replay-nopop",
+            "--set",
+            "upstream_cert=false",
+            "-S",
+            playback_file,
+            "-s",
+            playback_script,
+            video_id,
+        ]
+
+        proxy = get_playback(config)
+        if proxy is None:
+            raise Exception("Could not start Proxy")
+        proxy.start()
+        try:
+            yield proxy
+        finally:
+            proxy.stop()
+
+    @contextmanager
+    def youtube_video(self, video_id, **options):
+        proxy = options.get("proxy", True)
+        if proxy:
+            with self.using_proxy(video_id):
+                with using_page(video_id, self.marionette, **options) as page:
+                    yield page
+        else:
+            with using_page(video_id, self.marionette, **options) as page:
+                yield page
new file mode 100644
--- /dev/null
+++ b/dom/media/test/marionette/yttest/until_end_test.js
@@ -0,0 +1,18 @@
+%(force_hd)s
+
+const resolve = arguments[arguments.length - 1];
+
+// this script is injected by marionette to collect metrics
+var video = document.getElementsByTagName("video")[0];
+if (!video) {
+  return "Can't find the video tag";
+}
+
+video.addEventListener("ended", () => {
+    video.pause();
+    %(video_playback_quality)s
+    %(debug_info)s
+  }, {once: true}
+);
+
+video.play();
new file mode 100644
--- /dev/null
+++ b/dom/media/test/marionette/yttest/video_playback_quality.js
@@ -0,0 +1,7 @@
+var vpq = video.getVideoPlaybackQuality();
+var result = {"currentTime": video.currentTime};
+result["creationTime"] = vpq.creationTime;
+result["corruptedVideoFrames"] = vpq.corruptedVideoFrames;
+result["droppedVideoFrames"] = vpq.droppedVideoFrames;
+result["totalVideoFrames"] = vpq.totalVideoFrames;
+result["defaultPlaybackRate"] = video.playbackRate;
new file mode 100644
--- /dev/null
+++ b/dom/media/test/marionette/yttest/ytpage.py
@@ -0,0 +1,82 @@
+# 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/.
+"""
+Drives the browser during the playback test.
+"""
+import contextlib
+import os
+
+
+here = os.path.dirname(__file__)
+js = os.path.join(here, "until_end_test.js")
+with open(js) as f:
+    UNTIL_END_TEST = f.read()
+
+js = os.path.join(here, "duration_test.js")
+with open(js) as f:
+    DURATION_TEST = f.read()
+
+JS_MACROS = {"video_playback_quality": "", "debug_info": "", "force_hd": ""}
+for script in JS_MACROS:
+    js = os.path.join(here, "%s.js" % script)
+    with open(js) as f:
+        JS_MACROS[script] = f.read()
+
+
+class YoutubePage:
+    def __init__(self, video_id, marionette, **options):
+        self.video_id = video_id
+        self.marionette = marionette
+        self.url = "https://www.youtube.com/watch?v=%s" % self.video_id
+        self.started = False
+        self.capabilities = {
+            # We're not using upstream cert sniffing, let's make sure
+            # the browser accepts mitmproxy ones for all requests
+            # even if they are incorrect.
+            "acceptInsecureCerts": True
+        }
+        self.options = options
+        if options.get("proxy", True):
+            self.capabilities["proxy"] = {
+                "proxyType": "manual",
+                "httpProxy": "localhost:8080",
+                "sslProxy": "localhost:8080",
+                "noProxy": ["localhost"],
+            }
+
+    def start_video(self):
+        self.marionette.start_session(self.capabilities)
+        self.marionette.timeout.script = 600
+        self.marionette.navigate(self.url)
+        self.started = True
+
+    def run_test(self):
+        self.start_video()
+        options = dict(JS_MACROS)
+        options.update(self.options)
+        if "duration" in options:
+            script = DURATION_TEST % options
+        else:
+            script = UNTIL_END_TEST % options
+        self.marionette.set_pref("media.autoplay.default", 0)
+        return self.execute_async_script(script)
+
+    def execute_async_script(self, script, context=None):
+        if context is None:
+            context = self.marionette.CONTEXT_CONTENT
+        with self.marionette.using_context(context):
+            return self.marionette.execute_async_script(script, sandbox="system")
+
+    def close(self):
+        if self.started:
+            self.marionette.delete_session()
+
+
+@contextlib.contextmanager
+def using_page(video_id, marionette, **options):
+    page = YoutubePage(video_id, marionette, **options)
+    try:
+        yield page
+    finally:
+        page.close()
--- a/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html
+++ b/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html
@@ -26,17 +26,19 @@
             test.originalOffer.sdp.replace(/a=mid:.*\r\n/g,
                                            "a=mid:really_long_mid_over_16_chars\r\n");
         },
         function PC_LOCAL_EXPECT_SET_LOCAL_DESCRIPTION_FAIL(test) {
           return test.setLocalDescription(test.pcLocal,
                                           test.originalOffer,
                                           HAVE_LOCAL_OFFER)
            .then(() => ok(false, "setLocalDescription must fail"),
-                 e => is(e.name, "InvalidSessionDescriptionError",
+                 // This needs to be RTCError once we support it, and once we
+                 // stop allowing any modification, InvalidModificationError
+                 e => is(e.name, "OperationError",
                          "setLocalDescription must fail and did"));
         }
       ], 0 // first occurance
     );
 
     test.run();
   });
 </script>
--- a/dom/media/tests/mochitest/test_peerConnection_restartIceBadAnswer.html
+++ b/dom/media/tests/mochitest/test_peerConnection_restartIceBadAnswer.html
@@ -38,17 +38,17 @@
                                             "a=ice-ufrag:bad-ufrag\r\n");
         },
 
         function PC_LOCAL_EXPECT_SET_REMOTE_DESCRIPTION_FAIL(test) {
           return test.setRemoteDescription(test.pcLocal,
                                            test._remote_answer,
                                            STABLE)
            .then(() => ok(false, "setRemoteDescription must fail"),
-                 e => is(e.name, "InvalidSessionDescriptionError",
+                 e => is(e.name, "InvalidAccessError",
                          "setRemoteDescription must fail and did"));
          }
       ], 1 // replace after the second PC_LOCAL_GET_ANSWER
     );
 
     test.setMediaConstraints([{audio: true}], []);
     test.run();
   });
--- a/dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInHaveLocalOffer.html
+++ b/dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInHaveLocalOffer.html
@@ -16,17 +16,17 @@ runNetworkTest(function () {
   test.setMediaConstraints([{audio: true}], [{audio: true}]);
   test.chain.removeAfter("PC_LOCAL_SET_LOCAL_DESCRIPTION");
 
   test.chain.append([
     function PC_LOCAL_SET_LOCAL_ANSWER(test) {
       test.pcLocal._latest_offer.type = "answer";
       return test.pcLocal.setLocalDescriptionAndFail(test.pcLocal._latest_offer)
         .then(err => {
-          is(err.name, "InvalidStateError", "Error is InvalidStateError");
+          is(err.name, "InvalidModificationError", "Error is InvalidModificationError");
         });
     }
   ]);
 
   test.run();
 });
 </script>
 </pre>
--- a/dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInStable.html
+++ b/dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInStable.html
@@ -16,17 +16,17 @@ runNetworkTest(function () {
   test.setMediaConstraints([{audio: true}], [{audio: true}]);
   test.chain.removeAfter("PC_LOCAL_CREATE_OFFER");
 
   test.chain.append([
     function PC_LOCAL_SET_LOCAL_ANSWER(test) {
       test.pcLocal._latest_offer.type = "answer";
       return test.pcLocal.setLocalDescriptionAndFail(test.pcLocal._latest_offer)
         .then(err => {
-          is(err.name, "InvalidStateError", "Error is InvalidStateError");
+          is(err.name, "InvalidModificationError", "Error is InvalidModificationError");
         });
     }
   ]);
 
   test.run();
 });
 </script>
 </pre>
--- a/dom/media/webaudio/AudioContext.cpp
+++ b/dom/media/webaudio/AudioContext.cpp
@@ -156,17 +156,16 @@ AudioContext::AudioContext(nsPIDOMWindow
       mIsOffline(aIsOffline),
       mIsStarted(!aIsOffline),
       mIsShutDown(false),
       mCloseCalled(false),
       mSuspendCalled(false),
       mIsDisconnecting(false),
       mWasAllowedToStart(true),
       mSuspendedByContent(false),
-      mSuspendedByChrome(false),
       mWasEverAllowedToStart(false),
       mWasEverBlockedToStart(false),
       mWouldBeAllowedToStart(true) {
   bool mute = aWindow->AddAudioContext(this);
 
   // Note: AudioDestinationNode needs an AudioContext that must already be
   // bound to the window.
   const bool allowedToStart = AutoplayPolicy::IsAllowedToPlay(*this);
@@ -202,17 +201,17 @@ void AudioContext::StartBlockedAudioCont
   const bool isAllowedToPlay = AutoplayPolicy::IsAllowedToPlay(*this);
   AUTOPLAY_LOG("Trying to start AudioContext %p, IsAllowedToPlay=%d", this,
                isAllowedToPlay);
 
   // Only start the AudioContext if this resume() call was initiated by content,
   // not if it was a result of the AudioContext starting after having been
   // blocked because of the auto-play policy.
   if (isAllowedToPlay && !mSuspendedByContent) {
-    ResumeInternal();
+    ResumeInternal(AudioContextOperationFlags::SendStateChange);
   } else {
     ReportBlocked();
   }
 }
 
 nsresult AudioContext::Init() {
   if (!mIsOffline) {
     nsresult rv = mDestination->CreateAudioChannelAgent();
@@ -688,17 +687,17 @@ void AudioContext::Shutdown() {
   if (!mIsShutDown) {
     MaybeUpdateAutoplayTelemetryWhenShutdown();
   }
   mIsShutDown = true;
 
   // We don't want to touch promises if the global is going away soon.
   if (!mIsDisconnecting) {
     if (!mIsOffline) {
-      CloseInternal(nullptr);
+      CloseInternal(nullptr, AudioContextOperationFlags::None);
     }
 
     for (auto p : mPromiseGripArray) {
       p->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
     }
 
     mPromiseGripArray.Clear();
 
@@ -922,57 +921,51 @@ already_AddRefed<Promise> AudioContext::
 
   if (mAudioContextState == AudioContextState::Closed || mCloseCalled) {
     promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
     return promise.forget();
   }
 
   mSuspendedByContent = true;
   mPromiseGripArray.AppendElement(promise);
-  SuspendInternal(promise);
+  SuspendInternal(promise, AudioContextOperationFlags::SendStateChange);
   return promise.forget();
 }
 
 void AudioContext::SuspendFromChrome() {
-  // Not support suspend call for these situations.
-  if (mAudioContextState == AudioContextState::Suspended || mIsOffline ||
-      (mAudioContextState == AudioContextState::Closed || mCloseCalled) ||
-      mIsShutDown) {
+  if (mIsOffline || mIsShutDown) {
     return;
   }
-  SuspendInternal(nullptr);
-  mSuspendedByChrome = true;
+  SuspendInternal(nullptr, AudioContextOperationFlags::None);
 }
 
-void AudioContext::SuspendInternal(void* aPromise) {
+void AudioContext::SuspendInternal(void* aPromise,
+                                   AudioContextOperationFlags aFlags) {
   Destination()->Suspend();
 
   nsTArray<MediaStream*> streams;
   // If mSuspendCalled is true then we already suspended all our streams,
   // so don't suspend them again (since suspend(); suspend(); resume(); should
   // cancel both suspends). But we still need to do ApplyAudioContextOperation
   // to ensure our new promise is resolved.
   if (!mSuspendCalled) {
     streams = GetAllStreams();
   }
   Graph()->ApplyAudioContextOperation(DestinationStream(), streams,
-                                      AudioContextOperation::Suspend, aPromise);
+                                      AudioContextOperation::Suspend, aPromise,
+                                      aFlags);
 
   mSuspendCalled = true;
 }
 
 void AudioContext::ResumeFromChrome() {
-  // Not support resume call for these situations.
-  if (mAudioContextState == AudioContextState::Running || mIsOffline ||
-      (mAudioContextState == AudioContextState::Closed || mCloseCalled) ||
-      mIsShutDown || !mSuspendedByChrome) {
+  if (mIsOffline || mIsShutDown) {
     return;
   }
-  ResumeInternal();
-  mSuspendedByChrome = false;
+  ResumeInternal(AudioContextOperationFlags::None);
 }
 
 already_AddRefed<Promise> AudioContext::Resume(ErrorResult& aRv) {
   nsCOMPtr<nsIGlobalObject> parentObject = do_QueryInterface(GetParentObject());
   RefPtr<Promise> promise;
   promise = Promise::Create(parentObject, aRv);
   if (aRv.Failed()) {
     return nullptr;
@@ -990,47 +983,44 @@ already_AddRefed<Promise> AudioContext::
 
   mSuspendedByContent = false;
   mPendingResumePromises.AppendElement(promise);
 
   const bool isAllowedToPlay = AutoplayPolicy::IsAllowedToPlay(*this);
   AUTOPLAY_LOG("Trying to resume AudioContext %p, IsAllowedToPlay=%d", this,
                isAllowedToPlay);
   if (isAllowedToPlay) {
-    ResumeInternal();
+    ResumeInternal(AudioContextOperationFlags::SendStateChange);
   } else {
     ReportBlocked();
   }
 
   MaybeUpdateAutoplayTelemetry();
 
   return promise.forget();
 }
 
-void AudioContext::ResumeInternal() {
+void AudioContext::ResumeInternal(AudioContextOperationFlags aFlags) {
   AUTOPLAY_LOG("Allow to resume AudioContext %p", this);
   mWasAllowedToStart = true;
 
   Destination()->Resume();
 
   nsTArray<MediaStream*> streams;
   // If mSuspendCalled is false then we already resumed all our streams,
   // so don't resume them again (since suspend(); resume(); resume(); should
   // be OK). But we still need to do ApplyAudioContextOperation
   // to ensure our new promise is resolved.
   if (mSuspendCalled) {
     streams = GetAllStreams();
   }
   Graph()->ApplyAudioContextOperation(DestinationStream(), streams,
-                                      AudioContextOperation::Resume, nullptr);
+                                      AudioContextOperation::Resume, nullptr,
+                                      aFlags);
   mSuspendCalled = false;
-  // AudioContext will be resumed later, so we have no need to keep the suspend
-  // flag from Chrome, in case to avoid to resume the suspended Audio Context
-  // which is requested by content.
-  mSuspendedByChrome = false;
 }
 
 void AudioContext::UpdateAutoplayAssumptionStatus() {
   if (AutoplayPolicy::WouldBeAllowedToPlayIfAutoplayDisabled(*this)) {
     mWasEverAllowedToStart |= true;
     mWouldBeAllowedToStart = true;
   } else {
     mWasEverBlockedToStart |= true;
@@ -1117,35 +1107,36 @@ already_AddRefed<Promise> AudioContext::
   }
 
   if (Destination()) {
     Destination()->DestroyAudioChannelAgent();
   }
 
   mPromiseGripArray.AppendElement(promise);
 
-  CloseInternal(promise);
+  CloseInternal(promise, AudioContextOperationFlags::SendStateChange);
 
   return promise.forget();
 }
 
-void AudioContext::CloseInternal(void* aPromise) {
+void AudioContext::CloseInternal(void* aPromise,
+                                 AudioContextOperationFlags aFlags) {
   // This can be called when freeing a document, and the streams are dead at
   // this point, so we need extra null-checks.
   AudioNodeStream* ds = DestinationStream();
   if (ds) {
     nsTArray<MediaStream*> streams;
     // If mSuspendCalled or mCloseCalled are true then we already suspended
     // all our streams, so don't suspend them again. But we still need to do
     // ApplyAudioContextOperation to ensure our new promise is resolved.
     if (!mSuspendCalled && !mCloseCalled) {
       streams = GetAllStreams();
     }
-    Graph()->ApplyAudioContextOperation(ds, streams,
-                                        AudioContextOperation::Close, aPromise);
+    Graph()->ApplyAudioContextOperation(
+        ds, streams, AudioContextOperation::Close, aPromise, aFlags);
   }
   mCloseCalled = true;
 }
 
 void AudioContext::RegisterNode(AudioNode* aNode) {
   MOZ_ASSERT(!mAllNodes.Contains(aNode));
   mAllNodes.PutEntry(aNode);
 }
--- a/dom/media/webaudio/AudioContext.h
+++ b/dom/media/webaudio/AudioContext.h
@@ -10,16 +10,17 @@
 #include "AudioParamDescriptorMap.h"
 #include "mozilla/dom/OfflineAudioContextBinding.h"
 #include "MediaBufferDecoder.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/DOMEventTargetHelper.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/dom/TypedArray.h"
 #include "mozilla/RelativeTimeline.h"
+#include "mozilla/TypedEnumBits.h"
 #include "mozilla/UniquePtr.h"
 #include "nsCOMPtr.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsHashKeys.h"
 #include "nsTHashtable.h"
 #include "js/TypeDecls.h"
 #include "nsIMemoryReporter.h"
 
@@ -113,16 +114,25 @@ class StateChangeTask final : public Run
  private:
   RefPtr<AudioContext> mAudioContext;
   void* mPromise;
   RefPtr<AudioNodeStream> mAudioNodeStream;
   AudioContextState mNewState;
 };
 
 enum class AudioContextOperation { Suspend, Resume, Close };
+// When suspending or resuming an AudioContext, some operations have to notify
+// the main thread, so that the Promise is resolved, the state is modified, and
+// the statechanged event is sent. Some other operations don't go back to the
+// main thread, for example when the AudioContext is paused by something that is
+// not caused by the page itself: opening a debugger, breaking on a breakpoint,
+// reloading a document.
+enum class AudioContextOperationFlags { None, SendStateChange };
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(AudioContextOperationFlags);
+
 struct AudioContextOptions;
 
 class AudioContext final : public DOMEventTargetHelper,
                            public nsIMemoryReporter,
                            public RelativeTimeline {
   AudioContext(nsPIDOMWindowInner* aParentWindow, bool aIsOffline,
                uint32_t aNumberOfChannels = 0, uint32_t aLength = 0,
                float aSampleRate = 0.0f);
@@ -328,19 +338,19 @@ class AudioContext final : public DOMEve
 
   size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
   NS_DECL_NSIMEMORYREPORTER
 
   friend struct ::mozilla::WebAudioDecodeJob;
 
   nsTArray<MediaStream*> GetAllStreams() const;
 
-  void ResumeInternal();
-  void SuspendInternal(void* aPromise);
-  void CloseInternal(void* aPromise);
+  void ResumeInternal(AudioContextOperationFlags aFlags);
+  void SuspendInternal(void* aPromise, AudioContextOperationFlags aFlags);
+  void CloseInternal(void* aPromise, AudioContextOperationFlags aFlags);
 
   // Will report error message to console and dispatch testing event if needed
   // when AudioContext is blocked by autoplay policy.
   void ReportBlocked();
 
   void ReportToConsole(uint32_t aErrorFlags, const char* aMsg) const;
 
   // This function should be called everytime we decide whether allow to start
@@ -394,18 +404,16 @@ class AudioContext final : public DOMEve
   // Suspend has been called with no following resume.
   bool mSuspendCalled;
   bool mIsDisconnecting;
   // This flag stores the value of previous status of `allowed-to-start`.
   bool mWasAllowedToStart;
 
   // True if this AudioContext has been suspended by the page.
   bool mSuspendedByContent;
-  // True if this AudioContext has been suspended by the chrome.
-  bool mSuspendedByChrome;
 
   // These variables are used for telemetry, they're not reflect the actual
   // status of AudioContext, they are based on the "assumption" of enabling
   // blocking web audio. Because we want to record Telemetry no matter user
   // enable blocking autoplay or not.
   // - 'mWasEverAllowedToStart' would be true when AudioContext had ever been
   //   allowed to start if we enable blocking web audio.
   // - 'mWasEverBlockedToStart' would be true when AudioContext had ever been
--- a/dom/plugins/ipc/IpdlTuple.h
+++ b/dom/plugins/ipc/IpdlTuple.h
@@ -143,30 +143,19 @@ struct ParamTraits<IpdlTuple::IpdlTupleE
 
   static bool Read(const Message* aMsg, PickleIterator* aIter,
                    paramType* aParam) {
     bool ret = ReadParam(aMsg, aIter, &aParam->GetVariant());
     MOZ_RELEASE_ASSERT(!aParam->GetVariant().is<IpdlTuple::InvalidType>());
     return ret;
   }
 
-  struct LogMatcher {
-    explicit LogMatcher(std::wstring* aLog) : mLog(aLog) {}
-
-    template <typename EntryType>
-    void match(const EntryType& aParam) {
-      LogParam(aParam, mLog);
-    }
-
-   private:
-    std::wstring* mLog;
-  };
-
   static void Log(const paramType& aParam, std::wstring* aLog) {
-    aParam.GetVariant().match(LogMatcher(aLog));
+    aParam.GetVariant().match(
+        [aLog](const auto& aParam) { LogParam(aParam, aLog); });
   }
 };
 
 template <>
 struct ParamTraits<IpdlTuple::InvalidType> {
   typedef IpdlTuple::InvalidType paramType;
 
   static void Write(Message* aMsg, const paramType& aParam) {
--- a/dom/quota/OriginScope.h
+++ b/dom/quota/OriginScope.h
@@ -207,23 +207,29 @@ class OriginScope {
   }
 
   bool Matches(const OriginScope& aOther) const {
     struct Matcher {
       const OriginScope& mThis;
 
       explicit Matcher(const OriginScope& aThis) : mThis(aThis) {}
 
-      bool match(const Origin& aOther) { return mThis.MatchesOrigin(aOther); }
-
-      bool match(const Prefix& aOther) { return mThis.MatchesPrefix(aOther); }
+      bool operator()(const Origin& aOther) {
+        return mThis.MatchesOrigin(aOther);
+      }
 
-      bool match(const Pattern& aOther) { return mThis.MatchesPattern(aOther); }
+      bool operator()(const Prefix& aOther) {
+        return mThis.MatchesPrefix(aOther);
+      }
 
-      bool match(const Null& aOther) { return true; }
+      bool operator()(const Pattern& aOther) {
+        return mThis.MatchesPattern(aOther);
+      }
+
+      bool operator()(const Null& aOther) { return true; }
     };
 
     return aOther.mData.match(Matcher(*this));
   }
 
   OriginScope Clone() { return OriginScope(mData); }
 
  private:
@@ -240,89 +246,89 @@ class OriginScope {
   explicit OriginScope(const DataType& aOther) : mData(aOther) {}
 
   bool MatchesOrigin(const Origin& aOther) const {
     struct OriginMatcher {
       const Origin& mOther;
 
       explicit OriginMatcher(const Origin& aOther) : mOther(aOther) {}
 
-      bool match(const Origin& aThis) {
+      bool operator()(const Origin& aThis) {
         return aThis.GetOrigin().Equals(mOther.GetOrigin());
       }
 
-      bool match(const Prefix& aThis) {
+      bool operator()(const Prefix& aThis) {
         return aThis.GetOriginNoSuffix().Equals(mOther.GetOriginNoSuffix());
       }
 
-      bool match(const Pattern& aThis) {
+      bool operator()(const Pattern& aThis) {
         return aThis.GetPattern().Matches(mOther.GetAttributes());
       }
 
-      bool match(const Null& aThis) {
+      bool operator()(const Null& aThis) {
         // Null covers everything.
         return true;
       }
     };
 
     return mData.match(OriginMatcher(aOther));
   }
 
   bool MatchesPrefix(const Prefix& aOther) const {
     struct PrefixMatcher {
       const Prefix& mOther;
 
       explicit PrefixMatcher(const Prefix& aOther) : mOther(aOther) {}
 
-      bool match(const Origin& aThis) {
+      bool operator()(const Origin& aThis) {
         return aThis.GetOriginNoSuffix().Equals(mOther.GetOriginNoSuffix());
       }
 
-      bool match(const Prefix& aThis) {
+      bool operator()(const Prefix& aThis) {
         return aThis.GetOriginNoSuffix().Equals(mOther.GetOriginNoSuffix());
       }
 
-      bool match(const Pattern& aThis) {
+      bool operator()(const Pattern& aThis) {
         // The match will be always true here because any origin attributes
         // pattern overlaps any origin prefix (an origin prefix targets all
         // origin attributes).
         return true;
       }
 
-      bool match(const Null& aThis) {
+      bool operator()(const Null& aThis) {
         // Null covers everything.
         return true;
       }
     };
 
     return mData.match(PrefixMatcher(aOther));
   }
 
   bool MatchesPattern(const Pattern& aOther) const {
     struct PatternMatcher {
       const Pattern& mOther;
 
       explicit PatternMatcher(const Pattern& aOther) : mOther(aOther) {}
 
-      bool match(const Origin& aThis) {
+      bool operator()(const Origin& aThis) {
         return mOther.GetPattern().Matches(aThis.GetAttributes());
       }
 
-      bool match(const Prefix& aThis) {
+      bool operator()(const Prefix& aThis) {
         // The match will be always true here because any origin attributes
         // pattern overlaps any origin prefix (an origin prefix targets all
         // origin attributes).
         return true;
       }
 
-      bool match(const Pattern& aThis) {
+      bool operator()(const Pattern& aThis) {
         return aThis.GetPattern().Overlaps(mOther.GetPattern());
       }
 
-      bool match(const Null& aThis) {
+      bool operator()(const Null& aThis) {
         // Null covers everything.
         return true;
       }
     };
 
     PatternMatcher patternMatcher(aOther);
     return mData.match(PatternMatcher(aOther));
   }
--- a/dom/svg/SVGAElement.h
+++ b/dom/svg/SVGAElement.h
@@ -37,17 +37,18 @@ class SVGAElement final : public SVGAEle
 
  public:
   NS_DECL_ISUPPORTS_INHERITED
 
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SVGAElement, SVGAElementBase)
 
   // nsINode interface methods
   void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
-  virtual nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
+  MOZ_CAN_RUN_SCRIPT
+  nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
   virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
 
   // nsIContent
   virtual nsresult BindToTree(Document* aDocument, nsIContent* aParent,
                               nsIContent* aBindingParent) override;
   virtual void UnbindFromTree(bool aDeep = true,
                               bool aNullParent = true) override;
   NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
--- a/dom/svg/SVGAnimatedPreserveAspectRatio.h
+++ b/dom/svg/SVGAnimatedPreserveAspectRatio.h
@@ -41,27 +41,29 @@ class SVGAnimatedPreserveAspectRatio fin
   void GetBaseValueString(nsAString& aValue) const;
 
   void SetBaseValue(const SVGPreserveAspectRatio& aValue,
                     dom::SVGElement* aSVGElement);
   nsresult SetBaseAlign(uint16_t aAlign, dom::SVGElement* aSVGElement) {
     if (aAlign < SVG_ALIGN_MIN_VALID || aAlign > SVG_ALIGN_MAX_VALID) {
       return NS_ERROR_FAILURE;
     }
-    SetBaseValue(SVGPreserveAspectRatio(aAlign, mBaseVal.GetMeetOrSlice()),
+    SetBaseValue(SVGPreserveAspectRatio(static_cast<uint8_t>(aAlign),
+                                        mBaseVal.GetMeetOrSlice()),
                  aSVGElement);
     return NS_OK;
   }
   nsresult SetBaseMeetOrSlice(uint16_t aMeetOrSlice,
                               dom::SVGElement* aSVGElement) {
     if (aMeetOrSlice < SVG_MEETORSLICE_MIN_VALID ||
         aMeetOrSlice > SVG_MEETORSLICE_MAX_VALID) {
       return NS_ERROR_FAILURE;
     }
-    SetBaseValue(SVGPreserveAspectRatio(mBaseVal.GetAlign(), aMeetOrSlice),
+    SetBaseValue(SVGPreserveAspectRatio(mBaseVal.GetAlign(),
+                                        static_cast<uint8_t>(aMeetOrSlice)),
                  aSVGElement);
     return NS_OK;
   }
   void SetAnimValue(uint64_t aPackedValue, dom::SVGElement* aSVGElement);
 
   const SVGPreserveAspectRatio& GetBaseValue() const { return mBaseVal; }
   const SVGPreserveAspectRatio& GetAnimValue() const { return mAnimVal; }
   bool IsAnimated() const { return mIsAnimated; }
--- a/dom/svg/SVGPreserveAspectRatio.h
+++ b/dom/svg/SVGPreserveAspectRatio.h
@@ -38,43 +38,43 @@ class SVGPreserveAspectRatio final {
 
  public:
   explicit SVGPreserveAspectRatio()
       : mAlign(dom::SVGPreserveAspectRatio_Binding::
                    SVG_PRESERVEASPECTRATIO_UNKNOWN),
         mMeetOrSlice(
             dom::SVGPreserveAspectRatio_Binding::SVG_MEETORSLICE_UNKNOWN) {}
 
-  SVGPreserveAspectRatio(uint16_t aAlign, uint16_t aMeetOrSlice)
+  SVGPreserveAspectRatio(uint8_t aAlign, uint8_t aMeetOrSlice)
       : mAlign(aAlign), mMeetOrSlice(aMeetOrSlice) {}
 
   static nsresult FromString(const nsAString& aString,
                              SVGPreserveAspectRatio* aValue);
   void ToString(nsAString& aValueAsString) const;
 
   bool operator==(const SVGPreserveAspectRatio& aOther) const;
 
   nsresult SetAlign(uint16_t aAlign) {
     if (aAlign < SVG_ALIGN_MIN_VALID || aAlign > SVG_ALIGN_MAX_VALID)
       return NS_ERROR_FAILURE;
     mAlign = static_cast<uint8_t>(aAlign);
     return NS_OK;
   }
 
-  uint16_t GetAlign() const { return mAlign; }
+  auto GetAlign() const { return mAlign; }
 
   nsresult SetMeetOrSlice(uint16_t aMeetOrSlice) {
     if (aMeetOrSlice < SVG_MEETORSLICE_MIN_VALID ||
         aMeetOrSlice > SVG_MEETORSLICE_MAX_VALID)
       return NS_ERROR_FAILURE;
     mMeetOrSlice = static_cast<uint8_t>(aMeetOrSlice);
     return NS_OK;
   }
 
-  uint16_t GetMeetOrSlice() const { return mMeetOrSlice; }
+  auto GetMeetOrSlice() const { return mMeetOrSlice; }
 
   PLDHashNumber Hash() const { return HashGeneric(mAlign, mMeetOrSlice); }
 
  private:
   // We can't use enum types here because some compilers fail to pack them.
   uint8_t mAlign;
   uint8_t mMeetOrSlice;
 };
--- a/dom/u2f/U2F.cpp
+++ b/dom/u2f/U2F.cpp
@@ -50,17 +50,17 @@ NS_INTERFACE_MAP_END_INHERITING(WebAuthn
 
 NS_IMPL_ADDREF_INHERITED(U2F, WebAuthnManagerBase)
 NS_IMPL_RELEASE_INHERITED(U2F, WebAuthnManagerBase)
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(U2F)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(U2F, WebAuthnManagerBase)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransaction)
   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
-  tmp->ClearTransaction();
+  tmp->mTransaction.reset();
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(U2F, WebAuthnManagerBase)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(U2F)
 
 /***********************************************************************
  * Utility Functions
--- a/dom/webauthn/U2FTokenManager.cpp
+++ b/dom/webauthn/U2FTokenManager.cpp
@@ -161,32 +161,38 @@ U2FTokenManager* U2FTokenManager::Get() 
 }
 
 void U2FTokenManager::AbortTransaction(const uint64_t& aTransactionId,
                                        const nsresult& aError) {
   Unused << mTransactionParent->SendAbort(aTransactionId, aError);
   ClearTransaction();
 }
 
+void U2FTokenManager::AbortOngoingTransaction() {
+  if (mLastTransactionId > 0 && mTransactionParent) {
+    // Send an abort to any other ongoing transaction
+    Unused << mTransactionParent->SendAbort(mLastTransactionId,
+                                            NS_ERROR_DOM_ABORT_ERR);
+  }
+  ClearTransaction();
+}
+
 void U2FTokenManager::MaybeClearTransaction(
     PWebAuthnTransactionParent* aParent) {
   // Only clear if we've been requested to do so by our current transaction
   // parent.
   if (mTransactionParent == aParent) {
     ClearTransaction();
   }
 }
 
 void U2FTokenManager::ClearTransaction() {
-  if (mLastTransactionId > 0 && mTransactionParent) {
+  if (mLastTransactionId) {
     // Remove any prompts we might be showing for the current transaction.
     SendPromptNotification(kCancelPromptNotifcation, mLastTransactionId);
-    // Send an abort to any other ongoing transaction
-    Unused << mTransactionParent->SendAbort(mLastTransactionId,
-                                            NS_ERROR_DOM_ABORT_ERR);
   }
 
   mTransactionParent = nullptr;
 
   // Drop managers at the end of all transactions
   if (mTokenManagerImpl) {
     mTokenManagerImpl->Drop();
     mTokenManagerImpl = nullptr;
@@ -266,17 +272,17 @@ RefPtr<U2FTokenTransport> U2FTokenManage
 }
 
 void U2FTokenManager::Register(
     PWebAuthnTransactionParent* aTransactionParent,
     const uint64_t& aTransactionId,
     const WebAuthnMakeCredentialInfo& aTransactionInfo) {
   MOZ_LOG(gU2FTokenManagerLog, LogLevel::Debug, ("U2FAuthRegister"));
 
-  ClearTransaction();
+  AbortOngoingTransaction();
   mTransactionParent = aTransactionParent;
   mTokenManagerImpl = GetTokenManagerImpl();
 
   if (!mTokenManagerImpl) {
     AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR);
     return;
   }
 
@@ -363,17 +369,17 @@ void U2FTokenManager::MaybeAbortRegister
   AbortTransaction(aTransactionId, aError);
 }
 
 void U2FTokenManager::Sign(PWebAuthnTransactionParent* aTransactionParent,
                            const uint64_t& aTransactionId,
                            const WebAuthnGetAssertionInfo& aTransactionInfo) {
   MOZ_LOG(gU2FTokenManagerLog, LogLevel::Debug, ("U2FAuthSign"));
 
-  ClearTransaction();
+  AbortOngoingTransaction();
   mTransactionParent = aTransactionParent;
   mTokenManagerImpl = GetTokenManagerImpl();
 
   if (!mTokenManagerImpl) {
     AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR);
     return;
   }
 
--- a/dom/webauthn/U2FTokenManager.h
+++ b/dom/webauthn/U2FTokenManager.h
@@ -44,16 +44,17 @@ class U2FTokenManager final : public nsI
   void MaybeClearTransaction(PWebAuthnTransactionParent* aParent);
   static void Initialize();
 
  private:
   U2FTokenManager();
   ~U2FTokenManager() {}
   RefPtr<U2FTokenTransport> GetTokenManagerImpl();
   void AbortTransaction(const uint64_t& aTransactionId, const nsresult& aError);
+  void AbortOngoingTransaction();
   void ClearTransaction();
   // Step two of "Register", kicking off the actual transaction.
   void DoRegister(const WebAuthnMakeCredentialInfo& aInfo,
                   bool aForceNoneAttestation);
   void MaybeConfirmRegister(const uint64_t& aTransactionId,
                             const WebAuthnMakeCredentialResult& aResult);
   void MaybeAbortRegister(const uint64_t& aTransactionId,
                           const nsresult& aError);
--- a/dom/webauthn/WebAuthnManager.cpp
+++ b/dom/webauthn/WebAuthnManager.cpp
@@ -38,17 +38,17 @@ static mozilla::LazyLogModule gWebAuthnM
 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(WebAuthnManager,
                                                WebAuthnManagerBase)
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(WebAuthnManager)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WebAuthnManager,
                                                 WebAuthnManagerBase)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mFollowingSignal)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransaction)
-  tmp->ClearTransaction();
+  tmp->mTransaction.reset();
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WebAuthnManager,
                                                   WebAuthnManagerBase)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFollowingSignal)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 /***********************************************************************
--- a/dom/webidl/PeerConnectionObserver.webidl
+++ b/dom/webidl/PeerConnectionObserver.webidl
@@ -1,37 +1,44 @@
 /* -*- 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/.
  */
 
 interface nsISupports;
 
+dictionary PCErrorData
+{
+  required PCError name;
+  required DOMString message;
+  // Will need to add more stuff (optional) for RTCError
+};
+
 [ChromeOnly,
  JSImplementation="@mozilla.org/dom/peerconnectionobserver;1",
  Constructor (RTCPeerConnection domPC)]
 interface PeerConnectionObserver
 {
   /* JSEP callbacks */
   void onCreateOfferSuccess(DOMString offer);
-  void onCreateOfferError(unsigned long name, DOMString message);
+  void onCreateOfferError(PCErrorData error);
   void onCreateAnswerSuccess(DOMString answer);
-  void onCreateAnswerError(unsigned long name, DOMString message);
+  void onCreateAnswerError(PCErrorData error);
   void onSetLocalDescriptionSuccess();
   void onSetRemoteDescriptionSuccess();
-  void onSetLocalDescriptionError(unsigned long name, DOMString message);
-  void onSetRemoteDescriptionError(unsigned long name, DOMString message);
+  void onSetLocalDescriptionError(PCErrorData error);
+  void onSetRemoteDescriptionError(PCErrorData error);
   void onAddIceCandidateSuccess();
-  void onAddIceCandidateError(unsigned long name, DOMString message);
+  void onAddIceCandidateError(PCErrorData error);
   void onIceCandidate(unsigned short level, DOMString mid, DOMString candidate, DOMString ufrag);
 
   /* Stats callbacks */
   void onGetStatsSuccess(optional RTCStatsReportInternal report);
-  void onGetStatsError(unsigned long name, DOMString message);
+  void onGetStatsError(DOMString message);
 
   /* Data channel callbacks */
   void notifyDataChannel(RTCDataChannel channel);
 
   /* Notification of one of several types of state changed */
   void onStateChange(PCObserverStateType state);
 
   /* Transceiver management; called when setRemoteDescription causes a
--- a/dom/webidl/PeerConnectionObserverEnums.webidl
+++ b/dom/webidl/PeerConnectionObserverEnums.webidl
@@ -7,8 +7,22 @@
  */
 
 enum PCObserverStateType {
     "None",
     "IceConnectionState",
     "IceGatheringState",
     "SignalingState"
 };
+
+enum PCError {
+  "UnknownError",
+  "InvalidAccessError",
+  "InvalidStateError",
+  "InvalidModificationError",
+  "OperationError",
+  "NotSupportedError",
+  "SyntaxError",
+  "NotReadableError",
+  "TypeError",
+  "RangeError",
+  "InvalidCharacterError"
+};
--- a/dom/webidl/XMLHttpRequest.webidl
+++ b/dom/webidl/XMLHttpRequest.webidl
@@ -15,19 +15,16 @@ interface MozChannel;
 
 enum XMLHttpRequestResponseType {
   "",
   "arraybuffer",
   "blob",
   "document",
   "json",
   "text",
-
-  // Mozilla-specific stuff
-  "moz-chunked-arraybuffer",
 };
 
 /**
  * Parameters for instantiating an XMLHttpRequest. They are passed as an
  * optional argument to the constructor:
  *
  *  new XMLHttpRequest({anon: true, system: true});
  */
--- a/dom/xhr/XMLHttpRequestMainThread.cpp
+++ b/dom/xhr/XMLHttpRequestMainThread.cpp
@@ -637,28 +637,16 @@ void XMLHttpRequestMainThread::SetRespon
   if (HasOrHasHadOwner() && mState != XMLHttpRequest_Binding::UNSENT &&
       mFlagSynchronous) {
     LogMessage("ResponseTypeSyncXHRWarning", GetOwner());
     aRv.Throw(
         NS_ERROR_DOM_INVALID_ACCESS_XHR_TIMEOUT_AND_RESPONSETYPE_UNSUPPORTED_FOR_SYNC);
     return;
   }
 
-  if (mFlagSynchronous &&
-      aResponseType == XMLHttpRequestResponseType::Moz_chunked_arraybuffer) {
-    aRv.Throw(
-        NS_ERROR_DOM_INVALID_STATE_XHR_CHUNKED_RESPONSETYPES_UNSUPPORTED_FOR_SYNC);
-    return;
-  }
-
-  // We want to get rid of this moz-only types. Bug 1335365.
-  if (aResponseType == XMLHttpRequestResponseType::Moz_chunked_arraybuffer) {
-    Telemetry::Accumulate(Telemetry::MOZ_CHUNKED_ARRAYBUFFER_IN_XHR, 1);
-  }
-
   // Set the responseType attribute's value to the given value.
   mResponseType = aResponseType;
 }
 
 void XMLHttpRequestMainThread::GetResponse(
     JSContext* aCx, JS::MutableHandle<JS::Value> aResponse, ErrorResult& aRv) {
   switch (mResponseType) {
     case XMLHttpRequestResponseType::_empty:
@@ -669,23 +657,18 @@ void XMLHttpRequestMainThread::GetRespon
         return;
       }
       if (!xpc::StringToJsval(aCx, str, aResponse)) {
         aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
       }
       return;
     }
 
-    case XMLHttpRequestResponseType::Arraybuffer:
-    case XMLHttpRequestResponseType::Moz_chunked_arraybuffer: {
-      if (!(mResponseType == XMLHttpRequestResponseType::Arraybuffer &&
-            mState == XMLHttpRequest_Binding::DONE) &&
-          !(mResponseType ==
-                XMLHttpRequestResponseType::Moz_chunked_arraybuffer &&
-            mInLoadProgressEvent)) {
+    case XMLHttpRequestResponseType::Arraybuffer: {
+      if (mState != XMLHttpRequest_Binding::DONE) {
         aResponse.setNull();
         return;
       }
 
       if (!mResultArrayBuffer) {
         mResultArrayBuffer = mArrayBufferBuilder.getArrayBuffer(aCx);
         if (!mResultArrayBuffer) {
           aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
@@ -1237,24 +1220,16 @@ void XMLHttpRequestMainThread::DispatchP
   RefPtr<ProgressEvent> event =
       ProgressEvent::Constructor(aTarget, typeString, init);
   event->SetTrusted(true);
 
   DispatchOrStoreEvent(aTarget, event);
 
   if (aType == ProgressEventType::progress) {
     mInLoadProgressEvent = false;
-
-    // clear chunked responses after every progress event
-    if (mResponseType == XMLHttpRequestResponseType::Moz_chunked_arraybuffer) {
-      mResponseBody.Truncate();
-      TruncateResponseText();
-      mResultArrayBuffer = nullptr;
-      mArrayBufferBuilder.reset();
-    }
   }
 
   // If we're sending a load, error, timeout or abort event, then
   // also dispatch the subsequent loadend event.
   if (aType == ProgressEventType::load || aType == ProgressEventType::error ||
       aType == ProgressEventType::timeout ||
       aType == ProgressEventType::abort) {
     DispatchProgressEvent(aTarget, ProgressEventType::loadend, aLoaded, aTotal);
@@ -1510,21 +1485,19 @@ nsresult XMLHttpRequestMainThread::Strea
     return NS_ERROR_FAILURE;
   }
 
   nsresult rv = NS_OK;
 
   if (xmlHttpRequest->mResponseType == XMLHttpRequestResponseType::Blob) {
     xmlHttpRequest->MaybeCreateBlobStorage();
     rv = xmlHttpRequest->mBlobStorage->Append(fromRawSegment, count);
-  } else if ((xmlHttpRequest->mResponseType ==
+  } else if (xmlHttpRequest->mResponseType ==
                   XMLHttpRequestResponseType::Arraybuffer &&
-              !xmlHttpRequest->mIsMappedArrayBuffer) ||
-             xmlHttpRequest->mResponseType ==
-                 XMLHttpRequestResponseType::Moz_chunked_arraybuffer) {
+              !xmlHttpRequest->mIsMappedArrayBuffer) {
     // get the initial capacity to something reasonable to avoid a bunch of
     // reallocs right at the start
     if (xmlHttpRequest->mArrayBufferBuilder.capacity() == 0)
       xmlHttpRequest->mArrayBufferBuilder.setCapacity(
           std::max(count, XML_HTTP_REQUEST_ARRAYBUFFER_MIN_SIZE));
 
     if (NS_WARN_IF(!xmlHttpRequest->mArrayBufferBuilder.append(
             reinterpret_cast<const uint8_t*>(fromRawSegment), count,
@@ -2158,21 +2131,18 @@ XMLHttpRequestMainThread::OnStopRequest(
     // mBlobStorage can be null if the channel is non-file non-cacheable
     // and if the response length is zero.
     MaybeCreateBlobStorage();
     mBlobStorage->GetBlobWhenReady(GetOwner(), contentType, this);
     waitingForBlobCreation = true;
 
     NS_ASSERTION(mResponseBody.IsEmpty(), "mResponseBody should be empty");
     NS_ASSERTION(mResponseText.IsEmpty(), "mResponseText should be empty");
-  } else if (NS_SUCCEEDED(status) &&
-             ((mResponseType == XMLHttpRequestResponseType::Arraybuffer &&
-               !mIsMappedArrayBuffer) ||
-              mResponseType ==
-                  XMLHttpRequestResponseType::Moz_chunked_arraybuffer)) {
+  } else if (NS_SUCCEEDED(status) && !mIsMappedArrayBuffer &&
+             mResponseType == XMLHttpRequestResponseType::Arraybuffer) {
     // set the capacity down to the actual length, to realloc back
     // down to the actual size
     if (!mArrayBufferBuilder.setCapacity(mArrayBufferBuilder.length())) {
       // this should never happen!
       status = NS_ERROR_UNEXPECTED;
     }
   }
 
--- a/dom/xhr/tests/test_XHR.html
+++ b/dom/xhr/tests/test_XHR.html
@@ -124,29 +124,27 @@ function checkOpenThrows(xhr, method, ur
 // test if setting responseType before calling open() works
 xhr = new XMLHttpRequest();
 checkSetResponseType(xhr, "");
 checkSetResponseType(xhr, "text");
 checkSetResponseType(xhr, "document");
 checkSetResponseType(xhr, "arraybuffer");
 checkSetResponseType(xhr, "blob");
 checkSetResponseType(xhr, "json");
-checkSetResponseType(xhr, "moz-chunked-arraybuffer");
 checkOpenThrows(xhr, "GET", "file_XHR_pass2.txt", false);
 
 // test response (sync, responseType is not changeable)
 xhr = new XMLHttpRequest();
 xhr.open("GET", 'file_XHR_pass2.txt', false);
 checkSetResponseTypeThrows(xhr, "");
 checkSetResponseTypeThrows(xhr, "text");
 checkSetResponseTypeThrows(xhr, "document");
 checkSetResponseTypeThrows(xhr, "arraybuffer");
 checkSetResponseTypeThrows(xhr, "blob");
 checkSetResponseTypeThrows(xhr, "json");
-checkSetResponseTypeThrows(xhr, "moz-chunked-arraybuffer");
 xhr.send(null);
 checkSetResponseTypeThrows(xhr, "document");
 is(xhr.status, 200, "wrong status");
 is(xhr.response, "hello pass\n", "wrong response");
 
 // test response (responseType='document')
 xhr = new XMLHttpRequest();
 xhr.open("GET", 'file_XHR_pass1.xml');
--- a/dom/xhr/tests/test_xhr_progressevents.html
+++ b/dom/xhr/tests/test_xhr_progressevents.html
@@ -51,23 +51,19 @@ function updateProgress(e, data, testNam
     response = bufferToString(e.target.response);
   }
   is(e.target.response, e.target.response, "reflexivity should hold" + test);
 
   if (!data.nodata && !data.encoded) {
     if (data.blob) {
       is(e.loaded, response.size, "event.loaded matches response size" + test);
     }
-    else if (!data.chunked) {
+    else {
       is(e.loaded, response.length, "event.loaded matches response size" + test);
     }
-    else {
-      is(e.loaded - data.receivedBytes, response.length,
-         "event.loaded grew by response size" + test);
-    }
   }
   ok(e.loaded > data.receivedBytes, "event.loaded increased" + test);
   ok(e.loaded - data.receivedBytes <= data.pendingBytes,
      "event.loaded didn't increase too much" + test);
 
   if (!data.nodata && !data.blob) {
     var newData;
     ok(startsWith(response, data.receivedResult),
@@ -130,17 +126,16 @@ function* runTests() {
   xhr.onprogress = xhr.onload = xhr.onerror = xhr.onreadystatechange = xhr.onloadend = getEvent;
 
   var responseTypes = [{ type: "text", text: true },
                        { type: "arraybuffer", text: false, nodata: true },
                        { type: "blob", text: false, nodata: true, blob: true },
                        { type: "document", text: true, nodata: true },
                        { type: "json", text: true, nodata: true },
                        { type: "", text: true },
-                       { type: "moz-chunked-arraybuffer", text: false, chunked: true },
                       ];
   var responseType;
   var fileExpectedResult = "";
   for (var i = 0; i < 65536; i++) {
     fileExpectedResult += String.fromCharCode(i & 255);
   }
   while (responseType = responseTypes.shift()) {
     let tests = [{ open: "Content-Type=text/plain", name: "simple test" },
@@ -187,17 +182,16 @@ function* runTests() {
                       index: 0,
                       pendingResult: "ready",
                       pendingBytes: 5,
                       receivedResult: "",
                       receivedBytes: 0,
                       total: test.total,
                       encoded: test.encoded,
                       nodata: responseType.nodata,
-                      chunked: responseType.chunked,
                       text: responseType.text,
                       blob: responseType.blob,
                       file: test.file };
 
         xhr.onreadystatechange = null;
         if (testState.file)
           xhr.open("GET", test.file);
         else
@@ -226,50 +220,37 @@ function* runTests() {
         if (!testState.file)
           xhrClose = closeConn();
 
         e = yield undefined;
         is(e.type, "readystatechange", "should readystate to done closing " + testState.name);
         is(xhr.readyState, xhr.DONE, "should be in state DONE closing " + testState.name);
         log("readystate to 4");
 
-        if (responseType.chunked) {
-          xhr.responseType;
-          is(xhr.response, null, "chunked data has null response for " + testState.name);
-        }
-
         e = yield undefined;
         is(e.type, "load", "should fire load closing " + testState.name);
         is(e.lengthComputable, e.total != 0, "length should " + (e.total == 0 ? "not " : "") + "be computable during load closing " + testState.name);
         log("got load");
 
-        if (responseType.chunked) {
-          is(xhr.response, null, "chunked data has null response for " + testState.name);
-        }
-
         e = yield undefined;
         is(e.type, "loadend", "should fire loadend closing " + testState.name);
         is(e.lengthComputable, e.total != 0, "length should " + (e.total == 0 ? "not " : "") + "be computable during loadend closing " + testState.name);
         log("got loadend");
 
         // if we closed the connection using an explicit request, make sure that goes through before
         // running the next test in order to avoid reordered requests from closing the wrong
         // connection.
         if (xhrClose && xhrClose.readyState != xhrClose.DONE) {
           log("wait for closeConn to finish");
           xhrClose.onloadend = getEvent;
           yield undefined;
           is(xhrClose.readyState, xhrClose.DONE, "closeConn finished");
         }
 
-        if (responseType.chunked) {
-          is(xhr.response, null, "chunked data has null response for " + testState.name);
-        }
-
-        if (!testState.nodata && !responseType.blob || responseType.chunked) {
+        if (!testState.nodata && !responseType.blob) {
           // This branch intentionally left blank
           // Under these conditions we check the response during updateProgress
         }
         else if (responseType.type === "arraybuffer") {
           is(bufferToString(xhr.response), testState.pendingResult,
              "full response for " + testState.name);
         }
         else if (responseType.blob) {
@@ -299,19 +280,16 @@ function* runTests() {
       while(testState.pendingBytes) {
         log("waiting for more bytes: " + testState.pendingBytes);
         e = yield undefined;
         // Readystate can fire several times between each progress event.
         if (e.type === "readystatechange")
           continue;
 
         updateProgress(e, testState, "data for " + testState.name + "[" + testState.index + "]");
-        if (responseType.chunked) {
-          testState.receivedResult = "";
-        }
       }
 
       if (!testState.nodata && !testState.blob) {
         is(testState.pendingResult, "",
            "should have consumed the expected result");
       }
 
       log("done with this test");
--- a/gfx/2d/FilterNodeCapture.cpp
+++ b/gfx/2d/FilterNodeCapture.cpp
@@ -8,41 +8,42 @@
 
 namespace mozilla {
 namespace gfx {
 
 struct Setter {
   Setter(FilterNode* aNode, DrawTarget* aDT, bool aInputsChanged)
       : mNode{aNode}, mIndex{0}, mDT{aDT}, mInputsChanged{aInputsChanged} {}
   template <typename T>
-  void match(T& aValue) {
+  void operator()(T& aValue) {
     mNode->SetAttribute(mIndex, aValue);
   }
 
   FilterNode* mNode;
   uint32_t mIndex;
   DrawTarget* mDT;
   bool mInputsChanged;
 };
 
 template <>
-void Setter::match<std::vector<Float>>(std::vector<Float>& aValue) {
+void Setter::operator()<std::vector<Float>>(std::vector<Float>& aValue) {
   mNode->SetAttribute(mIndex, aValue.data(), aValue.size());
 }
 
 template <>
-void Setter::match<RefPtr<SourceSurface>>(RefPtr<SourceSurface>& aSurface) {
+void Setter::operator()<RefPtr<SourceSurface>>(
+    RefPtr<SourceSurface>& aSurface) {
   if (!mInputsChanged) {
     return;
   }
   mNode->SetInput(mIndex, aSurface);
 }
 
 template <>
-void Setter::match<RefPtr<FilterNode>>(RefPtr<FilterNode>& aNode) {
+void Setter::operator()<RefPtr<FilterNode>>(RefPtr<FilterNode>& aNode) {
   RefPtr<FilterNode> node = aNode;
   if (node->GetBackendType() == FilterBackend::FILTER_BACKEND_CAPTURE) {
     FilterNodeCapture* captureNode =
         static_cast<FilterNodeCapture*>(node.get());
     node = captureNode->Validate(mDT);
   }
   if (!mInputsChanged) {
     return;
--- a/gfx/layers/apz/src/FocusState.cpp
+++ b/gfx/layers/apz/src/FocusState.cpp
@@ -97,17 +97,17 @@ void FocusState::Update(LayersId aRootLa
     // Match on the data stored in mData
     // The match functions return true or false depending on whether the
     // enclosing method, FocusState::Update, should return or continue to the
     // next iteration of the while loop, respectively.
     struct FocusTargetDataMatcher {
       FocusState& mFocusState;
       const uint64_t mSequenceNumber;
 
-      bool match(const FocusTarget::NoFocusTarget& aNoFocusTarget) {
+      bool operator()(const FocusTarget::NoFocusTarget& aNoFocusTarget) {
         FS_LOG("Setting target to nil (reached a nil target) with seq=%" PRIu64
                "\n",
                mSequenceNumber);
 
         // Mark what sequence number this target has for debugging purposes so
         // we can always accurately report on whether we are stale or not
         mFocusState.mLastContentProcessedEvent = mSequenceNumber;
 
@@ -118,17 +118,17 @@ void FocusState::Update(LayersId aRootLa
             mFocusState.mLastContentProcessedEvent >
                 mFocusState.mLastAPZProcessedEvent) {
           mFocusState.mLastAPZProcessedEvent =
               mFocusState.mLastContentProcessedEvent;
         }
         return true;
       }
 
-      bool match(const LayersId& aRefLayerId) {
+      bool operator()(const LayersId& aRefLayerId) {
         // Guard against infinite loops
         MOZ_ASSERT(mFocusState.mFocusLayersId != aRefLayerId);
         if (mFocusState.mFocusLayersId == aRefLayerId) {
           FS_LOG(
               "Setting target to nil (bailing out of infinite loop, lt=%" PRIu64
               ")\n",
               mFocusState.mFocusLayersId);
           return true;
@@ -136,17 +136,17 @@ void FocusState::Update(LayersId aRootLa
 
         FS_LOG("Looking for target in lt=%" PRIu64 "\n", aRefLayerId);
 
         // The focus target is in a child layer tree
         mFocusState.mFocusLayersId = aRefLayerId;
         return false;
       }
 
-      bool match(const FocusTarget::ScrollTargets& aScrollTargets) {
+      bool operator()(const FocusTarget::ScrollTargets& aScrollTargets) {
         FS_LOG("Setting target to h=%" PRIu64 ", v=%" PRIu64
                ", and seq=%" PRIu64 "\n",
                aScrollTargets.mHorizontal, aScrollTargets.mVertical,
                mSequenceNumber);
 
         // This is the global focus target
         mFocusState.mFocusHorizontalTarget = aScrollTargets.mHorizontal;
         mFocusState.mFocusVerticalTarget = aScrollTargets.mVertical;
--- a/gfx/layers/apz/test/mochitest/helper_touch_action_zero_opacity_bug1500864.html
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action_zero_opacity_bug1500864.html
@@ -28,13 +28,17 @@ waitUntilApzStable()
 .then(subtestDone);
 
   </script>
 </head>
 <body style="border: solid 1px green">
   <div id="spacer" style="height: 2000px">
     Inside the black border is a zero-opacity touch-action none.
     <div id="border" style="border: solid 1px black">
-        <div id="target" style="opacity: 0; height: 300px; touch-action: none">this text shouldn't be visible</div>
+        <div style="opacity: 0; height: 300px;">
+            <div style="transform:translate(0px)">
+                <div id="target" style="height: 300px; touch-action: none">this text shouldn't be visible</div>
+            </div>
+        </div>
     </div>
   </div>
 </body>
 </html>
--- a/gfx/layers/client/ClientLayerManager.cpp
+++ b/gfx/layers/client/ClientLayerManager.cpp
@@ -711,16 +711,17 @@ void ClientLayerManager::ForwardTransact
     NS_WARNING("failed to forward Layers transaction");
   }
 
   if (!sent) {
     // Clear the transaction id so that it doesn't get returned
     // unless we forwarded to somewhere that doesn't actually
     // have a compositor.
     mTransactionIdAllocator->RevokeTransactionId(mLatestTransactionId);
+    mLatestTransactionId = mLatestTransactionId.Prev();
   }
 
   mPhase = PHASE_NONE;
 
   // this may result in Layers being deleted, which results in
   // PLayer::Send__delete__() and DeallocShmem()
   mKeepAlive.Clear();
 
--- a/gfx/layers/wr/WebRenderCommandBuilder.cpp
+++ b/gfx/layers/wr/WebRenderCommandBuilder.cpp
@@ -1623,23 +1623,16 @@ void WebRenderCommandBuilder::CreateWebR
   bool apzEnabled = mManager->AsyncPanZoomEnabled();
 
   FlattenedDisplayListIterator iter(aDisplayListBuilder, aDisplayList);
   while (iter.HasNext()) {
     nsDisplayItem* item = iter.GetNextItem();
 
     DisplayItemType itemType = item->GetType();
 
-    if (mForEventsAndPluginsOnly &&
-        (itemType != DisplayItemType::TYPE_COMPOSITOR_HITTEST_INFO &&
-         itemType != DisplayItemType::TYPE_PLUGIN)) {
-      // Only process hit test info items or plugin items.
-      continue;
-    }
-
     bool forceNewLayerData = false;
     size_t layerCountBeforeRecursing =
         mLayerScrollDatas.GetLayerCount(aBuilder.GetRenderRoot());
     if (apzEnabled) {
       // For some types of display items we want to force a new
       // WebRenderLayerScrollData object, to ensure we preserve the APZ-relevant
       // data that is in the display item.
       forceNewLayerData = item->UpdateScrollData(nullptr, nullptr);
@@ -1706,23 +1699,16 @@ void WebRenderCommandBuilder::CreateWebR
               aDisplayListBuilder, clippedBounds, &innerClippedBounds);
           MOZ_ASSERT(result);
 
           mClippedGroupBounds = Some(innerClippedBounds);
         }
         GP("attempting to enter the grouping code\n");
       }
 
-      AutoRestore<bool> restoreForEventsAndPluginsOnly(
-          mForEventsAndPluginsOnly);
-      if (itemType == DisplayItemType::TYPE_OPACITY &&
-          static_cast<nsDisplayOpacity*>(item)->ForEventsAndPluginsOnly()) {
-        mForEventsAndPluginsOnly = true;
-      }
-
       if (dumpEnabled) {
         std::stringstream ss;
         nsFrame::PrintDisplayItem(aDisplayListBuilder, item, ss,
                                   static_cast<uint32_t>(mDumpIndent));
         printf_stderr("%s", ss.str().c_str());
       }
 
       // Note: this call to CreateWebRenderCommands can recurse back into
--- a/gfx/layers/wr/WebRenderCommandBuilder.h
+++ b/gfx/layers/wr/WebRenderCommandBuilder.h
@@ -85,17 +85,16 @@ class WebRenderCommandBuilder {
   explicit WebRenderCommandBuilder(WebRenderLayerManager* aManager)
       : mManager(aManager),
         mRootStackingContexts(nullptr),
         mCurrentClipManager(nullptr),
         mLastAsr(nullptr),
         mBuilderDumpIndex(0),
         mDumpIndent(0),
         mDoGrouping(false),
-        mForEventsAndPluginsOnly(false),
         mContainsSVGGroup(false) {}
 
   void Destroy();
 
   void EmptyTransaction();
 
   bool NeedsEmptyTransaction();
 
@@ -257,20 +256,16 @@ class WebRenderCommandBuilder {
   wr::usize mDumpIndent;
 
  public:
   // Whether consecutive inactive display items should be grouped into one
   // blob image.
   bool mDoGrouping;
   Maybe<nsRect> mClippedGroupBounds;
 
-  // True if we're currently within an opacity:0 container, and only
-  // plugin and hit test items should be considered.
-  bool mForEventsAndPluginsOnly;
-
   // True if the most recently build display list contained an svg that
   // we did grouping for.
   bool mContainsSVGGroup;
 };
 
 }  // namespace layers
 }  // namespace mozilla
 
--- a/gfx/layers/wr/WebRenderLayerManager.cpp
+++ b/gfx/layers/wr/WebRenderLayerManager.cpp
@@ -203,16 +203,17 @@ bool WebRenderLayerManager::EndEmptyTran
         break;
       }
     }
     if (!haveScrollUpdates) {
       MOZ_ASSERT(!mTarget);
       WrBridge()->SendSetFocusTarget(mFocusTarget);
       // Revoke TransactionId to trigger next paint.
       mTransactionIdAllocator->RevokeTransactionId(mLatestTransactionId);
+      mLatestTransactionId = mLatestTransactionId.Prev();
       return true;
     }
   }
 
   LayoutDeviceIntSize size = mWidget->GetClientSize();
   WrBridge()->BeginTransaction();
 
   mWebRenderCommandBuilder.EmptyTransaction();
--- a/gfx/src/FilterSupport.cpp
+++ b/gfx/src/FilterSupport.cpp
@@ -663,22 +663,22 @@ static already_AddRefed<FilterNode> Filt
           mInputImages(aInputImages) {}
 
     const FilterPrimitiveDescription& mDescription;
     DrawTarget* mDT;
     nsTArray<RefPtr<FilterNode>>& mSources;
     nsTArray<IntRect>& mSourceRegions;
     nsTArray<RefPtr<SourceSurface>>& mInputImages;
 
-    already_AddRefed<FilterNode> match(
+    already_AddRefed<FilterNode> operator()(
         const EmptyAttributes& aEmptyAttributes) {
       return nullptr;
     }
 
-    already_AddRefed<FilterNode> match(const BlendAttributes& aBlend) {
+    already_AddRefed<FilterNode> operator()(const BlendAttributes& aBlend) {
       uint32_t mode = aBlend.mBlendMode;
       RefPtr<FilterNode> filter;
       if (mode == SVG_FEBLEND_MODE_UNKNOWN) {
         return nullptr;
       }
       if (mode == SVG_FEBLEND_MODE_NORMAL) {
         filter = mDT->CreateFilter(FilterType::COMPOSITE);
         if (!filter) {
@@ -713,17 +713,17 @@ static already_AddRefed<FilterNode> Filt
         // The correct input order for both software and D2D filters is flipped
         // from our source order, so flip here.
         filter->SetInput(IN_BLEND_IN, mSources[1]);
         filter->SetInput(IN_BLEND_IN2, mSources[0]);
       }
       return filter.forget();
     }
 
-    already_AddRefed<FilterNode> match(
+    already_AddRefed<FilterNode> operator()(
         const ColorMatrixAttributes& aMatrixAttributes) {
       float colorMatrix[20];
       if (!ComputeColorMatrix(aMatrixAttributes, colorMatrix)) {
         RefPtr<FilterNode> filter(mSources[0]);
         return filter.forget();
       }
 
       Matrix5x4 matrix(
@@ -739,17 +739,17 @@ static already_AddRefed<FilterNode> Filt
       }
       filter->SetAttribute(ATT_COLOR_MATRIX_MATRIX, matrix);
       filter->SetAttribute(ATT_COLOR_MATRIX_ALPHA_MODE,
                            (uint32_t)ALPHA_MODE_STRAIGHT);
       filter->SetInput(IN_COLOR_MATRIX_IN, mSources[0]);
       return filter.forget();
     }
 
-    already_AddRefed<FilterNode> match(
+    already_AddRefed<FilterNode> operator()(
         const MorphologyAttributes& aMorphology) {
       Size radii = aMorphology.mRadii;
       int32_t rx = radii.width;
       int32_t ry = radii.height;
       if (rx < 0 || ry < 0) {
         // XXX SVGContentUtils::ReportToConsole()
         return nullptr;
       }
@@ -770,37 +770,37 @@ static already_AddRefed<FilterNode> Filt
         return nullptr;
       }
       filter->SetAttribute(ATT_MORPHOLOGY_RADII, IntSize(rx, ry));
       filter->SetAttribute(ATT_MORPHOLOGY_OPERATOR, (uint32_t)op);
       filter->SetInput(IN_MORPHOLOGY_IN, mSources[0]);
       return filter.forget();
     }
 
-    already_AddRefed<FilterNode> match(const FloodAttributes& aFlood) {
+    already_AddRefed<FilterNode> operator()(const FloodAttributes& aFlood) {
       Color color = aFlood.mColor;
       RefPtr<FilterNode> filter = mDT->CreateFilter(FilterType::FLOOD);
       if (!filter) {
         return nullptr;
       }
       filter->SetAttribute(ATT_FLOOD_COLOR, color);
       return filter.forget();
     }
 
-    already_AddRefed<FilterNode> match(const TileAttributes& aTile) {
+    already_AddRefed<FilterNode> operator()(const TileAttributes& aTile) {
       RefPtr<FilterNode> filter = mDT->CreateFilter(FilterType::TILE);
       if (!filter) {
         return nullptr;
       }
       filter->SetAttribute(ATT_TILE_SOURCE_RECT, mSourceRegions[0]);
       filter->SetInput(IN_TILE_IN, mSources[0]);
       return filter.forget();
     }
 
-    already_AddRefed<FilterNode> match(
+    already_AddRefed<FilterNode> operator()(
         const ComponentTransferAttributes& aComponentTransfer) {
       RefPtr<FilterNode> filters[4];  // one for each FILTER_*_TRANSFER type
       bool useRgb = aComponentTransfer.mTypes[kChannelG] ==
                         SVG_FECOMPONENTTRANSFER_TYPE_UNKNOWN &&
                     aComponentTransfer.mTypes[kChannelB] ==
                         SVG_FECOMPONENTTRANSFER_TYPE_UNKNOWN;
 
       for (int32_t i = 0; i < 4; i++) {
@@ -817,27 +817,27 @@ static already_AddRefed<FilterNode> Filt
           filters[i]->SetInput(0, lastFilter);
           lastFilter = filters[i];
         }
       }
 
       return lastFilter.forget();
     }
 
-    already_AddRefed<FilterNode> match(const OpacityAttributes& aOpacity) {
+    already_AddRefed<FilterNode> operator()(const OpacityAttributes& aOpacity) {
       RefPtr<FilterNode> filter = mDT->CreateFilter(FilterType::OPACITY);
       if (!filter) {
         return nullptr;
       }
       filter->SetAttribute(ATT_OPACITY_VALUE, aOpacity.mOpacity);
       filter->SetInput(IN_OPACITY_IN, mSources[0]);
       return filter.forget();
     }
 
-    already_AddRefed<FilterNode> match(
+    already_AddRefed<FilterNode> operator()(
         const ConvolveMatrixAttributes& aConvolveMatrix) {
       RefPtr<FilterNode> filter =
           mDT->CreateFilter(FilterType::CONVOLVE_MATRIX);
       if (!filter) {
         return nullptr;
       }
       filter->SetAttribute(ATT_CONVOLVE_MATRIX_KERNEL_SIZE,
                            aConvolveMatrix.mKernelSize);
@@ -861,21 +861,21 @@ static already_AddRefed<FilterNode> Filt
       filter->SetAttribute(ATT_CONVOLVE_MATRIX_KERNEL_UNIT_LENGTH,
                            aConvolveMatrix.mKernelUnitLength);
       filter->SetAttribute(ATT_CONVOLVE_MATRIX_PRESERVE_ALPHA,
                            aConvolveMatrix.mPreserveAlpha);
       filter->SetInput(IN_CONVOLVE_MATRIX_IN, mSources[0]);
       return filter.forget();
     }
 
-    already_AddRefed<FilterNode> match(const OffsetAttributes& aOffset) {
+    already_AddRefed<FilterNode> operator()(const OffsetAttributes& aOffset) {
       return FilterWrappers::Offset(mDT, mSources[0], aOffset.mValue);
     }
 
-    already_AddRefed<FilterNode> match(
+    already_AddRefed<FilterNode> operator()(
         const DisplacementMapAttributes& aDisplacementMap) {
       RefPtr<FilterNode> filter =
           mDT->CreateFilter(FilterType::DISPLACEMENT_MAP);
       if (!filter) {
         return nullptr;
       }
       filter->SetAttribute(ATT_DISPLACEMENT_MAP_SCALE, aDisplacementMap.mScale);
       static const uint8_t channel[SVG_CHANNEL_A + 1] = {
@@ -889,17 +889,17 @@ static already_AddRefed<FilterNode> Filt
                            (uint32_t)channel[aDisplacementMap.mXChannel]);
       filter->SetAttribute(ATT_DISPLACEMENT_MAP_Y_CHANNEL,
                            (uint32_t)channel[aDisplacementMap.mYChannel]);
       filter->SetInput(IN_DISPLACEMENT_MAP_IN, mSources[0]);
       filter->SetInput(IN_DISPLACEMENT_MAP_IN2, mSources[1]);
       return filter.forget();
     }
 
-    already_AddRefed<FilterNode> match(
+    already_AddRefed<FilterNode> operator()(
         const TurbulenceAttributes& aTurbulence) {
       RefPtr<FilterNode> filter = mDT->CreateFilter(FilterType::TURBULENCE);
       if (!filter) {
         return nullptr;
       }
       filter->SetAttribute(ATT_TURBULENCE_BASE_FREQUENCY,
                            aTurbulence.mBaseFrequency);
       filter->SetAttribute(ATT_TURBULENCE_NUM_OCTAVES, aTurbulence.mOctaves);
@@ -913,17 +913,18 @@ static already_AddRefed<FilterNode> Filt
       filter->SetAttribute(ATT_TURBULENCE_TYPE,
                            (uint32_t)type[aTurbulence.mType]);
       filter->SetAttribute(
           ATT_TURBULENCE_RECT,
           mDescription.PrimitiveSubregion() - aTurbulence.mOffset);
       return FilterWrappers::Offset(mDT, filter, aTurbulence.mOffset);
     }
 
-    already_AddRefed<FilterNode> match(const CompositeAttributes& aComposite) {
+    already_AddRefed<FilterNode> operator()(
+        const CompositeAttributes& aComposite) {
       RefPtr<FilterNode> filter;
       uint32_t op = aComposite.mOperator;
       if (op == SVG_FECOMPOSITE_OPERATOR_ARITHMETIC) {
         const nsTArray<float>& coefficients = aComposite.mCoefficients;
         static const float allZero[4] = {0, 0, 0, 0};
         filter = mDT->CreateFilter(FilterType::ARITHMETIC_COMBINE);
         // All-zero coefficients sometimes occur in junk filters.
         if (!filter || (coefficients.Length() == ArrayLength(allZero) &&
@@ -950,17 +951,17 @@ static already_AddRefed<FilterNode> Filt
         };
         filter->SetAttribute(ATT_COMPOSITE_OPERATOR, (uint32_t)operators[op]);
         filter->SetInput(IN_COMPOSITE_IN_START, mSources[1]);
         filter->SetInput(IN_COMPOSITE_IN_START + 1, mSources[0]);
       }
       return filter.forget();
     }
 
-    already_AddRefed<FilterNode> match(const MergeAttributes& aMerge) {
+    already_AddRefed<FilterNode> operator()(const MergeAttributes& aMerge) {
       if (mSources.Length() == 0) {
         return nullptr;
       }
       if (mSources.Length() == 1) {
         RefPtr<FilterNode> filter(mSources[0]);
         return filter.forget();
       }
       RefPtr<FilterNode> filter = mDT->CreateFilter(FilterType::COMPOSITE);
@@ -970,23 +971,23 @@ static already_AddRefed<FilterNode> Filt
       filter->SetAttribute(ATT_COMPOSITE_OPERATOR,
                            (uint32_t)COMPOSITE_OPERATOR_OVER);
       for (size_t i = 0; i < mSources.Length(); i++) {
         filter->SetInput(IN_COMPOSITE_IN_START + i, mSources[i]);
       }
       return filter.forget();
     }
 
-    already_AddRefed<FilterNode> match(
+    already_AddRefed<FilterNode> operator()(
         const GaussianBlurAttributes& aGaussianBlur) {
       return FilterWrappers::GaussianBlur(mDT, mSources[0],
                                           aGaussianBlur.mStdDeviation);
     }
 
-    already_AddRefed<FilterNode> match(
+    already_AddRefed<FilterNode> operator()(
         const DropShadowAttributes& aDropShadow) {
       RefPtr<FilterNode> alpha = FilterWrappers::ToAlpha(mDT, mSources[0]);
       RefPtr<FilterNode> blur =
           FilterWrappers::GaussianBlur(mDT, alpha, aDropShadow.mStdDeviation);
       RefPtr<FilterNode> offsetBlur =
           FilterWrappers::Offset(mDT, blur, aDropShadow.mOffset);
       RefPtr<FilterNode> flood = mDT->CreateFilter(FilterType::FLOOD);
       if (!flood) {
@@ -1015,23 +1016,23 @@ static already_AddRefed<FilterNode> Filt
       }
       filter->SetAttribute(ATT_COMPOSITE_OPERATOR,
                            (uint32_t)COMPOSITE_OPERATOR_OVER);
       filter->SetInput(IN_COMPOSITE_IN_START, composite);
       filter->SetInput(IN_COMPOSITE_IN_START + 1, mSources[0]);
       return filter.forget();
     }
 
-    already_AddRefed<FilterNode> match(
+    already_AddRefed<FilterNode> operator()(
         const SpecularLightingAttributes& aLighting) {
-      return match(
+      return operator()(
           *(static_cast<const DiffuseLightingAttributes*>(&aLighting)));
     }
 
-    already_AddRefed<FilterNode> match(
+    already_AddRefed<FilterNode> operator()(
         const DiffuseLightingAttributes& aLighting) {
       bool isSpecular =
           mDescription.Attributes().is<SpecularLightingAttributes>();
 
       if (aLighting.mLightType == LightType::None) {
         return nullptr;
       }
 
@@ -1112,17 +1113,17 @@ static already_AddRefed<FilterNode> Filt
         }
       }
 
       filter->SetInput(IN_LIGHTING_IN, mSources[0]);
 
       return filter.forget();
     }
 
-    already_AddRefed<FilterNode> match(const ImageAttributes& aImage) {
+    already_AddRefed<FilterNode> operator()(const ImageAttributes& aImage) {
       const Matrix& TM = aImage.mTransform;
       if (!TM.Determinant()) {
         return nullptr;
       }
 
       // Pull the image from the additional image list using the index that's
       // stored in the primitive description.
       RefPtr<SourceSurface> inputImage = mInputImages[aImage.mInputIndex];
@@ -1132,17 +1133,17 @@ static already_AddRefed<FilterNode> Filt
         return nullptr;
       }
       transform->SetInput(IN_TRANSFORM_IN, inputImage);
       transform->SetAttribute(ATT_TRANSFORM_MATRIX, TM);
       transform->SetAttribute(ATT_TRANSFORM_FILTER, aImage.mFilter);
       return transform.forget();
     }
 
-    already_AddRefed<FilterNode> match(const ToAlphaAttributes& aToAlpha) {
+    already_AddRefed<FilterNode> operator()(const ToAlphaAttributes& aToAlpha) {
       return FilterWrappers::ToAlpha(mDT, mSources[0]);
     }
   };
 
   return aDescription.Attributes().match(PrimitiveAttributesMatcher(
       aDescription, aDT, aSources, aSourceRegions, aInputImages));
 }
 
@@ -1362,123 +1363,128 @@ static nsIntRegion ResultChangeRegionFor
     PrimitiveAttributesMatcher(const FilterPrimitiveDescription& aDescription,
                                const nsTArray<nsIntRegion>& aInputChangeRegions)
         : mDescription(aDescription),
           mInputChangeRegions(aInputChangeRegions) {}
 
     const FilterPrimitiveDescription& mDescription;
     const nsTArray<nsIntRegion>& mInputChangeRegions;
 
-    nsIntRegion match(const EmptyAttributes& aEmptyAttributes) {
+    nsIntRegion operator()(const EmptyAttributes& aEmptyAttributes) {
       return nsIntRegion();
     }
 
-    nsIntRegion match(const BlendAttributes& aBlend) {
+    nsIntRegion operator()(const BlendAttributes& aBlend) {
       return UnionOfRegions(mInputChangeRegions);
     }
 
-    nsIntRegion match(const ColorMatrixAttributes& aColorMatrix) {
+    nsIntRegion operator()(const ColorMatrixAttributes& aColorMatrix) {
       return mInputChangeRegions[0];
     }
 
-    nsIntRegion match(const MorphologyAttributes& aMorphology) {
+    nsIntRegion operator()(const MorphologyAttributes& aMorphology) {
       Size radii = aMorphology.mRadii;
       int32_t rx = clamped(int32_t(ceil(radii.width)), 0, kMorphologyMaxRadius);
       int32_t ry =
           clamped(int32_t(ceil(radii.height)), 0, kMorphologyMaxRadius);
       return mInputChangeRegions[0].Inflated(nsIntMargin(ry, rx, ry, rx));
     }
 
-    nsIntRegion match(const FloodAttributes& aFlood) { return nsIntRegion(); }
+    nsIntRegion operator()(const FloodAttributes& aFlood) {
+      return nsIntRegion();
+    }
 
-    nsIntRegion match(const TileAttributes& aTile) {
+    nsIntRegion operator()(const TileAttributes& aTile) {
       return mDescription.PrimitiveSubregion();
     }
 
-    nsIntRegion match(const ComponentTransferAttributes& aComponentTransfer) {
+    nsIntRegion operator()(
+        const ComponentTransferAttributes& aComponentTransfer) {
       return mInputChangeRegions[0];
     }
 
-    nsIntRegion match(const OpacityAttributes& aOpacity) {
+    nsIntRegion operator()(const OpacityAttributes& aOpacity) {
       return UnionOfRegions(mInputChangeRegions);
     }
 
-    nsIntRegion match(const ConvolveMatrixAttributes& aConvolveMatrix) {
+    nsIntRegion operator()(const ConvolveMatrixAttributes& aConvolveMatrix) {
       if (aConvolveMatrix.mEdgeMode != EDGE_MODE_NONE) {
         return mDescription.PrimitiveSubregion();
       }
       Size kernelUnitLength = aConvolveMatrix.mKernelUnitLength;
       IntSize kernelSize = aConvolveMatrix.mKernelSize;
       IntPoint target = aConvolveMatrix.mTarget;
       nsIntMargin m(
           ceil(kernelUnitLength.width * (target.x)),
           ceil(kernelUnitLength.height * (target.y)),
           ceil(kernelUnitLength.width * (kernelSize.width - target.x - 1)),
           ceil(kernelUnitLength.height * (kernelSize.height - target.y - 1)));
       return mInputChangeRegions[0].Inflated(m);
     }
 
-    nsIntRegion match(const OffsetAttributes& aOffset) {
+    nsIntRegion operator()(const OffsetAttributes& aOffset) {
       IntPoint offset = aOffset.mValue;