Merge inbound to mozilla-central. a=merge
authorBogdan Tara <btara@mozilla.com>
Tue, 18 Sep 2018 00:58:07 +0300
changeset 495305 87a95e1b7ec691bef7b938e722fe1b01cce68664
parent 495229 ed612eec41a44867a1330aa893040bacb7ac5b74 (current diff)
parent 495304 5ab4db62dc822625b054a9afec6f065e96ebd771 (diff)
child 495324 68024a7f328bcfe2482bdc8fec1cdc0c55d6b23f
child 495332 a11fe256f24e4e6a2692f394c881b04f50eab2b1
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone64.0a1
first release with
nightly linux32
87a95e1b7ec6 / 64.0a1 / 20180917220115 / files
nightly linux64
87a95e1b7ec6 / 64.0a1 / 20180917220115 / files
nightly mac
87a95e1b7ec6 / 64.0a1 / 20180917220115 / files
nightly win32
87a95e1b7ec6 / 64.0a1 / 20180917220115 / files
nightly win64
87a95e1b7ec6 / 64.0a1 / 20180917220115 / 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 inbound to mozilla-central. a=merge
devtools/client/webconsole/test/mochitest/browser_webconsole_closure_inspection.js
dom/base/nsGlobalWindowInner.cpp
dom/commandhandler/nsCommandGroup.cpp
dom/commandhandler/nsCommandGroup.h
testing/web-platform/meta/content-security-policy/inside-worker/shared-script.html.ini
testing/web-platform/meta/content-security-policy/script-src/script-src-1_4_2.html.ini
testing/web-platform/meta/content-security-policy/script-src/worker-set-timeout-blocked.sub.html.ini
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -394,16 +394,21 @@ description#identity-popup-content-verif
 }
 
 #identity-popup-content-blocking-content:not([enabled]) #identity-popup-content-blocking-disabled-label {
   display: -moz-box;
   background-color: #d70022;
   stroke: #d70022;
 }
 
+#identity-popup-content-blocking-content:not([contentBlockingUI]):not([enabled]) #identity-popup-content-blocking-disabled-label {
+  background-color: #b1b1b3;
+  stroke: #b1b1b3;
+}
+
 #identity-popup-content-blocking-content:not([enabled]) #identity-popup-content-blocking-disabled-label-exception {
   display: none;
 }
 
 #identity-popup-content-blocking-disabled-label > label {
   margin: 0;
   line-height: 18px;
 }
--- a/browser/themes/shared/incontentprefs/privacy.css
+++ b/browser/themes/shared/incontentprefs/privacy.css
@@ -122,25 +122,33 @@
   margin: 16px 0;
 }
 
 .content-blocking-category-labels {
   padding-inline-start: 4px;
   margin-inline-start: 25px !important;
 }
 
-#trackingProtectionMenu,
-#blockCookiesCB {
+#trackingProtectionMenu {
   margin-top: 0.75em;
 }
 
 #blockCookiesCBDeck {
   max-width: 444px;
 }
 
+#blockCookiesCBDeck:not([selectedIndex]) > .warning-description,
+#blockCookiesCBDeck[selectedIndex="0"] > .warning-description {
+  display: none;
+}
+
+#blockCookiesCBDeck > .warning-description {
+  margin-bottom: 0.75em !important;
+}
+
 #changeBlockListLink {
   font-size: 90%;
   /* In order to override the margins set in preferences.inc.css, we have to use !important. */
   margin-top: 1em !important;
 }
 
 .content-blocking-category-description {
   font-size: 90%;
--- a/devtools/client/application/test/browser.ini
+++ b/devtools/client/application/test/browser.ini
@@ -7,16 +7,17 @@ support-files =
   service-workers/debug.html
   service-workers/dynamic-registration.html
   service-workers/empty.html
   service-workers/empty-sw.js
   service-workers/scope-page.html
   service-workers/simple.html
   service-workers/simple-unicode.html
   !/devtools/client/debugger/new/test/mochitest/helpers.js
+  !/devtools/client/debugger/new/test/mochitest/helpers/context.js
   !/devtools/client/shared/test/frame-script-utils.js
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
 
 [browser_application_panel_debug-service-worker.js]
 [browser_application_panel_list-domain-workers.js]
 [browser_application_panel_list-several-workers.js]
 [browser_application_panel_list-single-worker.js]
--- a/devtools/client/application/test/browser_application_panel_debug-service-worker.js
+++ b/devtools/client/application/test/browser_application_panel_debug-service-worker.js
@@ -3,16 +3,21 @@
 
 "use strict";
 
 /* import-globals-from ../../debugger/new/test/mochitest/helpers.js */
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js",
   this);
 
+/* import-globals-from ../../debugger/new/test/mochitest/helpers/context.js */
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers/context.js",
+  this);
+
 const TAB_URL = URL_ROOT + "service-workers/debug.html";
 
 add_task(async function() {
   await enableApplicationPanel();
 
   const { panel, tab, target } = await openNewTabAndApplicationPanel(TAB_URL);
   const doc = panel.panelWin.document;
 
--- a/devtools/client/canvasdebugger/test/browser.ini
+++ b/devtools/client/canvasdebugger/test/browser.ini
@@ -12,16 +12,17 @@ support-files =
   doc_simple-canvas-transparent.html
   doc_webgl-bindings.html
   doc_webgl-enum.html
   doc_webgl-drawArrays.html
   doc_webgl-drawElements.html
   head.js
   !/devtools/client/shared/test/frame-script-utils.js
   !/devtools/client/shared/test/shared-head.js
+  !/devtools/client/debugger/new/test/mochitest/helpers/context.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
 
 [browser_canvas-actor-test-01.js]
 [browser_canvas-actor-test-02.js]
 [browser_canvas-actor-test-03.js]
 [browser_canvas-actor-test-04.js]
 [browser_canvas-actor-test-05.js]
 [browser_canvas-actor-test-06.js]
--- a/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js
@@ -1,20 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if the a function call's stack is properly displayed in the UI.
  */
 
-// Force the old debugger UI since it's directly used (see Bug 1301705)
-Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
-registerCleanupFunction(function() {
-  Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
-});
+requestLongerTimeout(2);
 
 async function ifTestingSupported() {
   const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
   const { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
 
   await reload(target);
 
   const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
@@ -65,18 +61,14 @@ async function ifTestingSupported() {
     "doc_simple-canvas-deep-stack.html:35",
     "The fourth function on the stack has the correct location.");
 
   const jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
   EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-stack-fn-location", callItem.target));
   await jumpedToSource;
 
   const toolbox = await gDevTools.getToolbox(target);
-  const { panelWin: { DebuggerView: view } } = toolbox.getPanel("jsdebugger");
-
-  is(view.Sources.selectedValue, getSourceActor(view.Sources, SIMPLE_CANVAS_DEEP_STACK_URL),
-    "The expected source was shown in the debugger.");
-  is(view.editor.getCursor().line, 25,
-    "The expected source line is highlighted in the debugger.");
+  const dbg = createDebuggerContext(toolbox);
+  await validateDebuggerLocation(dbg, SIMPLE_CANVAS_DEEP_STACK_URL, 26);
 
   await teardown(panel);
   finish();
 }
--- a/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js
@@ -1,21 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if the a function call's stack is properly displayed in the UI
  * and jumping to source in the debugger for the topmost call item works.
  */
 
-// Force the old debugger UI since it's directly used (see Bug 1301705)
-Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
-registerCleanupFunction(function() {
-  Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
-});
+requestLongerTimeout(2);
 
 async function ifTestingSupported() {
   const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
   const { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
 
   await reload(target);
 
   const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
@@ -40,18 +36,14 @@ async function ifTestingSupported() {
   ok($all(".call-item-stack-fn", callItem.target).length >= 4,
      "There should be at least 4 functions on the stack for the draw call.");
 
   const jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
   EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-location", callItem.target));
   await jumpedToSource;
 
   const toolbox = await gDevTools.getToolbox(target);
-  const { panelWin: { DebuggerView: view } } = toolbox.getPanel("jsdebugger");
-
-  is(view.Sources.selectedValue, getSourceActor(view.Sources, SIMPLE_CANVAS_DEEP_STACK_URL),
-    "The expected source was shown in the debugger.");
-  is(view.editor.getCursor().line, 23,
-    "The expected source line is highlighted in the debugger.");
+  const dbg = createDebuggerContext(toolbox);
+  await validateDebuggerLocation(dbg, SIMPLE_CANVAS_DEEP_STACK_URL, 24);
 
   await teardown(panel);
   finish();
 }
--- a/devtools/client/canvasdebugger/test/head.js
+++ b/devtools/client/canvasdebugger/test/head.js
@@ -1,21 +1,27 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* eslint no-unused-vars: [2, {"vars": "local"}] */
 /* import-globals-from ../../shared/test/shared-head.js */
+/* import-globals-from ../../debugger/new/test/mochitest/helpers/context.js */
 
 "use strict";
 
 // Load the shared-head file first.
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
   this);
 
+// Import helpers for the new debugger
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers/context.js",
+  this);
+
 var { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
 
 var { DebuggerClient } = require("devtools/shared/client/debugger-client");
 var { DebuggerServer } = require("devtools/server/main");
 var { CallWatcherFront } = require("devtools/shared/fronts/call-watcher");
 var { CanvasFront } = require("devtools/shared/fronts/canvas");
 var { Toolbox } = require("devtools/client/framework/toolbox");
 var { isWebGLSupported } = require("devtools/client/shared/webgl-utils");
@@ -168,8 +174,18 @@ function teardown({target}) {
     removeTab(tab);
   });
 }
 
 function getSourceActor(aSources, aURL) {
   const item = aSources.getItemForAttachment(a => a.source.url === aURL);
   return item ? item.value : null;
 }
+
+async function validateDebuggerLocation(dbg, url, line) {
+  const location = dbg.selectors.getSelectedLocation(dbg.getState());
+  const sourceUrl = dbg.selectors.getSelectedSource(dbg.getState()).url;
+
+  is(sourceUrl, url,
+    "The expected source was shown in the debugger.");
+  is(location.line, line,
+    "The expected source line is highlighted in the debugger.");
+}
--- a/devtools/client/debugger/new/test/mochitest/browser.ini
+++ b/devtools/client/debugger/new/test/mochitest/browser.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 skip-if = (os == 'linux' && debug && bits == 32)
 support-files =
   head.js
   helpers.js
+  helpers/context.js
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
   ## START-SOURCEMAPPED-FIXTURES - Generated by examples/sourcemapped/build.js
   examples/sourcemapped/polyfill-bundle.js
   examples/sourcemapped/output/parcel/babel-bindings-with-flow.js
   examples/sourcemapped/output/parcel/babel-bindings-with-flow.map
   examples/sourcemapped/output/parcel/babel-classes.js
   examples/sourcemapped/output/parcel/babel-classes.map
--- a/devtools/client/debugger/new/test/mochitest/helpers.js
+++ b/devtools/client/debugger/new/test/mochitest/helpers.js
@@ -1,16 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Helper methods to drive with the debugger during mochitests. This file can be safely
  * required from other panel test files.
  */
 
+// Import helpers for the new debugger
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers/context.js",
+  this);
+
 var { Toolbox } = require("devtools/client/framework/toolbox");
 var { Task } = require("devtools/shared/task");
 var asyncStorage = require("devtools/shared/async-storage");
 
 const sourceUtils = {
   isLoaded: source => source.loadedState === "loaded"
 };
 
@@ -457,33 +462,16 @@ function isSelectedFrameSelected(dbg, st
   const isLoaded = source.loadedState && sourceUtils.isLoaded(source);
   if (!isLoaded) {
     return false;
   }
 
   return source.id == sourceId;
 }
 
-function createDebuggerContext(toolbox) {
-  const panel = toolbox.getPanel("jsdebugger");
-  const win = panel.panelWin;
-  const { store, client, selectors, actions } = panel.getVarsForTests();
-
-  return {
-    actions: actions,
-    selectors: selectors,
-    getState: store.getState,
-    store: store,
-    client: client,
-    toolbox: toolbox,
-    win: win,
-    panel: panel
-  };
-}
-
 /**
  * Clear all the debugger related preferences.
  */
 function clearDebuggerPreferences() {
   asyncStorage.clear()
   Services.prefs.clearUserPref("devtools.recordreplay.enabled");
   Services.prefs.clearUserPref("devtools.debugger.pause-on-exceptions");
   Services.prefs.clearUserPref("devtools.debugger.pause-on-caught-exceptions");
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/helpers/context.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Helper method to create a "dbg" context for other tools to use
+ */
+
+function createDebuggerContext(toolbox) {
+  const panel = toolbox.getPanel("jsdebugger");
+  const win = panel.panelWin;
+  const { store, client, selectors, actions } = panel.getVarsForTests();
+
+  return {
+    actions: actions,
+    selectors: selectors,
+    getState: store.getState,
+    store: store,
+    client: client,
+    toolbox: toolbox,
+    win: win,
+    panel: panel
+  };
+}
\ No newline at end of file
--- a/devtools/client/framework/test/browser.ini
+++ b/devtools/client/framework/test/browser.ini
@@ -40,16 +40,17 @@ support-files =
   browser_toolbox_options_enable_serviceworkers_testing_frame_script.js
   browser_toolbox_options_enable_serviceworkers_testing.html
   serviceworker.js
   sjs_code_reload.sjs
   sjs_code_bundle_reload_map.sjs
   test_browser_toolbox_debugger.js
   !/devtools/client/debugger/new/test/mochitest/head.js
   !/devtools/client/debugger/new/test/mochitest/helpers.js
+  !/devtools/client/debugger/new/test/mochitest/helpers/context.js
   !/devtools/client/shared/test/frame-script-utils.js
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/shared/test/shared-redux-head.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
 
 [browser_browser_toolbox.js]
 skip-if = coverage # Bug 1387827
 [browser_browser_toolbox_debugger.js]
--- a/devtools/client/framework/test/browser_browser_toolbox_debugger.js
+++ b/devtools/client/framework/test/browser_browser_toolbox_debugger.js
@@ -14,16 +14,17 @@ PromiseTestUtils.whitelistRejectionsGlob
 
 // On debug test runner, it takes about 50s to run the test.
 requestLongerTimeout(4);
 
 const { fetch } = require("devtools/shared/DevToolsUtils");
 
 const debuggerHeadURL = CHROME_URL_ROOT + "../../debugger/new/test/mochitest/head.js";
 const helpersURL = CHROME_URL_ROOT + "../../debugger/new/test/mochitest/helpers.js";
+const helpersContextURL = CHROME_URL_ROOT + "../../debugger/new/test/mochitest/helpers/context.js";
 const testScriptURL = CHROME_URL_ROOT + "test_browser_toolbox_debugger.js";
 
 add_task(async function runTest() {
   await new Promise(done => {
     const options = {"set": [
       ["devtools.debugger.prompt-connection", false],
       ["devtools.debugger.remote-enabled", true],
       ["devtools.chrome.enabled", true],
@@ -119,25 +120,26 @@ add_task(async function runTest() {
     }
   }).toSource().replace(/^\(function\(\) \{|\}\)$/g, "");
   /* eslint-enable no-unused-vars */
   // Stringify testHead's function and remove `(function {` prefix and `})` suffix
   // to ensure inner symbols gets exposed to next pieces of code
   // Then inject new debugger head file
   let { content: debuggerHead } = await fetch(debuggerHeadURL);
 
+  // Also include the debugger helpers which are separated from debugger's head to be
+  // reused in other modules.
+  const { content: debuggerHelpers } = await fetch(helpersURL);
+  const { content: debuggerContextHelpers } = await fetch(helpersContextURL);
+  debuggerHead = debuggerHead + debuggerContextHelpers + debuggerHelpers;
+
   // We remove its import of shared-head, which isn't available in browser toolbox process
   // And isn't needed thanks to testHead's symbols
   debuggerHead = debuggerHead.replace(/Services.scriptloader.loadSubScript[^\)]*\);/g, "");
 
-  // Also include the debugger helpers which are separated from debugger's head to be
-  // reused in other modules.
-  const { content: debuggerHelpers } = await fetch(helpersURL);
-  debuggerHead = debuggerHead + debuggerHelpers;
-
   // Finally, fetch the debugger test script that is going to be execute in the browser
   // toolbox process
   const testScript = (await fetch(testScriptURL)).content;
   const source =
     "try { let testUrl = \"" + testUrl + "\";" + testHead + debuggerHead + testScript + "} catch (e) {" +
     "  dump('Exception: '+ e + ' at ' + e.fileName + ':' + " +
     "       e.lineNumber + '\\nStack: ' + e.stack + '\\n');" +
     "}";
--- a/devtools/client/framework/test/browser_keybindings_01.js
+++ b/devtools/client/framework/test/browser_keybindings_01.js
@@ -46,22 +46,16 @@ function buildDevtoolsKeysetMap(keyset) 
 
 function setupKeyBindingsTest() {
   for (const win of gDevToolsBrowser._trackedBrowserWindows) {
     buildDevtoolsKeysetMap(win.document.getElementById("devtoolsKeyset"));
   }
 }
 
 add_task(async function() {
-  // Use the new debugger frontend because the old one swallows the netmonitor shortcut:
-  // https://bugzilla.mozilla.org/show_bug.cgi?id=1370442#c7
-  await SpecialPowers.pushPrefEnv({set: [
-    ["devtools.debugger.new-debugger-frontend", true]
-  ]});
-
   await addTab(TEST_URL);
   await new Promise(done => waitForFocus(done));
 
   setupKeyBindingsTest();
 
   info("Test the first inspector key (there are 2 of them on Mac)");
   const inspectorKeys = allKeys.filter(({ toolId }) => {
     return toolId === "inspector" || toolId === "inspectorMac";
--- a/devtools/client/framework/test/browser_source_map-01.js
+++ b/devtools/client/framework/test/browser_source_map-01.js
@@ -16,18 +16,16 @@ PromiseTestUtils.whitelistRejectionsGlob
 PromiseTestUtils.whitelistRejectionsGlobally(/Component not initialized/);
 
 // Empty page
 const PAGE_URL = `${URL_ROOT}doc_empty-tab-01.html`;
 const JS_URL = `${URL_ROOT}code_binary_search.js`;
 const COFFEE_URL = `${URL_ROOT}code_binary_search.coffee`;
 
 add_task(async function() {
-  await pushPref("devtools.debugger.new-debugger-frontend", true);
-
   const toolbox = await openNewTabAndToolbox(PAGE_URL, "jsdebugger");
   const service = toolbox.sourceMapURLService;
 
   // Inject JS script
   const sourceSeen = waitForSourceLoad(toolbox, JS_URL);
   await createScript(JS_URL);
   await sourceSeen;
 
--- a/devtools/client/framework/test/browser_source_map-absolute.js
+++ b/devtools/client/framework/test/browser_source_map-absolute.js
@@ -11,18 +11,16 @@ const { PromiseTestUtils } = scopedCuImp
 PromiseTestUtils.whitelistRejectionsGlobally(/this\.worker is null/);
 
 // Empty page
 const PAGE_URL = `${URL_ROOT}doc_empty-tab-01.html`;
 const JS_URL = `${URL_ROOT}code_binary_search_absolute.js`;
 const ORIGINAL_URL = `${URL_ROOT}code_binary_search.coffee`;
 
 add_task(async function() {
-  await pushPref("devtools.debugger.new-debugger-frontend", true);
-
   const toolbox = await openNewTabAndToolbox(PAGE_URL, "jsdebugger");
   const service = toolbox.sourceMapURLService;
 
   // Inject JS script
   const sourceSeen = waitForSourceLoad(toolbox, JS_URL);
   await createScript(JS_URL);
   await sourceSeen;
 
--- a/devtools/client/framework/test/browser_source_map-inline.js
+++ b/devtools/client/framework/test/browser_source_map-inline.js
@@ -13,18 +13,16 @@ PromiseTestUtils.whitelistRejectionsGlob
 
 const TEST_ROOT = "http://example.com/browser/devtools/client/framework/test/";
 // Empty page
 const PAGE_URL = `${TEST_ROOT}doc_empty-tab-01.html`;
 const JS_URL = `${TEST_ROOT}code_inline_bundle.js`;
 const ORIGINAL_URL = "webpack:///code_inline_original.js";
 
 add_task(async function() {
-  await pushPref("devtools.debugger.new-debugger-frontend", true);
-
   const toolbox = await openNewTabAndToolbox(PAGE_URL, "jsdebugger");
   const service = toolbox.sourceMapURLService;
 
   // Inject JS script
   const sourceSeen = waitForSourceLoad(toolbox, JS_URL);
   await createScript(JS_URL);
   await sourceSeen;
 
--- a/devtools/client/framework/test/browser_source_map-reload.js
+++ b/devtools/client/framework/test/browser_source_map-reload.js
@@ -11,18 +11,16 @@ const JS_URL = URL_ROOT + "sjs_code_relo
 
 const ORIGINAL_URL_1 = "webpack:///code_reload_1.js";
 const ORIGINAL_URL_2 = "webpack:///code_reload_2.js";
 
 const GENERATED_LINE = 86;
 const ORIGINAL_LINE = 13;
 
 add_task(async function() {
-  await pushPref("devtools.debugger.new-debugger-frontend", true);
-
   // Start with the empty page, then navigate, so that we can properly
   // listen for new sources arriving.
   const toolbox = await openNewTabAndToolbox(INITIAL_URL, "webconsole");
   const service = toolbox.sourceMapURLService;
   const tab = toolbox.target.tab;
 
   let sourceSeen = waitForSourceLoad(toolbox, JS_URL);
   tab.linkedBrowser.loadURI(PAGE_URL);
--- a/devtools/client/framework/test/browser_toolbox_split_console.js
+++ b/devtools/client/framework/test/browser_toolbox_split_console.js
@@ -9,22 +9,16 @@
 //  * toolbox.useKeyWithSplitConsole()
 //  * toolbox.isSplitConsoleFocused
 
 let gToolbox = null;
 let panelWin = null;
 
 const URL = "data:text/html;charset=utf8,test split console key delegation";
 
-// Force the old debugger UI since it's directly used (see Bug 1301705)
-Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
-registerCleanupFunction(function() {
-  Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
-});
-
 add_task(async function() {
   const tab = await addTab(URL);
   const target = TargetFactory.forTab(tab);
   gToolbox = await gDevTools.showToolbox(target, "jsdebugger");
   panelWin = gToolbox.getPanel("jsdebugger").panelWin;
 
   await gToolbox.openSplitConsole();
   await testIsSplitConsoleFocused();
--- a/devtools/client/framework/test/browser_toolbox_view_source_01.js
+++ b/devtools/client/framework/test/browser_toolbox_view_source_01.js
@@ -6,39 +6,26 @@
 /**
  * Tests that Toolbox#viewSourceInDebugger works when debugger is not
  * yet opened.
  */
 
 var URL = `${URL_ROOT}doc_viewsource.html`;
 var JS_URL = `${URL_ROOT}code_math.js`;
 
-// Force the old debugger UI since it's directly used (see Bug 1301705)
-Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
-registerCleanupFunction(function() {
-  Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
-});
-
 async function viewSource() {
   const toolbox = await openNewTabAndToolbox(URL);
 
   await toolbox.viewSourceInDebugger(JS_URL, 2);
 
   const debuggerPanel = toolbox.getPanel("jsdebugger");
   ok(debuggerPanel, "The debugger panel was opened.");
   is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected.");
 
-  const { DebuggerView } = debuggerPanel.panelWin;
-  const Sources = DebuggerView.Sources;
-
-  is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
-    "The correct source is shown in the debugger.");
-  is(DebuggerView.editor.getCursor().line + 1, 2,
-    "The correct line is highlighted in the debugger's source editor.");
-
+  assertSelectedLocationInDebugger(debuggerPanel, 2, undefined);
   await closeToolboxAndTab(toolbox);
   finish();
 }
 
 function test() {
   viewSource().then(finish, (aError) => {
     ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
     finish();
--- a/devtools/client/framework/test/browser_toolbox_view_source_02.js
+++ b/devtools/client/framework/test/browser_toolbox_view_source_02.js
@@ -5,47 +5,27 @@
 
 /**
  * Tests that Toolbox#viewSourceInDebugger works when debugger is already loaded.
  */
 
 var URL = `${URL_ROOT}doc_viewsource.html`;
 var JS_URL = `${URL_ROOT}code_math.js`;
 
-// Force the old debugger UI since it's directly used (see Bug 1301705)
-Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
-registerCleanupFunction(function() {
-  Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
-});
-
 async function viewSource() {
   const toolbox = await openNewTabAndToolbox(URL);
-  const { panelWin: debuggerWin } = await toolbox.selectTool("jsdebugger");
-  const debuggerEvents = debuggerWin.EVENTS;
-  const { DebuggerView } = debuggerWin;
-  const Sources = DebuggerView.Sources;
-
-  await debuggerWin.once(debuggerEvents.SOURCE_SHOWN);
-  ok("A source was shown in the debugger.");
-
-  is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
-    "The correct source is initially shown in the debugger.");
-  is(DebuggerView.editor.getCursor().line, 0,
-    "The correct line is initially highlighted in the debugger's source editor.");
+  await toolbox.selectTool("jsdebugger");
 
   await toolbox.viewSourceInDebugger(JS_URL, 2);
 
   const debuggerPanel = toolbox.getPanel("jsdebugger");
   ok(debuggerPanel, "The debugger panel was opened.");
   is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected.");
 
-  is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
-    "The correct source is shown in the debugger.");
-  is(DebuggerView.editor.getCursor().line + 1, 2,
-    "The correct line is highlighted in the debugger's source editor.");
+  assertSelectedLocationInDebugger(debuggerPanel, 2, undefined);
 
   await closeToolboxAndTab(toolbox);
   finish();
 }
 
 function test() {
   viewSource().then(finish, (aError) => {
     ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
--- a/devtools/client/framework/test/head.js
+++ b/devtools/client/framework/test/head.js
@@ -366,8 +366,16 @@ async function resizeWindow(toolbox, wid
   const originalHeight = hostWindow.outerHeight;
   const toWidth = width || originalWidth;
   const toHeight = height || originalHeight;
 
   const onResize = once(hostWindow, "resize");
   hostWindow.resizeTo(toWidth, toHeight);
   await onResize;
 }
+
+function assertSelectedLocationInDebugger(debuggerPanel, line, column) {
+  const location = debuggerPanel._selectors.getSelectedLocation(
+    debuggerPanel._getState()
+  );
+  is(location.line, line);
+  is(location.column, column);
+}
--- a/devtools/client/framework/toolbox-options.js
+++ b/devtools/client/framework/toolbox-options.js
@@ -363,21 +363,16 @@ OptionsPanel.prototype = {
     const isNightly = AppConstants.NIGHTLY_BUILD;
     if (!isNightly) {
       return;
     }
 
     // Labels for these new buttons are nightly only and mostly intended for working on
     // devtools.
     const prefDefinitions = [{
-      pref: "devtools.debugger.new-debugger-frontend",
-      label: L10N.getStr("toolbox.options.enableNewDebugger.label"),
-      id: "devtools-new-debugger",
-      parentId: "debugger-options"
-    }, {
       pref: "devtools.performance.new-panel-enabled",
       label: "Enable new performance recorder (then re-open DevTools)",
       id: "devtools-new-performance",
       parentId: "context-options"
     }];
 
     const createPreferenceOption = ({pref, label, id}) => {
       const inputLabel = this.panelDoc.createElement("label");
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -103,17 +103,16 @@ var connect = async function() {
 function setPrefDefaults() {
   Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true);
   Services.prefs.setBoolPref("devtools.performance.ui.show-platform-data", true);
   Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
   Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
   Services.prefs.setBoolPref("devtools.command-button-noautohide.enabled", true);
   // Bug 1225160 - Using source maps with browser debugging can lead to a crash
   Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false);
-  Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", true);
   Services.prefs.setBoolPref("devtools.preference.new-panel-enabled", false);
   Services.prefs.setBoolPref("layout.css.emulate-moz-box-with-flex", false);
 
   Services.prefs.setBoolPref("devtools.performance.enabled", false);
 }
 
 window.addEventListener("load", async function() {
   gShortcuts = new KeyShortcuts({window});
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -65,16 +65,17 @@ support-files =
   lib_react_16.2.0_min.js
   lib_react_dom_15.3.1_min.js
   lib_react_dom_15.4.1.js
   lib_react_dom_16.2.0_min.js
   lib_react_with_addons_15.3.1_min.js
   lib_react_with_addons_15.4.1.js
   react_external_listeners.js
   !/devtools/client/debugger/new/test/mochitest/helpers.js
+  !/devtools/client/debugger/new/test/mochitest/helpers/context.js
   !/devtools/client/inspector/test/head.js
   !/devtools/client/inspector/test/shared-head.js
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/shared/test/shared-redux-head.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
   !/devtools/client/shared/test/test-actor.js
   !/devtools/client/shared/test/test-actor-registry.js
 
--- a/devtools/client/inspector/markup/test/browser_markup_shadowdom_open_debugger.js
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_open_debugger.js
@@ -10,16 +10,21 @@
 // Test that the markup view is correctly updated to show those items if the custom
 // element definition happens after opening the inspector.
 
 /* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js",
   this);
 
+/* import-globals-from ../../../debugger/new/test/mochitest/helpers/context.js */
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers/context.js",
+  this);
+
 const TEST_URL = `data:text/html;charset=utf-8,` + encodeURIComponent(`
 <test-component></test-component>
 <other-component>some-content</other-component>
 
 <script>
   "use strict";
   window.attachTestComponent = function() {
     customElements.define("test-component", class extends HTMLElement {
--- a/devtools/client/shared/view-source.js
+++ b/devtools/client/shared/view-source.js
@@ -1,17 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var Services = require("Services");
 var { gDevTools } = require("devtools/client/framework/devtools");
-var { getSourceText } = require("devtools/client/debugger/content/queries");
 
 /**
  * Tries to open a Stylesheet file in the Style Editor. If the file is not
  * found, it is opened in source view instead.
  * Returns a promise resolving to a boolean indicating whether or not
  * the source was able to be displayed in the StyleEditor, as the built-in
  * Firefox View Source is the fallback.
  *
@@ -46,83 +45,24 @@ exports.viewSourceInStyleEditor = async 
  * @param {string} sourceURL
  * @param {number} sourceLine
  * @param {string} [reason=unknown]
  *
  * @return {Promise<boolean>}
  */
 exports.viewSourceInDebugger = async function(toolbox, sourceURL, sourceLine,
                                               reason = "unknown") {
-  // If the Debugger was already open, switch to it and try to show the
-  // source immediately. Otherwise, initialize it and wait for the sources
-  // to be added first.
-  const debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
   const dbg = await toolbox.loadTool("jsdebugger");
-
-  // New debugger frontend
-  if (Services.prefs.getBoolPref("devtools.debugger.new-debugger-frontend")) {
-    const source = dbg.getSource(sourceURL);
-    if (source) {
-      await toolbox.selectTool("jsdebugger", reason);
-      dbg.selectSource(sourceURL, sourceLine);
-      return true;
-    }
-
-    exports.viewSource(toolbox, sourceURL, sourceLine);
-    return false;
-  }
-
-  const win = dbg.panelWin;
-
-  // Old debugger frontend
-  if (!debuggerAlreadyOpen) {
-    await win.DebuggerController.waitForSourcesLoaded();
-  }
-
-  const { DebuggerView } = win;
-  const { Sources } = DebuggerView;
-
-  const item = Sources.getItemForAttachment(a => a.source.url === sourceURL);
-  if (item) {
+  const source = dbg.getSource(sourceURL);
+  if (source) {
     await toolbox.selectTool("jsdebugger", reason);
-
-    // Determine if the source has already finished loading. There's two cases
-    // in which we need to wait for the source to be shown:
-    // 1) The requested source is not yet selected and will be shown once it is
-    //    selected and loaded
-    // 2) The requested source is selected BUT the source text is still loading.
-    const { actor } = item.attachment.source;
-    const state = win.DebuggerController.getState();
-
-    // (1) Is the source selected?
-    const selected = state.sources.selectedSource;
-    const isSelected = selected === actor;
-
-    // (2) Has the source text finished loading?
-    let isLoading = false;
-
-    // Only check if the source is loading when the source is already selected.
-    // If the source is not selected, we will select it below and the already
-    // pending load will be cancelled and this check is useless.
-    if (isSelected) {
-      const sourceTextInfo = getSourceText(state, selected);
-      isLoading = sourceTextInfo && sourceTextInfo.loading;
-    }
-
-    // Select the requested source
-    DebuggerView.setEditorLocation(actor, sourceLine, { noDebug: true });
-
-    // Wait for it to load
-    if (!isSelected || isLoading) {
-      await win.DebuggerController.waitForSourceShown(sourceURL);
-    }
+    dbg.selectSource(sourceURL, sourceLine);
     return true;
   }
 
-  // If not found, still attempt to open in View Source
   exports.viewSource(toolbox, sourceURL, sourceLine);
   return false;
 };
 
 /**
  * Tries to open a JavaScript file in the corresponding Scratchpad.
  *
  * @param {string} sourceURL
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -151,16 +151,18 @@ support-files =
   test-trackingprotection-securityerrors.html
   test-webconsole-error-observer.html
   test-websocket.html
   test-websocket.js
   testscript.js
   !/devtools/client/netmonitor/test/sjs_cors-test-server.sjs
   !/image/test/mochitest/blue.png
   !/devtools/client/shared/test/shared-head.js
+  !/devtools/client/debugger/new/test/mochitest/helpers.js
+  !/devtools/client/debugger/new/test/mochitest/helpers/context.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
   !/devtools/client/shared/test/test-actor.js
   !/devtools/client/shared/test/test-actor-registry.js
 
 [browser_console.js]
 [browser_console_clear_cache.js]
 [browser_console_clear_method.js]
 skip-if = true # Bug 1437843
@@ -246,17 +248,16 @@ tags = mcb
 [browser_webconsole_cd_iframe.js]
 [browser_webconsole_certificate_messages.js]
 [browser_webconsole_clear_cache.js]
 [browser_webconsole_click_function_to_source.js]
 [browser_webconsole_clickable_urls.js]
 [browser_webconsole_close_unfocused_window.js]
 [browser_webconsole_closing_after_completion.js]
 [browser_webconsole_close_sidebar.js]
-[browser_webconsole_closure_inspection.js]
 skip-if = true # Bug 1405250
 [browser_webconsole_console_api_iframe.js]
 [browser_webconsole_console_dir.js]
 [browser_webconsole_console_dir_uninspectable.js]
 [browser_webconsole_console_error_expand_object.js]
 [browser_webconsole_console_group.js]
 [browser_webconsole_console_logging_workers_api.js]
 [browser_webconsole_console_table.js]
--- a/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_in_debugger_stackframe.js
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_in_debugger_stackframe.js
@@ -3,32 +3,35 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that makes sure web console autocomplete happens in the user-selected
 // stackframe from the js debugger.
 
 "use strict";
 
+// Import helpers for the new debugger
+/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js",
+  this);
+
 const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
                  "test/mochitest/test-autocomplete-in-stackframe.html";
 
 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.
   await pushPref("devtools.webconsole.jsterm.codeMirror", true);
   await performTests();
 });
 
 async function performTests() {
-  // Force the old debugger UI since it's directly used (see Bug 1301705)
-  await pushPref("devtools.debugger.new-debugger-frontend", false);
-
   const { jsterm } = await openNewTabAndConsole(TEST_URI);
   const {
     autocompletePopup: popup,
   } = jsterm;
 
   const target = TargetFactory.forTab(gBrowser.selectedTab);
   const toolbox = gDevTools.getToolbox(target);
 
@@ -55,34 +58,36 @@ async function performTests() {
     `"foo1Obj." gave the expected suggestions`);
 
   // Test if 'foo1Obj.prop2.' gives 'prop21'
   await jstermComplete("foo1Obj.prop2.");
   ok(getPopupLabels(popup).includes("prop21"),
     `"foo1Obj.prop2." gave the expected suggestions`);
 
   info("Opening Debugger");
-  const {panel} = await openDebugger();
+  await openDebugger();
+  const dbg = createDebuggerContext(toolbox);
 
   info("Waiting for pause");
-  const stackFrames = await pauseDebugger(panel);
+  await pauseDebugger(dbg);
+  const stackFrames = dbg.selectors.getCallStackFrames(dbg.getState());
 
   info("Opening Console again");
   await toolbox.selectTool("webconsole");
 
   // Test if 'foo' gives 'foo3' and 'foo1' but not 'foo2', since we are paused in
   // the `secondCall` function (called by `firstCall`, which we call in `pauseDebugger`).
   await jstermComplete("foo");
   is(getPopupLabels(popup).join("-"), "foo1-foo1Obj-foo3-foo3Obj",
     `"foo" gave the expected suggestions`);
 
   await openDebugger();
 
   // Select the frame for the `firstCall` function.
-  stackFrames.selectFrame(1);
+  await dbg.actions.selectFrame(stackFrames[1]);
 
   info("openConsole");
   await toolbox.selectTool("webconsole");
 
   // Test if 'foo' gives 'foo2' and 'foo1' but not 'foo3', since we are now in the
   // `firstCall` frame.
   await jstermComplete("foo");
   is(getPopupLabels(popup).join("-"), "foo1-foo1Obj-foo2-foo2Obj",
@@ -104,23 +109,15 @@ async function performTests() {
   await jstermComplete("foo2Obj[0].");
   is(getPopupLabels(popup).length, 0, "no items for foo2Obj[0]");
 }
 
 function getPopupLabels(popup) {
   return popup.getItems().map(item => item.label);
 }
 
-function pauseDebugger(debuggerPanel) {
-  const debuggerWin = debuggerPanel.panelWin;
-  const debuggerController = debuggerWin.DebuggerController;
-  const thread = debuggerController.activeThread;
-
-  return new Promise(resolve => {
-    thread.addOneTimeListener("framesadded", () =>
-      resolve(debuggerController.StackFrames));
-
-    info("firstCall()");
-    ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
-      content.wrappedJSObject.firstCall();
-    });
+async function pauseDebugger(dbg) {
+  info("Waiting for debugger to pause");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() {
+    content.wrappedJSObject.firstCall();
   });
+  await waitForPaused(dbg);
 }
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_click_function_to_source.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_click_function_to_source.js
@@ -2,45 +2,53 @@
 /* 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/ */
 
 // Tests that clicking on a function displays its source in the debugger. See Bug 1050691.
 
 "use strict";
 
+// Import helpers for the new debugger
+/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js",
+  this);
+
+requestLongerTimeout(5);
+
 const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
                  "test/mochitest/" +
                  "test-click-function-to-source.html";
 
-// Force the old debugger UI since it's directly used (see Bug 1301705)
-pushPref("devtools.debugger.new-debugger-frontend", false);
+const TEST_SCRIPT_URI = "http://example.com/browser/devtools/client/webconsole/" +
+                 "test/mochitest/" +
+                 "test-click-function-to-source.js";
 
 add_task(async function() {
   const hud = await openNewTabAndConsole(TEST_URI);
 
   info("Open the Debugger panel.");
-  const {panel} = await openDebugger();
-  const panelWin = panel.panelWin;
+  await openDebugger();
 
   info("And right after come back to the Console panel.");
   await openConsole();
 
   info("Log a function");
   const onLoggedFunction = waitForMessage(hud, "function foo");
   ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
     content.wrappedJSObject.foo();
   });
   const {node} = await onLoggedFunction;
   const jumpIcon = node.querySelector(".jump-definition");
   ok(jumpIcon, "A jump to definition button is rendered, as expected");
 
   info("Click on the jump to definition button.");
-  const onEditorLocationSet = panelWin.once(panelWin.EVENTS.EDITOR_LOCATION_SET);
   jumpIcon.click();
-  await onEditorLocationSet;
 
-  const {editor} = panelWin.DebuggerView;
-  const {line, ch} = editor.getCursor();
-  // Source editor starts counting line and column numbers from 0.
-  is(line, 8, "Debugger is open at the expected line");
-  is(ch, 0, "Debugger is open at the expected character");
+  const toolbox = gDevTools.getToolbox(hud.target);
+  const dbg = createDebuggerContext(toolbox);
+  await waitForSelectedSource(dbg, TEST_SCRIPT_URI);
+
+  const pendingLocation = dbg.selectors.getPendingSelectedLocation(dbg.getState());
+  const {line} = pendingLocation;
+  is(line, 9, "Debugger is open at the expected line");
 });
deleted file mode 100644
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_closure_inspection.js
+++ /dev/null
@@ -1,104 +0,0 @@
-/* -*- 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/ */
-
-// XXX Remove this when the file is migrated to the new frontend.
-/* eslint-disable no-undef */
-
-// Check that inspecting a closure in the variables view sidebar works when
-// execution is paused.
-
-"use strict";
-
-const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
-                 "test/mochitest/test-closures.html";
-
-var gWebConsole, gJSTerm, gVariablesView;
-
-// Force the old debugger UI since it's directly used (see Bug 1301705)
-Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
-registerCleanupFunction(function() {
-  Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
-});
-
-function test() {
-  registerCleanupFunction(() => {
-    gWebConsole = gJSTerm = gVariablesView = null;
-  });
-
-  function fetchScopes(hud, toolbox, panelWin, deferred) {
-    panelWin.once(panelWin.EVENTS.FETCHED_SCOPES, () => {
-      ok(true, "Scopes were fetched");
-      toolbox.selectTool("webconsole").then(() => consoleOpened(hud));
-      deferred.resolve();
-    });
-  }
-
-  loadTab(TEST_URI).then(() => {
-    openConsole().then((hud) => {
-      openDebugger().then(({ toolbox, panelWin }) => {
-        const deferred = defer();
-        fetchScopes(hud, toolbox, panelWin, deferred);
-
-        // eslint-disable-next-line
-        ContentTask.spawn(gBrowser.selectedBrowser, {}, () => {
-          const button = content.document.querySelector("button");
-          ok(button, "button element found");
-          button.click();
-        });
-
-        return deferred.promise;
-      });
-    });
-  });
-}
-
-function consoleOpened(hud) {
-  gWebConsole = hud;
-  gJSTerm = hud.jsterm;
-  gJSTerm.execute("window.george.getName");
-
-  waitForMessages({
-    webconsole: gWebConsole,
-    messages: [{
-      text: "getName()",
-      category: CATEGORY_OUTPUT,
-      objects: true,
-    }],
-  }).then(onExecuteGetName);
-}
-
-function onExecuteGetName(results) {
-  const clickable = results[0].clickableElements[0];
-  ok(clickable, "clickable object found");
-
-  gJSTerm.once("variablesview-fetched", onGetNameFetch);
-  const contextMenu =
-      gWebConsole.iframeWindow.document.getElementById("output-contextmenu");
-  waitForContextMenu(contextMenu, clickable, () => {
-    const openInVarView = contextMenu.querySelector("#menu_openInVarView");
-    ok(openInVarView.disabled === false,
-       "the \"Open In Variables View\" context menu item should be clickable");
-    // EventUtils.synthesizeMouseAtCenter seems to fail here in Mac OSX
-    openInVarView.click();
-  });
-}
-
-function onGetNameFetch(view) {
-  gVariablesView = view._variablesView;
-  ok(gVariablesView, "variables view object");
-
-  findVariableViewProperties(view, [
-    { name: /_pfactory/, value: "" },
-  ], { webconsole: gWebConsole }).then(onExpandClosure);
-}
-
-function onExpandClosure(results) {
-  const prop = results[0].matchedProp;
-  ok(prop, "matched the name property in the variables view");
-
-  gVariablesView.window.focus();
-  gJSTerm.once("sidebar-closed", finishTest);
-  EventUtils.synthesizeKey("VK_ESCAPE", {}, gVariablesView.window);
-}
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_eval_in_debugger_stackframe.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_eval_in_debugger_stackframe.js
@@ -3,23 +3,26 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that makes sure web console eval happens in the user-selected stackframe
 // from the js debugger.
 
 "use strict";
 
+// Import helpers for the new debugger
+/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js",
+  this);
+
 const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
                  "test/mochitest/test-eval-in-stackframe.html";
 
 add_task(async function() {
-  // Force the old debugger UI since it's directly used (see Bug 1301705).
-  await pushPref("devtools.debugger.new-debugger-frontend", false);
-
   info("open the console");
   const hud = await openNewTabAndConsole(TEST_URI);
   const {jsterm} = hud;
 
   info("Check `foo` value");
   let onResultMessage = waitForMessage(hud, "globalFooBug783499");
   jsterm.execute("foo");
   await onResultMessage;
@@ -27,47 +30,46 @@ add_task(async function() {
 
   info("Assign and check `foo2` value");
   onResultMessage = waitForMessage(hud, "newFoo");
   jsterm.execute("foo2 = 'newFoo'; window.foo2");
   await onResultMessage;
   ok(true, "'newFoo' is displayed after adding `foo2`");
 
   info("Open the debugger and then select the console again");
-  const {panel} = await openDebugger();
-  const {activeThread, StackFrames: stackFrames} = panel.panelWin.DebuggerController;
+  await openDebugger();
+  const toolbox = gDevTools.getToolbox(hud.target);
+  const dbg = createDebuggerContext(toolbox);
 
   await openConsole();
 
   info("Check `foo + foo2` value");
   onResultMessage = waitForMessage(hud, "globalFooBug783499newFoo");
   jsterm.execute("foo + foo2");
   await onResultMessage;
 
   info("Select the debugger again");
   await openDebugger();
+  await pauseDebugger(dbg);
 
-  const onFirstCallFramesAdded = activeThread.addOneTimeListener("framesadded");
-  // firstCall calls secondCall, which has a debugger statement, so we'll be paused.
-  ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
-    content.wrappedJSObject.firstCall();
-  });
-  await onFirstCallFramesAdded;
+  const stackFrames = dbg.selectors.getCallStackFrames(dbg.getState());
 
   info("frames added, select the console again");
   await openConsole();
 
   info("Check `foo + foo2` value when paused");
   onResultMessage = waitForMessage(hud, "globalFooBug783499foo2SecondCall");
   jsterm.execute("foo + foo2");
   ok(true, "`foo + foo2` from `secondCall()`");
 
   info("select the debugger and select the frame (1)");
   await openDebugger();
-  stackFrames.selectFrame(1);
+
+  await dbg.actions.selectFrame(stackFrames[1]);
+
   await openConsole();
 
   info("Check `foo + foo2 + foo3` value when paused on a given frame");
   onResultMessage = waitForMessage(hud, "fooFirstCallnewFoofoo3FirstCall");
   jsterm.execute("foo + foo2 + foo3");
   await onResultMessage;
   ok(true, "`foo + foo2 + foo3` from `firstCall()`");
 
@@ -77,8 +79,16 @@ add_task(async function() {
   ok(true, "`foo + foo3` updated in `firstCall()`");
 
   await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
     is(content.wrappedJSObject.foo, "globalFooBug783499", "`foo` in content window");
     is(content.wrappedJSObject.foo2, "newFoo", "`foo2` in content window");
     ok(!content.wrappedJSObject.foo3, "`foo3` was not added to the content window");
   });
 });
+
+async function pauseDebugger(dbg) {
+  info("Waiting for debugger to pause");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() {
+    content.wrappedJSObject.firstCall();
+  });
+  await waitForPaused(dbg);
+}
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_eval_in_debugger_stackframe2.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_eval_in_debugger_stackframe2.js
@@ -4,44 +4,48 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test to make sure that web console commands can fire while paused at a
 // breakpoint that was triggered from a JS call.  Relies on asynchronous js
 // evaluation over the protocol - see Bug 1088861.
 
 "use strict";
 
+// Import helpers for the new debugger
+/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js",
+  this);
+
 const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
                  "test/mochitest/test-eval-in-stackframe.html";
 
 add_task(async function() {
-  // Force the old debugger UI since it's directly used (see Bug 1301705).
-  await pushPref("devtools.debugger.new-debugger-frontend", false);
-
   info("open the console");
   const hud = await openNewTabAndConsole(TEST_URI);
   const {jsterm} = hud;
 
   info("open the debugger");
-  const {panel} = await openDebugger();
-  const {activeThread} = panel.panelWin.DebuggerController;
+  await openDebugger();
 
-  const onFirstCallFramesAdded = activeThread.addOneTimeListener("framesadded");
+  const toolbox = gDevTools.getToolbox(hud.target);
+  const dbg = createDebuggerContext(toolbox);
+
   // firstCall calls secondCall, which has a debugger statement, so we'll be paused.
   const onFirstCallMessageReceived = waitForMessage(hud, "undefined");
 
   const unresolvedSymbol = Symbol();
   let firstCallEvaluationResult = unresolvedSymbol;
   onFirstCallMessageReceived.then(message => {
     firstCallEvaluationResult = message;
   });
   jsterm.execute("firstCall()");
 
   info("Waiting for a frame to be added");
-  await onFirstCallFramesAdded;
+  await waitForPaused(dbg);
 
   info("frames added, select the console again");
   await openConsole();
 
   info("Executing basic command while paused");
   let onMessageReceived = waitForMessage(hud, "3");
   jsterm.execute("1 + 2");
   let message = await onMessageReceived;
@@ -52,14 +56,14 @@ add_task(async function() {
   jsterm.execute("foo + foo2");
   message = await onMessageReceived;
   ok(message, "`foo + foo2` was evaluated as expected with debugger paused");
 
   info("Checking the first command, which is the last to resolve since it paused");
   ok(firstCallEvaluationResult === unresolvedSymbol, "firstCall was not evaluated yet");
 
   info("Resuming the thread");
-  activeThread.resume();
+  dbg.actions.resume(dbg.getState());
 
   message = await onFirstCallMessageReceived;
   ok(firstCallEvaluationResult !== unresolvedSymbol,
     "firstCall() returned correct value");
 });
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_location_debugger_link.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_location_debugger_link.js
@@ -14,19 +14,16 @@ const { PromiseTestUtils } = scopedCuImp
 PromiseTestUtils.whitelistRejectionsGlobally(/this\.worker is null/);
 
 requestLongerTimeout(2);
 
 const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
                  "test/mochitest/test-location-debugger-link.html";
 
 add_task(async function() {
-  // Force the new debugger UI, in case this gets uplifted with the old
-  // debugger still turned on
-  await pushPref("devtools.debugger.new-debugger-frontend", true);
   await pushPref("devtools.webconsole.filter.error", true);
   await pushPref("devtools.webconsole.filter.log", true);
 
   // On e10s, the exception thrown in test-location-debugger-link-errors.js
   // is triggered in child process and is ignored by test harness
   if (!Services.appinfo.browserTabsRemoteAutostart) {
     expectUncaughtException();
   }
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_object_inspector_while_debugging_and_inspecting.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_object_inspector_while_debugging_and_inspecting.js
@@ -3,33 +3,39 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that makes sure web console eval works while the js debugger paused the
 // page, and while the inspector is active. See bug 886137.
 
 "use strict";
 
+// Import helpers for the new debugger
+/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js",
+  this);
+
 const TEST_URI = "https://example.com/browser/devtools/client/webconsole/" +
                  "test/mochitest/test-eval-in-stackframe.html";
 
 add_task(async function() {
-  // Force the old debugger UI since it's directly used (see Bug 1301705)
-  await pushPref("devtools.debugger.new-debugger-frontend", false);
   const hud = await openNewTabAndConsole(TEST_URI);
 
   info("Switch to the debugger");
   await openDebugger();
 
   info("Switch to the inspector");
   const target = TargetFactory.forTab(gBrowser.selectedTab);
   await gDevTools.showToolbox(target, "inspector");
 
   info("Call firstCall() and wait for the debugger statement to be reached.");
-  await waitForFrameAdded();
+  const toolbox = gDevTools.getToolbox(target);
+  const dbg = createDebuggerContext(toolbox);
+  await pauseDebugger(dbg);
 
   info("Switch back to the console");
   await gDevTools.showToolbox(target, "webconsole");
 
   info("Test logging and inspecting objects while on a breakpoint.");
   const jsterm = hud.jsterm;
 
   const onMessage = waitForMessage(hud, '{ testProp2: "testValue2" }');
@@ -58,21 +64,15 @@ add_task(async function() {
   const oiNodes = oi.querySelectorAll(".node");
   is(oiNodes.length, 3, "There is the expected number of nodes in the tree");
 
   ok(oiNodes[0].textContent.includes(`{\u2026}`));
   ok(oiNodes[1].textContent.includes(`testProp2: "testValue2"`));
   ok(oiNodes[2].textContent.includes(`<prototype>: Object { \u2026 }`));
 });
 
-async function waitForFrameAdded() {
-  const target = TargetFactory.forTab(gBrowser.selectedTab);
-  const toolbox = gDevTools.getToolbox(target);
-  const thread = toolbox.threadClient;
-
-  info("Waiting for framesadded");
-  await new Promise(resolve => {
-    thread.addOneTimeListener("framesadded", resolve);
-    ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() {
-      content.wrappedJSObject.firstCall();
-    });
+async function pauseDebugger(dbg) {
+  info("Waiting for debugger to pause");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() {
+    content.wrappedJSObject.firstCall();
   });
+  await waitForPaused(dbg);
 }
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_optimized_out_vars.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_optimized_out_vars.js
@@ -3,57 +3,61 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Check that inspecting an optimized out variable works when execution is
 // paused.
 
 "use strict";
 
+// Import helpers for the new debugger
+/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */
+Services.scriptloader.loadSubScript(
+    "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js",
+    this);
+
 const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
                  "test/mochitest/" +
                  "test-closure-optimized-out.html";
 
 add_task(async function() {
-  // Force the old debugger UI since it's directly used (see Bug 1301705)
-  await pushPref("devtools.debugger.new-debugger-frontend", false);
-
   const hud = await openNewTabAndConsole(TEST_URI);
-  const { toolbox, panel: debuggerPanel } = await openDebugger();
+  await openDebugger();
 
-  const sources = debuggerPanel.panelWin.DebuggerView.Sources;
-  await debuggerPanel.addBreakpoint({ actor: sources.values[0], line: 18 });
-  await ensureThreadClientState(debuggerPanel, "resumed");
+  const toolbox = gDevTools.getToolbox(hud.target);
+  const dbg = createDebuggerContext(toolbox);
 
-  const { FETCHED_SCOPES } = debuggerPanel.panelWin.EVENTS;
-  const fetchedScopes = debuggerPanel.panelWin.once(FETCHED_SCOPES);
+  await addBreakpoint(dbg, "test-closure-optimized-out.html", 18);
+  await waitForThreadEvents(dbg, "resumed");
 
   // Cause the debuggee to pause
-  ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() {
-    const button = content.document.querySelector("button");
-    button.click();
-  });
-
-  await fetchedScopes;
-  ok(true, "Scopes were fetched");
+  await pauseDebugger(dbg);
 
   await toolbox.selectTool("webconsole");
 
   // This is the meat of the test: evaluate the optimized out variable.
   const onMessage = waitForMessage(hud, "optimized out");
   hud.jsterm.execute("upvar");
 
   info("Waiting for optimized out message");
   await onMessage;
 
   ok(true, "Optimized out message logged");
+
+  info("Open the debugger");
+  await openDebugger();
+
+  info("Resume");
+  await resume(dbg);
+
+  info("Remove the breakpoint");
+  const source = findSource(dbg, "test-closure-optimized-out.html");
+  await removeBreakpoint(dbg, source.id, 18);
 });
 
-// Debugger helper functions adapted from devtools/client/debugger/test/head.js.
-
-async function ensureThreadClientState(debuggerPanel, state) {
-  const thread = debuggerPanel.panelWin.gThreadClient;
-  info(`Thread is: '${thread.state}'.`);
-  if (thread.state != state) {
-    info("Waiting for thread event: '${state}'.");
-    await thread.addOneTimeListener(state);
-  }
+async function pauseDebugger(dbg) {
+  info("Waiting for debugger to pause");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() {
+    const button = content.document.querySelector("button");
+    button.click();
+  });
+  await waitForPaused(dbg);
 }
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_sourcemap_nosource.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_sourcemap_nosource.js
@@ -20,19 +20,16 @@ const PAGE_URL = `data:text/html,
 
   <body>
     <script src="${JS_URL}"></script>
   </body>
 
 </html>`;
 
 add_task(async function() {
-  // Force the new debugger UI, in case this gets uplifted with the old
-  // debugger still turned on
-  await pushPref("devtools.debugger.new-debugger-frontend", true);
   await pushPref("devtools.source-map.client-service.enabled", true);
 
   const hud = await openNewTabAndConsole(PAGE_URL);
   const toolbox = hud.ui.consoleOutput.toolbox;
 
   info("Finding \"here\" message and waiting for source map to be applied");
   await waitFor(() => {
     const node = findMessage(hud, "here");
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_stacktrace_location_debugger_link.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_stacktrace_location_debugger_link.js
@@ -14,22 +14,18 @@ const { PromiseTestUtils } = scopedCuImp
 PromiseTestUtils.whitelistRejectionsGlobally(/Component not initialized/);
 PromiseTestUtils.whitelistRejectionsGlobally(/this\.worker is null/);
 
 const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
                  "test/mochitest/" +
                  "test-stacktrace-location-debugger-link.html";
 
 add_task(async function() {
-  // Force the new debugger UI, in case this gets uplifted with the old
-  // debugger still turned on
-  Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", true);
   Services.prefs.setBoolPref("devtools.webconsole.filter.log", true);
   registerCleanupFunction(async function() {
-    Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
     Services.prefs.clearUserPref("devtools.webconsole.filter.log");
   });
 
   const hud = await openNewTabAndConsole(TEST_URI);
   const target = TargetFactory.forTab(gBrowser.selectedTab);
   const toolbox = gDevTools.getToolbox(target);
 
   await testOpenInDebugger(hud, toolbox, "console.trace()");
--- a/devtools/client/webconsole/test/mochitest/head.js
+++ b/devtools/client/webconsole/test/mochitest/head.js
@@ -14,16 +14,22 @@ Services.scriptloader.loadSubScript(
 
 // shared-head.js handles imports, constants, and utility functions
 // Load the shared-head file first.
 /* import-globals-from ../../../shared/test/shared-head.js */
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
   this);
 
+// Import helpers for the new debugger
+/* import-globals-from ../../../debugger/new/test/mochitest/helpers/context.js */
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers/context.js",
+  this);
+
 var {HUDService} = require("devtools/client/webconsole/hudservice");
 var WCUL10n = require("devtools/client/webconsole/webconsole-l10n");
 const DOCS_GA_PARAMS = `?${new URLSearchParams({
   "utm_source": "mozilla",
   "utm_medium": "firefox-console-errors",
   "utm_campaign": "default"
 })}`;
 const STATUS_CODES_GA_PARAMS = `?${new URLSearchParams({
@@ -530,19 +536,27 @@ async function openDebugger(options = {}
       panel: toolbox.getCurrentPanel()
     };
   }
 
   toolbox = await gDevTools.showToolbox(target, "jsdebugger");
   const panel = toolbox.getCurrentPanel();
 
   // Do not clear VariableView lazily so it doesn't disturb test ending.
-  panel._view.Variables.lazyEmpty = false;
+  if (panel._view) {
+    panel._view.Variables.lazyEmpty = false;
+  }
 
-  await panel.panelWin.DebuggerController.waitForSourcesLoaded();
+  // Old debugger
+  if (panel.panelWin && panel.panelWin.DebuggerController) {
+    await panel.panelWin.DebuggerController.waitForSourcesLoaded();
+  } else {
+    // New debugger
+    await toolbox.threadClient.getSources();
+  }
   return {target, toolbox, panel};
 }
 
 async function openInspector(options = {}) {
   if (!options.tab) {
     options.tab = gBrowser.selectedTab;
   }
 
--- a/devtools/server/tests/unit/test_objectgrips-17.js
+++ b/devtools/server/tests/unit/test_objectgrips-17.js
@@ -86,16 +86,23 @@ async function testPrincipal(globalPrinc
   for (const globalHasXrays of [true, false]) {
     gIsOpaque = gSubsumes && globalPrincipal !== systemPrincipal
                 && (sameOrigin && gDebuggeeHasXrays || globalHasXrays);
     for (gGlobalIsInvisible of [true, false]) {
       gGlobal = Cu.Sandbox(globalPrincipal, {
         wantXrays: globalHasXrays,
         invisibleToDebugger: gGlobalIsInvisible
       });
+      // Previously, the Sandbox constructor would (bizarrely) waive xrays on
+      // the return Sandbox if wantXrays was false. This has now been fixed,
+      // but we need to mimic that behavior here to make the test continue
+      // to pass.
+      if (!globalHasXrays) {
+        gGlobal = Cu.waiveXrays(gGlobal);
+      }
       await test();
     }
   }
 }
 
 function test() {
   return new Promise(function(resolve) {
     gThreadClient.addOneTimeListener("paused", async function(event, packet) {
--- a/dom/base/IframeSandboxKeywordList.h
+++ b/dom/base/IframeSandboxKeywordList.h
@@ -20,8 +20,10 @@ SANDBOX_KEYWORD("allow-pointer-lock", al
 SANDBOX_KEYWORD("allow-orientation-lock", alloworientationlock,
 		SANDBOXED_ORIENTATION_LOCK)
 SANDBOX_KEYWORD("allow-popups", allowpopups, SANDBOXED_AUXILIARY_NAVIGATION)
 SANDBOX_KEYWORD("allow-modals", allowmodals, SANDBOXED_MODALS)
 SANDBOX_KEYWORD("allow-popups-to-escape-sandbox", allowpopupstoescapesandbox,
                 SANDBOX_PROPAGATES_TO_AUXILIARY_BROWSING_CONTEXTS)
 SANDBOX_KEYWORD("allow-presentation", allowpresentation,
                 SANDBOXED_PRESENTATION)
+SANDBOX_KEYWORD("allow-storage-access-by-user-activation",
+                allowstorageaccessbyuseractivatetion, SANDBOXED_STORAGE_ACCESS)
--- a/dom/base/nsContentList.cpp
+++ b/dom/base/nsContentList.cpp
@@ -20,16 +20,17 @@
 #include "nsGkAtoms.h"
 #include "mozilla/dom/HTMLCollectionBinding.h"
 #include "mozilla/dom/NodeListBinding.h"
 #include "mozilla/Likely.h"
 #include "nsGenericHTMLElement.h"
 #include "jsfriendapi.h"
 #include <algorithm>
 #include "mozilla/dom/NodeInfoInlines.h"
+#include "mozilla/MruCache.h"
 
 #include "PLDHashTable.h"
 
 #ifdef DEBUG_CONTENT_LIST
 #include "nsIContentIterator.h"
 #define ASSERT_IN_SYNC AssertInSync()
 #else
 #define ASSERT_IN_SYNC PR_BEGIN_MACRO PR_END_MACRO
@@ -167,25 +168,30 @@ nsIContent*
 nsEmptyContentList::Item(uint32_t aIndex)
 {
   return nullptr;
 }
 
 // Hashtable for storing nsContentLists
 static PLDHashTable* gContentListHashTable;
 
-#define RECENTLY_USED_CONTENT_LIST_CACHE_SIZE 31
-static nsContentList*
-  sRecentlyUsedContentLists[RECENTLY_USED_CONTENT_LIST_CACHE_SIZE] = {};
+struct ContentListCache :
+  public MruCache<nsContentListKey, nsContentList*, ContentListCache>
+{
+  static HashNumber Hash(const nsContentListKey& aKey)
+  {
+    return aKey.GetHash();
+  }
+  static bool Match(const nsContentListKey& aKey, const nsContentList* aVal)
+  {
+    return aVal->MatchesKey(aKey);
+  }
+};
 
-static MOZ_ALWAYS_INLINE uint32_t
-RecentlyUsedCacheIndex(const nsContentListKey& aKey)
-{
-  return aKey.GetHash() % RECENTLY_USED_CONTENT_LIST_CACHE_SIZE;
-}
+static ContentListCache sRecentlyUsedContentLists;
 
 struct ContentListHashEntry : public PLDHashEntryHdr
 {
   nsContentList* mContentList;
 };
 
 static PLDHashNumber
 ContentListHashtableHashKey(const void *key)
@@ -210,20 +216,19 @@ NS_GetContentList(nsINode* aRootNode,
                   int32_t  aMatchNameSpaceId,
                   const nsAString& aTagname)
 {
   NS_ASSERTION(aRootNode, "content list has to have a root");
 
   RefPtr<nsContentList> list;
   nsContentListKey hashKey(aRootNode, aMatchNameSpaceId, aTagname,
                            aRootNode->OwnerDoc()->IsHTMLDocument());
-  uint32_t recentlyUsedCacheIndex = RecentlyUsedCacheIndex(hashKey);
-  nsContentList* cachedList = sRecentlyUsedContentLists[recentlyUsedCacheIndex];
-  if (cachedList && cachedList->MatchesKey(hashKey)) {
-    list = cachedList;
+  auto p = sRecentlyUsedContentLists.Lookup(hashKey);
+  if (p) {
+    list = p.Data();
     return list.forget();
   }
 
   static const PLDHashTableOps hash_table_ops =
   {
     ContentListHashtableHashKey,
     ContentListHashtableMatchEntry,
     PLDHashTable::MoveEntryStub,
@@ -255,17 +260,17 @@ NS_GetContentList(nsINode* aRootNode,
       htmlAtom = xmlAtom;
     }
     list = new nsContentList(aRootNode, aMatchNameSpaceId, htmlAtom, xmlAtom);
     if (entry) {
       entry->mContentList = list;
     }
   }
 
-  sRecentlyUsedContentLists[recentlyUsedCacheIndex] = list;
+  p.Set(list);
   return list.forget();
 }
 
 #ifdef DEBUG
 const nsCacheableFuncStringContentList::ContentListType
   nsCachableElementsByNameNodeList::sType = nsCacheableFuncStringContentList::eNodeList;
 const nsCacheableFuncStringContentList::ContentListType
   nsCacheableFuncStringHTMLCollection::sType = nsCacheableFuncStringContentList::eHTMLCollection;
@@ -923,20 +928,17 @@ nsContentList::RemoveFromHashtable()
 {
   if (mFunc) {
     // This can't be in the table anyway
     return;
   }
 
   nsDependentAtomString str(mXMLMatchAtom);
   nsContentListKey key(mRootNode, mMatchNameSpaceId, str, mIsHTMLDocument);
-  uint32_t recentlyUsedCacheIndex = RecentlyUsedCacheIndex(key);
-  if (sRecentlyUsedContentLists[recentlyUsedCacheIndex] == this) {
-    sRecentlyUsedContentLists[recentlyUsedCacheIndex] = nullptr;
-  }
+  sRecentlyUsedContentLists.Remove(key);
 
   if (!gContentListHashTable)
     return;
 
   gContentListHashTable->Remove(&key);
 
   if (gContentListHashTable->EntryCount() == 0) {
     delete gContentListHashTable;
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -112,16 +112,17 @@
 #include "nsIPermissionManager.h"
 #include "nsIPrincipal.h"
 #include "ExpandedPrincipal.h"
 #include "mozilla/NullPrincipal.h"
 
 #include "nsIDOMWindow.h"
 #include "nsPIDOMWindow.h"
 #include "nsFocusManager.h"
+#include "nsICookieService.h"
 
 // for radio group stuff
 #include "nsIRadioVisitor.h"
 #include "nsIFormControl.h"
 
 #include "nsBidiUtils.h"
 
 #include "nsContentCreatorFunctions.h"
@@ -13542,16 +13543,222 @@ nsIDocument::GetSelection(ErrorResult& a
 
   if (!window->IsCurrentInnerWindow()) {
     return nullptr;
   }
 
   return nsGlobalWindowInner::Cast(window)->GetSelection(aRv);
 }
 
+already_AddRefed<mozilla::dom::Promise>
+nsIDocument::HasStorageAccess(mozilla::ErrorResult& aRv)
+{
+  nsIGlobalObject* global = GetScopeObject();
+  if (!global) {
+    aRv.Throw(NS_ERROR_NOT_AVAILABLE);
+    return nullptr;
+  }
+
+  RefPtr<Promise> promise = Promise::Create(global, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  if (NodePrincipal()->GetIsNullPrincipal()) {
+    promise->MaybeResolve(false);
+    return promise.forget();
+  }
+
+  if (IsTopLevelContentDocument()) {
+    promise->MaybeResolve(true);
+    return promise.forget();
+  }
+
+  nsCOMPtr<nsIDocument> topLevelDoc = GetTopLevelContentDocument();
+  if (!topLevelDoc) {
+    aRv.Throw(NS_ERROR_NOT_AVAILABLE);
+    return nullptr;
+  }
+  if (topLevelDoc->NodePrincipal()->Equals(NodePrincipal())) {
+    promise->MaybeResolve(true);
+    return promise.forget();
+  }
+
+  if (StaticPrefs::browser_contentblocking_enabled() &&
+      StaticPrefs::network_cookie_cookieBehavior() ==
+        nsICookieService::BEHAVIOR_REJECT_TRACKER) {
+    // If we need to abide by Content Blocking cookie restrictions, ensure to
+    // first do all of our storage access checks.  If storage access isn't
+    // disabled in our document, given that we're a third-party, we must either
+    // not be a tracker, or be whitelisted for some reason (e.g. a storage
+    // access permission being granted).  In that case, resolve the promise and
+    // say we have obtained storage access.
+    if (!nsContentUtils::StorageDisabledByAntiTracking(this, nullptr)) {
+      // Note, storage might be allowed because the top-level document is on
+      // the content blocking allowlist!  In that case, don't provide special
+      // treatment here.
+      bool isOnAllowList = false;
+      if (NS_SUCCEEDED(AntiTrackingCommon::IsOnContentBlockingAllowList(
+                         topLevelDoc->GetDocumentURI(), isOnAllowList)) &&
+          !isOnAllowList) {
+        promise->MaybeResolve(true);
+        return promise.forget();
+      }
+    }
+  }
+
+  nsPIDOMWindowInner* inner = GetInnerWindow();
+  nsGlobalWindowOuter* outer = nullptr;
+  if (inner) {
+    outer = nsGlobalWindowOuter::Cast(inner->GetOuterWindow());
+    promise->MaybeResolve(outer->HasStorageAccess());
+  } else {
+    promise->MaybeRejectWithUndefined();
+  }
+  return promise.forget();
+}
+
+already_AddRefed<mozilla::dom::Promise>
+nsIDocument::RequestStorageAccess(mozilla::ErrorResult& aRv)
+{
+  nsIGlobalObject* global = GetScopeObject();
+  if (!global) {
+    aRv.Throw(NS_ERROR_NOT_AVAILABLE);
+    return nullptr;
+  }
+
+  RefPtr<Promise> promise = Promise::Create(global, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  // Step 1. If the document already has been granted access, resolve.
+  nsPIDOMWindowInner* inner = GetInnerWindow();
+  nsGlobalWindowOuter* outer = nullptr;
+  if (inner) {
+    outer = nsGlobalWindowOuter::Cast(inner->GetOuterWindow());
+    if (outer->HasStorageAccess()) {
+      promise->MaybeResolveWithUndefined();
+      return promise.forget();
+    }
+  }
+
+  // Step 2. If the document has a null origin, reject.
+  if (NodePrincipal()->GetIsNullPrincipal()) {
+    promise->MaybeRejectWithUndefined();
+    return promise.forget();
+  }
+
+  // Only enforce third-party checks when there is a reason to enforce them.
+  if (StaticPrefs::network_cookie_cookieBehavior() !=
+        nsICookieService::BEHAVIOR_ACCEPT) {
+    // Step 3. If the document's frame is the main frame, resolve.
+    if (IsTopLevelContentDocument()) {
+      promise->MaybeResolveWithUndefined();
+      return promise.forget();
+    }
+
+    // Step 4. If the sub frame's origin is equal to the main frame's, resolve.
+    nsCOMPtr<nsIDocument> topLevelDoc = GetTopLevelContentDocument();
+    if (!topLevelDoc) {
+      aRv.Throw(NS_ERROR_NOT_AVAILABLE);
+      return nullptr;
+    }
+    if (topLevelDoc->NodePrincipal()->Equals(NodePrincipal())) {
+      promise->MaybeResolveWithUndefined();
+      return promise.forget();
+    }
+  }
+
+  // Step 5. If the sub frame is not sandboxed, skip to step 7.
+  // Step 6. If the sub frame doesn't have the token
+  //         "allow-storage-access-by-user-activation", reject.
+  if (mSandboxFlags & SANDBOXED_STORAGE_ACCESS) {
+    promise->MaybeRejectWithUndefined();
+    return promise.forget();
+  }
+
+  // Step 7. If the sub frame's parent frame is not the top frame, reject.
+  nsIDocument* parent = GetParentDocument();
+  if (parent && !parent->IsTopLevelContentDocument()) {
+    promise->MaybeRejectWithUndefined();
+    return promise.forget();
+  }
+
+  // Step 8. If the browser is not processing a user gesture, reject.
+  if (!EventStateManager::IsHandlingUserInput()) {
+    promise->MaybeRejectWithUndefined();
+    return promise.forget();
+  }
+
+  // Step 9. Check any additional rules that the browser has.
+  //         Examples: Whitelists, blacklists, on-device classification,
+  //         user settings, anti-clickjacking heuristics, or prompting the
+  //         user for explicit permission. Reject if some rule is not fulfilled.
+
+  if (nsContentUtils::IsInPrivateBrowsing(this)) {
+    // If the document is in PB mode, it doesn't have access to its persistent
+    // cookie jar, so reject the promise here.
+    promise->MaybeRejectWithUndefined();
+    return promise.forget();
+  }
+
+  bool granted = true;
+  bool isTrackingWindow = false;
+  if (StaticPrefs::browser_contentblocking_enabled() &&
+      StaticPrefs::network_cookie_cookieBehavior() ==
+        nsICookieService::BEHAVIOR_REJECT_TRACKER) {
+    // Only do something special for third-party tracking content.
+    if (nsContentUtils::StorageDisabledByAntiTracking(this, nullptr)) {
+      // Note: If this has returned true, the top-level document is guaranteed
+      // to not be on the Content Blocking allow list.
+      DebugOnly<bool> isOnAllowList = false;
+      MOZ_ASSERT_IF(NS_SUCCEEDED(AntiTrackingCommon::IsOnContentBlockingAllowList(
+                                   parent->GetDocumentURI(), isOnAllowList)),
+                    !isOnAllowList);
+
+      isTrackingWindow = true;
+      // TODO: prompt for permission
+    }
+  }
+
+  // Step 10. Grant the document access to cookies and store that fact for
+  //          the purposes of future calls to hasStorageAccess() and
+  //          requestStorageAccess().
+  if (granted && inner) {
+    outer->SetHasStorageAccess(true);
+    if (isTrackingWindow) {
+      nsCOMPtr<nsIURI> uri = GetDocumentURI();
+      if (NS_WARN_IF(!uri)) {
+        aRv.Throw(NS_ERROR_NOT_AVAILABLE);
+        return nullptr;
+      }
+      nsAutoString origin;
+      nsresult rv = nsContentUtils::GetUTFOrigin(uri, origin);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        aRv.Throw(rv);
+        return nullptr;
+      }
+      AntiTrackingCommon::AddFirstPartyStorageAccessGrantedFor(origin,
+                                                               inner,
+                                                               AntiTrackingCommon::eStorageAccessAPI)
+        ->Then(GetCurrentThreadSerialEventTarget(), __func__,
+               [promise] (bool) {
+                 promise->MaybeResolveWithUndefined();
+               },
+               [promise] (bool) {
+                 promise->MaybeRejectWithUndefined();
+               });
+    } else {
+      promise->MaybeResolveWithUndefined();
+    }
+  }
+  return promise.forget();
+}
+
 void
 nsIDocument::RecordNavigationTiming(ReadyState aReadyState)
 {
   if (!XRE_IsContentProcess()) {
     return;
   }
   if (!IsTopLevelContentDocument()) {
     return;
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -6269,16 +6269,22 @@ nsGlobalWindowInner::GetTopLevelPrincipa
   }
 
   return topLevelPrincipal;
 }
 
 nsIPrincipal*
 nsGlobalWindowInner::GetTopLevelStorageAreaPrincipal()
 {
+  if (mDoc && ((mDoc->GetSandboxFlags() & SANDBOXED_STORAGE_ACCESS) != 0 ||
+               nsContentUtils::IsInPrivateBrowsing(mDoc))) {
+    // Storage access is disabled
+    return nullptr;
+  }
+
   nsPIDOMWindowOuter* outerWindow = GetParentInternal();
   if (!outerWindow) {
     // No outer window available!
     return nullptr;
   }
 
   if (!outerWindow->IsTopLevelWindow()) {
     return nullptr;
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -11,16 +11,17 @@
 #include "mozilla/MemoryReporting.h"
 
 // Local Includes
 #include "Navigator.h"
 #include "nsContentSecurityManager.h"
 #include "nsScreen.h"
 #include "nsHistory.h"
 #include "nsDOMNavigationTiming.h"
+#include "nsICookieService.h"
 #include "nsIDOMStorageManager.h"
 #include "nsISecureBrowserUI.h"
 #include "nsIWebProgressListener.h"
 #include "mozilla/AntiTrackingCommon.h"
 #include "mozilla/dom/ContentFrameMessageManager.h"
 #include "mozilla/dom/EventTarget.h"
 #include "mozilla/dom/LocalStorage.h"
 #include "mozilla/dom/Storage.h"
@@ -86,16 +87,17 @@
 #include "mozilla/dom/ToJSValue.h"
 #include "nsJSPrincipals.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Debug.h"
 #include "mozilla/EventListenerManager.h"
 #include "mozilla/EventStates.h"
 #include "mozilla/MouseEvents.h"
 #include "mozilla/ProcessHangMonitor.h"
+#include "mozilla/StaticPrefs.h"
 #include "mozilla/ThrottledEventQueue.h"
 #include "AudioChannelService.h"
 #include "nsAboutProtocolUtils.h"
 #include "nsCharTraits.h" // NS_IS_HIGH/LOW_SURROGATE
 #include "PostMessageEvent.h"
 #include "mozilla/dom/DocGroup.h"
 #include "mozilla/dom/TabGroup.h"
 
@@ -824,16 +826,17 @@ nsGlobalWindowOuter::nsGlobalWindowOuter
     mHadOriginalOpener(false),
     mIsPopupSpam(false),
     mBlockScriptedClosingFlag(false),
     mWasOffline(false),
     mCreatingInnerWindow(false),
     mIsChrome(false),
     mAllowScriptsToClose(false),
     mTopLevelOuterContentWindow(false),
+    mHasStorageAccess(false),
     mSerial(0),
 #ifdef DEBUG
     mSetOpenerWindowCalled(false),
 #endif
     mCleanedUp(false),
 #ifdef DEBUG
     mIsValidatingTabGroup(false),
 #endif
@@ -2010,16 +2013,35 @@ nsGlobalWindowOuter::SetNewDocument(nsID
 
   PreloadLocalStorage();
 
   // If we have a recorded interesting Large-Allocation header status, report it
   // to the newly attached document.
   ReportLargeAllocStatus();
   mLargeAllocStatus = LargeAllocStatus::NONE;
 
+  mHasStorageAccess = false;
+  nsIURI* uri = aDocument->GetDocumentURI();
+  if (newInnerWindow) {
+    if (StaticPrefs::browser_contentblocking_enabled() &&
+        StaticPrefs::network_cookie_cookieBehavior() ==
+          nsICookieService::BEHAVIOR_REJECT_TRACKER &&
+        nsContentUtils::IsThirdPartyWindowOrChannel(newInnerWindow, nullptr,
+                                                    uri) &&
+        nsContentUtils::IsTrackingResourceWindow(newInnerWindow)) {
+      // Grant storage access by default if the first-party storage access
+      // permission has been granted already.
+      // Don't notify in this case, since we would be notifying the user needlessly.
+      mHasStorageAccess =
+        AntiTrackingCommon::IsFirstPartyStorageAccessGrantedFor(newInnerWindow,
+                                                                uri,
+                                                                nullptr);
+    }
+  }
+
   return NS_OK;
 }
 
 void
 nsGlobalWindowOuter::PreloadLocalStorage()
 {
   if (!Storage::StoragePrefIsEnabled()) {
     return;
@@ -2919,17 +2941,17 @@ nsGlobalWindowOuter::IndexedGetterOuter(
 
   return windows->IndexedGetter(aIndex);
 }
 
 nsIControllers*
 nsGlobalWindowOuter::GetControllersOuter(ErrorResult& aError)
 {
   if (!mControllers) {
-    mControllers = NS_NewXULControllers();
+    mControllers = new nsXULControllers();
     if (!mControllers) {
       aError.Throw(NS_ERROR_FAILURE);
       return nullptr;
     }
 
     // Add in the default controller
     nsCOMPtr<nsIController> controller =
       nsBaseCommandController::CreateWindowController();
--- a/dom/base/nsGlobalWindowOuter.h
+++ b/dom/base/nsGlobalWindowOuter.h
@@ -959,16 +959,25 @@ public:
   bool WindowExists(const nsAString& aName, bool aForceNoOpener,
                     bool aLookForCallerOnJSStack);
 
   already_AddRefed<nsIWidget> GetMainWidget();
   nsIWidget* GetNearestWidget() const;
 
   bool IsInModalState();
 
+  bool HasStorageAccess() const
+  {
+    return mHasStorageAccess;
+  }
+  void SetHasStorageAccess(bool aHasStorageAccess)
+  {
+    mHasStorageAccess = aHasStorageAccess;
+  }
+
   // Convenience functions for the many methods that need to scale
   // from device to CSS pixels or vice versa.  Note: if a presentation
   // context is not available, they will assume a 1:1 ratio.
   int32_t DevToCSSIntPixels(int32_t px);
   int32_t CSSToDevIntPixels(int32_t px);
   nsIntSize DevToCSSIntPixels(nsIntSize px);
   nsIntSize CSSToDevIntPixels(nsIntSize px);
 
@@ -1083,16 +1092,19 @@ protected:
   bool                          mIsChrome : 1;
 
   // whether scripts may close the window,
   // even if "dom.allow_scripts_to_close_windows" is false.
   bool                   mAllowScriptsToClose : 1;
 
   bool mTopLevelOuterContentWindow : 1;
 
+  // whether storage access has been granted to this frame.
+  bool mHasStorageAccess : 1;
+
   nsCOMPtr<nsIScriptContext>    mContext;
   nsWeakPtr                     mOpener;
   nsCOMPtr<nsIControllers>      mControllers;
 
   // For |window.arguments|, via |openDialog|.
   nsCOMPtr<nsIArray>            mArguments;
 
   RefPtr<nsDOMWindowList>     mFrames;
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -1233,16 +1233,21 @@ public:
 
   /**
    * Return the root element for this document.
    */
   Element* GetRootElement() const;
 
   mozilla::dom::Selection* GetSelection(mozilla::ErrorResult& aRv);
 
+  already_AddRefed<mozilla::dom::Promise>
+    HasStorageAccess(mozilla::ErrorResult& aRv);
+  already_AddRefed<mozilla::dom::Promise>
+    RequestStorageAccess(mozilla::ErrorResult& aRv);
+
   /**
    * Gets the event target to dispatch key events to if there is no focused
    * content in the document.
    */
   virtual nsIContent* GetUnfocusedKeyEventTarget();
 
   /**
    * Retrieve information about the viewport as a data structure.
--- a/dom/base/nsJSTimeoutHandler.cpp
+++ b/dom/base/nsJSTimeoutHandler.cpp
@@ -4,16 +4,17 @@
  * 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 <algorithm>
 
 #include "mozilla/Attributes.h"
 #include "mozilla/Likely.h"
 #include "mozilla/Maybe.h"
+#include "mozilla/dom/CSPEvalChecker.h"
 #include "mozilla/dom/FunctionBinding.h"
 #include "mozilla/dom/WorkerPrivate.h"
 #include "nsCOMPtr.h"
 #include "nsContentUtils.h"
 #include "nsError.h"
 #include "nsGlobalWindow.h"
 #include "nsIContentSecurityPolicy.h"
 #include "nsIDocument.h"
@@ -40,17 +41,18 @@ public:
                            ErrorResult& aError);
   nsJSScriptTimeoutHandler(JSContext* aCx, nsGlobalWindowInner* aWindow,
                            const nsAString& aExpression, bool* aAllowEval,
                            ErrorResult& aError);
   nsJSScriptTimeoutHandler(JSContext* aCx, WorkerPrivate* aWorkerPrivate,
                            Function& aFunction,
                            nsTArray<JS::Heap<JS::Value>>&& aArguments);
   nsJSScriptTimeoutHandler(JSContext* aCx, WorkerPrivate* aWorkerPrivate,
-                           const nsAString& aExpression);
+                           const nsAString& aExpression, bool* aAllowEval,
+                           ErrorResult& aRv);
 
   virtual const nsAString& GetHandlerText() override;
 
   virtual Function* GetCallback() override
   {
     return mFunction;
   }
 
@@ -158,64 +160,16 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
   NS_INTERFACE_MAP_ENTRY(nsIScriptTimeoutHandler)
   NS_INTERFACE_MAP_ENTRY(nsITimeoutHandler)
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(nsJSScriptTimeoutHandler)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsJSScriptTimeoutHandler)
 
-static bool
-CheckCSPForEval(JSContext* aCx, nsGlobalWindowInner* aWindow,
-                const nsAString& aExpression, ErrorResult& aError)
-{
-  // if CSP is enabled, and setTimeout/setInterval was called with a string,
-  // disable the registration and log an error
-  nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc();
-  if (!doc) {
-    // if there's no document, we don't have to do anything.
-    return true;
-  }
-
-  nsCOMPtr<nsIContentSecurityPolicy> csp;
-  aError = doc->NodePrincipal()->GetCsp(getter_AddRefs(csp));
-  if (aError.Failed()) {
-    return false;
-  }
-
-  if (!csp) {
-    return true;
-  }
-
-  bool allowsEval = true;
-  bool reportViolation = false;
-  aError = csp->GetAllowsEval(&reportViolation, &allowsEval);
-  if (aError.Failed()) {
-    return false;
-  }
-
-  if (reportViolation) {
-    // Get the calling location.
-    uint32_t lineNum = 0;
-    uint32_t columnNum = 0;
-    nsAutoString fileNameString;
-    if (!nsJSUtils::GetCallingLocation(aCx, fileNameString, &lineNum,
-                                       &columnNum)) {
-      fileNameString.AssignLiteral("unknown");
-    }
-
-    csp->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL,
-                             nullptr, // triggering element
-                             fileNameString, aExpression, lineNum, columnNum,
-                             EmptyString(), EmptyString());
-  }
-
-  return allowsEval;
-}
-
 nsJSScriptTimeoutHandler::nsJSScriptTimeoutHandler()
   : mLineNo(0)
   , mColumn(0)
 {
 }
 
 nsJSScriptTimeoutHandler::nsJSScriptTimeoutHandler(JSContext* aCx,
                                                    nsGlobalWindowInner *aWindow,
@@ -247,18 +201,19 @@ nsJSScriptTimeoutHandler::nsJSScriptTime
 {
   if (!aWindow->GetContextInternal() || !aWindow->FastGetGlobalJSObject()) {
     // This window was already closed, or never properly initialized,
     // don't let a timer be scheduled on such a window.
     aError.Throw(NS_ERROR_NOT_INITIALIZED);
     return;
   }
 
-  *aAllowEval = CheckCSPForEval(aCx, aWindow, aExpression, aError);
-  if (aError.Failed() || !*aAllowEval) {
+  aError = CSPEvalChecker::CheckForWindow(aCx, aWindow, aExpression,
+                                          aAllowEval);
+  if (NS_WARN_IF(aError.Failed()) || !*aAllowEval) {
     return;
   }
 
   Init(aCx);
 }
 
 nsJSScriptTimeoutHandler::nsJSScriptTimeoutHandler(JSContext* aCx,
                                                    WorkerPrivate* aWorkerPrivate,
@@ -271,24 +226,32 @@ nsJSScriptTimeoutHandler::nsJSScriptTime
   MOZ_ASSERT(aWorkerPrivate);
   aWorkerPrivate->AssertIsOnWorkerThread();
 
   Init(aCx, std::move(aArguments));
 }
 
 nsJSScriptTimeoutHandler::nsJSScriptTimeoutHandler(JSContext* aCx,
                                                    WorkerPrivate* aWorkerPrivate,
-                                                   const nsAString& aExpression)
+                                                   const nsAString& aExpression,
+                                                   bool* aAllowEval,
+                                                   ErrorResult& aError)
   : mLineNo(0)
   , mColumn(0)
   , mExpr(aExpression)
 {
   MOZ_ASSERT(aWorkerPrivate);
   aWorkerPrivate->AssertIsOnWorkerThread();
 
+  aError = CSPEvalChecker::CheckForWorker(aCx, aWorkerPrivate, aExpression,
+                                          aAllowEval);
+  if (NS_WARN_IF(aError.Failed()) || !*aAllowEval) {
+    return;
+  }
+
   Init(aCx);
 }
 
 nsJSScriptTimeoutHandler::~nsJSScriptTimeoutHandler()
 {
   ReleaseJSObjects();
 }
 
@@ -371,14 +334,20 @@ NS_CreateJSTimeoutHandler(JSContext *aCx
 
   RefPtr<nsJSScriptTimeoutHandler> handler =
     new nsJSScriptTimeoutHandler(aCx, aWorkerPrivate, aFunction, std::move(args));
   return handler.forget();
 }
 
 already_AddRefed<nsIScriptTimeoutHandler>
 NS_CreateJSTimeoutHandler(JSContext* aCx, WorkerPrivate* aWorkerPrivate,
-                          const nsAString& aExpression)
+                          const nsAString& aExpression, ErrorResult& aRv)
 {
+  bool allowEval = false;
   RefPtr<nsJSScriptTimeoutHandler> handler =
-    new nsJSScriptTimeoutHandler(aCx, aWorkerPrivate, aExpression);
+    new nsJSScriptTimeoutHandler(aCx, aWorkerPrivate, aExpression, &allowEval,
+                                 aRv);
+  if (aRv.Failed() || !allowEval) {
+    return nullptr;
+  }
+
   return handler.forget();
 }
--- a/dom/base/nsNodeInfoManager.cpp
+++ b/dom/base/nsNodeInfoManager.cpp
@@ -42,17 +42,17 @@ static const uint32_t kInitialNodeInfoHa
 
 nsNodeInfoManager::nsNodeInfoManager()
   : mNodeInfoHash(kInitialNodeInfoHashSize),
     mDocument(nullptr),
     mNonDocumentNodeInfos(0),
     mTextNodeInfo(nullptr),
     mCommentNodeInfo(nullptr),
     mDocumentNodeInfo(nullptr),
-    mRecentlyUsedNodeInfos{},
+    mRecentlyUsedNodeInfos(),
     mSVGEnabled(eTriUnset),
     mMathMLEnabled(eTriUnset)
 {
   nsLayoutStatics::AddRef();
 
   if (gNodeInfoManagerLeakPRLog)
     MOZ_LOG(gNodeInfoManagerLeakPRLog, LogLevel::Debug,
            ("NODEINFOMANAGER %p created", this));
@@ -149,20 +149,19 @@ nsNodeInfoManager::GetNodeInfo(nsAtom *a
                                int32_t aNamespaceID, uint16_t aNodeType,
                                nsAtom* aExtraName /* = nullptr */)
 {
   CheckValidNodeInfo(aNodeType, aName, aNamespaceID, aExtraName);
 
   NodeInfo::NodeInfoInner tmpKey(aName, aPrefix, aNamespaceID, aNodeType,
                                  aExtraName);
 
-  uint32_t index = tmpKey.Hash() % RECENTLY_USED_NODEINFOS_SIZE;
-  NodeInfo* ni = mRecentlyUsedNodeInfos[index];
-  if (ni && tmpKey == ni->mInner) {
-    RefPtr<NodeInfo> nodeInfo = ni;
+  auto p = mRecentlyUsedNodeInfos.Lookup(tmpKey);
+  if (p) {
+    RefPtr<NodeInfo> nodeInfo = p.Data();
     return nodeInfo.forget();
   }
 
   // We don't use LookupForAdd here as that would end up storing the temporary
   // key instead of using `mInner`.
   RefPtr<NodeInfo> nodeInfo = mNodeInfoHash.Get(&tmpKey);
   if (!nodeInfo) {
     ++mNonDocumentNodeInfos;
@@ -171,17 +170,17 @@ nsNodeInfoManager::GetNodeInfo(nsAtom *a
     }
 
     nodeInfo = new NodeInfo(aName, aPrefix, aNamespaceID, aNodeType, aExtraName, this);
     mNodeInfoHash.Put(&nodeInfo->mInner, nodeInfo);
   }
 
   // Have to do the swap thing, because already_AddRefed<nsNodeInfo>
   // doesn't cast to already_AddRefed<mozilla::dom::NodeInfo>
-  mRecentlyUsedNodeInfos[index] = nodeInfo;
+  p.Set(nodeInfo);
   return nodeInfo.forget();
 }
 
 
 nsresult
 nsNodeInfoManager::GetNodeInfo(const nsAString& aName, nsAtom *aPrefix,
                                int32_t aNamespaceID, uint16_t aNodeType,
                                NodeInfo** aNodeInfo)
@@ -191,37 +190,36 @@ nsNodeInfoManager::GetNodeInfo(const nsA
   {
     RefPtr<nsAtom> nameAtom = NS_Atomize(aName);
     CheckValidNodeInfo(aNodeType, nameAtom, aNamespaceID, nullptr);
   }
 #endif
 
   NodeInfo::NodeInfoInner tmpKey(aName, aPrefix, aNamespaceID, aNodeType);
 
-  uint32_t index = tmpKey.Hash() % RECENTLY_USED_NODEINFOS_SIZE;
-  NodeInfo* ni = mRecentlyUsedNodeInfos[index];
-  if (ni && ni->mInner == tmpKey) {
-    RefPtr<NodeInfo> nodeInfo = ni;
+  auto p = mRecentlyUsedNodeInfos.Lookup(tmpKey);
+  if (p) {
+    RefPtr<NodeInfo> nodeInfo = p.Data();
     nodeInfo.forget(aNodeInfo);
     return NS_OK;
   }
 
   RefPtr<NodeInfo> nodeInfo = mNodeInfoHash.Get(&tmpKey);
   if (!nodeInfo) {
     ++mNonDocumentNodeInfos;
     if (mNonDocumentNodeInfos == 1) {
       NS_IF_ADDREF(mDocument);
     }
 
     RefPtr<nsAtom> nameAtom = NS_Atomize(aName);
     nodeInfo = new NodeInfo(nameAtom, aPrefix, aNamespaceID, aNodeType, nullptr, this);
     mNodeInfoHash.Put(&nodeInfo->mInner, nodeInfo);
   }
 
-  mRecentlyUsedNodeInfos[index] = nodeInfo;
+  p.Set(nodeInfo);
   nodeInfo.forget(aNodeInfo);
 
   return NS_OK;
 }
 
 
 nsresult
 nsNodeInfoManager::GetNodeInfo(const nsAString& aName, nsAtom *aPrefix,
@@ -336,21 +334,17 @@ nsNodeInfoManager::RemoveNodeInfo(NodeIn
     if (aNodeInfo == mTextNodeInfo) {
       mTextNodeInfo = nullptr;
     }
     else if (aNodeInfo == mCommentNodeInfo) {
       mCommentNodeInfo = nullptr;
     }
   }
 
-  uint32_t index = aNodeInfo->mInner.Hash() % RECENTLY_USED_NODEINFOS_SIZE;
-  if (mRecentlyUsedNodeInfos[index] == aNodeInfo) {
-    mRecentlyUsedNodeInfos[index] = nullptr;
-  }
-
+  mRecentlyUsedNodeInfos.Remove(aNodeInfo->mInner);
   DebugOnly<bool> ret = mNodeInfoHash.Remove(&aNodeInfo->mInner);
   MOZ_ASSERT(ret, "Can't find mozilla::dom::NodeInfo to remove!!!");
 }
 
 bool
 nsNodeInfoManager::InternalSVGEnabled()
 {
   // If the svg.disabled pref. is true, convert all SVG nodes into
--- a/dom/base/nsNodeInfoManager.h
+++ b/dom/base/nsNodeInfoManager.h
@@ -8,30 +8,29 @@
  * A class for handing out nodeinfos and ensuring sharing of them as needed.
  */
 
 #ifndef nsNodeInfoManager_h___
 #define nsNodeInfoManager_h___
 
 #include "mozilla/Attributes.h"           // for final
 #include "mozilla/dom/NodeInfo.h"
+#include "mozilla/MruCache.h"
 #include "nsCOMPtr.h"                     // for member
 #include "nsCycleCollectionParticipant.h" // for NS_DECL_CYCLE_*
 #include "nsDataHashtable.h"
 #include "nsStringFwd.h"
 
 class nsBindingManager;
 class nsAtom;
 class nsIDocument;
 class nsIPrincipal;
 class nsWindowSizes;
 template<class T> struct already_AddRefed;
 
-#define RECENTLY_USED_NODEINFOS_SIZE 31
-
 class nsNodeInfoManager final
 {
 private:
   ~nsNodeInfoManager();
 
 public:
   nsNodeInfoManager();
 
@@ -149,23 +148,40 @@ private:
   {
   public:
     explicit NodeInfoInnerKey(KeyTypePointer aKey) : nsPtrHashKey(aKey) {}
     ~NodeInfoInnerKey() = default;
     bool KeyEquals(KeyTypePointer aKey) const { return *mKey == *aKey; }
     static PLDHashNumber HashKey(KeyTypePointer aKey) { return aKey->Hash(); }
   };
 
+  struct NodeInfoCache : public mozilla::MruCache<
+                              mozilla::dom::NodeInfo::NodeInfoInner,
+                              mozilla::dom::NodeInfo*,
+                              NodeInfoCache>
+  {
+    static mozilla::HashNumber Hash(
+        const mozilla::dom::NodeInfo::NodeInfoInner& aKey)
+    {
+      return aKey.Hash();
+    }
+    static bool Match(const mozilla::dom::NodeInfo::NodeInfoInner& aKey,
+                      const mozilla::dom::NodeInfo* aVal)
+    {
+      return aKey == aVal->mInner;
+    }
+  };
+
   nsDataHashtable<NodeInfoInnerKey, mozilla::dom::NodeInfo*> mNodeInfoHash;
   nsIDocument * MOZ_NON_OWNING_REF mDocument; // WEAK
   uint32_t mNonDocumentNodeInfos;
   nsCOMPtr<nsIPrincipal> mPrincipal; // Never null after Init() succeeds.
   nsCOMPtr<nsIPrincipal> mDefaultPrincipal; // Never null after Init() succeeds
   mozilla::dom::NodeInfo * MOZ_NON_OWNING_REF mTextNodeInfo; // WEAK to avoid circular ownership
   mozilla::dom::NodeInfo * MOZ_NON_OWNING_REF mCommentNodeInfo; // WEAK to avoid circular ownership
   mozilla::dom::NodeInfo * MOZ_NON_OWNING_REF mDocumentNodeInfo; // WEAK to avoid circular ownership
   RefPtr<nsBindingManager> mBindingManager;
-  mozilla::dom::NodeInfo* mRecentlyUsedNodeInfos[RECENTLY_USED_NODEINFOS_SIZE];
+  NodeInfoCache mRecentlyUsedNodeInfos;
   Tri mSVGEnabled;
   Tri mMathMLEnabled;
 };
 
 #endif /* nsNodeInfoManager_h___ */
--- a/dom/base/nsSandboxFlags.h
+++ b/dom/base/nsSandboxFlags.h
@@ -107,10 +107,15 @@ const unsigned long SANDBOX_PROPAGATES_T
  */
 const unsigned long SANDBOXED_ORIENTATION_LOCK = 0x2000;
 
 /**
  * This flag disables the Presentation API.
  */
 const unsigned long SANDBOXED_PRESENTATION = 0x4000;
 
-const unsigned long SANDBOX_ALL_FLAGS = 0x7FFF;
+/**
+ * This flag disables access to the first-party storage area by user activation.
+ */
+const unsigned long SANDBOXED_STORAGE_ACCESS = 0x8000;
+
+const unsigned long SANDBOX_ALL_FLAGS = 0xFFFF;
 #endif
--- a/dom/commandhandler/moz.build
+++ b/dom/commandhandler/moz.build
@@ -22,17 +22,16 @@ XPIDL_SOURCES += [
     'nsIControllerContext.idl',
     'nsPICommandUpdater.idl',
 ]
 
 XPIDL_MODULE = 'commandhandler'
 
 UNIFIED_SOURCES += [
     'nsBaseCommandController.cpp',
-    'nsCommandGroup.cpp',
     'nsCommandManager.cpp',
     'nsCommandParams.cpp',
     'nsControllerCommandTable.cpp',
 ]
 
 LOCAL_INCLUDES += [
     '/dom/base',
 ]
deleted file mode 100644
--- a/dom/commandhandler/nsCommandGroup.cpp
+++ /dev/null
@@ -1,296 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-#include "nsString.h"
-#include "nsReadableUtils.h"
-#include "nsTArray.h"
-#include "nsSimpleEnumerator.h"
-#include "nsXPCOM.h"
-#include "nsSupportsPrimitives.h"
-#include "nsIComponentManager.h"
-#include "nsCommandGroup.h"
-#include "nsIControllerCommand.h"
-#include "nsCRT.h"
-
-class nsGroupsEnumerator : public nsSimpleEnumerator
-{
-public:
-  explicit nsGroupsEnumerator(
-    nsControllerCommandGroup::GroupsHashtable& aInHashTable);
-
-  NS_DECL_NSISIMPLEENUMERATOR
-
-  const nsID& DefaultInterface() override
-  {
-    return NS_GET_IID(nsISupportsCString);
-  }
-
-protected:
-  ~nsGroupsEnumerator() override;
-
-  nsresult Initialize();
-
-protected:
-  nsControllerCommandGroup::GroupsHashtable& mHashTable;
-  int32_t mIndex;
-  const char** mGroupNames;  // array of pointers to char16_t* in the hash table
-  bool mInitted;
-};
-
-nsGroupsEnumerator::nsGroupsEnumerator(
-      nsControllerCommandGroup::GroupsHashtable& aInHashTable)
-  : mHashTable(aInHashTable)
-  , mIndex(-1)
-  , mGroupNames(nullptr)
-  , mInitted(false)
-{
-}
-
-nsGroupsEnumerator::~nsGroupsEnumerator()
-{
-  delete[] mGroupNames;
-}
-
-NS_IMETHODIMP
-nsGroupsEnumerator::HasMoreElements(bool* aResult)
-{
-  nsresult rv = NS_OK;
-
-  NS_ENSURE_ARG_POINTER(aResult);
-
-  if (!mInitted) {
-    rv = Initialize();
-    if (NS_FAILED(rv)) {
-      return rv;
-    }
-  }
-
-  *aResult = (mIndex < static_cast<int32_t>(mHashTable.Count()) - 1);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsGroupsEnumerator::GetNext(nsISupports** aResult)
-{
-  nsresult rv = NS_OK;
-
-  NS_ENSURE_ARG_POINTER(aResult);
-
-  if (!mInitted) {
-    rv = Initialize();
-    if (NS_FAILED(rv)) {
-      return rv;
-    }
-  }
-
-  mIndex++;
-  if (mIndex >= static_cast<int32_t>(mHashTable.Count())) {
-    return NS_ERROR_FAILURE;
-  }
-
-  const char* thisGroupName = mGroupNames[mIndex];
-
-  nsCOMPtr<nsISupportsCString> supportsString =
-    do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID, &rv);
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
-
-  supportsString->SetData(nsDependentCString(thisGroupName));
-  return CallQueryInterface(supportsString, aResult);
-}
-
-nsresult
-nsGroupsEnumerator::Initialize()
-{
-  if (mInitted) {
-    return NS_OK;
-  }
-
-  mGroupNames = new const char*[mHashTable.Count()];
-  if (!mGroupNames) {
-    return NS_ERROR_OUT_OF_MEMORY;
-  }
-
-  mIndex = 0;
-  for (auto iter = mHashTable.Iter(); !iter.Done(); iter.Next()) {
-    mGroupNames[mIndex] = iter.Key().Data();
-    mIndex++;
-  }
-
-  mIndex = -1;
-  mInitted = true;
-  return NS_OK;
-}
-
-class nsNamedGroupEnumerator : public nsSimpleEnumerator
-{
-public:
-  explicit nsNamedGroupEnumerator(nsTArray<nsCString>* aInArray);
-
-  NS_DECL_NSISIMPLEENUMERATOR
-
-  const nsID& DefaultInterface() override
-  {
-    return NS_GET_IID(nsISupportsCString);
-  }
-
-protected:
-  ~nsNamedGroupEnumerator() override;
-
-  nsTArray<nsCString>* mGroupArray;
-  int32_t mIndex;
-};
-
-nsNamedGroupEnumerator::nsNamedGroupEnumerator(nsTArray<nsCString>* aInArray)
-  : mGroupArray(aInArray)
-  , mIndex(-1)
-{
-}
-
-nsNamedGroupEnumerator::~nsNamedGroupEnumerator()
-{
-}
-
-NS_IMETHODIMP
-nsNamedGroupEnumerator::HasMoreElements(bool* aResult)
-{
-  NS_ENSURE_ARG_POINTER(aResult);
-
-  int32_t arrayLen = mGroupArray ? mGroupArray->Length() : 0;
-  *aResult = (mIndex < arrayLen - 1);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsNamedGroupEnumerator::GetNext(nsISupports** aResult)
-{
-  NS_ENSURE_ARG_POINTER(aResult);
-
-  if (!mGroupArray) {
-    return NS_ERROR_FAILURE;
-  }
-
-  mIndex++;
-  if (mIndex >= int32_t(mGroupArray->Length())) {
-    return NS_ERROR_FAILURE;
-  }
-
-  const nsCString& thisGroupName = mGroupArray->ElementAt(mIndex);
-
-  nsresult rv;
-  nsCOMPtr<nsISupportsCString> supportsString =
-    do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID, &rv);
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
-
-  supportsString->SetData(thisGroupName);
-  return CallQueryInterface(supportsString, aResult);
-}
-
-NS_IMPL_ISUPPORTS(nsControllerCommandGroup, nsIControllerCommandGroup)
-
-nsControllerCommandGroup::nsControllerCommandGroup()
-{
-}
-
-nsControllerCommandGroup::~nsControllerCommandGroup()
-{
-  ClearGroupsHash();
-}
-
-void
-nsControllerCommandGroup::ClearGroupsHash()
-{
-  mGroupsHash.Clear();
-}
-
-NS_IMETHODIMP
-nsControllerCommandGroup::AddCommandToGroup(const char* aCommand,
-                                            const char* aGroup)
-{
-  nsDependentCString groupKey(aGroup);
-  auto commandList = mGroupsHash.LookupForAdd(groupKey).OrInsert([]() {
-      return new AutoTArray<nsCString, 8>();
-    });
-
-#ifdef DEBUG
-  nsCString* appended =
-#endif
-  commandList->AppendElement(aCommand);
-  NS_ASSERTION(appended, "Append failed");
-
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsControllerCommandGroup::RemoveCommandFromGroup(const char* aCommand,
-                                                 const char* aGroup)
-{
-  nsDependentCString groupKey(aGroup);
-  nsTArray<nsCString>* commandList = mGroupsHash.Get(groupKey);
-  if (!commandList) {
-    return NS_OK; // no group
-  }
-
-  uint32_t numEntries = commandList->Length();
-  for (uint32_t i = 0; i < numEntries; i++) {
-    nsCString commandString = commandList->ElementAt(i);
-    if (nsDependentCString(aCommand) != commandString) {
-      commandList->RemoveElementAt(i);
-      break;
-    }
-  }
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsControllerCommandGroup::IsCommandInGroup(const char* aCommand,
-                                           const char* aGroup, bool* aResult)
-{
-  NS_ENSURE_ARG_POINTER(aResult);
-  *aResult = false;
-
-  nsDependentCString groupKey(aGroup);
-  nsTArray<nsCString>* commandList = mGroupsHash.Get(groupKey);
-  if (!commandList) {
-    return NS_OK; // no group
-  }
-
-  uint32_t numEntries = commandList->Length();
-  for (uint32_t i = 0; i < numEntries; i++) {
-    nsCString commandString = commandList->ElementAt(i);
-    if (nsDependentCString(aCommand) != commandString) {
-      *aResult = true;
-      break;
-    }
-  }
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsControllerCommandGroup::GetGroupsEnumerator(nsISimpleEnumerator** aResult)
-{
-  RefPtr<nsGroupsEnumerator> groupsEnum = new nsGroupsEnumerator(mGroupsHash);
-
-  groupsEnum.forget(aResult);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsControllerCommandGroup::GetEnumeratorForGroup(const char* aGroup,
-                                                nsISimpleEnumerator** aResult)
-{
-  nsDependentCString groupKey(aGroup);
-  nsTArray<nsCString>* commandList = mGroupsHash.Get(groupKey); // may be null
-
-  RefPtr<nsNamedGroupEnumerator> theGroupEnum =
-    new nsNamedGroupEnumerator(commandList);
-
-  theGroupEnum.forget(aResult);
-  return NS_OK;
-}
deleted file mode 100644
--- a/dom/commandhandler/nsCommandGroup.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-#ifndef nsCommandGroup_h__
-#define nsCommandGroup_h__
-
-#include "nsIController.h"
-#include "nsClassHashtable.h"
-#include "nsHashKeys.h"
-
-// {ecd55a01-2780-11d5-a73c-ca641a6813bc}
-#define NS_CONTROLLER_COMMAND_GROUP_CID \
-  { 0xecd55a01, 0x2780, 0x11d5, { 0xa7, 0x3c, 0xca, 0x64, 0x1a, 0x68, 0x13, 0xbc } }
-
-#define NS_CONTROLLER_COMMAND_GROUP_CONTRACTID \
-  "@mozilla.org/embedcomp/controller-command-group;1"
-
-class nsControllerCommandGroup : public nsIControllerCommandGroup
-{
-public:
-  nsControllerCommandGroup();
-
-  NS_DECL_ISUPPORTS
-  NS_DECL_NSICONTROLLERCOMMANDGROUP
-
-public:
-  typedef nsClassHashtable<nsCStringHashKey, nsTArray<nsCString>>
-    GroupsHashtable;
-
-protected:
-  virtual ~nsControllerCommandGroup();
-
-  void ClearGroupsHash();
-
-protected:
-  // Hash keyed on command group. This could be made more space-efficient,
-  // maybe with atoms.
-  GroupsHashtable mGroupsHash;
-};
-
-#endif // nsCommandGroup_h__
--- a/dom/commandhandler/nsICommandParams.idl
+++ b/dom/commandhandler/nsICommandParams.idl
@@ -86,14 +86,8 @@ interface nsICommandParams : nsISupports
    * In order to avoid circular dependency issues, these methods are defined
    * in nsCommandParams.h.  Consumers need to #include that header.
    */
   inline nsCommandParams* AsCommandParams();
   inline const nsCommandParams* AsCommandParams() const;
 %}
 };
 
-// {f7fa4581-238e-11d5-a73c-ab64fb68f2bc}
-%{C++
-#define NS_COMMAND_PARAMS_CID { 0xf7fa4581, 0x238e, 0x11d5, { 0xa7, 0x3c, 0xab, 0x64, 0xfb, 0x68, 0xf2, 0xbc } }
-#define NS_COMMAND_PARAMS_CONTRACTID "@mozilla.org/embedcomp/command-params;1"
-%}
-
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -5792,17 +5792,17 @@ HTMLInputElement::GetFiles(bool aRecursi
 nsIControllers*
 HTMLInputElement::GetControllers(ErrorResult& aRv)
 {
   //XXX: what about type "file"?
   if (IsSingleLineTextControl(false))
   {
     if (!mControllers)
     {
-      mControllers = NS_NewXULControllers();
+      mControllers = new nsXULControllers();
       if (!mControllers) {
         aRv.Throw(NS_ERROR_FAILURE);
         return nullptr;
       }
 
       nsCOMPtr<nsIController> controller =
         nsBaseCommandController::CreateEditorController();
       if (!controller) {
--- a/dom/html/HTMLTextAreaElement.cpp
+++ b/dom/html/HTMLTextAreaElement.cpp
@@ -618,17 +618,17 @@ HTMLTextAreaElement::IsDoneAddingChildre
 
 // Controllers Methods
 
 nsIControllers*
 HTMLTextAreaElement::GetControllers(ErrorResult& aError)
 {
   if (!mControllers)
   {
-    mControllers = NS_NewXULControllers();
+    mControllers = new nsXULControllers();
     if (!mControllers) {
       aError.Throw(NS_ERROR_FAILURE);
       return nullptr;
     }
 
     nsCOMPtr<nsIController> controller =
       nsBaseCommandController::CreateEditorController();
     if (!controller) {
new file mode 100644
--- /dev/null
+++ b/dom/security/CSPEvalChecker.cpp
@@ -0,0 +1,185 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/CSPEvalChecker.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/WorkerRunnable.h"
+#include "mozilla/ErrorResult.h"
+#include "nsGlobalWindowInner.h"
+#include "nsIDocument.h"
+#include "nsCOMPtr.h"
+#include "nsJSUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+namespace {
+
+nsresult
+CheckInternal(nsIContentSecurityPolicy* aCSP,
+              const nsAString& aExpression,
+              const nsAString& aFileNameString,
+              uint32_t aLineNum,
+              uint32_t aColumnNum,
+              bool* aAllowed)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aAllowed);
+
+  // The value is set at any "return", but better to have a default value here.
+  *aAllowed = false;
+
+  if (!aCSP) {
+    *aAllowed = true;
+    return NS_OK;
+  }
+
+  bool reportViolation = false;
+  nsresult rv = aCSP->GetAllowsEval(&reportViolation, aAllowed);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    *aAllowed = false;
+    return rv;
+  }
+
+  if (reportViolation) {
+    aCSP->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL,
+                              nullptr, // triggering element
+                              aFileNameString, aExpression, aLineNum,
+                              aColumnNum, EmptyString(), EmptyString());
+  }
+
+  return NS_OK;
+}
+
+class WorkerCSPCheckRunnable final : public WorkerMainThreadRunnable
+{
+public:
+  WorkerCSPCheckRunnable(WorkerPrivate* aWorkerPrivate,
+                         const nsAString& aExpression,
+                         const nsAString& aFileNameString,
+                         uint32_t aLineNum,
+                         uint32_t aColumnNum)
+    : WorkerMainThreadRunnable(aWorkerPrivate,
+                               NS_LITERAL_CSTRING("CSP Eval Check"))
+    , mExpression(aExpression)
+    , mFileNameString(aFileNameString)
+    , mLineNum(aLineNum)
+    , mColumnNum(aColumnNum)
+    , mEvalAllowed(false)
+  {}
+
+  bool
+  MainThreadRun() override
+  {
+    mResult = CheckInternal(mWorkerPrivate->GetCSP(), mExpression,
+                            mFileNameString, mLineNum, mColumnNum,
+                            &mEvalAllowed);
+    return true;
+  }
+
+  nsresult
+  GetResult(bool* aAllowed)
+  {
+    MOZ_ASSERT(aAllowed);
+    *aAllowed = mEvalAllowed;
+    return mResult;
+  }
+
+private:
+  const nsString mExpression;
+  const nsString mFileNameString;
+  const uint32_t mLineNum;
+  const uint32_t mColumnNum;
+  bool mEvalAllowed;
+  nsresult mResult;
+};
+
+} // anonymous
+
+/* static */ nsresult
+CSPEvalChecker::CheckForWindow(JSContext* aCx, nsGlobalWindowInner* aWindow,
+                               const nsAString& aExpression, bool* aAllowEval)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aWindow);
+  MOZ_ASSERT(aAllowEval);
+
+  // The value is set at any "return", but better to have a default value here.
+  *aAllowEval = false;
+
+  // if CSP is enabled, and setTimeout/setInterval was called with a string,
+  // disable the registration and log an error
+  nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc();
+  if (!doc) {
+    // if there's no document, we don't have to do anything.
+    *aAllowEval = true;
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsIContentSecurityPolicy> csp;
+  nsresult rv = doc->NodePrincipal()->GetCsp(getter_AddRefs(csp));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    *aAllowEval = false;
+    return rv;
+  }
+
+  // Get the calling location.
+  uint32_t lineNum = 0;
+  uint32_t columnNum = 0;
+  nsAutoString fileNameString;
+  if (!nsJSUtils::GetCallingLocation(aCx, fileNameString, &lineNum,
+                                     &columnNum)) {
+    fileNameString.AssignLiteral("unknown");
+  }
+
+  rv = CheckInternal(csp, aExpression, fileNameString, lineNum, columnNum,
+                     aAllowEval);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    *aAllowEval = false;
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+/* static */ nsresult
+CSPEvalChecker::CheckForWorker(JSContext* aCx, WorkerPrivate* aWorkerPrivate,
+                               const nsAString& aExpression, bool* aAllowEval)
+{
+  MOZ_ASSERT(aWorkerPrivate);
+  aWorkerPrivate->AssertIsOnWorkerThread();
+  MOZ_ASSERT(aAllowEval);
+
+  // The value is set at any "return", but better to have a default value here.
+  *aAllowEval = false;
+
+  // Get the calling location.
+  uint32_t lineNum = 0;
+  uint32_t columnNum = 0;
+  nsAutoString fileNameString;
+  if (!nsJSUtils::GetCallingLocation(aCx, fileNameString, &lineNum,
+                                     &columnNum)) {
+    fileNameString.AssignLiteral("unknown");
+  }
+
+  RefPtr<WorkerCSPCheckRunnable> r =
+    new WorkerCSPCheckRunnable(aWorkerPrivate, aExpression, fileNameString,
+                               lineNum, columnNum);
+  ErrorResult error;
+  r->Dispatch(Canceling, error);
+  if (NS_WARN_IF(error.Failed())) {
+    *aAllowEval = false;
+    return error.StealNSResult();
+  }
+
+  nsresult rv = r->GetResult(aAllowEval);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    *aAllowEval = false;
+    return rv;
+  }
+
+  return NS_OK;
+}
new file mode 100644
--- /dev/null
+++ b/dom/security/CSPEvalChecker.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_CSPEvalChecker_h
+#define mozilla_dom_CSPEvalChecker_h
+
+#include "nsString.h"
+
+struct JSContext;
+class nsGlobalWindowInner;
+
+namespace mozilla {
+namespace dom {
+
+class WorkerPrivate;
+
+class CSPEvalChecker final
+{
+public:
+  static nsresult
+  CheckForWindow(JSContext* aCx, nsGlobalWindowInner* aWindow,
+                 const nsAString& aExpression, bool* aAllowEval);
+
+  static nsresult
+  CheckForWorker(JSContext* aCx, WorkerPrivate* aWorkerPrivate,
+                 const nsAString& aExpression, bool* aAllowEval);
+};
+
+} // dom namespace
+} // mozilla namespace
+
+#endif // mozilla_dom_CSPEvalChecker_h
--- a/dom/security/moz.build
+++ b/dom/security/moz.build
@@ -6,16 +6,17 @@
 
 with Files('*'):
     BUG_COMPONENT = ('Core', 'DOM: Security')
 
 TEST_DIRS += ['test']
 
 EXPORTS.mozilla.dom += [
     'ContentVerifier.h',
+    'CSPEvalChecker.h',
     'FramingChecker.h',
     'nsContentSecurityManager.h',
     'nsCSPContext.h',
     'nsCSPService.h',
     'nsCSPUtils.h',
     'nsMixedContentBlocker.h',
     'PolicyTokenizer.h',
     'SRICheck.h',
@@ -25,16 +26,17 @@ EXPORTS.mozilla.dom += [
 
 EXPORTS += [
     'nsContentSecurityManager.h',
     'nsMixedContentBlocker.h',
 ]
 
 UNIFIED_SOURCES += [
     'ContentVerifier.cpp',
+    'CSPEvalChecker.cpp',
     'FramingChecker.cpp',
     'nsContentSecurityManager.cpp',
     'nsCSPContext.cpp',
     'nsCSPParser.cpp',
     'nsCSPService.cpp',
     'nsCSPUtils.cpp',
     'nsMixedContentBlocker.cpp',
     'PolicyTokenizer.cpp',
--- a/dom/webidl/Document.webidl
+++ b/dom/webidl/Document.webidl
@@ -478,16 +478,24 @@ partial interface Document {
 };
 
 // http://w3c.github.io/selection-api/#extensions-to-document-interface
 partial interface Document {
   [Throws]
   Selection? getSelection();
 };
 
+// https://github.com/whatwg/html/issues/3338
+partial interface Document {
+  [Pref="dom.storage_access.enabled", Throws]
+  Promise<boolean> hasStorageAccess();
+  [Pref="dom.storage_access.enabled", Throws]
+  Promise<void> requestStorageAccess();
+};
+
 // Extension to give chrome JS the ability to determine whether
 // the user has interacted with the document or not.
 partial interface Document {
   [ChromeOnly] readonly attribute boolean userHasInteracted;
 };
 
 // Extension to give chrome JS the ability to simulate activate the docuement
 // by user gesture.
--- a/dom/workers/ScriptLoader.cpp
+++ b/dom/workers/ScriptLoader.cpp
@@ -1219,17 +1219,17 @@ private:
       // importScripts() and the Worker constructor do not support integrity metadata
       //  (or any fetch options). Until then, we can just block.
       //  If we ever have those data in the future, we'll have to the check to
       //  by using the SRICheck module
       MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug,
             ("Scriptloader::Load, SRI required but not supported in workers"));
       nsCOMPtr<nsIContentSecurityPolicy> wcsp;
       chanLoadInfo->LoadingPrincipal()->GetCsp(getter_AddRefs(wcsp));
-      MOZ_ASSERT(wcsp, "We sould have a CSP for the worker here");
+      MOZ_ASSERT(wcsp, "We should have a CSP for the worker here");
       if (wcsp) {
         wcsp->LogViolationDetails(
             nsIContentSecurityPolicy::VIOLATION_TYPE_REQUIRE_SRI_FOR_SCRIPT,
             nullptr, // triggering element
             aLoadInfo.mURL, EmptyString(), 0, 0, EmptyString(), EmptyString());
       }
       return NS_ERROR_SRI_CORRUPT;
     }
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -3445,16 +3445,20 @@ WorkerPrivate::EnsureClientSource()
 bool
 WorkerPrivate::EnsureCSPEventListener()
 {
   mCSPEventListener = WorkerCSPEventListener::Create(this);
   if (NS_WARN_IF(!mCSPEventListener)) {
     return false;
   }
 
+  if (mLoadInfo.mCSP) {
+    mLoadInfo.mCSP->SetEventListener(mCSPEventListener);
+  }
+
   return true;
 }
 
 void
 WorkerPrivate::EnsurePerformanceStorage()
 {
   AssertIsOnWorkerThread();
 
--- a/dom/workers/WorkerScope.cpp
+++ b/dom/workers/WorkerScope.cpp
@@ -62,17 +62,18 @@ NS_CreateJSTimeoutHandler(JSContext* aCx
                           mozilla::dom::WorkerPrivate* aWorkerPrivate,
                           mozilla::dom::Function& aFunction,
                           const mozilla::dom::Sequence<JS::Value>& aArguments,
                           mozilla::ErrorResult& aError);
 
 extern already_AddRefed<nsIScriptTimeoutHandler>
 NS_CreateJSTimeoutHandler(JSContext* aCx,
                           mozilla::dom::WorkerPrivate* aWorkerPrivate,
-                          const nsAString& aExpression);
+                          const nsAString& aExpression,
+                          mozilla::ErrorResult& aRv);
 
 namespace mozilla {
 namespace dom {
 
 using mozilla::dom::cache::CacheStorage;
 using mozilla::ipc::PrincipalInfo;
 
 WorkerGlobalScope::WorkerGlobalScope(WorkerPrivate* aWorkerPrivate)
@@ -271,34 +272,38 @@ WorkerGlobalScope::SetTimeout(JSContext*
                               const int32_t aTimeout,
                               const Sequence<JS::Value>& aArguments,
                               ErrorResult& aRv)
 {
   mWorkerPrivate->AssertIsOnWorkerThread();
 
   nsCOMPtr<nsIScriptTimeoutHandler> handler =
     NS_CreateJSTimeoutHandler(aCx, mWorkerPrivate, aHandler, aArguments, aRv);
-  if (NS_WARN_IF(aRv.Failed())) {
+  if (!handler) {
     return 0;
   }
 
   return mWorkerPrivate->SetTimeout(aCx, handler, aTimeout, false, aRv);
 }
 
 int32_t
 WorkerGlobalScope::SetTimeout(JSContext* aCx,
                               const nsAString& aHandler,
                               const int32_t aTimeout,
                               const Sequence<JS::Value>& /* unused */,
                               ErrorResult& aRv)
 {
   mWorkerPrivate->AssertIsOnWorkerThread();
 
   nsCOMPtr<nsIScriptTimeoutHandler> handler =
-    NS_CreateJSTimeoutHandler(aCx, mWorkerPrivate, aHandler);
+    NS_CreateJSTimeoutHandler(aCx, mWorkerPrivate, aHandler, aRv);
+  if (!handler) {
+    return 0;
+  }
+
   return mWorkerPrivate->SetTimeout(aCx, handler, aTimeout, false, aRv);
 }
 
 void
 WorkerGlobalScope::ClearTimeout(int32_t aHandle)
 {
   mWorkerPrivate->AssertIsOnWorkerThread();
   mWorkerPrivate->ClearTimeout(aHandle);
@@ -329,17 +334,21 @@ WorkerGlobalScope::SetInterval(JSContext
                                const Sequence<JS::Value>& /* unused */,
                                ErrorResult& aRv)
 {
   mWorkerPrivate->AssertIsOnWorkerThread();
 
   Sequence<JS::Value> dummy;
 
   nsCOMPtr<nsIScriptTimeoutHandler> handler =
-    NS_CreateJSTimeoutHandler(aCx, mWorkerPrivate, aHandler);
+    NS_CreateJSTimeoutHandler(aCx, mWorkerPrivate, aHandler, aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return 0;
+  }
+
   return mWorkerPrivate->SetTimeout(aCx, handler, aTimeout, true, aRv);
 }
 
 void
 WorkerGlobalScope::ClearInterval(int32_t aHandle)
 {
   mWorkerPrivate->AssertIsOnWorkerThread();
   mWorkerPrivate->ClearTimeout(aHandle);
--- a/dom/xul/nsIController.idl
+++ b/dom/xul/nsIController.idl
@@ -31,34 +31,8 @@ interface nsICommandController : nsISupp
   
   void        getCommandStateWithParams( in string command, in nsICommandParams aCommandParams);
     
   void        doCommandWithParams(in string command, in nsICommandParams aCommandParams);
 
   void getSupportedCommands(out unsigned long count,
                             [array, size_is(count), retval] out string commands);
 };
-
-
-/*
-  An API for registering commands in groups, to allow for 
-  updating via nsIDOMWindow::UpdateCommands.
-*/
-interface nsISimpleEnumerator;
-
-[scriptable, uuid(9F82C404-1C7B-11D5-A73C-ECA43CA836FC)]
-interface nsIControllerCommandGroup : nsISupports
-{
-
-  void  addCommandToGroup(in string aCommand, in string aGroup);
-  void  removeCommandFromGroup(in string aCommand, in string aGroup);
-  
-  boolean isCommandInGroup(in string aCommand, in string aGroup);
-
-  /*
-    We should expose some methods that allow for enumeration.
-  */
-  nsISimpleEnumerator getGroupsEnumerator();
-  
-  nsISimpleEnumerator getEnumeratorForGroup(in string aGroup);
-
-};
-
--- a/dom/xul/nsXULControllers.cpp
+++ b/dom/xul/nsXULControllers.cpp
@@ -37,24 +37,16 @@ nsXULControllers::DeleteControllers()
   {
     nsXULControllerData* controllerData = mControllers.ElementAt(i);
     delete controllerData;    // releases the nsIController
   }
 
   mControllers.Clear();
 }
 
-
-already_AddRefed<nsIControllers>
-NS_NewXULControllers()
-{
-  RefPtr<nsXULControllers> controllers = new nsXULControllers();
-  return controllers.forget();
-}
-
 NS_IMPL_CYCLE_COLLECTION_CLASS(nsXULControllers)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsXULControllers)
   tmp->DeleteControllers();
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsXULControllers)
   {
     uint32_t i, count = tmp->mControllers.Length();
--- a/dom/xul/nsXULControllers.h
+++ b/dom/xul/nsXULControllers.h
@@ -14,17 +14,17 @@
 
 #include "nsCOMPtr.h"
 #include "nsTArray.h"
 #include "nsWeakPtr.h"
 #include "nsIControllers.h"
 #include "nsCycleCollectionParticipant.h"
 
 /* non-XPCOM class for holding controllers and their IDs */
-class nsXULControllerData
+class nsXULControllerData final
 {
 public:
                             nsXULControllerData(uint32_t inControllerID, nsIController* inController)
                             : mControllerID(inControllerID)
                             , mController(inController)
                             {
                             }
 
@@ -37,30 +37,26 @@ public:
                               NS_IF_ADDREF(*outController = mController);
                               return NS_OK;
                             }
 
     uint32_t                mControllerID;
     nsCOMPtr<nsIController> mController;
 };
 
-
-already_AddRefed<nsIControllers> NS_NewXULControllers();
-
-class nsXULControllers : public nsIControllers
+class nsXULControllers final : public nsIControllers
 {
 public:
-    friend already_AddRefed<nsIControllers> NS_NewXULControllers();
+    nsXULControllers();
 
     NS_DECL_CYCLE_COLLECTING_ISUPPORTS
     NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsXULControllers, nsIControllers)
     NS_DECL_NSICONTROLLERS
 
 protected:
-    nsXULControllers();
     virtual ~nsXULControllers(void);
 
     void        DeleteControllers();
 
     nsTArray<nsXULControllerData*>   mControllers;
     uint32_t                         mCurControllerID;
 };
 
--- a/dom/xul/nsXULElement.cpp
+++ b/dom/xul/nsXULElement.cpp
@@ -1133,17 +1133,17 @@ nsXULElement::IsAttributeMapped(const ns
 }
 
 nsIControllers*
 nsXULElement::GetControllers(ErrorResult& rv)
 {
     if (! Controllers()) {
         nsExtendedDOMSlots* slots = ExtendedDOMSlots();
 
-        slots->mControllers = NS_NewXULControllers();
+        slots->mControllers = new nsXULControllers();
     }
 
     return Controllers();
 }
 
 already_AddRefed<BoxObject>
 nsXULElement::GetBoxObject(ErrorResult& rv)
 {
--- a/editor/composer/ComposerCommandsUpdater.cpp
+++ b/editor/composer/ComposerCommandsUpdater.cpp
@@ -276,18 +276,16 @@ ComposerCommandsUpdater::UpdateDirtyStat
 
 nsresult
 ComposerCommandsUpdater::UpdateCommandGroup(const nsAString& aCommandGroup)
 {
   nsCOMPtr<nsPICommandUpdater> commandUpdater = GetCommandUpdater();
   NS_ENSURE_TRUE(commandUpdater, NS_ERROR_FAILURE);
 
 
-  // This hardcoded list of commands is temporary.
-  // This code should use nsIControllerCommandGroup.
   if (aCommandGroup.EqualsLiteral("undo")) {
     commandUpdater->CommandStatusChanged("cmd_undo");
     commandUpdater->CommandStatusChanged("cmd_redo");
     return NS_OK;
   }
 
   if (aCommandGroup.EqualsLiteral("select") ||
       aCommandGroup.EqualsLiteral("style")) {
--- a/gfx/layers/PaintThread.cpp
+++ b/gfx/layers/PaintThread.cpp
@@ -192,35 +192,35 @@ PaintThread::UpdateRenderMode()
       mPaintWorkers = nullptr;
     } else {
       InitPaintWorkers();
     }
   }
 }
 
 void
-PaintThread::QueuePaintTask(PaintTask* aTask)
+PaintThread::QueuePaintTask(UniquePtr<PaintTask>&& aTask)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aTask);
 
   if (gfxPrefs::LayersOMTPDumpCapture() && aTask->mCapture) {
     aTask->mCapture->Dump();
   }
 
+  MOZ_RELEASE_ASSERT(aTask->mCapture->hasOneRef());
+
   RefPtr<CompositorBridgeChild> cbc(CompositorBridgeChild::Get());
-  RefPtr<PaintTask> state(aTask);
-
-  cbc->NotifyBeginAsyncPaint(state);
+  cbc->NotifyBeginAsyncPaint(aTask.get());
 
   RefPtr<PaintThread> self = this;
   RefPtr<Runnable> task = NS_NewRunnableFunction("PaintThread::AsyncPaintTask",
-    [self, cbc, state]() -> void
+    [self, cbc, task = std::move(aTask)]() -> void
   {
-    self->AsyncPaintTask(cbc, state);
+    self->AsyncPaintTask(cbc, task.get());
   });
 
   nsIEventTarget* paintThread = mPaintWorkers ?
     static_cast<nsIEventTarget*>(mPaintWorkers.get()) :
     static_cast<nsIEventTarget*>(sThread.get());
 
 #ifndef OMTP_FORCE_SYNC
   paintThread->Dispatch(task.forget());
--- a/gfx/layers/PaintThread.h
+++ b/gfx/layers/PaintThread.h
@@ -29,28 +29,25 @@ namespace layers {
 // on the paint thread or paint worker pool.
 //
 // More specifically it contains:
 // 1. A capture command list of drawing commands
 // 2. A destination draw target to replay the draw commands upon
 // 3. A list of dependent texture clients that must be kept alive for the
 //    task's duration, and then destroyed on the main thread
 class PaintTask {
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PaintTask)
 public:
   PaintTask() {}
+  ~PaintTask() {}
 
   void DropTextureClients();
 
   RefPtr<gfx::DrawTarget> mTarget;
   RefPtr<gfx::DrawTargetCapture> mCapture;
   AutoTArray<RefPtr<TextureClient>, 4> mClients;
-
-protected:
-  virtual ~PaintTask() {}
 };
 
 class CompositorBridgeChild;
 
 class PaintThread final
 {
   friend void DestroyPaintThread(UniquePtr<PaintThread>&& aPaintThread);
 
@@ -68,17 +65,17 @@ public:
 
   // This allows the paint thread to dynamically toggle between a paint worker
   // thread pool used with tiling, and a single paint thread used with rotated
   // buffer.
   void UpdateRenderMode();
 
   // Must be called on the main thread. Queues an async paint
   // task to be completed on the paint thread.
-  void QueuePaintTask(PaintTask* aTask);
+  void QueuePaintTask(UniquePtr<PaintTask>&& aTask);
 
   // Must be called on the main thread. Signifies that the current
   // layer tree transaction has been finished and any async paints
   // for it have been queued on the paint thread. This MUST be called
   // at the end of a layer transaction as it will be used to do an optional
   // texture sync and then unblock the main thread if it is waiting to paint
   // a new frame.
   void QueueEndLayerTransaction(SyncObjectClient* aSyncObject);
--- a/gfx/layers/apz/util/APZCCallbackHelper.cpp
+++ b/gfx/layers/apz/util/APZCCallbackHelper.cpp
@@ -288,18 +288,18 @@ APZCCallbackHelper::UpdateRootFrame(Fram
     return;
   }
 
   MOZ_ASSERT(aMetrics.GetUseDisplayPortMargins());
 
   if (gfxPrefs::APZAllowZooming() && aMetrics.GetScrollOffsetUpdated()) {
     // If zooming is disabled then we don't really want to let APZ fiddle
     // with these things. In theory setting the resolution here should be a
-    // no-op, but setting the SPCSPS is bad because it can cause a stale value
-    // to be returned by window.innerWidth/innerHeight (see bug 1187792).
+    // no-op, but setting the visual viewport size is bad because it can cause a
+    // stale value to be returned by window.innerWidth/innerHeight (see bug 1187792).
     //
     // We also skip this codepath unless the metrics has a scroll offset update
     // type other eNone, because eNone just means that this repaint request
     // was triggered by APZ in response to a main-thread update. In this
     // scenario we don't want to update the main-thread resolution because
     // it can trigger unnecessary reflows.
 
     float presShellResolution = shell->GetResolution();
--- a/gfx/layers/client/ClientPaintedLayer.cpp
+++ b/gfx/layers/client/ClientPaintedLayer.cpp
@@ -91,17 +91,17 @@ ClientPaintedLayer::UpdatePaintRegion(Pa
   return true;
 }
 
 void
 ClientPaintedLayer::FinishPaintState(PaintState& aState)
 {
   if (aState.mAsyncTask && !aState.mAsyncTask->mCapture->IsEmpty()) {
     ClientManager()->SetQueuedAsyncPaints();
-    PaintThread::Get()->QueuePaintTask(aState.mAsyncTask);
+    PaintThread::Get()->QueuePaintTask(std::move(aState.mAsyncTask));
   }
 }
 
 uint32_t
 ClientPaintedLayer::GetPaintFlags(ReadbackProcessor* aReadback)
 {
   uint32_t flags = ContentClient::PAINT_CAN_DRAW_ROTATED;
   #ifndef MOZ_IGNORE_PAINT_WILL_RESAMPLE
--- a/gfx/layers/client/ContentClient.cpp
+++ b/gfx/layers/client/ContentClient.cpp
@@ -158,17 +158,17 @@ ContentClient::BeginPaint(PaintedLayer* 
   OpenMode readMode = result.mAsyncPaint ? OpenMode::OPEN_READ_ASYNC
                                          : OpenMode::OPEN_READ;
   OpenMode writeMode = result.mAsyncPaint ? OpenMode::OPEN_READ_WRITE_ASYNC
                                           : OpenMode::OPEN_READ_WRITE;
 
   IntRect drawBounds = result.mRegionToDraw.GetBounds();
 
   if (result.mAsyncPaint) {
-    result.mAsyncTask = new PaintTask();
+    result.mAsyncTask.reset(new PaintTask());
   }
 
   // Try to acquire the back buffer, copy over contents if we are using a new buffer,
   // and rotate or unrotate the buffer as necessary
   if (mBuffer && dest.mCanReuseBuffer) {
     if (mBuffer->Lock(writeMode)) {
       auto newParameters = mBuffer->AdjustedParameters(dest.mBufferRect);
 
--- a/gfx/layers/client/ContentClient.h
+++ b/gfx/layers/client/ContentClient.h
@@ -20,16 +20,17 @@
 #include "mozilla/layers/CompositorTypes.h"  // for TextureInfo, etc
 #include "mozilla/layers/ISurfaceAllocator.h"
 #include "mozilla/layers/LayersSurfaces.h"  // for SurfaceDescriptor
 #include "mozilla/layers/LayersTypes.h"  // for TextureDumpMode
 #include "mozilla/layers/TextureClient.h"  // for TextureClient
 #include "mozilla/layers/PaintThread.h"  // for PaintTask
 #include "mozilla/Maybe.h"              // for Maybe
 #include "mozilla/mozalloc.h"           // for operator delete
+#include "mozilla/UniquePtr.h"          // for UniquePtr
 #include "ReadbackProcessor.h"          // For ReadbackProcessor::Update
 #include "nsCOMPtr.h"                   // for already_AddRefed
 #include "nsPoint.h"                    // for nsIntPoint
 #include "nsRect.h"                     // for mozilla::gfx::IntRect
 #include "nsRegion.h"                   // for nsIntRegion
 #include "nsTArray.h"                   // for nsTArray
 
 namespace mozilla {
@@ -112,17 +113,17 @@ public:
     {}
 
     nsIntRegion mRegionToDraw;
     nsIntRegion mRegionToInvalidate;
     SurfaceMode mMode;
     DrawRegionClip mClip;
     gfxContentType mContentType;
     bool mAsyncPaint;
-    RefPtr<PaintTask> mAsyncTask;
+    UniquePtr<PaintTask> mAsyncTask;
   };
 
   enum {
     PAINT_WILL_RESAMPLE = 0x01,
     PAINT_NO_ROTATION = 0x02,
     PAINT_CAN_DRAW_ROTATED = 0x04,
     PAINT_ASYNC = 0x08,
   };
--- a/gfx/layers/client/MultiTiledContentClient.cpp
+++ b/gfx/layers/client/MultiTiledContentClient.cpp
@@ -147,17 +147,17 @@ void ClientMultiTiledLayerBuffer::MaybeS
     AutoTArray<uint64_t, 10> syncTextureSerials;
     SurfaceMode mode;
     Unused << GetContentType(&mode);
 
     // Pre-pass through the tiles (mirroring the filter logic below) to gather
     // texture IDs that we need to ensure are unused by the GPU before we
     // continue.
     if (!aPaintRegion.IsEmpty()) {
-      MOZ_ASSERT(mPaintTasks.size() == 0);
+      MOZ_ASSERT(mPaintTasks.IsEmpty());
       for (size_t i = 0; i < mRetainedTiles.Length(); ++i) {
         const TileCoordIntPoint tileCoord = aNewTiles.TileCoord(i);
 
         IntPoint tileOffset = GetTileOffset(tileCoord);
         nsIntRegion tileDrawRegion = IntRect(tileOffset, aScaledTileSize);
         tileDrawRegion.AndWith(aPaintRegion);
 
         if (tileDrawRegion.IsEmpty()) {
@@ -215,17 +215,17 @@ void ClientMultiTiledLayerBuffer::Update
   oldRetainedTiles.Clear();
 
   nsIntRegion paintRegion = aPaintRegion;
   nsIntRegion dirtyRegion = aDirtyRegion;
 
   MaybeSyncTextures(paintRegion, newTiles, scaledTileSize);
 
   if (!paintRegion.IsEmpty()) {
-    MOZ_ASSERT(mPaintTasks.size() == 0);
+    MOZ_ASSERT(mPaintTasks.IsEmpty());
 
     for (size_t i = 0; i < newTileCount; ++i) {
       const TileCoordIntPoint tileCoord = newTiles.TileCoord(i);
 
       IntPoint tileOffset = GetTileOffset(tileCoord);
       nsIntRegion tileDrawRegion = IntRect(tileOffset, scaledTileSize);
       tileDrawRegion.AndWith(paintRegion);
 
@@ -238,24 +238,24 @@ void ClientMultiTiledLayerBuffer::Update
         gfxCriticalError() << "ValidateTile failed";
       }
 
       // Validating the tile may have required more to be painted.
       paintRegion.OrWith(tileDrawRegion);
       dirtyRegion.OrWith(tileDrawRegion);
     }
 
-    if (!mPaintTiles.empty()) {
+    if (!mPaintTiles.IsEmpty()) {
       // Create a tiled draw target
       gfx::TileSet tileset;
-      for (size_t i = 0; i < mPaintTiles.size(); ++i) {
+      for (size_t i = 0; i < mPaintTiles.Length(); ++i) {
         mPaintTiles[i].mTileOrigin -= mTilingOrigin;
       }
-      tileset.mTiles = &mPaintTiles[0];
-      tileset.mTileCount = mPaintTiles.size();
+      tileset.mTiles = mPaintTiles.Elements();
+      tileset.mTileCount = mPaintTiles.Length();
       RefPtr<DrawTarget> drawTarget = gfx::Factory::CreateTiledDrawTarget(tileset);
       if (!drawTarget || !drawTarget->IsValid()) {
         gfxDevCrash(LogReason::InvalidContext) << "Invalid tiled draw target";
         return;
       }
       drawTarget->SetTransform(Matrix());
 
       // Draw into the tiled draw target
@@ -269,37 +269,38 @@ void ClientMultiTiledLayerBuffer::Update
       ctx = nullptr;
 
       // Edge padding allows us to avoid resampling artifacts
       if (gfxPrefs::TileEdgePaddingEnabled() && mResolution == 1) {
         drawTarget->PadEdges(newValidRegion.MovedBy(-mTilingOrigin));
       }
 
       // Reset
-      mPaintTiles.clear();
+      mPaintTiles.Clear();
       mTilingOrigin = IntPoint(std::numeric_limits<int32_t>::max(),
                                std::numeric_limits<int32_t>::max());
     }
 
     // Dispatch to the paint thread
     if (aFlags & TilePaintFlags::Async) {
       bool queuedTask = false;
 
-      for (const auto& state : mPaintTasks) {
-        if (!state->mCapture->IsEmpty()) {
-          PaintThread::Get()->QueuePaintTask(state);
+      while (!mPaintTasks.IsEmpty()) {
+        UniquePtr<PaintTask> task = mPaintTasks.PopLastElement();
+        if (!task->mCapture->IsEmpty()) {
+          PaintThread::Get()->QueuePaintTask(std::move(task));
           queuedTask = true;
         }
       }
 
       if (queuedTask) {
         mManager->SetQueuedAsyncPaints();
       }
 
-      mPaintTasks.clear();
+      mPaintTasks.Clear();
     }
 
     for (uint32_t i = 0; i < mRetainedTiles.Length(); ++i) {
       TileClient& tile = mRetainedTiles[i];
       UnlockTile(tile);
     }
   }
 
@@ -381,24 +382,24 @@ ClientMultiTiledLayerBuffer::ValidateTil
                                iter.Get().Width(), iter.Get().Height());
       backBuffer->mTarget->ClearRect(drawRect);
     }
   }
 
   gfx::Tile paintTile;
   paintTile.mTileOrigin = gfx::IntPoint(aTileOrigin.x, aTileOrigin.y);
   paintTile.mDrawTarget = backBuffer->mTarget;
-  mPaintTiles.push_back(paintTile);
+  mPaintTiles.AppendElement(paintTile);
 
   if (aFlags & TilePaintFlags::Async) {
-    RefPtr<PaintTask> task = new PaintTask();
+    UniquePtr<PaintTask> task(new PaintTask());
     task->mCapture = backBuffer->mCapture;
     task->mTarget = backBuffer->mBackBuffer;
     task->mClients = std::move(backBuffer->mTextureClients);
-    mPaintTasks.push_back(task);
+    mPaintTasks.AppendElement(std::move(task));
   } else {
     MOZ_RELEASE_ASSERT(backBuffer->mTarget == backBuffer->mBackBuffer);
     MOZ_RELEASE_ASSERT(backBuffer->mCapture == nullptr);
   }
 
   mTilingOrigin.x = std::min(mTilingOrigin.x, paintTile.mTileOrigin.x);
   mTilingOrigin.y = std::min(mTilingOrigin.y, paintTile.mTileOrigin.y);
 
--- a/gfx/layers/client/MultiTiledContentClient.h
+++ b/gfx/layers/client/MultiTiledContentClient.h
@@ -9,16 +9,17 @@
 
 #include "ClientLayerManager.h"                 // for ClientLayerManager
 #include "nsRegion.h"                           // for nsIntRegion
 #include "mozilla/gfx/2D.h"                     // for gfx::Tile
 #include "mozilla/gfx/Point.h"                  // for IntPoint
 #include "mozilla/layers/CompositableClient.h"  // for CompositableClient
 #include "mozilla/layers/LayersMessages.h"      // for TileDescriptor
 #include "mozilla/layers/TiledContentClient.h"  // for ClientTiledPaintedLayer
+#include "mozilla/UniquePtr.h"                  // for UniquePtr
 #include "TiledLayerBuffer.h"                   // for TiledLayerBuffer
 
 namespace mozilla {
 namespace layers {
 
 class ClientLayerManager;
 
 class ClientMultiTiledLayerBuffer
@@ -116,18 +117,18 @@ private:
   // The region that will be made valid during Update(). Once Update() is
   // completed then this is identical to mValidRegion.
   nsIntRegion mNewValidRegion;
 
   SharedFrameMetricsHelper*  mSharedFrameMetricsHelper;
 
   // Parameters that are collected during Update for a paint before they
   // are either executed or replayed on the paint thread.
-  std::vector<gfx::Tile> mPaintTiles;
-  std::vector<RefPtr<PaintTask>> mPaintTasks;
+  AutoTArray<gfx::Tile, 4> mPaintTiles;
+  AutoTArray<UniquePtr<PaintTask>, 4> mPaintTasks;
 
   /**
    * While we're adding tiles, this is used to keep track of the position of
    * the top-left of the top-left-most tile.  When we come to wrap the tiles in
    * TiledDrawTarget we subtract the value of this member from each tile's
    * offset so that all the tiles have a positive offset, then add a
    * translation to the TiledDrawTarget to compensate.  This is important so
    * that the mRect of the TiledDrawTarget is always at a positive x/y
--- a/gfx/layers/client/SingleTiledContentClient.cpp
+++ b/gfx/layers/client/SingleTiledContentClient.cpp
@@ -3,16 +3,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 "mozilla/layers/SingleTiledContentClient.h"
 
 #include "ClientTiledPaintedLayer.h"
 #include "mozilla/Maybe.h"
+#include "mozilla/UniquePtr.h"
 
 namespace mozilla {
 namespace layers {
 
 
 SingleTiledContentClient::SingleTiledContentClient(ClientTiledPaintedLayer& aPaintedLayer,
                                                    ClientLayerManager* aManager)
   : TiledContentClient(aManager, "Single")
@@ -231,21 +232,27 @@ ClientSingleTiledLayerBuffer::PaintThebe
     }
     ctx->SetMatrix(ctx->CurrentMatrix().PreTranslate(-mTilingOrigin.x, -mTilingOrigin.y));
 
     aCallback(&mPaintedLayer, ctx, paintRegion, paintRegion, DrawRegionClip::DRAW, nsIntRegion(), aCallbackData);
   }
 
   if (asyncPaint) {
     if (!backBuffer->mCapture->IsEmpty()) {
-      RefPtr<PaintTask> task = new PaintTask();
+      UniquePtr<PaintTask> task(new PaintTask());
       task->mCapture = backBuffer->mCapture;
       task->mTarget = backBuffer->mBackBuffer;
       task->mClients = std::move(backBuffer->mTextureClients);
-      PaintThread::Get()->QueuePaintTask(task);
+
+      // The target is an alias for the capture, and the paint thread expects
+      // to be the only one with a reference to the capture
+      backBuffer->mTarget = nullptr;
+      backBuffer->mCapture = nullptr;
+
+      PaintThread::Get()->QueuePaintTask(std::move(task));
       mManager->SetQueuedAsyncPaints();
     }
   } else {
     MOZ_ASSERT(backBuffer->mTarget == backBuffer->mBackBuffer);
     MOZ_ASSERT(!backBuffer->mCapture);
   }
 
   // The new buffer is now validated, remove the dirty region from it.
--- a/gfx/thebes/gfxPrefs.h
+++ b/gfx/thebes/gfxPrefs.h
@@ -538,16 +538,17 @@ private:
 #if defined(XP_MACOSX)
   DECL_GFX_PREF(Live, "gl.multithreaded",                      GLMultithreaded, bool, false);
 #endif
   DECL_GFX_PREF(Live, "gl.require-hardware",                   RequireHardwareGL, bool, false);
   DECL_GFX_PREF(Live, "gl.use-tls-is-current",                 UseTLSIsCurrent, int32_t, 0);
 
   DECL_GFX_PREF(Live, "image.animated.decode-on-demand.threshold-kb", ImageAnimatedDecodeOnDemandThresholdKB, uint32_t, 20480);
   DECL_GFX_PREF(Live, "image.animated.decode-on-demand.batch-size", ImageAnimatedDecodeOnDemandBatchSize, uint32_t, 6);
+  DECL_GFX_PREF(Live, "image.animated.generate-full-frames",   ImageAnimatedGenerateFullFrames, bool, false);
   DECL_GFX_PREF(Live, "image.animated.resume-from-last-displayed", ImageAnimatedResumeFromLastDisplayed, bool, false);
   DECL_GFX_PREF(Live, "image.cache.factor2.threshold-surfaces", ImageCacheFactor2ThresholdSurfaces, int32_t, -1);
   DECL_GFX_PREF(Once, "image.cache.size",                      ImageCacheSize, int32_t, 5*1024*1024);
   DECL_GFX_PREF(Once, "image.cache.timeweight",                ImageCacheTimeWeight, int32_t, 500);
   DECL_GFX_PREF(Live, "image.decode-immediately.enabled",      ImageDecodeImmediatelyEnabled, bool, false);
   DECL_GFX_PREF(Live, "image.downscale-during-decode.enabled", ImageDownscaleDuringDecodeEnabled, bool, true);
   DECL_GFX_PREF(Live, "image.infer-src-animation.threshold-ms", ImageInferSrcAnimationThresholdMS, uint32_t, 2000);
   DECL_GFX_PREF(Live, "image.layout_network_priority",         ImageLayoutNetworkPriority, bool, true);
--- a/image/AnimationSurfaceProvider.cpp
+++ b/image/AnimationSurfaceProvider.cpp
@@ -27,20 +27,20 @@ AnimationSurfaceProvider::AnimationSurfa
   , mDecoder(aDecoder.get())
   , mFramesMutex("AnimationSurfaceProvider::mFrames")
 {
   MOZ_ASSERT(!mDecoder->IsMetadataDecode(),
              "Use MetadataDecodingTask for metadata decodes");
   MOZ_ASSERT(!mDecoder->IsFirstFrameDecode(),
              "Use DecodedSurfaceProvider for single-frame image decodes");
 
-  // We still produce paletted surfaces for GIF which means the frames are
-  // smaller than one would expect for APNG. This may be removed if/when
-  // bug 1337111 lands and it is enabled by default.
-  size_t pixelSize = aDecoder->GetType() == DecoderType::GIF
+  // We may produce paletted surfaces for GIF which means the frames are smaller
+  // than one would expect.
+  size_t pixelSize = !aDecoder->ShouldBlendAnimation() &&
+                     aDecoder->GetType() == DecoderType::GIF
                      ? sizeof(uint8_t) : sizeof(uint32_t);
 
   // Calculate how many frames we need to decode in this animation before we
   // enter decode-on-demand mode.
   IntSize frameSize = aSurfaceKey.Size();
   size_t threshold =
     (size_t(gfxPrefs::ImageAnimatedDecodeOnDemandThresholdKB()) * 1024) /
     (pixelSize * frameSize.width * frameSize.height);
--- a/image/Decoder.cpp
+++ b/image/Decoder.cpp
@@ -286,17 +286,17 @@ nsresult
 Decoder::AllocateFrame(const gfx::IntSize& aOutputSize,
                        const gfx::IntRect& aFrameRect,
                        gfx::SurfaceFormat aFormat,
                        uint8_t aPaletteDepth,
                        const Maybe<AnimationParams>& aAnimParams)
 {
   mCurrentFrame = AllocateFrameInternal(aOutputSize, aFrameRect, aFormat,
                                         aPaletteDepth, aAnimParams,
-                                        mCurrentFrame.get());
+                                        std::move(mCurrentFrame));
 
   if (mCurrentFrame) {
     mHasFrameToTake = true;
 
     // Gather the raw pointers the decoders will use.
     mCurrentFrame->GetImageData(&mImageData, &mImageDataLength);
     mCurrentFrame->GetPaletteData(&mColormap, &mColormapSize);
 
@@ -316,17 +316,17 @@ Decoder::AllocateFrame(const gfx::IntSiz
 }
 
 RawAccessFrameRef
 Decoder::AllocateFrameInternal(const gfx::IntSize& aOutputSize,
                                const gfx::IntRect& aFrameRect,
                                SurfaceFormat aFormat,
                                uint8_t aPaletteDepth,
                                const Maybe<AnimationParams>& aAnimParams,
-                               imgFrame* aPreviousFrame)
+                               RawAccessFrameRef&& aPreviousFrame)
 {
   if (HasError()) {
     return RawAccessFrameRef();
   }
 
   uint32_t frameNum = aAnimParams ? aAnimParams->mFrameNum : 0;
   if (frameNum != mFrameCount) {
     MOZ_ASSERT_UNREACHABLE("Allocating frames out of order");
@@ -338,17 +338,17 @@ Decoder::AllocateFrameInternal(const gfx
     NS_WARNING("Trying to add frame with zero or negative size");
     return RawAccessFrameRef();
   }
 
   auto frame = MakeNotNull<RefPtr<imgFrame>>();
   bool nonPremult = bool(mSurfaceFlags & SurfaceFlags::NO_PREMULTIPLY_ALPHA);
   if (NS_FAILED(frame->InitForDecoder(aOutputSize, aFrameRect, aFormat,
                                       aPaletteDepth, nonPremult,
-                                      aAnimParams))) {
+                                      aAnimParams, ShouldBlendAnimation()))) {
     NS_WARNING("imgFrame::Init should succeed");
     return RawAccessFrameRef();
   }
 
   RawAccessFrameRef ref = frame->RawAccessRef();
   if (!ref) {
     frame->Abort();
     return RawAccessFrameRef();
@@ -369,17 +369,37 @@ Decoder::AllocateFrameInternal(const gfx
     }
   }
 
   if (frameNum > 0) {
     ref->SetRawAccessOnly();
 
     // Some GIFs are huge but only have a small area that they animate. We only
     // need to refresh that small area when frame 0 comes around again.
-    mFirstFrameRefreshArea.UnionRect(mFirstFrameRefreshArea, frame->GetRect());
+    mFirstFrameRefreshArea.UnionRect(mFirstFrameRefreshArea,
+                                     ref->GetBoundedBlendRect());
+
+    if (ShouldBlendAnimation()) {
+      if (aPreviousFrame->GetDisposalMethod() !=
+          DisposalMethod::RESTORE_PREVIOUS) {
+        // If the new restore frame is the direct previous frame, then we know
+        // the dirty rect is composed only of the current frame's blend rect and
+        // the restore frame's clear rect (if applicable) which are handled in
+        // filters.
+        mRestoreFrame = std::move(aPreviousFrame);
+        mRestoreDirtyRect.SetBox(0, 0, 0, 0);
+      } else {
+        // We only need the previous frame's dirty rect, because while there may
+        // have been several frames between us and mRestoreFrame, the only areas
+        // that changed are the restore frame's clear rect, the current frame
+        // blending rect, and the previous frame's blending rect. All else is
+        // forgotten due to us restoring the same frame again.
+        mRestoreDirtyRect = aPreviousFrame->GetBoundedBlendRect();
+      }
+    }
   }
 
   mFrameCount++;
 
   return ref;
 }
 
 /*
--- a/image/Decoder.h
+++ b/image/Decoder.h
@@ -264,16 +264,25 @@ public:
    * Should we stop decoding after the first frame?
    */
   bool IsFirstFrameDecode() const
   {
     return bool(mDecoderFlags & DecoderFlags::FIRST_FRAME_ONLY);
   }
 
   /**
+   * Should blend the current frame with the previous frames to produce a
+   * complete frame instead of a partial frame for animated images.
+   */
+  bool ShouldBlendAnimation() const
+  {
+    return bool(mDecoderFlags & DecoderFlags::BLEND_ANIMATION);
+  }
+
+  /**
    * @return the number of complete animation frames which have been decoded so
    * far, if it has changed since the last call to TakeCompleteFrameCount();
    * otherwise, returns Nothing().
    */
   Maybe<uint32_t> TakeCompleteFrameCount();
 
   // The number of frames we have, including anything in-progress. Thus, this
   // is only 0 if we haven't begun any frames.
@@ -405,24 +414,51 @@ public:
   RasterImage* GetImageMaybeNull() const { return mImage.get(); }
 
   RawAccessFrameRef GetCurrentFrameRef()
   {
     return mCurrentFrame ? mCurrentFrame->RawAccessRef()
                          : RawAccessFrameRef();
   }
 
+  /**
+   * For use during decoding only. Allows the BlendAnimationFilter to get the
+   * current frame we are producing for its animation parameters.
+   */
+  imgFrame* GetCurrentFrame()
+  {
+    MOZ_ASSERT(ShouldBlendAnimation());
+    return mCurrentFrame.get();
+  }
+
+  /**
+   * For use during decoding only. Allows the BlendAnimationFilter to get the
+   * frame it should be pulling the previous frame data from.
+   */
+  const RawAccessFrameRef& GetRestoreFrameRef() const
+  {
+    MOZ_ASSERT(ShouldBlendAnimation());
+    return mRestoreFrame;
+  }
+
+  const gfx::IntRect& GetRestoreDirtyRect() const
+  {
+    MOZ_ASSERT(ShouldBlendAnimation());
+    return mRestoreDirtyRect;
+  }
+
   bool HasFrameToTake() const { return mHasFrameToTake; }
   void ClearHasFrameToTake() {
     MOZ_ASSERT(mHasFrameToTake);
     mHasFrameToTake = false;
   }
 
 protected:
   friend class AutoRecordDecoderTelemetry;
+  friend class DecoderTestHelper;
   friend class nsICODecoder;
   friend class PalettedSurfaceSink;
   friend class SurfaceSink;
 
   virtual ~Decoder();
 
   /*
    * Internal hooks. Decoder implementations may override these and
@@ -539,32 +575,42 @@ private:
     return mInFrame ? mFrameCount - 1 : mFrameCount;
   }
 
   RawAccessFrameRef AllocateFrameInternal(const gfx::IntSize& aOutputSize,
                                           const gfx::IntRect& aFrameRect,
                                           gfx::SurfaceFormat aFormat,
                                           uint8_t aPaletteDepth,
                                           const Maybe<AnimationParams>& aAnimParams,
-                                          imgFrame* aPreviousFrame);
+                                          RawAccessFrameRef&& aPreviousFrame);
 
 protected:
   Maybe<Downscaler> mDownscaler;
 
   uint8_t* mImageData;  // Pointer to image data in either Cairo or 8bit format
   uint32_t mImageDataLength;
   uint32_t* mColormap;  // Current colormap to be used in Cairo format
   uint32_t mColormapSize;
 
 private:
   RefPtr<RasterImage> mImage;
   Maybe<SourceBufferIterator> mIterator;
+
+  // The current frame the decoder is producing.
   RawAccessFrameRef mCurrentFrame;
+
+  // The complete frame to combine with the current partial frame to produce
+  // a complete current frame.
+  RawAccessFrameRef mRestoreFrame;
+
   ImageMetadata mImageMetadata;
-  gfx::IntRect mInvalidRect; // Tracks an invalidation region in the current frame.
+
+  gfx::IntRect mInvalidRect; // Tracks new rows as the current frame is decoded.
+  gfx::IntRect mRestoreDirtyRect; // Tracks an invalidation region between the
+                                  // restore frame and the previous frame.
   Maybe<gfx::IntSize> mOutputSize;  // The size of our output surface.
   Maybe<gfx::IntSize> mExpectedSize; // The expected size of the image.
   Progress mProgress;
 
   uint32_t mFrameCount; // Number of frames, including anything in-progress
   FrameTimeout mLoopLength;  // Length of a single loop of this image.
   gfx::IntRect mFirstFrameRefreshArea;  // The area of the image that needs to
                                         // be invalidated when the animation loops.
--- a/image/DecoderFactory.cpp
+++ b/image/DecoderFactory.cpp
@@ -327,16 +327,17 @@ DecoderFactory::CreateDecoderForICOResou
 
   return decoder.forget();
 }
 
 /* static */ already_AddRefed<Decoder>
 DecoderFactory::CreateAnonymousDecoder(DecoderType aType,
                                        NotNull<SourceBuffer*> aSourceBuffer,
                                        const Maybe<IntSize>& aOutputSize,
+                                       DecoderFlags aDecoderFlags,
                                        SurfaceFlags aSurfaceFlags)
 {
   if (aType == DecoderType::UNKNOWN) {
     return nullptr;
   }
 
   RefPtr<Decoder> decoder =
     GetDecoder(aType, /* aImage = */ nullptr, /* aIsRedecode = */ false);
@@ -345,24 +346,17 @@ DecoderFactory::CreateAnonymousDecoder(D
   // Initialize the decoder.
   decoder->SetMetadataDecode(false);
   decoder->SetIterator(aSourceBuffer->Iterator());
 
   // Anonymous decoders are always transient; we don't want to optimize surfaces
   // or do any other expensive work that might be wasted.
   DecoderFlags decoderFlags = DecoderFlags::IMAGE_IS_TRANSIENT;
 
-  // Without an image, the decoder can't store anything in the SurfaceCache, so
-  // callers will only be able to retrieve the most recent frame via
-  // Decoder::GetCurrentFrame(). That means that anonymous decoders should
-  // always be first-frame-only decoders, because nobody ever wants the *last*
-  // frame.
-  decoderFlags |= DecoderFlags::FIRST_FRAME_ONLY;
-
-  decoder->SetDecoderFlags(decoderFlags);
+  decoder->SetDecoderFlags(aDecoderFlags | decoderFlags);
   decoder->SetSurfaceFlags(aSurfaceFlags);
 
   // Set an output size for downscale-during-decode if requested.
   if (aOutputSize) {
     decoder->SetOutputSize(*aOutputSize);
   }
 
   if (NS_FAILED(decoder->Init())) {
--- a/image/DecoderFactory.h
+++ b/image/DecoderFactory.h
@@ -172,23 +172,25 @@ public:
    *
    * @param aType Which type of decoder to create - JPEG, PNG, etc.
    * @param aSourceBuffer The SourceBuffer which the decoder will read its data
    *                      from.
    * @param aOutputSize If Some(), the output size for the decoder. If this is
    *                    smaller than the intrinsic size, the decoder will
    *                    downscale the image. If Nothing(), the output size will
    *                    be the intrinsic size.
+   * @param aDecoderFlags Flags specifying the behavior of this decoder.
    * @param aSurfaceFlags Flags specifying the type of output this decoder
    *                      should produce.
    */
   static already_AddRefed<Decoder>
   CreateAnonymousDecoder(DecoderType aType,
                          NotNull<SourceBuffer*> aSourceBuffer,
                          const Maybe<gfx::IntSize>& aOutputSize,
+                         DecoderFlags aDecoderFlags,
                          SurfaceFlags aSurfaceFlags);
 
   /**
    * Creates and initializes an anonymous metadata decoder (one which isn't
    * associated with an Image object). This decoder will only decode the image's
    * header, extracting metadata like the size of the image. No actual image
    * data will be decoded and no surfaces will be allocated.
    *
--- a/image/DecoderFlags.h
+++ b/image/DecoderFlags.h
@@ -26,17 +26,25 @@ enum class DecoderFlags : uint8_t
   ASYNC_NOTIFY                   = 1 << 3,
 
   /**
    * By default, a surface is considered substitutable. That means callers are
    * willing to accept a less than ideal match to display. If a caller requires
    * a specific size and won't accept alternatives, then this flag should be
    * set.
    */
-  CANNOT_SUBSTITUTE              = 1 << 4
+  CANNOT_SUBSTITUTE              = 1 << 4,
+
+  /**
+   * By default, an animation decoder will produce partial frames that need to
+   * be combined with the previously displayed/composited frame by FrameAnimator
+   * to produce a complete frame. If this flag is set, the decoder will perform
+   * this blending at decode time, and the frames produced are complete.
+   */
+  BLEND_ANIMATION                = 1 << 5
 };
 MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(DecoderFlags)
 
 /**
  * @return the default set of decode flags.
  */
 inline DecoderFlags
 DefaultDecoderFlags()
--- a/image/FrameAnimator.cpp
+++ b/image/FrameAnimator.cpp
@@ -311,36 +311,38 @@ FrameAnimator::AdvanceFrame(AnimationSta
     return ret;
   }
 
   if (nextFrame->GetTimeout() == FrameTimeout::Forever()) {
     ret.mAnimationFinished = true;
   }
 
   if (nextFrameIndex == 0) {
+    MOZ_ASSERT(nextFrame->IsFullFrame());
     ret.mDirtyRect = aState.FirstFrameRefreshArea();
-  } else {
+  } else if (!nextFrame->IsFullFrame()) {
     MOZ_ASSERT(nextFrameIndex == currentFrameIndex + 1);
-
     // Change frame
     if (!DoBlend(aCurrentFrame, nextFrame, nextFrameIndex, &ret.mDirtyRect)) {
       // something went wrong, move on to next
       NS_WARNING("FrameAnimator::AdvanceFrame(): Compositing of frame failed");
       nextFrame->SetCompositingFailed(true);
       aState.mCurrentAnimationFrameTime =
         GetCurrentImgFrameEndTime(aState, aCurrentFrame->GetTimeout());
       aState.mCurrentAnimationFrameIndex = nextFrameIndex;
       aState.mCompositedFrameRequested = false;
       aCurrentFrame = std::move(nextFrame);
       aFrames.Advance(nextFrameIndex);
 
       return ret;
     }
 
     nextFrame->SetCompositingFailed(false);
+  } else {
+    ret.mDirtyRect = nextFrame->GetDirtyRect();
   }
 
   aState.mCurrentAnimationFrameTime =
     GetCurrentImgFrameEndTime(aState, aCurrentFrame->GetTimeout());
 
   // If we can get closer to the current time by a multiple of the image's loop
   // time, we should. We can only do this if we're done decoding; otherwise, we
   // don't know the full loop length, and LoopLength() will have to return
@@ -478,18 +480,23 @@ FrameAnimator::RequestRefresh(AnimationS
     // If we didn't advance a frame, and our frame end time didn't change,
     // then we need to break out of this loop & wait for the frame(s)
     // to finish downloading.
     if (!frameRes.mFrameAdvanced && currentFrameEndTime == oldFrameEndTime) {
       break;
     }
   }
 
-  // Advanced to the correct frame, the composited frame is now valid to be drawn.
-  if (currentFrameEndTime > aTime) {
+  // We should only mark the composited frame as valid and reset the dirty rect
+  // if we advanced (meaning the next frame was actually produced somehow), the
+  // composited frame was previously invalid (so we may need to repaint
+  // everything) and the frame index is valid (to know we were doing blending
+  // on the main thread, instead of on the decoder threads in advance).
+  if (currentFrameEndTime > aTime && aState.mCompositedFrameInvalid &&
+      mLastCompositedFrameIndex >= 0) {
     aState.mCompositedFrameInvalid = false;
     ret.mDirtyRect = IntRect(IntPoint(0,0), mSize);
   }
 
   MOZ_ASSERT(!aState.mIsCurrentlyDecoded || !aState.mCompositedFrameInvalid);
 
   return ret;
 }
--- a/image/ImageOps.cpp
+++ b/image/ImageOps.cpp
@@ -220,17 +220,19 @@ ImageOps::DecodeToSurface(ImageBuffer* a
   }
 
   // Create a decoder.
   DecoderType decoderType =
     DecoderFactory::GetDecoderType(PromiseFlatCString(aMimeType).get());
   RefPtr<Decoder> decoder =
     DecoderFactory::CreateAnonymousDecoder(decoderType,
                                            WrapNotNull(sourceBuffer),
-                                           aSize, ToSurfaceFlags(aFlags));
+                                           aSize,
+                                           DecoderFlags::FIRST_FRAME_ONLY,
+                                           ToSurfaceFlags(aFlags));
   if (!decoder) {
     return nullptr;
   }
 
   // Run the decoder synchronously.
   RefPtr<IDecodingTask> task = new AnonymousDecodingTask(WrapNotNull(decoder));
   task->Run();
   if (!decoder->GetDecodeDone() || decoder->HasError()) {
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -1281,16 +1281,20 @@ RasterImage::Decode(const IntSize& aSize
     surfaceFlags &= ~SurfaceFlags::NO_PREMULTIPLY_ALPHA;
   }
 
   // Create a decoder.
   RefPtr<IDecodingTask> task;
   nsresult rv;
   bool animated = mAnimationState && aPlaybackType == PlaybackType::eAnimated;
   if (animated) {
+    if (gfxPrefs::ImageAnimatedGenerateFullFrames()) {
+      decoderFlags |= DecoderFlags::BLEND_ANIMATION;
+    }
+
     size_t currentFrame = mAnimationState->GetCurrentAnimationFrameIndex();
     rv = DecoderFactory::CreateAnimationDecoder(mDecoderType, WrapNotNull(this),
                                                 mSourceBuffer, mSize,
                                                 decoderFlags, surfaceFlags,
                                                 currentFrame,
                                                 getter_AddRefs(task));
   } else {
     rv = DecoderFactory::CreateDecoder(mDecoderType, WrapNotNull(this),
--- a/image/SurfaceFilters.h
+++ b/image/SurfaceFilters.h
@@ -15,16 +15,17 @@
 #include <algorithm>
 #include <stdint.h>
 #include <string.h>
 
 #include "mozilla/Likely.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/gfx/2D.h"
+#include "skia/src/core/SkBlitRow.h"
 
 #include "DownscalingFilter.h"
 #include "SurfaceCache.h"
 #include "SurfacePipe.h"
 
 namespace mozilla {
 namespace image {
 
@@ -323,16 +324,389 @@ private:
   UniquePtr<uint8_t[]> mBuffer;  /// The buffer used to store reordered rows.
   int32_t mInputRow;             /// The current row we're reading. (0-indexed)
   int32_t mOutputRow;            /// The current row we're writing. (0-indexed)
   uint8_t mPass;                 /// Which pass we're on. (0-indexed)
   bool mProgressiveDisplay;      /// If true, duplicate rows to optimize for
                                  /// progressive display.
 };
 
+//////////////////////////////////////////////////////////////////////////////
+// BlendAnimationFilter
+//////////////////////////////////////////////////////////////////////////////
+
+template <typename Next> class BlendAnimationFilter;
+
+/**
+ * A configuration struct for BlendAnimationFilter.
+ */
+struct BlendAnimationConfig
+{
+  template <typename Next> using Filter = BlendAnimationFilter<Next>;
+  Decoder* mDecoder;           /// The decoder producing the animation.
+};
+
+/**
+ * BlendAnimationFilter turns a partial image as part of an animation into a
+ * complete frame given its frame rect, blend method, and the base frame's
+ * data buffer, frame rect and disposal method. Any excess data caused by a
+ * frame rect not being contained by the output size will be discarded.
+ *
+ * The base frame is an already produced complete frame from the animation.
+ * It may be any previous frame depending on the disposal method, although
+ * most often it will be the immediate previous frame to the current we are
+ * generating.
+ *
+ * The 'Next' template parameter specifies the next filter in the chain.
+ */
+template <typename Next>
+class BlendAnimationFilter final : public SurfaceFilter
+{
+public:
+  BlendAnimationFilter()
+    : mRow(0)
+    , mRowLength(0)
+    , mOverProc(nullptr)
+    , mBaseFrameStartPtr(nullptr)
+    , mBaseFrameRowPtr(nullptr)
+  { }
+
+  template <typename... Rest>
+  nsresult Configure(const BlendAnimationConfig& aConfig, const Rest&... aRest)
+  {
+    nsresult rv = mNext.Configure(aRest...);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+
+    if (!aConfig.mDecoder || !aConfig.mDecoder->ShouldBlendAnimation()) {
+      MOZ_ASSERT_UNREACHABLE("Expected image decoder that is blending!");
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    imgFrame* currentFrame = aConfig.mDecoder->GetCurrentFrame();
+    if (!currentFrame) {
+      MOZ_ASSERT_UNREACHABLE("Decoder must have current frame!");
+      return NS_ERROR_FAILURE;
+    }
+
+    mFrameRect = mUnclampedFrameRect = currentFrame->GetBlendRect();
+    gfx::IntSize outputSize = mNext.InputSize();
+    mRowLength = outputSize.width * sizeof(uint32_t);
+
+    // Forbid frame rects with negative size.
+    if (mUnclampedFrameRect.width < 0 || mUnclampedFrameRect.height < 0) {
+      return NS_ERROR_FAILURE;
+    }
+
+    // Clamp mFrameRect to the output size.
+    gfx::IntRect outputRect(0, 0, outputSize.width, outputSize.height);
+    mFrameRect = mFrameRect.Intersect(outputRect);
+    bool fullFrame = outputRect.IsEqualEdges(mFrameRect);
+
+    // If there's no intersection, |mFrameRect| will be an empty rect positioned
+    // at the maximum of |inputRect|'s and |aFrameRect|'s coordinates, which is
+    // not what we want. Force it to (0, 0) in that case.
+    if (mFrameRect.IsEmpty()) {
+      mFrameRect.MoveTo(0, 0);
+    }
+
+    BlendMethod blendMethod = currentFrame->GetBlendMethod();
+    switch (blendMethod) {
+      default:
+        blendMethod = BlendMethod::SOURCE;
+        MOZ_FALLTHROUGH_ASSERT("Unexpected blend method!");
+      case BlendMethod::SOURCE:
+        // Default, overwrites base frame data (if any) with new.
+        break;
+      case BlendMethod::OVER:
+        // OVER only has an impact on the output if we have new data to blend
+        // with.
+        if (mFrameRect.IsEmpty()) {
+          blendMethod = BlendMethod::SOURCE;
+        }
+        break;
+    }
+
+    // Determine what we need to clear and what we need to copy. If this frame
+    // is a full frame and uses source blending, there is no need to consider
+    // the disposal method of the previous frame.
+    gfx::IntRect dirtyRect(outputRect);
+    if (!fullFrame || blendMethod != BlendMethod::SOURCE) {
+      const RawAccessFrameRef& restoreFrame =
+        aConfig.mDecoder->GetRestoreFrameRef();
+      if (restoreFrame) {
+        MOZ_ASSERT(restoreFrame->GetImageSize() == outputSize);
+        MOZ_ASSERT(restoreFrame->IsFinished());
+
+        // We can safely use this pointer without holding a RawAccessFrameRef
+        // because the decoder will keep it alive for us.
+        mBaseFrameStartPtr = restoreFrame.Data();
+        MOZ_ASSERT(mBaseFrameStartPtr);
+
+        gfx::IntRect restoreBlendRect = restoreFrame->GetBoundedBlendRect();
+        gfx::IntRect restoreDirtyRect = aConfig.mDecoder->GetRestoreDirtyRect();
+        switch (restoreFrame->GetDisposalMethod()) {
+          default:
+          case DisposalMethod::RESTORE_PREVIOUS:
+            MOZ_FALLTHROUGH_ASSERT("Unexpected DisposalMethod");
+          case DisposalMethod::NOT_SPECIFIED:
+          case DisposalMethod::KEEP:
+            dirtyRect = mFrameRect.Union(restoreDirtyRect);
+            break;
+          case DisposalMethod::CLEAR:
+            // We only need to clear if the rect is outside the frame rect (i.e.
+            // overwrites a non-overlapping area) or the blend method may cause
+            // us to combine old data and new.
+            if (!mFrameRect.Contains(restoreBlendRect) ||
+                blendMethod == BlendMethod::OVER) {
+              mClearRect = restoreBlendRect;
+            }
+
+            // If we are clearing the whole frame, we do not need to retain a
+            // reference to the base frame buffer.
+            if (outputRect.IsEqualEdges(mClearRect)) {
+              mBaseFrameStartPtr = nullptr;
+            } else {
+              dirtyRect = mFrameRect.Union(restoreDirtyRect).Union(mClearRect);
+            }
+            break;
+        }
+      } else if (!fullFrame) {
+        // This must be the first frame, clear everything.
+        mClearRect = outputRect;
+      }
+    }
+
+    // The dirty rect, or delta between the current frame and the previous frame
+    // (chronologically, not necessarily the restore frame) is the last
+    // animation parameter we need to initialize the new frame with.
+    currentFrame->SetDirtyRect(dirtyRect);
+
+    if (!mBaseFrameStartPtr) {
+      // Switch to SOURCE if no base frame to ensure we don't allocate an
+      // intermediate buffer below. OVER does nothing without the base frame
+      // data.
+      blendMethod = BlendMethod::SOURCE;
+    }
+
+    // Skia provides arch-specific accelerated methods to perform blending.
+    // Note that this is an internal Skia API and may be prone to change,
+    // but we avoid the overhead of setting up Skia objects.
+    if (blendMethod == BlendMethod::OVER) {
+      mOverProc = SkBlitRow::Factory32(SkBlitRow::kSrcPixelAlpha_Flag32);
+      MOZ_ASSERT(mOverProc);
+    }
+
+    // We don't need an intermediate buffer unless the unclamped frame rect
+    // width is larger than the clamped frame rect width. In that case, the
+    // caller will end up writing data that won't end up in the final image at
+    // all, and we'll need a buffer to give that data a place to go.
+    if (mFrameRect.width < mUnclampedFrameRect.width || mOverProc) {
+      mBuffer.reset(new (fallible) uint8_t[mUnclampedFrameRect.width *
+                                           sizeof(uint32_t)]);
+      if (MOZ_UNLIKELY(!mBuffer)) {
+        return NS_ERROR_OUT_OF_MEMORY;
+      }
+
+      memset(mBuffer.get(), 0, mUnclampedFrameRect.width * sizeof(uint32_t));
+    }
+
+    ConfigureFilter(mUnclampedFrameRect.Size(), sizeof(uint32_t));
+    return NS_OK;
+  }
+
+  Maybe<SurfaceInvalidRect> TakeInvalidRect() override
+  {
+    return mNext.TakeInvalidRect();
+  }
+
+protected:
+  uint8_t* DoResetToFirstRow() override
+  {
+    uint8_t* rowPtr = mNext.ResetToFirstRow();
+    if (rowPtr == nullptr) {
+      mRow = mFrameRect.YMost();
+      return nullptr;
+    }
+
+    mRow = 0;
+    mBaseFrameRowPtr = mBaseFrameStartPtr;
+
+    while (mRow < mFrameRect.y) {
+      WriteBaseFrameRow();
+      AdvanceRowOutsideFrameRect();
+    }
+
+    // We're at the beginning of the frame rect now, so return if we're either
+    // ready for input or we're already done.
+    rowPtr = mBuffer ? mBuffer.get() : mNext.CurrentRowPointer();
+    if (!mFrameRect.IsEmpty() || rowPtr == nullptr) {
+      // Note that the pointer we're returning is for the next row we're
+      // actually going to write to, but we may discard writes before that point
+      // if mRow < mFrameRect.y.
+      mRow = mUnclampedFrameRect.y;
+      WriteBaseFrameRow();
+      return AdjustRowPointer(rowPtr);
+    }
+
+    // We've finished the region specified by the frame rect, but the frame rect
+    // is empty, so we need to output the rest of the image immediately. Advance
+    // to the end of the next pipeline stage's buffer, outputting rows that are
+    // copied from the base frame and/or cleared.
+    WriteBaseFrameRowsUntilComplete();
+
+    mRow = mFrameRect.YMost();
+    return nullptr;  // We're done.
+  }
+
+  uint8_t* DoAdvanceRow() override
+  {
+    uint8_t* rowPtr = nullptr;
+
+    const int32_t currentRow = mRow;
+    mRow++;
+
+    // The unclamped frame rect has a negative offset which means -y rows from
+    // the decoder need to be discarded before we advance properly.
+    if (currentRow >= 0 && mBaseFrameRowPtr) {
+      mBaseFrameRowPtr += mRowLength;
+    }
+
+    if (currentRow < mFrameRect.y) {
+      // This row is outside of the frame rect, so just drop it on the floor.
+      rowPtr = mBuffer ? mBuffer.get() : mNext.CurrentRowPointer();
+      return AdjustRowPointer(rowPtr);
+    } else if (NS_WARN_IF(currentRow >= mFrameRect.YMost())) {
+      return nullptr;
+    }
+
+    // If we had to buffer, merge the data into the row. Otherwise we had the
+    // decoder write directly to the next stage's buffer.
+    if (mBuffer) {
+      int32_t width = mFrameRect.width;
+      uint32_t* dst = reinterpret_cast<uint32_t*>(mNext.CurrentRowPointer());
+      uint32_t* src = reinterpret_cast<uint32_t*>(mBuffer.get()) -
+                      std::min(mUnclampedFrameRect.x, 0);
+      dst += mFrameRect.x;
+      if (mOverProc) {
+        mOverProc(dst, src, width, 0xFF);
+      } else {
+        memcpy(dst, src, width * sizeof(uint32_t));
+      }
+      rowPtr = mNext.AdvanceRow() ? mBuffer.get() : nullptr;
+    } else {
+      MOZ_ASSERT(!mOverProc);
+      rowPtr = mNext.AdvanceRow();
+    }
+
+    // If there's still more data coming or we're already done, just adjust the
+    // pointer and return.
+    if (mRow < mFrameRect.YMost() || rowPtr == nullptr) {
+      WriteBaseFrameRow();
+      return AdjustRowPointer(rowPtr);
+    }
+
+    // We've finished the region specified by the frame rect. Advance to the end
+    // of the next pipeline stage's buffer, outputting rows that are copied from
+    // the base frame and/or cleared.
+    WriteBaseFrameRowsUntilComplete();
+
+    return nullptr;  // We're done.
+  }
+
+private:
+  void WriteBaseFrameRowsUntilComplete()
+  {
+    do {
+      WriteBaseFrameRow();
+    } while (AdvanceRowOutsideFrameRect());
+  }
+
+  void WriteBaseFrameRow()
+  {
+    uint8_t* dest = mNext.CurrentRowPointer();
+    if (!dest) {
+      return;
+    }
+
+    if (!mBaseFrameRowPtr) {
+      // No base frame, so we are clearing everything.
+      memset(dest, 0, mRowLength);
+    } else if (mClearRect.height > 0 &&
+               mClearRect.y <= mRow &&
+               mClearRect.YMost() > mRow) {
+      // We have a base frame, but we are inside the area to be cleared.
+      // Only copy the data we need from the source.
+      size_t prefixLength = mClearRect.x * sizeof(uint32_t);
+      size_t clearLength = mClearRect.width * sizeof(uint32_t);
+      size_t postfixOffset = prefixLength + clearLength;
+      size_t postfixLength = mRowLength - postfixOffset;
+      MOZ_ASSERT(prefixLength + clearLength + postfixLength == mRowLength);
+      memcpy(dest, mBaseFrameRowPtr, prefixLength);
+      memset(dest + prefixLength, 0, clearLength);
+      memcpy(dest + postfixOffset, mBaseFrameRowPtr + postfixOffset, postfixLength);
+    } else {
+      memcpy(dest, mBaseFrameRowPtr, mRowLength);
+    }
+  }
+
+  bool AdvanceRowOutsideFrameRect()
+  {
+    // The unclamped frame rect may have a negative offset however we should
+    // never be advancing the row via this path (otherwise mBaseFrameRowPtr
+    // will be wrong.
+    MOZ_ASSERT(mRow >= 0);
+    MOZ_ASSERT(mRow < mFrameRect.y || mRow >= mFrameRect.YMost());
+
+    mRow++;
+    if (mBaseFrameRowPtr) {
+      mBaseFrameRowPtr += mRowLength;
+    }
+
+    return mNext.AdvanceRow() != nullptr;
+  }
+
+  uint8_t* AdjustRowPointer(uint8_t* aNextRowPointer) const
+  {
+    if (mBuffer) {
+      MOZ_ASSERT(aNextRowPointer == mBuffer.get() || aNextRowPointer == nullptr);
+      return aNextRowPointer;  // No adjustment needed for an intermediate buffer.
+    }
+
+    if (mFrameRect.IsEmpty() ||
+        mRow >= mFrameRect.YMost() ||
+        aNextRowPointer == nullptr) {
+      return nullptr;  // Nothing left to write.
+    }
+
+    MOZ_ASSERT(!mOverProc);
+    return aNextRowPointer + mFrameRect.x * sizeof(uint32_t);
+  }
+
+  Next mNext;                          /// The next SurfaceFilter in the chain.
+
+  gfx::IntRect mFrameRect;             /// The surface subrect which contains data,
+                                       /// clamped to the image size.
+  gfx::IntRect mUnclampedFrameRect;    /// The frame rect before clamping.
+  UniquePtr<uint8_t[]> mBuffer;        /// The intermediate buffer, if one is
+                                       /// necessary because the frame rect width
+                                       /// is larger than the image's logical width.
+  int32_t  mRow;                       /// The row in unclamped frame rect space
+                                       /// that we're currently writing.
+  size_t mRowLength;                   /// Length in bytes of a row that is the input
+                                       /// for the next filter.
+  SkBlitRow::Proc32 mOverProc;         /// Function pointer to perform over blending.
+  const uint8_t* mBaseFrameStartPtr;   /// Starting row pointer to the base frame
+                                       /// data from which we copy pixel data from.
+  const uint8_t* mBaseFrameRowPtr;     /// Current row pointer to the base frame
+                                       /// data.
+  gfx::IntRect mClearRect;             /// The frame area to clear before blending
+                                       /// the current frame.
+};
 
 //////////////////////////////////////////////////////////////////////////////
 // RemoveFrameRectFilter
 //////////////////////////////////////////////////////////////////////////////
 
 template <typename Next> class RemoveFrameRectFilter;
 
 /**
--- a/image/SurfacePipeFactory.h
+++ b/image/SurfacePipeFactory.h
@@ -49,21 +49,25 @@ enum class SurfacePipeFlags
   DEINTERLACE         = 1 << 0,  // If set, deinterlace the image.
 
   ADAM7_INTERPOLATE   = 1 << 1,  // If set, the caller is deinterlacing the
                                  // image using ADAM7, and we may want to
                                  // interpolate it for better intermediate results.
 
   FLIP_VERTICALLY     = 1 << 2,  // If set, flip the image vertically.
 
-  PROGRESSIVE_DISPLAY = 1 << 3   // If set, we expect the image to be displayed
+  PROGRESSIVE_DISPLAY = 1 << 3,  // If set, we expect the image to be displayed
                                  // progressively. This enables features that
                                  // result in a better user experience for
                                  // progressive display but which may be more
                                  // computationally expensive.
+
+  BLEND_ANIMATION     = 1 << 4   // If set, produce the next full frame of an
+                                 // animation instead of a partial frame to be
+                                 // blended later.
 };
 MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(SurfacePipeFlags)
 
 class SurfacePipeFactory
 {
 public:
   /**
    * Creates and initializes a normal (i.e., non-paletted) SurfacePipe.
@@ -96,45 +100,50 @@ public:
                     SurfacePipeFlags aFlags)
   {
     const bool deinterlace = bool(aFlags & SurfacePipeFlags::DEINTERLACE);
     const bool flipVertically = bool(aFlags & SurfacePipeFlags::FLIP_VERTICALLY);
     const bool progressiveDisplay = bool(aFlags & SurfacePipeFlags::PROGRESSIVE_DISPLAY);
     const bool downscale = aInputSize != aOutputSize;
     const bool removeFrameRect =
       !aFrameRect.IsEqualEdges(nsIntRect(0, 0, aInputSize.width, aInputSize.height));
+    const bool blendAnimation = bool(aFlags & SurfacePipeFlags::BLEND_ANIMATION);
 
     // Don't interpolate if we're sure we won't show this surface to the user
     // until it's completely decoded. The final pass of an ADAM7 image doesn't
     // need interpolation, so we only need to interpolate if we'll be displaying
     // the image while it's still being decoded.
     const bool adam7Interpolate = bool(aFlags & SurfacePipeFlags::ADAM7_INTERPOLATE) &&
                                   progressiveDisplay;
 
     if (deinterlace && adam7Interpolate) {
       MOZ_ASSERT_UNREACHABLE("ADAM7 deinterlacing is handled by libpng");
       return Nothing();
     }
 
+    MOZ_ASSERT_IF(blendAnimation, aAnimParams);
+
     // Construct configurations for the SurfaceFilters. Note that the order of
     // these filters is significant. We want to deinterlace or interpolate raw
     // input rows, before any other transformations, and we want to remove the
     // frame rect (which may involve adding blank rows or columns to the image)
     // before any downscaling, so that the new rows and columns are taken into
     // account.
     DeinterlacingConfig<uint32_t> deinterlacingConfig { progressiveDisplay };
     ADAM7InterpolatingConfig interpolatingConfig;
     RemoveFrameRectConfig removeFrameRectConfig { aFrameRect };
+    BlendAnimationConfig blendAnimationConfig { aDecoder };
     DownscalingConfig downscalingConfig { aInputSize, aFormat };
     SurfaceConfig surfaceConfig { aDecoder, aOutputSize, aFormat,
                                   flipVertically, aAnimParams };
 
     Maybe<SurfacePipe> pipe;
 
     if (downscale) {
+      MOZ_ASSERT(!blendAnimation);
       if (removeFrameRect) {
         if (deinterlace) {
           pipe = MakePipe(deinterlacingConfig, removeFrameRectConfig,
                           downscalingConfig, surfaceConfig);
         } else if (adam7Interpolate) {
           pipe = MakePipe(interpolatingConfig, removeFrameRectConfig,
                           downscalingConfig, surfaceConfig);
         } else {  // (deinterlace and adam7Interpolate are false)
@@ -145,25 +154,33 @@ public:
           pipe = MakePipe(deinterlacingConfig, downscalingConfig, surfaceConfig);
         } else if (adam7Interpolate) {
           pipe = MakePipe(interpolatingConfig, downscalingConfig, surfaceConfig);
         } else {  // (deinterlace and adam7Interpolate are false)
           pipe = MakePipe(downscalingConfig, surfaceConfig);
         }
       }
     } else {  // (downscale is false)
-      if (removeFrameRect) {
+      if (blendAnimation) {
+        if (deinterlace) {
+          pipe = MakePipe(deinterlacingConfig, blendAnimationConfig, surfaceConfig);
+        } else if (adam7Interpolate) {
+          pipe = MakePipe(interpolatingConfig, blendAnimationConfig, surfaceConfig);
+        } else {  // (deinterlace and adam7Interpolate are false)
+          pipe = MakePipe(blendAnimationConfig, surfaceConfig);
+        }
+      } else if (removeFrameRect) {
         if (deinterlace) {
           pipe = MakePipe(deinterlacingConfig, removeFrameRectConfig, surfaceConfig);
         } else if (adam7Interpolate) {
           pipe = MakePipe(interpolatingConfig, removeFrameRectConfig, surfaceConfig);
         } else {  // (deinterlace and adam7Interpolate are false)
           pipe = MakePipe(removeFrameRectConfig, surfaceConfig);
         }
-      } else {  // (removeFrameRect is false)
+      } else {  // (blendAnimation and removeFrameRect is false)
         if (deinterlace) {
           pipe = MakePipe(deinterlacingConfig, surfaceConfig);
         } else if (adam7Interpolate) {
           pipe = MakePipe(interpolatingConfig, surfaceConfig);
         } else {  // (deinterlace and adam7Interpolate are false)
           pipe = MakePipe(surfaceConfig);
         }
       }
--- a/image/decoders/GIF2.h
+++ b/image/decoders/GIF2.h
@@ -32,16 +32,18 @@ typedef struct gif_struct {
 
     // Output state machine
     int64_t pixels_remaining;  // Pixels remaining to be output.
 
     // Parameters for image frame currently being decoded
     int tpixel;                 // Index of transparent pixel
     int32_t disposal_method;    // Restore to background, leave in place, etc.
     uint32_t* local_colormap;   // Per-image colormap
+    uint32_t local_colormap_buffer_size; // Size of the buffer containing the
+                                         // local colormap.
     int local_colormap_size;    // Size of local colormap array.
     uint32_t delay_time;        // Display time, in milliseconds,
                                 // for this image in a multi-image GIF
 
     // Global (multi-image) state
     int version;                // Either 89 for GIF89 or 87 for GIF87
     int32_t screen_width;       // Logical screen width & height
     int32_t screen_height;
--- a/image/decoders/nsGIFDecoder2.cpp
+++ b/image/decoders/nsGIFDecoder2.cpp
@@ -174,40 +174,50 @@ nsGIFDecoder2::CheckForTransparency(cons
 nsresult
 nsGIFDecoder2::BeginImageFrame(const IntRect& aFrameRect,
                                uint16_t aDepth,
                                bool aIsInterlaced)
 {
   MOZ_ASSERT(HasSize());
 
   bool hasTransparency = CheckForTransparency(aFrameRect);
+  bool blendAnimation = ShouldBlendAnimation();
 
   // Make sure there's no animation if we're downscaling.
   MOZ_ASSERT_IF(Size() != OutputSize(), !GetImageMetadata().HasAnimation());
 
   AnimationParams animParams {
     aFrameRect,
     FrameTimeout::FromRawMilliseconds(mGIFStruct.delay_time),
     uint32_t(mGIFStruct.images_decoded),
     BlendMethod::OVER,
     DisposalMethod(mGIFStruct.disposal_method)
   };
 
   SurfacePipeFlags pipeFlags = aIsInterlaced
                              ? SurfacePipeFlags::DEINTERLACE
                              : SurfacePipeFlags();
 
-  Maybe<SurfacePipe> pipe;
+  gfx::SurfaceFormat format;
   if (mGIFStruct.images_decoded == 0) {
-    gfx::SurfaceFormat format = hasTransparency ? SurfaceFormat::B8G8R8A8
-                                                : SurfaceFormat::B8G8R8X8;
-
     // The first frame may be displayed progressively.
     pipeFlags |= SurfacePipeFlags::PROGRESSIVE_DISPLAY;
 
+    format = hasTransparency ? SurfaceFormat::B8G8R8A8
+                             : SurfaceFormat::B8G8R8X8;
+  } else {
+    format = SurfaceFormat::B8G8R8A8;
+  }
+
+  if (blendAnimation) {
+    pipeFlags |= SurfacePipeFlags::BLEND_ANIMATION;
+  }
+
+  Maybe<SurfacePipe> pipe;
+  if (mGIFStruct.images_decoded == 0 || blendAnimation) {
     // The first frame is always decoded into an RGB surface.
     pipe =
       SurfacePipeFactory::CreateSurfacePipe(this, Size(), OutputSize(),
                                             aFrameRect, format,
                                             Some(animParams), pipeFlags);
   } else {
     // This is an animation frame (and not the first). To minimize the memory
     // usage of animations, the image data is stored in paletted form.
@@ -215,18 +225,18 @@ nsGIFDecoder2::BeginImageFrame(const Int
     // We should never use paletted surfaces with a draw target directly, so
     // the only practical difference between B8G8R8A8 and B8G8R8X8 is the
     // cleared pixel value if we get truncated. We want 0 in that case to
     // ensure it is an acceptable value for the color map as was the case
     // historically.
     MOZ_ASSERT(Size() == OutputSize());
     pipe =
       SurfacePipeFactory::CreatePalettedSurfacePipe(this, Size(), aFrameRect,
-                                                    SurfaceFormat::B8G8R8A8,
-                                                    aDepth, Some(animParams),
+                                                    format, aDepth,
+                                                    Some(animParams),
                                                     pipeFlags);
   }
 
   mCurrentFrameIndex = mGIFStruct.images_decoded;
 
   if (!pipe) {
     mPipe = SurfacePipe();
     return NS_ERROR_FAILURE;
@@ -903,28 +913,40 @@ nsGIFDecoder2::FinishImageDescriptor(con
   mGIFStruct.pixels_remaining =
     int64_t(frameRect.Width()) * int64_t(frameRect.Height());
 
   if (haveLocalColorTable) {
     // We have a local color table, so prepare to read it into the palette of
     // the current frame.
     mGIFStruct.local_colormap_size = 1 << depth;
 
-    if (mGIFStruct.images_decoded == 0) {
-      // The first frame has a local color table. Allocate space for it as we
-      // use a BGRA or BGRX surface for the first frame; such surfaces don't
-      // have their own palettes internally.
+    if (!mColormap) {
+      // Allocate a buffer to store the local color tables. This could be if the
+      // first frame has a local color table, or for subsequent frames when
+      // blending the animation during decoding.
+      MOZ_ASSERT(mGIFStruct.images_decoded == 0 || ShouldBlendAnimation());
+
+      // Ensure our current colormap buffer is large enough to hold the new one.
       mColormapSize = sizeof(uint32_t) << realDepth;
-      if (!mGIFStruct.local_colormap) {
+      if (mGIFStruct.local_colormap_buffer_size < mColormapSize) {
+        if (mGIFStruct.local_colormap) {
+          free(mGIFStruct.local_colormap);
+        }
+        mGIFStruct.local_colormap_buffer_size = mColormapSize;
         mGIFStruct.local_colormap =
           static_cast<uint32_t*>(moz_xmalloc(mColormapSize));
+      } else {
+        mColormapSize = mGIFStruct.local_colormap_buffer_size;
       }
+
       mColormap = mGIFStruct.local_colormap;
     }
 
+    MOZ_ASSERT(mColormap);
+
     const size_t size = 3 << depth;
     if (mColormapSize > size) {
       // Clear the part of the colormap which will be unused with this palette.
       // If a GIF references an invalid palette entry, ensure the entry is opaque white.
       // This is needed for Skia as if it isn't, RGBX surfaces will cause blending issues
       // with Skia.
       memset(reinterpret_cast<uint8_t*>(mColormap) + size, 0xFF,
              mColormapSize - size);
@@ -936,17 +958,17 @@ nsGIFDecoder2::FinishImageDescriptor(con
     // large and it'd be preferable to avoid unnecessary copies.
     return Transition::ToUnbuffered(State::FINISHED_LOCAL_COLOR_TABLE,
                                     State::LOCAL_COLOR_TABLE,
                                     size);
   }
 
   // There's no local color table; copy the global color table into the palette
   // of the current frame.
-  if (mGIFStruct.images_decoded > 0) {
+  if (mColormap) {
     memcpy(mColormap, mGIFStruct.global_colormap, mColormapSize);
   } else {
     mColormap = mGIFStruct.global_colormap;
   }
 
   return Transition::To(State::IMAGE_DATA_BLOCK, BLOCK_HEADER_LEN);
 }
 
@@ -1046,17 +1068,17 @@ nsGIFDecoder2::ReadLZWData(const char* a
 {
   const uint8_t* data = reinterpret_cast<const uint8_t*>(aData);
   size_t length = aLength;
 
   while (mGIFStruct.pixels_remaining > 0 &&
          (length > 0 || mGIFStruct.bits >= mGIFStruct.codesize)) {
     size_t bytesRead = 0;
 
-    auto result = mGIFStruct.images_decoded == 0
+    auto result = mGIFStruct.images_decoded == 0 || ShouldBlendAnimation()
       ? mPipe.WritePixelBlocks<uint32_t>([&](uint32_t* aPixelBlock, int32_t aBlockSize) {
           return YieldPixels<uint32_t>(data, length, &bytesRead, aPixelBlock, aBlockSize);
         })
       : mPipe.WritePixelBlocks<uint8_t>([&](uint8_t* aPixelBlock, int32_t aBlockSize) {
           return YieldPixels<uint8_t>(data, length, &bytesRead, aPixelBlock, aBlockSize);
         });
 
     if (MOZ_UNLIKELY(bytesRead > length)) {
--- a/image/decoders/nsPNGDecoder.cpp
+++ b/image/decoders/nsPNGDecoder.cpp
@@ -228,16 +228,20 @@ nsPNGDecoder::CreateFrame(const FrameInf
                              ? SurfacePipeFlags::ADAM7_INTERPOLATE
                              : SurfacePipeFlags();
 
   if (mNumFrames == 0) {
     // The first frame may be displayed progressively.
     pipeFlags |= SurfacePipeFlags::PROGRESSIVE_DISPLAY;
   }
 
+  if (ShouldBlendAnimation()) {
+    pipeFlags |= SurfacePipeFlags::BLEND_ANIMATION;
+  }
+
   Maybe<SurfacePipe> pipe =
     SurfacePipeFactory::CreateSurfacePipe(this, Size(), OutputSize(),
                                           aFrameInfo.mFrameRect, mFormat,
                                           animParams, pipeFlags);
 
   if (!pipe) {
     mPipe = SurfacePipe();
     return NS_ERROR_FAILURE;
--- a/image/imgFrame.cpp
+++ b/image/imgFrame.cpp
@@ -103,34 +103,33 @@ ShouldUseHeap(const IntSize& aSize,
   }
 
   return false;
 }
 
 static already_AddRefed<DataSourceSurface>
 AllocateBufferForImage(const IntSize& size,
                        SurfaceFormat format,
-                       bool aIsAnimated = false)
+                       bool aIsAnimated = false,
+                       bool aIsFullFrame = true)
 {
   int32_t stride = VolatileSurfaceStride(size, format);
 
-  if (ShouldUseHeap(size, stride, aIsAnimated)) {
+  if (gfxVars::GetUseWebRenderOrDefault() &&
+      gfxPrefs::ImageMemShared() && aIsFullFrame) {
+    RefPtr<SourceSurfaceSharedData> newSurf = new SourceSurfaceSharedData();
+    if (newSurf->Init(size, stride, format)) {
+      return newSurf.forget();
+    }
+  } else if (ShouldUseHeap(size, stride, aIsAnimated)) {
     RefPtr<SourceSurfaceAlignedRawData> newSurf =
       new SourceSurfaceAlignedRawData();
     if (newSurf->Init(size, format, false, 0, stride)) {
       return newSurf.forget();
     }
-  }
-
-  if (!aIsAnimated && gfxVars::GetUseWebRenderOrDefault()
-                   && gfxPrefs::ImageMemShared()) {
-    RefPtr<SourceSurfaceSharedData> newSurf = new SourceSurfaceSharedData();
-    if (newSurf->Init(size, stride, format)) {
-      return newSurf.forget();
-    }
   } else {
     RefPtr<SourceSurfaceVolatileData> newSurf= new SourceSurfaceVolatileData();
     if (newSurf->Init(size, stride, format)) {
       return newSurf.forget();
     }
   }
   return nullptr;
 }
@@ -208,16 +207,17 @@ imgFrame::imgFrame()
   , mOptimizable(false)
   , mTimeout(FrameTimeout::FromRawMilliseconds(100))
   , mDisposalMethod(DisposalMethod::NOT_SPECIFIED)
   , mBlendMethod(BlendMethod::OVER)
   , mFormat(SurfaceFormat::UNKNOWN)
   , mPalettedImageData(nullptr)
   , mPaletteDepth(0)
   , mNonPremult(false)
+  , mIsFullFrame(false)
   , mCompositingFailed(false)
 {
 }
 
 imgFrame::~imgFrame()
 {
 #ifdef DEBUG
   MonitorAutoLock lock(mMonitor);
@@ -230,36 +230,44 @@ imgFrame::~imgFrame()
 }
 
 nsresult
 imgFrame::InitForDecoder(const nsIntSize& aImageSize,
                          const nsIntRect& aRect,
                          SurfaceFormat aFormat,
                          uint8_t aPaletteDepth /* = 0 */,
                          bool aNonPremult /* = false */,
-                         const Maybe<AnimationParams>& aAnimParams /* = Nothing() */)
+                         const Maybe<AnimationParams>& aAnimParams /* = Nothing() */,
+                         bool aIsFullFrame /* = false */)
 {
   // Assert for properties that should be verified by decoders,
   // warn for properties related to bad content.
   if (!AllowedImageAndFrameDimensions(aImageSize, aRect)) {
     NS_WARNING("Should have legal image size");
     mAborted = true;
     return NS_ERROR_FAILURE;
   }
 
   mImageSize = aImageSize;
   mFrameRect = aRect;
 
+  // May be updated shortly after InitForDecoder by BlendAnimationFilter
+  // because it needs to take into consideration the previous frames to
+  // properly calculate. We start with the whole frame as dirty.
+  mDirtyRect = aRect;
+
   if (aAnimParams) {
     mBlendRect = aAnimParams->mBlendRect;
     mTimeout = aAnimParams->mTimeout;
     mBlendMethod = aAnimParams->mBlendMethod;
     mDisposalMethod = aAnimParams->mDisposalMethod;
+    mIsFullFrame = aAnimParams->mFrameNum == 0 || aIsFullFrame;
   } else {
     mBlendRect = aRect;
+    mIsFullFrame = true;
   }
 
   // We only allow a non-trivial frame rect (i.e., a frame rect that doesn't
   // cover the entire image) for paletted animation frames. We never draw those
   // frames directly; we just use FrameAnimator to composite them and produce a
   // BGRA surface that we actually draw. We enforce this here to make sure that
   // imgFrame::Draw(), which is responsible for drawing all other kinds of
   // frames, never has to deal with a non-trivial frame rect.
@@ -290,17 +298,18 @@ imgFrame::InitForDecoder(const nsIntSize
     if (!mPalettedImageData) {
       NS_WARNING("Call to calloc for paletted image data should succeed");
     }
     NS_ENSURE_TRUE(mPalettedImageData, NS_ERROR_OUT_OF_MEMORY);
   } else {
     MOZ_ASSERT(!mLockedSurface, "Called imgFrame::InitForDecoder() twice?");
 
     bool postFirstFrame = aAnimParams && aAnimParams->mFrameNum > 0;
-    mRawSurface = AllocateBufferForImage(mFrameRect.Size(), mFormat, postFirstFrame);
+    mRawSurface = AllocateBufferForImage(mFrameRect.Size(), mFormat,
+                                         postFirstFrame, mIsFullFrame);
     if (!mRawSurface) {
       mAborted = true;
       return NS_ERROR_OUT_OF_MEMORY;
     }
 
     mLockedSurface = CreateLockedSurface(mRawSurface, mFrameRect.Size(), mFormat);
     if (!mLockedSurface) {
       NS_WARNING("Failed to create LockedSurface");
--- a/image/imgFrame.h
+++ b/image/imgFrame.h
@@ -54,27 +54,32 @@ public:
    * when drawing content into an imgFrame, as it may use a different graphics
    * backend than normal content drawing.
    */
   nsresult InitForDecoder(const nsIntSize& aImageSize,
                           const nsIntRect& aRect,
                           SurfaceFormat aFormat,
                           uint8_t aPaletteDepth = 0,
                           bool aNonPremult = false,
-                          const Maybe<AnimationParams>& aAnimParams = Nothing());
+                          const Maybe<AnimationParams>& aAnimParams = Nothing(),
+                          bool aIsFullFrame = false);
 
   nsresult InitForAnimator(const nsIntSize& aSize,
                            SurfaceFormat aFormat)
   {
     nsIntRect frameRect(0, 0, aSize.width, aSize.height);
     AnimationParams animParams { frameRect, FrameTimeout::Forever(),
                                  /* aFrameNum */ 1, BlendMethod::OVER,
                                  DisposalMethod::NOT_SPECIFIED };
-    return InitForDecoder(aSize, frameRect,
-                          aFormat, 0, false, Some(animParams));
+    // We set aIsFullFrame to false because we don't want the compositing frame
+    // to be allocated into shared memory for WebRender. mIsFullFrame is only
+    // otherwise used for frames produced by Decoder, so it isn't relevant.
+    return InitForDecoder(aSize, frameRect, aFormat, /* aPaletteDepth */ 0,
+                          /* aNonPremult */ false, Some(animParams),
+                          /* aIsFullFrame */ false);
   }
 
 
   /**
    * Initialize this imgFrame with a new surface and draw the provided
    * gfxDrawable into it.
    *
    * This is appropriate to use when drawing content into an imgFrame, as it
@@ -183,16 +188,21 @@ public:
   void GetImageData(uint8_t** aData, uint32_t* length) const;
   uint8_t* GetImageData() const;
 
   bool GetIsPaletted() const;
   void GetPaletteData(uint32_t** aPalette, uint32_t* length) const;
   uint32_t* GetPaletteData() const;
   uint8_t GetPaletteDepth() const { return mPaletteDepth; }
 
+  const IntRect& GetDirtyRect() const { return mDirtyRect; }
+  void SetDirtyRect(const IntRect& aDirtyRect) { mDirtyRect = aDirtyRect; }
+
+  bool IsFullFrame() const { return mIsFullFrame; }
+
   bool GetCompositingFailed() const;
   void SetCompositingFailed(bool val);
 
   void SetOptimizable();
 
   void FinalizeSurface();
   already_AddRefed<SourceSurface> GetSourceSurface();
 
@@ -290,36 +300,61 @@ private: // data
   bool mFinished;
   bool mOptimizable;
 
 
   //////////////////////////////////////////////////////////////////////////////
   // Effectively const data, only mutated in the Init methods.
   //////////////////////////////////////////////////////////////////////////////
 
+  //! The size of the buffer we are decoding to.
   IntSize      mImageSize;
+
+  //! XXX(aosmond): This means something different depending on the context. We
+  //!               should correct this.
+  //!
+  //! There are several different contexts for mFrameRect:
+  //! - If for non-animated image, it will be originate at (0, 0) and matches
+  //!   the dimensions of mImageSize.
+  //! - If for an APNG, it also matches the above.
+  //! - If for a GIF which is producing full frames, it matches the above.
+  //! - If for a GIF which is producing partial frames, it matches mBlendRect.
   IntRect      mFrameRect;
+
+  //! The contents for the frame, as represented in the encoded image. This may
+  //! differ from mImageSize because it may be a partial frame. For the first
+  //! frame, this means we need to shift the data in place, and for animated
+  //! frames, it likely need to combine with a previous frame to get the full
+  //! contents.
   IntRect      mBlendRect;
 
+  //! This is the region that has changed between this frame and the previous
+  //! frame of an animation. For the first frame, this will be the same as
+  //! mFrameRect.
+  IntRect      mDirtyRect;
+
   //! The timeout for this frame.
   FrameTimeout mTimeout;
 
   DisposalMethod mDisposalMethod;
   BlendMethod    mBlendMethod;
   SurfaceFormat  mFormat;
 
   // The palette and image data for images that are paletted, since Cairo
   // doesn't support these images.
   // The paletted data comes first, then the image data itself.
   // Total length is PaletteDataLength() + GetImageDataLength().
   uint8_t*     mPalettedImageData;
   uint8_t      mPaletteDepth;
 
   bool mNonPremult;
 
+  //! True if the frame has all of the data stored in it, false if it needs to
+  //! be combined with another frame (e.g. the previous frame) to be complete.
+  bool mIsFullFrame;
 
   //////////////////////////////////////////////////////////////////////////////
   // Main-thread-only mutable data.
   //////////////////////////////////////////////////////////////////////////////
 
   bool mCompositingFailed;
 };
 
--- a/image/test/gtest/Common.cpp
+++ b/image/test/gtest/Common.cpp
@@ -190,30 +190,31 @@ RectIsSolidColor(SourceSurface* aSurface
   DataSourceSurface::ScopedMap mapping(dataSurface,
                                        DataSourceSurface::MapType::READ);
   ASSERT_TRUE_OR_RETURN(mapping.IsMapped(), false);
   ASSERT_EQ_OR_RETURN(mapping.GetStride(), surfaceSize.width * 4, false);
 
   uint8_t* data = mapping.GetData();
   ASSERT_TRUE_OR_RETURN(data != nullptr, false);
 
+  BGRAColor pmColor = aColor.Premultiply();
   int32_t rowLength = mapping.GetStride();
   for (int32_t row = rect.Y(); row < rect.YMost(); ++row) {
     for (int32_t col = rect.X(); col < rect.XMost(); ++col) {
       int32_t i = row * rowLength + col * 4;
       if (aFuzz != 0) {
-        ASSERT_LE_OR_RETURN(abs(aColor.mBlue - data[i + 0]), aFuzz, false);
-        ASSERT_LE_OR_RETURN(abs(aColor.mGreen - data[i + 1]), aFuzz, false);
-        ASSERT_LE_OR_RETURN(abs(aColor.mRed - data[i + 2]), aFuzz, false);
-        ASSERT_LE_OR_RETURN(abs(aColor.mAlpha - data[i + 3]), aFuzz, false);
+        ASSERT_LE_OR_RETURN(abs(pmColor.mBlue - data[i + 0]), aFuzz, false);
+        ASSERT_LE_OR_RETURN(abs(pmColor.mGreen - data[i + 1]), aFuzz, false);
+        ASSERT_LE_OR_RETURN(abs(pmColor.mRed - data[i + 2]), aFuzz, false);
+        ASSERT_LE_OR_RETURN(abs(pmColor.mAlpha - data[i + 3]), aFuzz, false);
       } else {
-        ASSERT_EQ_OR_RETURN(aColor.mBlue,  data[i + 0], false);
-        ASSERT_EQ_OR_RETURN(aColor.mGreen, data[i + 1], false);
-        ASSERT_EQ_OR_RETURN(aColor.mRed,   data[i + 2], false);
-        ASSERT_EQ_OR_RETURN(aColor.mAlpha, data[i + 3], false);
+        ASSERT_EQ_OR_RETURN(pmColor.mBlue,  data[i + 0], false);
+        ASSERT_EQ_OR_RETURN(pmColor.mGreen, data[i + 1], false);
+        ASSERT_EQ_OR_RETURN(pmColor.mRed,   data[i + 2], false);
+        ASSERT_EQ_OR_RETURN(pmColor.mAlpha, data[i + 3], false);
       }
     }
   }
 
   return true;
 }
 
 bool
@@ -295,16 +296,17 @@ RowHasPixels(SourceSurface* aSurface,
 already_AddRefed<Decoder>
 CreateTrivialDecoder()
 {
   gfxPrefs::GetSingleton();
   DecoderType decoderType = DecoderFactory::GetDecoderType("image/gif");
   auto sourceBuffer = MakeNotNull<RefPtr<SourceBuffer>>();
   RefPtr<Decoder> decoder =
     DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, Nothing(),
+                                           DefaultDecoderFlags(),
                                            DefaultSurfaceFlags());
   return decoder.forget();
 }
 
 void
 AssertCorrectPipelineFinalState(SurfaceFilter* aFilter,
                                 const gfx::IntRect& aInputSpaceRect,
                                 const gfx::IntRect& aOutputSpaceRect)
--- a/image/test/gtest/Common.h
+++ b/image/test/gtest/Common.h
@@ -5,16 +5,17 @@
 
 #ifndef mozilla_image_test_gtest_Common_h
 #define mozilla_image_test_gtest_Common_h
 
 #include <vector>
 
 #include "gtest/gtest.h"
 
+#include "mozilla/Attributes.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/gfx/2D.h"
 #include "Decoder.h"
 #include "gfxColor.h"
 #include "imgITools.h"
 #include "nsCOMPtr.h"
 #include "SurfacePipe.h"
@@ -70,34 +71,53 @@ struct ImageTestCase
   gfx::IntSize mOutputSize;
   uint32_t mFlags;
 };
 
 struct BGRAColor
 {
   BGRAColor() : BGRAColor(0, 0, 0, 0) { }
 
-  BGRAColor(uint8_t aBlue, uint8_t aGreen, uint8_t aRed, uint8_t aAlpha)
+  BGRAColor(uint8_t aBlue, uint8_t aGreen, uint8_t aRed, uint8_t aAlpha, bool aPremultiplied = false)
     : mBlue(aBlue)
     , mGreen(aGreen)
     , mRed(aRed)
     , mAlpha(aAlpha)
+    , mPremultiplied(aPremultiplied)
   { }
 
   static BGRAColor Green() { return BGRAColor(0x00, 0xFF, 0x00, 0xFF); }
   static BGRAColor Red()   { return BGRAColor(0x00, 0x00, 0xFF, 0xFF); }
   static BGRAColor Blue()   { return BGRAColor(0xFF, 0x00, 0x00, 0xFF); }
   static BGRAColor Transparent() { return BGRAColor(0x00, 0x00, 0x00, 0x00); }
 
-  uint32_t AsPixel() const { return gfxPackedPixel(mAlpha, mRed, mGreen, mBlue); }
+  BGRAColor Premultiply() const
+  {
+    if (!mPremultiplied) {
+      return BGRAColor(gfxPreMultiply(mBlue, mAlpha),
+                       gfxPreMultiply(mGreen, mAlpha),
+                       gfxPreMultiply(mRed, mAlpha),
+                       mAlpha,
+                       true);
+    }
+    return *this;
+  }
+
+  uint32_t AsPixel() const {
+    if (!mPremultiplied) {
+      return gfxPackedPixel(mAlpha, mRed, mGreen, mBlue);
+    }
+    return gfxPackedPixelNoPreMultiply(mAlpha, mRed, mGreen, mBlue);
+  }
 
   uint8_t mBlue;
   uint8_t mGreen;
   uint8_t mRed;
   uint8_t mAlpha;
+  bool mPremultiplied;
 };
 
 
 ///////////////////////////////////////////////////////////////////////////////
 // General Helpers
 ///////////////////////////////////////////////////////////////////////////////
 
 /**
@@ -236,30 +256,38 @@ already_AddRefed<Decoder> CreateTrivialD
  * it to the provided lambda @aFunc. Assertions that the pipeline is constructly
  * correctly and cleanup of any allocated surfaces is handled automatically.
  *
  * @param aDecoder The decoder to use for allocating surfaces.
  * @param aFunc The lambda function to pass the filter pipeline to.
  * @param aConfigs The configuration for the pipeline.
  */
 template <typename Func, typename... Configs>
-void WithFilterPipeline(Decoder* aDecoder, Func aFunc, const Configs&... aConfigs)
+void WithFilterPipeline(Decoder* aDecoder, Func aFunc, bool aFinish, const Configs&... aConfigs)
 {
   auto pipe = MakeUnique<typename detail::FilterPipeline<Configs...>::Type>();
   nsresult rv = pipe->Configure(aConfigs...);
   ASSERT_TRUE(NS_SUCCEEDED(rv));
 
   aFunc(aDecoder, pipe.get());
 
-  RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
-  if (currentFrame) {
-    currentFrame->Finish();
+  if (aFinish) {
+    RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef();
+    if (currentFrame) {
+      currentFrame->Finish();
+    }
   }
 }
 
+template <typename Func, typename... Configs>
+void WithFilterPipeline(Decoder* aDecoder, Func aFunc, const Configs&... aConfigs)
+{
+  WithFilterPipeline(aDecoder, aFunc, true, aConfigs...);
+}
+
 /**
  * Creates a pipeline of SurfaceFilters from a list of Config structs and
  * asserts that configuring it fails. Cleanup of any allocated surfaces is
  * handled automatically.
  *
  * @param aDecoder The decoder to use for allocating surfaces.
  * @param aConfigs The configuration for the pipeline.
  */
@@ -364,16 +392,41 @@ void CheckWritePixels(Decoder* aDecoder,
 void CheckPalettedWritePixels(Decoder* aDecoder,
                               SurfaceFilter* aFilter,
                               const Maybe<gfx::IntRect>& aOutputRect = Nothing(),
                               const Maybe<gfx::IntRect>& aInputRect = Nothing(),
                               const Maybe<gfx::IntRect>& aInputWriteRect = Nothing(),
                               const Maybe<gfx::IntRect>& aOutputWriteRect = Nothing(),
                               uint8_t aFuzz = 0);
 
+///////////////////////////////////////////////////////////////////////////////
+// Decoder Helpers
+///////////////////////////////////////////////////////////////////////////////
+
+// Friend class of Decoder to access internals for tests.
+class MOZ_STACK_CLASS DecoderTestHelper final
+{
+public:
+  explicit DecoderTestHelper(Decoder* aDecoder)
+    : mDecoder(aDecoder)
+  { }
+
+  void PostIsAnimated(FrameTimeout aTimeout)
+  {
+    mDecoder->PostIsAnimated(aTimeout);
+  }
+
+  void PostFrameStop(Opacity aOpacity)
+  {
+    mDecoder->PostFrameStop(aOpacity);
+  }
+
+private:
+  Decoder* mDecoder;
+};
 
 ///////////////////////////////////////////////////////////////////////////////
 // Test Data
 ///////////////////////////////////////////////////////////////////////////////
 
 ImageTestCase GreenPNGTestCase();
 ImageTestCase GreenGIFTestCase();
 ImageTestCase GreenJPGTestCase();
new file mode 100644
--- /dev/null
+++ b/image/test/gtest/TestBlendAnimationFilter.cpp
@@ -0,0 +1,504 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include "mozilla/gfx/2D.h"
+#include "skia/include/core/SkColorPriv.h" // for SkPMSrcOver
+#include "Common.h"
+#include "Decoder.h"
+#include "DecoderFactory.h"
+#include "SourceBuffer.h"
+#include "SurfaceFilters.h"
+#include "SurfacePipe.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+static already_AddRefed<Decoder>
+CreateTrivialBlendingDecoder()
+{
+  gfxPrefs::GetSingleton();
+  DecoderType decoderType = DecoderFactory::GetDecoderType("image/gif");
+  DecoderFlags decoderFlags = DecoderFlags::BLEND_ANIMATION;
+  SurfaceFlags surfaceFlags = DefaultSurfaceFlags();
+  auto sourceBuffer = MakeNotNull<RefPtr<SourceBuffer>>();
+  return DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer,
+                                                Nothing(), decoderFlags,
+                                                surfaceFlags);
+}
+
+template <typename Func> RawAccessFrameRef
+WithBlendAnimationFilter(Decoder* aDecoder,
+                         const AnimationParams& aAnimParams,
+                         const IntSize& aOutputSize,
+                         Func aFunc)
+{
+  DecoderTestHelper decoderHelper(aDecoder);
+
+  if (!aDecoder->HasAnimation()) {
+    decoderHelper.PostIsAnimated(aAnimParams.mTimeout);
+  }
+
+  BlendAnimationConfig blendAnim { aDecoder };
+  SurfaceConfig surfaceSink { aDecoder, aOutputSize, SurfaceFormat::B8G8R8A8,
+                              false, Some(aAnimParams) };
+
+  auto func = [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+    aFunc(aDecoder, aFilter);
+  };
+
+  WithFilterPipeline(aDecoder, func, false, blendAnim, surfaceSink);
+
+  RawAccessFrameRef current = aDecoder->GetCurrentFrameRef();
+  if (current) {
+    decoderHelper.PostFrameStop(Opacity::SOME_TRANSPARENCY);
+  }
+
+  return current;
+}
+
+void
+AssertConfiguringBlendAnimationFilterFails(const IntRect& aFrameRect,
+                                           const IntSize& aOutputSize)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams animParams { aFrameRect, FrameTimeout::FromRawMilliseconds(0),
+                               0, BlendMethod::SOURCE, DisposalMethod::KEEP };
+  BlendAnimationConfig blendAnim { decoder };
+  SurfaceConfig surfaceSink { decoder, aOutputSize,
+                              SurfaceFormat::B8G8R8A8, false,
+                              Some(animParams) };
+  AssertConfiguringPipelineFails(decoder, blendAnim, surfaceSink);
+}
+
+TEST(ImageBlendAnimationFilter, BlendFailsForNegativeFrameRect)
+{
+  // A negative frame rect size is disallowed.
+  AssertConfiguringBlendAnimationFilterFails(IntRect(IntPoint(0, 0), IntSize(-1, -1)),
+                                             IntSize(100, 100));
+}
+
+TEST(ImageBlendAnimationFilter, WriteFullFirstFrame)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params { IntRect(0, 0, 100, 100),
+                           FrameTimeout::FromRawMilliseconds(0),
+                           /* aFrameNum */ 0, BlendMethod::SOURCE,
+                           DisposalMethod::KEEP };
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params, IntSize(100, 100),
+                             [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      CheckWritePixels(aDecoder, aFilter, Some(IntRect(0, 0, 100, 100)));
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+}
+
+TEST(ImageBlendAnimationFilter, WritePartialFirstFrame)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params { IntRect(25, 50, 50, 25),
+                           FrameTimeout::FromRawMilliseconds(0),
+                           /* aFrameNum */ 0, BlendMethod::SOURCE,
+                           DisposalMethod::KEEP };
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params, IntSize(100, 100),
+                             [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      CheckWritePixels(aDecoder, aFilter, Some(IntRect(0, 0, 100, 100)),
+                                          Nothing(),
+                                          Some(IntRect(25, 50, 50, 25)),
+                                          Some(IntRect(25, 50, 50, 25)));
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+}
+
+static void
+TestWithBlendAnimationFilterClear(BlendMethod aBlendMethod)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params0 { IntRect(0, 0, 100, 100),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 0, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params0, IntSize(100, 100),
+                             [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(BGRAColor::Green().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+  AnimationParams params1 { IntRect(0, 40, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 1, BlendMethod::SOURCE,
+                            DisposalMethod::CLEAR };
+  RawAccessFrameRef frame1 =
+    WithBlendAnimationFilter(decoder, params1, IntSize(100, 100),
+                             [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(BGRAColor::Red().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect());
+
+  ASSERT_TRUE(frame1.get() != nullptr);
+
+  RefPtr<SourceSurface> surface = frame1->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, BGRAColor::Red()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, BGRAColor::Green()));
+
+  AnimationParams params2 { IntRect(0, 50, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 2, aBlendMethod,
+                            DisposalMethod::KEEP };
+  RawAccessFrameRef frame2 =
+    WithBlendAnimationFilter(decoder, params2, IntSize(100, 100),
+                             [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(BGRAColor::Blue().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+
+  ASSERT_TRUE(frame2.get() != nullptr);
+
+  surface = frame2->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 40, 10, BGRAColor::Transparent()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 50, 20, BGRAColor::Blue()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 70, 30, BGRAColor::Green()));
+}
+
+TEST(ImageBlendAnimationFilter, ClearWithOver)
+{
+  TestWithBlendAnimationFilterClear(BlendMethod::OVER);
+}
+
+TEST(ImageBlendAnimationFilter, ClearWithSource)
+{
+  TestWithBlendAnimationFilterClear(BlendMethod::SOURCE);
+}
+
+TEST(ImageBlendAnimationFilter, KeepWithSource)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params0 { IntRect(0, 0, 100, 100),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 0, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params0, IntSize(100, 100),
+                             [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(BGRAColor::Green().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+  AnimationParams params1 { IntRect(0, 40, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 1, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  RawAccessFrameRef frame1 =
+    WithBlendAnimationFilter(decoder, params1, IntSize(100, 100),
+                             [](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(BGRAColor::Red().AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect());
+
+  ASSERT_TRUE(frame1.get() != nullptr);
+
+  RefPtr<SourceSurface> surface = frame1->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, BGRAColor::Red()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, BGRAColor::Green()));
+}
+
+TEST(ImageBlendAnimationFilter, KeepWithOver)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params0 { IntRect(0, 0, 100, 100),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 0, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor0(0, 0xFF, 0, 0x40);
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params0, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor0.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+  AnimationParams params1 { IntRect(0, 40, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 1, BlendMethod::OVER,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor1(0, 0, 0xFF, 0x80);
+  RawAccessFrameRef frame1 =
+    WithBlendAnimationFilter(decoder, params1, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor1.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect());
+
+  ASSERT_TRUE(frame1.get() != nullptr);
+
+  BGRAColor blendedColor(0, 0x20, 0x80, 0xA0, true); // already premultiplied
+  EXPECT_EQ(SkPMSrcOver(frameColor1.AsPixel(), frameColor0.AsPixel()),
+            blendedColor.AsPixel());
+
+  RefPtr<SourceSurface> surface = frame1->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, blendedColor));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0));
+}
+
+TEST(ImageBlendAnimationFilter, RestorePreviousWithOver)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params0 { IntRect(0, 0, 100, 100),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 0, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor0(0, 0xFF, 0, 0x40);
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params0, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor0.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+  AnimationParams params1 { IntRect(0, 10, 100, 80),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 1, BlendMethod::SOURCE,
+                            DisposalMethod::RESTORE_PREVIOUS };
+  BGRAColor frameColor1 = BGRAColor::Green();
+  RawAccessFrameRef frame1 =
+    WithBlendAnimationFilter(decoder, params1, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor1.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 10, 100, 80), frame1->GetDirtyRect());
+
+  AnimationParams params2 { IntRect(0, 40, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 2, BlendMethod::OVER,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor2(0, 0, 0xFF, 0x80);
+  RawAccessFrameRef frame2 =
+    WithBlendAnimationFilter(decoder, params2, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor2.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 10, 100, 80), frame2->GetDirtyRect());
+
+  ASSERT_TRUE(frame2.get() != nullptr);
+
+  BGRAColor blendedColor(0, 0x20, 0x80, 0xA0, true); // already premultiplied
+  EXPECT_EQ(SkPMSrcOver(frameColor2.AsPixel(), frameColor0.AsPixel()),
+            blendedColor.AsPixel());
+
+  RefPtr<SourceSurface> surface = frame2->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, blendedColor));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0));
+}
+
+TEST(ImageBlendAnimationFilter, RestorePreviousWithSource)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params0 { IntRect(0, 0, 100, 100),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 0, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor0(0, 0xFF, 0, 0x40);
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params0, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor0.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+  AnimationParams params1 { IntRect(0, 10, 100, 80),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 1, BlendMethod::SOURCE,
+                            DisposalMethod::RESTORE_PREVIOUS };
+  BGRAColor frameColor1 = BGRAColor::Green();
+  RawAccessFrameRef frame1 =
+    WithBlendAnimationFilter(decoder, params1, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor1.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 10, 100, 80), frame1->GetDirtyRect());
+
+  AnimationParams params2 { IntRect(0, 40, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 2, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor2(0, 0, 0xFF, 0x80);
+  RawAccessFrameRef frame2 =
+    WithBlendAnimationFilter(decoder, params2, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor2.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 10, 100, 80), frame2->GetDirtyRect());
+
+  ASSERT_TRUE(frame2.get() != nullptr);
+
+  RefPtr<SourceSurface> surface = frame2->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, frameColor2));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0));
+}
+
+TEST(ImageBlendAnimationFilter, RestorePreviousClearWithSource)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params0 { IntRect(0, 0, 100, 100),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 0, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor0 = BGRAColor::Red();
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params0, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor0.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+  AnimationParams params1 { IntRect(0, 0, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 1, BlendMethod::SOURCE,
+                            DisposalMethod::CLEAR };
+  BGRAColor frameColor1 = BGRAColor::Blue();
+  RawAccessFrameRef frame1 =
+    WithBlendAnimationFilter(decoder, params1, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor1.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 20), frame1->GetDirtyRect());
+
+  AnimationParams params2 { IntRect(0, 10, 100, 80),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 2, BlendMethod::SOURCE,
+                            DisposalMethod::RESTORE_PREVIOUS };
+  BGRAColor frameColor2 = BGRAColor::Green();
+  RawAccessFrameRef frame2 =
+    WithBlendAnimationFilter(decoder, params2, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor2.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 90), frame2->GetDirtyRect());
+
+  AnimationParams params3 { IntRect(0, 40, 100, 20),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 3, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor3 = BGRAColor::Blue();
+  RawAccessFrameRef frame3 =
+    WithBlendAnimationFilter(decoder, params3, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor3.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 90), frame3->GetDirtyRect());
+
+  ASSERT_TRUE(frame3.get() != nullptr);
+
+  RefPtr<SourceSurface> surface = frame3->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 20, BGRAColor::Transparent()));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 20, 20, frameColor0));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, frameColor3));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0));
+}
+
+TEST(ImageBlendAnimationFilter, PartialOverlapFrameRect)
+{
+  RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder();
+  ASSERT_TRUE(decoder != nullptr);
+
+  AnimationParams params0 { IntRect(-10, -20, 110, 100),
+                            FrameTimeout::FromRawMilliseconds(0),
+                            /* aFrameNum */ 0, BlendMethod::SOURCE,
+                            DisposalMethod::KEEP };
+  BGRAColor frameColor0 = BGRAColor::Red();
+  RawAccessFrameRef frame0 =
+    WithBlendAnimationFilter(decoder, params0, IntSize(100, 100),
+                             [&](Decoder* aDecoder, SurfaceFilter* aFilter) {
+      auto result = aFilter->WritePixels<uint32_t>([&] {
+        return AsVariant(frameColor0.AsPixel());
+      });
+      EXPECT_EQ(WriteState::FINISHED, result);
+    });
+  EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect());
+
+  RefPtr<SourceSurface> surface = frame0->GetSourceSurface();
+  EXPECT_TRUE(RowsAreSolidColor(surface, 0, 80, frameColor0));
+  EXPECT_TRUE(RowsAreSolidColor(surface, 80, 20, BGRAColor::Transparent()));
+}
+
--- a/image/test/gtest/TestDecoders.cpp
+++ b/image/test/gtest/TestDecoders.cpp
@@ -1,15 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "gtest/gtest.h"
 
 #include "Common.h"
+#include "AnimationSurfaceProvider.h"
 #include "Decoder.h"
 #include "DecoderFactory.h"
 #include "decoders/nsBMPDecoder.h"
 #include "IDecodingTask.h"
 #include "ImageOps.h"
 #include "imgIContainer.h"
 #include "imgITools.h"
 #include "ImageFactory.h"
@@ -110,16 +111,17 @@ void WithSingleChunkDecode(const ImageTe
   ASSERT_TRUE(NS_SUCCEEDED(rv));
   sourceBuffer->Complete(NS_OK);
 
   // Create a decoder.
   DecoderType decoderType =
     DecoderFactory::GetDecoderType(aTestCase.mMimeType);
   RefPtr<Decoder> decoder =
     DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, aOutputSize,
+                                           DecoderFlags::FIRST_FRAME_ONLY,
                                            DefaultSurfaceFlags());
   ASSERT_TRUE(decoder != nullptr);
   RefPtr<IDecodingTask> task = new AnonymousDecodingTask(WrapNotNull(decoder));
 
   // Run the full decoder synchronously.
   task->Run();
 
   // Call the lambda to verify the expected results.
@@ -147,16 +149,17 @@ CheckDecoderMultiChunk(const ImageTestCa
 
   // Create a SourceBuffer and a decoder.
   auto sourceBuffer = MakeNotNull<RefPtr<SourceBuffer>>();
   sourceBuffer->ExpectLength(length);
   DecoderType decoderType =
     DecoderFactory::GetDecoderType(aTestCase.mMimeType);
   RefPtr<Decoder> decoder =
     DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, Nothing(),
+                                           DecoderFlags::FIRST_FRAME_ONLY,
                                            DefaultSurfaceFlags());
   ASSERT_TRUE(decoder != nullptr);
   RefPtr<IDecodingTask> task = new AnonymousDecodingTask(WrapNotNull(decoder));
 
   for (uint64_t read = 0; read < length ; ++read) {
     uint64_t available = 0;
     rv = inputStream->Available(&available);
     ASSERT_TRUE(available > 0);
@@ -199,16 +202,138 @@ CheckDownscaleDuringDecode(const ImageTe
     // small amount of fuzz; this is just the nature of Lanczos downscaling.
     EXPECT_TRUE(RowsAreSolidColor(surface, 0, 4, BGRAColor::Green(), /* aFuzz = */ 47));
     EXPECT_TRUE(RowsAreSolidColor(surface, 6, 3, BGRAColor::Red(), /* aFuzz = */ 27));
     EXPECT_TRUE(RowsAreSolidColor(surface, 11, 3, BGRAColor::Green(), /* aFuzz = */ 47));
     EXPECT_TRUE(RowsAreSolidColor(surface, 16, 4, BGRAColor::Red(), /* aFuzz = */ 27));
   });
 }
 
+static void
+CheckAnimationDecoderResults(const ImageTestCase& aTestCase,
+                             AnimationSurfaceProvider* aProvider,
+                             Decoder* aDecoder)
+{
+  EXPECT_TRUE(aDecoder->GetDecodeDone());
+  EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_HAS_ERROR),
+            aDecoder->HasError());
+
+  if (aTestCase.mFlags & TEST_CASE_HAS_ERROR) {
+    return;  // That's all we can check for bad images.
+  }
+
+  // The decoder should get the correct size.
+  IntSize size = aDecoder->Size();
+  EXPECT_EQ(aTestCase.mSize.width, size.width);
+  EXPECT_EQ(aTestCase.mSize.height, size.height);
+
+  if (aTestCase.mFlags & TEST_CASE_IGNORE_OUTPUT) {
+    return;
+  }
+
+  // Check the output.
+  AutoTArray<BGRAColor, 2> framePixels;
+  framePixels.AppendElement(BGRAColor::Green());
+  framePixels.AppendElement(BGRAColor(0x7F, 0x7F, 0x7F, 0xFF));
+
+  DrawableSurface drawableSurface(WrapNotNull(aProvider));
+  for (size_t i = 0; i < framePixels.Length(); ++i) {
+    nsresult rv = drawableSurface.Seek(i);
+    EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+    // Check the first frame, all green.
+    RawAccessFrameRef rawFrame = drawableSurface->RawAccessRef();
+    RefPtr<SourceSurface> surface = rawFrame->GetSourceSurface();
+
+    // Verify that the resulting surfaces matches our expectations.
+    EXPECT_TRUE(surface->IsDataSourceSurface());
+    EXPECT_TRUE(surface->GetFormat() == SurfaceFormat::B8G8R8X8 ||
+                surface->GetFormat() == SurfaceFormat::B8G8R8A8);
+    EXPECT_EQ(aTestCase.mOutputSize, surface->GetSize());
+    EXPECT_TRUE(IsSolidColor(surface, framePixels[i],
+                             aTestCase.mFlags & TEST_CASE_IS_FUZZY ? 1 : 0));
+  }
+
+  // Should be no more frames.
+  nsresult rv = drawableSurface.Seek(framePixels.Length());
+  EXPECT_TRUE(NS_FAILED(rv));
+}
+
+template <typename Func>
+static void
+WithSingleChunkAnimationDecode(const ImageTestCase& aTestCase,
+                               Func aResultChecker)
+{
+  // Create an image.
+  RefPtr<Image> image =
+    ImageFactory::CreateAnonymousImage(nsDependentCString(aTestCase.mMimeType));
+  ASSERT_TRUE(!image->HasError());
+
+  NotNull<RefPtr<RasterImage>> rasterImage =
+    WrapNotNull(static_cast<RasterImage*>(image.get()));
+
+  nsCOMPtr<nsIInputStream> inputStream = LoadFile(aTestCase.mPath);
+  ASSERT_TRUE(inputStream != nullptr);
+
+  // Figure out how much data we have.
+  uint64_t length;
+  nsresult rv = inputStream->Available(&length);
+  ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+  // Write the data into a SourceBuffer.
+  NotNull<RefPtr<SourceBuffer>> sourceBuffer = WrapNotNull(new SourceBuffer());
+  sourceBuffer->ExpectLength(length);
+  rv = sourceBuffer->AppendFromInputStream(inputStream, length);
+  ASSERT_TRUE(NS_SUCCEEDED(rv));
+  sourceBuffer->Complete(NS_OK);
+
+  // Create a metadata decoder first, because otherwise RasterImage will get
+  // unhappy about finding out the image is animated during a full decode.
+  DecoderType decoderType =
+    DecoderFactory::GetDecoderType(aTestCase.mMimeType);
+  RefPtr<IDecodingTask> task =
+    DecoderFactory::CreateMetadataDecoder(decoderType, rasterImage, sourceBuffer);
+  ASSERT_TRUE(task != nullptr);
+
+  // Run the metadata decoder synchronously.
+  task->Run();
+
+  // Create a decoder.
+  DecoderFlags decoderFlags = DecoderFlags::BLEND_ANIMATION;
+  SurfaceFlags surfaceFlags = DefaultSurfaceFlags();
+  RefPtr<Decoder> decoder =
+    DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, Nothing(),
+                                           decoderFlags, surfaceFlags);
+  ASSERT_TRUE(decoder != nullptr);
+
+  // Create an AnimationSurfaceProvider which will manage the decoding process
+  // and make this decoder's output available in the surface cache.
+  SurfaceKey surfaceKey =
+    RasterSurfaceKey(aTestCase.mOutputSize, surfaceFlags, PlaybackType::eAnimated);
+  RefPtr<AnimationSurfaceProvider> provider =
+    new AnimationSurfaceProvider(rasterImage,
+                                 surfaceKey,
+                                 WrapNotNull(decoder),
+                                 /* aCurrentFrame */ 0);
+
+  // Run the full decoder synchronously.
+  provider->Run();
+
+  // Call the lambda to verify the expected results.
+  aResultChecker(provider, decoder);
+}
+
+static void
+CheckAnimationDecoderSingleChunk(const ImageTestCase& aTestCase)
+{
+  WithSingleChunkAnimationDecode(aTestCase, [&](AnimationSurfaceProvider* aProvider, Decoder* aDecoder) {
+    CheckAnimationDecoderResults(aTestCase, aProvider, aDecoder);
+  });
+}
+
 class ImageDecoders : public ::testing::Test
 {
 protected:
   AutoInitializeImageLib mInit;
 };
 
 TEST_F(ImageDecoders, PNGSingleChunk)
 {
@@ -310,26 +435,36 @@ TEST_F(ImageDecoders, AnimatedGIFSingleC
   CheckDecoderSingleChunk(GreenFirstFrameAnimatedGIFTestCase());
 }
 
 TEST_F(ImageDecoders, AnimatedGIFMultiChunk)
 {
   CheckDecoderMultiChunk(GreenFirstFrameAnimatedGIFTestCase());
 }
 
+TEST_F(ImageDecoders, AnimatedGIFWithBlendedFrames)
+{
+  CheckAnimationDecoderSingleChunk(GreenFirstFrameAnimatedGIFTestCase());
+}
+
 TEST_F(ImageDecoders, AnimatedPNGSingleChunk)
 {
   CheckDecoderSingleChunk(GreenFirstFrameAnimatedPNGTestCase());
 }
 
 TEST_F(ImageDecoders, AnimatedPNGMultiChunk)
 {
   CheckDecoderMultiChunk(GreenFirstFrameAnimatedPNGTestCase());
 }
 
+TEST_F(ImageDecoders, AnimatedPNGWithBlendedFrames)
+{
+  CheckAnimationDecoderSingleChunk(GreenFirstFrameAnimatedPNGTestCase());
+}
+
 TEST_F(ImageDecoders, CorruptSingleChunk)
 {
   CheckDecoderSingleChunk(CorruptTestCase());
 }
 
 TEST_F(ImageDecoders, CorruptMultiChunk)
 {
   CheckDecoderMultiChunk(CorruptTestCase());
--- a/image/test/gtest/TestMetadata.cpp
+++ b/image/test/gtest/TestMetadata.cpp
@@ -103,16 +103,17 @@ CheckMetadata(const ImageTestCase& aTest
   EXPECT_EQ(expectTransparency, bool(metadataProgress & FLAG_HAS_TRANSPARENCY));
 
   EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_IS_ANIMATED),
             bool(metadataProgress & FLAG_IS_ANIMATED));
 
   // Create a full decoder, so we can compare the result.
   decoder =
     DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, Nothing(),
+                                           DecoderFlags::FIRST_FRAME_ONLY,
                                            DefaultSurfaceFlags());
   ASSERT_TRUE(decoder != nullptr);
   task = new AnonymousDecodingTask(WrapNotNull(decoder));
 
   if (aBMPWithinICO == BMPWithinICO::YES) {
     static_cast<nsBMPDecoder*>(decoder.get())->SetIsWithinICO();
   }
 
--- a/image/test/gtest/moz.build
+++ b/image/test/gtest/moz.build
@@ -5,16 +5,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 Library('imagetest')
 
 UNIFIED_SOURCES = [
     'Common.cpp',
     'TestADAM7InterpolatingFilter.cpp',
     'TestAnimationFrameBuffer.cpp',
+    'TestBlendAnimationFilter.cpp',
     'TestContainers.cpp',
     'TestCopyOnWrite.cpp',
     'TestDecoders.cpp',
     'TestDecodeToSurface.cpp',
     'TestDeinterlacingFilter.cpp',
     'TestLoader.cpp',
     'TestMetadata.cpp',
     'TestRemoveFrameRectFilter.cpp',
--- a/js/src/jit-test/tests/cacheir/binaryarith.js
+++ b/js/src/jit-test/tests/cacheir/binaryarith.js
@@ -40,16 +40,21 @@ obj1 = new D('A');
 var funAdd4 = (a, b) => { return a + b; }
 warmup(funAdd4, [["x", obj1, "xA"], [obj1, "bba", "Abba"]]);
 
 // Add: Int32 Boolean
 var funAdd5 = (a, b) => { return a + b; }
 warmup(funAdd5, [[true, 10, 11], [false, 1, 1], [10, true, 11], [1, false, 1],
                  [2147483647, true, 2147483648],[true, 2147483647, 2147483648]]);
 
+// Add: String Number Concat
+var funAdd6 = (a, b) => { return a + b; }
+warmup(funAdd6, [["x", 10, "x10"], [10, "bba", "10bba"], ["x", 1.2, "x1.2"],
+                 [1.2, "bba", "1.2bba"]]);
+
 // Sub Int32
 var funSub1 = (a, b) => { return a - b; }
 warmup(funSub1, [[7, 0, 7], [7, 8, -1], [4294967295, 2, 4294967293], [0,0,0]]);
 
 // Sub Double
 var funSub2 = (a, b) => { return a - b; }
 warmup(funSub2, [[7.5, 0, 7.5], [7, 8.125, -1.125], [4294967295.3125, 2, 4294967293.3125], [NaN,10,NaN]]);
 
@@ -205,9 +210,40 @@ warmup(funURsh4, [[54772703898, 11, 1578
 for (var k=0; k < 30; k++) {
     A="01234567";
     res =""
     for (var i = 0; i < 8; ++i) {
         var v = A[7 - i];
         res+=v;
     }
     assertEq(res, "76543210");
-}
\ No newline at end of file
+}
+
+// Begin OOM testing:
+if (!('oomTest' in this))
+    quit();
+
+// Add: String Number Concat OOM test
+var addOom = (a, b) => { return a + b; }
+
+function generate_digits(prefix, start) {
+    digits = []
+    number = ""+start+".25";
+    for (var i = 1; i < 7; i++) {
+        number = i + number;
+        digits.push([prefix, Number(number), prefix + number]);
+    }
+    return digits;
+}
+
+// Trying to defeat dtoacache: Warm the IC with one set of digits, then actually oomTest
+// using another set.
+var warmup_digits = generate_digits("x", 1);
+var test_digits = generate_digits("x", 2);
+
+function ot(digits) {
+    warmup(addOom, digits);
+}
+
+// Ensure ICs are warmed
+ot(warmup_digits);
+
+oomTest(() => { ot(test_digits); });
\ No newline at end of file
--- a/js/src/jit/BaselineCacheIRCompiler.cpp
+++ b/js/src/jit/BaselineCacheIRCompiler.cpp
@@ -2370,16 +2370,18 @@ ICCacheIR_Updated::Clone(JSContext* cx, 
 bool
 BaselineCacheIRCompiler::emitCallStringConcatResult()
 {
     AutoOutputRegister output(*this);
     Register lhs = allocator.useRegister(masm, reader.stringOperandId());
     Register rhs = allocator.useRegister(masm, reader.stringOperandId());
     AutoScratchRegisterMaybeOutput scratch(allocator, masm, output);
 
+    allocator.discardStack(masm);
+
     AutoStubFrame stubFrame(*this);
     stubFrame.enter(masm, scratch);
 
     masm.push(rhs);
     masm.push(lhs);
 
     if (!callVM(masm, ConcatStringsInfo)) {
         return false;
--- a/js/src/jit/BaselineInspector.cpp
+++ b/js/src/jit/BaselineInspector.cpp
@@ -387,16 +387,18 @@ ParseCacheIRStub(ICStub* stub)
     switch (reader.readOp()) {
       case CacheOp::LoadUndefinedResult:
         return MIRType::Undefined;
       case CacheOp::LoadBooleanResult:
         return MIRType::Boolean;
       case CacheOp::LoadStringResult:
       case CacheOp::CallStringConcatResult:
       case CacheOp::CallStringObjectConcatResult:
+      case CacheOp::CallInt32ToString:
+      case CacheOp::CallNumberToString:
         return MIRType::String;
       case CacheOp::DoubleAddResult:
       case CacheOp::DoubleSubResult:
       case CacheOp::DoubleMulResult:
       case CacheOp::DoubleDivResult:
       case CacheOp::DoubleModResult:
       case CacheOp::DoubleNegationResult:
         return MIRType::Double;
--- a/js/src/jit/CacheIR.cpp
+++ b/js/src/jit/CacheIR.cpp
@@ -5820,16 +5820,19 @@ BinaryArithIRGenerator::tryAttachStub()
         return true;
     }
 
     // String x Object
     if (tryAttachStringObjectConcat()) {
         return true;
     }
 
+    if (tryAttachStringNumberConcat())
+        return true;
+
 
     trackAttached(IRGenerator::NotAttached);
     return false;
 }
 
 bool
 BinaryArithIRGenerator::tryAttachBitwise()
 {
@@ -6016,16 +6019,58 @@ BinaryArithIRGenerator::tryAttachInt32()
       default:
         MOZ_CRASH("Unhandled op in tryAttachInt32");
     }
 
     writer.returnFromIC();
     return true;
 }
 
+
+bool
+BinaryArithIRGenerator::tryAttachStringNumberConcat()
+{
+    // Only Addition
+    if (op_ != JSOP_ADD)
+        return false;
+
+    if (!(lhs_.isString() && rhs_.isNumber()) &&
+        !(lhs_.isNumber() && rhs_.isString()))
+    {
+        return false;
+    }
+
+    ValOperandId lhsId(writer.setInputOperandId(0));
+    ValOperandId rhsId(writer.setInputOperandId(1));
+
+    auto guardToString = [&](ValOperandId id, HandleValue v) {
+        if (v.isString()) {
+            return writer.guardIsString(id);
+        }
+        if (v.isInt32()) {
+            Int32OperandId intId = writer.guardIsInt32(id);
+            return writer.callInt32ToString(intId);
+        }
+        // At this point we are creating an IC that will handle
+        // both Int32 and Double cases.
+        MOZ_ASSERT(v.isNumber());
+        writer.guardIsNumber(id);
+        return writer.callNumberToString(id);
+    };
+
+    StringOperandId lhsStrId = guardToString(lhsId, lhs_);
+    StringOperandId rhsStrId = guardToString(rhsId, rhs_);
+
+    writer.callStringConcatResult(lhsStrId, rhsStrId);
+
+    writer.returnFromIC();
+    trackAttached("BinaryArith.StringNumberConcat");
+    return true;
+}
+
 bool
 BinaryArithIRGenerator::tryAttachStringConcat()
 {
     // Only Addition
     if (op_ != JSOP_ADD) {
         return false;
     }
 
--- a/js/src/jit/CacheIR.h
+++ b/js/src/jit/CacheIR.h
@@ -261,16 +261,18 @@ extern const char* const CacheKindNames[
     _(ArrayPush)                          \
     _(ArrayJoinResult)                    \
     _(StoreTypedElement)                  \
     _(CallNativeSetter)                   \
     _(CallScriptedSetter)                 \
     _(CallSetArrayLength)                 \
     _(CallProxySet)                       \
     _(CallProxySetByValue)                \
+    _(CallInt32ToString)                  \
+    _(CallNumberToString)                 \
                                           \
     /* The *Result ops load a value into the cache's result register. */ \
     _(LoadFixedSlotResult)                \
     _(LoadDynamicSlotResult)              \
     _(LoadUnboxedPropertyResult)          \
     _(LoadTypedObjectResult)              \
     _(LoadDenseElementResult)             \
     _(LoadDenseElementHoleResult)         \
@@ -1028,16 +1030,28 @@ class MOZ_RAII CacheIRWriter : public JS
         buffer_.writeByte(uint32_t(strict));
     }
     void callProxySetByValue(ObjOperandId obj, ValOperandId id, ValOperandId rhs, bool strict) {
         writeOpWithOperandId(CacheOp::CallProxySetByValue, obj);
         writeOperandId(id);
         writeOperandId(rhs);
         buffer_.writeByte(uint32_t(strict));
     }
+    StringOperandId callInt32ToString(Int32OperandId id) {
+        StringOperandId res(nextOperandId_++);
+        writeOpWithOperandId(CacheOp::CallInt32ToString, id);
+        writeOperandId(res);
+        return res;
+    }
+    StringOperandId callNumberToString(ValOperandId id) {
+        StringOperandId res(nextOperandId_++);
+        writeOpWithOperandId(CacheOp::CallNumberToString, id);
+        writeOperandId(res);
+        return res;
+    }
 
     void megamorphicLoadSlotResult(ObjOperandId obj, PropertyName* name, bool handleMissing) {
         writeOpWithOperandId(CacheOp::MegamorphicLoadSlotResult, obj);
         addStubField(uintptr_t(name), StubField::Type::String);
         buffer_.writeByte(uint32_t(handleMissing));
     }
     void megamorphicLoadSlotByValueResult(ObjOperandId obj, ValOperandId id, bool handleMissing) {
         writeOpWithOperandId(CacheOp::MegamorphicLoadSlotByValueResult, obj);
@@ -1970,16 +1984,17 @@ class MOZ_RAII BinaryArithIRGenerator : 
 
     void trackAttached(const char* name);
 
     bool tryAttachInt32();
     bool tryAttachDouble();
     bool tryAttachBitwise();
     bool tryAttachStringConcat();
     bool tryAttachStringObjectConcat();
+    bool tryAttachStringNumberConcat();
 
   public:
     BinaryArithIRGenerator(JSContext* cx, HandleScript, jsbytecode* pc, ICState::Mode,
                            JSOp op, HandleValue lhs, HandleValue rhs, HandleValue res);
 
     bool tryAttachStub();
 
 };
--- a/js/src/jit/CacheIRCompiler.cpp
+++ b/js/src/jit/CacheIRCompiler.cpp
@@ -4036,16 +4036,73 @@ bool
 CacheIRCompiler::emitLoadObject()
 {
     Register reg = allocator.defineRegister(masm, reader.objOperandId());
     StubFieldOffset obj(reader.stubOffset(), StubField::Type::JSObject);
     emitLoadStubField(obj, reg);
     return true;
 }
 
+bool
+CacheIRCompiler::emitCallInt32ToString() {
+    Register input = allocator.useRegister(masm, reader.int32OperandId());
+    Register result = allocator.defineRegister(masm, reader.stringOperandId());
+
+    FailurePath* failure;
+    if (!addFailurePath(&failure))
+        return false;
+
+    LiveRegisterSet volatileRegs(GeneralRegisterSet::Volatile(), liveVolatileFloatRegs());
+    volatileRegs.takeUnchecked(result);
+    masm.PushRegsInMask(volatileRegs);
+
+    masm.setupUnalignedABICall(result);
+    masm.loadJSContext(result);
+    masm.passABIArg(result);
+    masm.passABIArg(input);
+    masm.callWithABI(JS_FUNC_TO_DATA_PTR(void*, (js::Int32ToStringHelper)));
+
+    masm.mov(ReturnReg, result);
+    masm.PopRegsInMask(volatileRegs);
+
+    masm.branchPtr(Assembler::Equal, result, ImmPtr(0), failure->label());
+    return true;
+}
+
+bool
+CacheIRCompiler::emitCallNumberToString() {
+    // Float register must be preserved. The BinaryArith ICs use
+    // the fact that baseline has them available, as well as fixed temps on
+    // LBinaryCache.
+    allocator.ensureDoubleRegister(masm, reader.valOperandId(), FloatReg0);
+    Register result = allocator.defineRegister(masm, reader.stringOperandId());
+
+    FailurePath* failure;
+    if (!addFailurePath(&failure))
+        return false;
+
+    LiveRegisterSet volatileRegs(GeneralRegisterSet::Volatile(), liveVolatileFloatRegs());
+    volatileRegs.takeUnchecked(result);
+    volatileRegs.addUnchecked(FloatReg0);
+    masm.PushRegsInMask(volatileRegs);
+
+    masm.setupUnalignedABICall(result);
+    masm.loadJSContext(result);
+    masm.passABIArg(result);
+    masm.passABIArg(FloatReg0, MoveOp::DOUBLE);
+    masm.callWithABI(JS_FUNC_TO_DATA_PTR(void*, (js::NumberToStringHelper)));
+
+    masm.mov(ReturnReg, result);
+    masm.PopRegsInMask(volatileRegs);
+
+    masm.branchPtr(Assembler::Equal, result, ImmPtr(0), failure->label());
+    return true;
+}
+
+
 void
 js::jit::LoadTypedThingData(MacroAssembler& masm, TypedThingLayout layout, Register obj, Register result)
 {
     switch (layout) {
       case Layout_TypedArray:
         masm.loadPtr(Address(obj, TypedArrayObject::dataOffset()), result);
         break;
       case Layout_OutlineTypedObject:
--- a/js/src/jit/CacheIRCompiler.h
+++ b/js/src/jit/CacheIRCompiler.h
@@ -107,16 +107,18 @@ namespace jit {
     _(ArrayJoinResult)                    \
     _(CallPrintString)                    \
     _(Breakpoint)                         \
     _(MegamorphicLoadSlotResult)          \
     _(MegamorphicLoadSlotByValueResult)   \
     _(MegamorphicStoreSlot)               \
     _(MegamorphicHasPropResult)           \
     _(CallObjectHasSparseElementResult)   \
+    _(CallInt32ToString)                  \
+    _(CallNumberToString)                 \
     _(WrapResult)
 
 // Represents a Value on the Baseline frame's expression stack. Slot 0 is the
 // value on top of the stack (the most recently pushed value), slot 1 is the
 // value pushed before that, etc.
 class BaselineFrameSlot
 {
     uint32_t slot_;
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -151,17 +151,17 @@ MSG_DEF(JSMSG_CALLER_IS_STRICT,        0
 MSG_DEF(JSMSG_DEPRECATED_USAGE,        1, JSEXN_REFERENCEERR, "deprecated {0} usage")
 MSG_DEF(JSMSG_NOT_SCRIPTED_FUNCTION,   1, JSEXN_TYPEERR, "{0} is not a scripted function")
 MSG_DEF(JSMSG_NO_REST_NAME,            0, JSEXN_SYNTAXERR, "no parameter name after ...")
 MSG_DEF(JSMSG_PARAMETER_AFTER_REST,    0, JSEXN_SYNTAXERR, "parameter after rest parameter")
 MSG_DEF(JSMSG_TOO_MANY_ARGUMENTS,      0, JSEXN_RANGEERR, "too many arguments provided for a function call")
 
 // CSP
 MSG_DEF(JSMSG_CSP_BLOCKED_EVAL,        0, JSEXN_EVALERR, "call to eval() blocked by CSP")
-MSG_DEF(JSMSG_CSP_BLOCKED_FUNCTION,    0, JSEXN_ERR, "call to Function() blocked by CSP")
+MSG_DEF(JSMSG_CSP_BLOCKED_FUNCTION,    0, JSEXN_EVALERR, "call to Function() blocked by CSP")
 
 // Wrappers
 MSG_DEF(JSMSG_ACCESSOR_DEF_DENIED,     1, JSEXN_ERR, "Permission denied to define accessor property {0}")
 MSG_DEF(JSMSG_DEAD_OBJECT,             0, JSEXN_TYPEERR, "can't access dead object")
 MSG_DEF(JSMSG_OBJECT_ACCESS_DENIED,    0, JSEXN_ERR, "Permission denied to access object")
 MSG_DEF(JSMSG_PROPERTY_ACCESS_DENIED,  1, JSEXN_ERR, "Permission denied to access property {0}")
 
 // JSAPI-only (Not thrown as JS exceptions)
--- a/js/src/jsnum.cpp
+++ b/js/src/jsnum.cpp
@@ -714,16 +714,27 @@ js::Int32ToString(JSContext* cx, int32_t
 }
 
 template JSFlatString*
 js::Int32ToString<CanGC>(JSContext* cx, int32_t si);
 
 template JSFlatString*
 js::Int32ToString<NoGC>(JSContext* cx, int32_t si);
 
+JSFlatString*
+js::Int32ToStringHelper(JSContext* cx, int32_t si)
+{
+    AutoUnsafeCallWithABI unsafe;
+    JSFlatString* res = Int32ToString<NoGC>(cx, si);
+    if (!res) {
+        cx->recoverFromOutOfMemory();
+    }
+    return res;
+}
+
 JSAtom*
 js::Int32ToAtom(JSContext* cx, int32_t si)
 {
     if (JSFlatString* str = LookupInt32ToString(cx, si)) {
         return js::AtomizeString(cx, str);
     }
 
     char buffer[JSFatInlineString::MAX_LENGTH_TWO_BYTE + 1];
@@ -1513,16 +1524,27 @@ js::NumberToString(JSContext* cx, double
 }
 
 template JSString*
 js::NumberToString<CanGC>(JSContext* cx, double d);
 
 template JSString*
 js::NumberToString<NoGC>(JSContext* cx, double d);
 
+JSString*
+js::NumberToStringHelper(JSContext* cx, double d)
+{
+    AutoUnsafeCallWithABI unsafe;
+    JSString* res = NumberToString<NoGC>(cx, d);
+    if (!res) {
+        cx->recoverFromOutOfMemory();
+    }
+    return res;
+}
+
 JSAtom*
 js::NumberToAtom(JSContext* cx, double d)
 {
     int32_t si;
     if (mozilla::NumberEqualsInt32(d, &si)) {
         return Int32ToAtom(cx, si);
     }
 
--- a/js/src/jsnum.h
+++ b/js/src/jsnum.h
@@ -55,23 +55,29 @@ InitNumberClass(JSContext* cx, Handle<Gl
  * When base == 10, this function implements ToString() as specified by
  * ECMA-262-5 section 9.8.1; but note that it handles integers specially for
  * performance.  See also js::NumberToCString().
  */
 template <AllowGC allowGC>
 extern JSString*
 NumberToString(JSContext* cx, double d);
 
+extern JSString*
+NumberToStringHelper(JSContext* cx, double d);
+
 extern JSAtom*
 NumberToAtom(JSContext* cx, double d);
 
 template <AllowGC allowGC>
 extern JSFlatString*
 Int32ToString(JSContext* cx, int32_t i);
 
+extern JSFlatString*
+Int32ToStringHelper(JSContext* cx, int32_t i);
+
 extern JSAtom*
 Int32ToAtom(JSContext* cx, int32_t si);
 
 // ES6 15.7.3.12
 extern bool
 IsInteger(const Value& val);
 
 /*
--- a/js/xpconnect/idl/xpccomponents.idl
+++ b/js/xpconnect/idl/xpccomponents.idl
@@ -6,16 +6,17 @@
 #include "nsISupports.idl"
 
 %{C++
 #include "jspubtd.h"
 %}
 
 interface xpcIJSWeakReference;
 interface nsIClassInfo;
+interface nsICommandParams;
 interface nsIComponentManager;
 interface nsICycleCollectorListener;
 interface nsIEditorSpellCheck;
 interface nsIFile;
 interface nsIURI;
 interface nsIJSCID;
 interface nsIJSIID;
 interface nsIPrincipal;
@@ -718,16 +719,19 @@ interface nsIXPCComponents_Utils : nsISu
     /* Give a directive to the record/replay system. */
     void recordReplayDirective(in long directive);
 
     /* Create a spellchecker object. */
     nsIEditorSpellCheck createSpellChecker();
 
     /* Create a commandline object. */
     nsISupports createCommandLine();
+
+    /* Create a command params object. */
+    nsICommandParams createCommandParams();
 };
 
 /**
 * Interface for the 'Components' object.
 *
 * The first interface contains things that are available to non-chrome XBL code
 * that runs in a scope with an ExpandedPrincipal. The second interface
 * includes members that are only exposed to chrome.
--- a/js/xpconnect/src/Sandbox.cpp
+++ b/js/xpconnect/src/Sandbox.cpp
@@ -1873,23 +1873,16 @@ nsXPCComponents_utils_Sandbox::CallOrCon
     }
 
     rv = CreateSandboxObject(cx, args.rval(), prinOrSop, options);
 
     if (NS_FAILED(rv)) {
         return ThrowAndFail(rv, cx, _retval);
     }
 
-    // We have this crazy behavior where wantXrays=false also implies that the
-    // returned sandbox is implicitly waived. We've stopped advertising it, but
-    // keep supporting it for now.
-    if (!options.wantXrays && !xpc::WrapperFactory::WaiveXrayAndWrap(cx, args.rval())) {
-        return NS_ERROR_UNEXPECTED;
-    }
-
     *_retval = true;
     return NS_OK;
 }
 
 nsresult
 xpc::EvalInSandbox(JSContext* cx, HandleObject sandboxArg, const nsAString& source,
                    const nsACString& filename, int32_t lineNo,
                    MutableHandleValue rval)
--- a/js/xpconnect/src/XPCComponents.cpp
+++ b/js/xpconnect/src/XPCComponents.cpp
@@ -38,16 +38,17 @@
 #include "nsIScriptError.h"
 #include "nsISimpleEnumerator.h"
 #include "nsPIDOMWindow.h"
 #include "nsGlobalWindow.h"
 #include "nsScriptError.h"
 #include "GeckoProfiler.h"
 #include "mozilla/EditorSpellCheck.h"
 #include "nsCommandLine.h"
+#include "nsCommandParams.h"
 
 using namespace mozilla;
 using namespace JS;
 using namespace js;
 using namespace xpc;
 using mozilla::dom::Exception;
 
 /***************************************************************************/
@@ -3226,16 +3227,25 @@ NS_IMETHODIMP
 nsXPCComponents_Utils::CreateCommandLine(nsISupports** aCommandLine)
 {
     NS_ENSURE_ARG_POINTER(aCommandLine);
     nsCOMPtr<nsISupports> commandLine = new nsCommandLine();
     commandLine.forget(aCommandLine);
     return NS_OK;
 }
 
+NS_IMETHODIMP
+nsXPCComponents_Utils::CreateCommandParams(nsICommandParams** aCommandParams)
+{
+    NS_ENSURE_ARG_POINTER(aCommandParams);
+    nsCOMPtr<nsICommandParams> commandParams = new nsCommandParams();
+    commandParams.forget(aCommandParams);
+    return NS_OK;
+}
+
 /***************************************************************************/
 /***************************************************************************/
 /***************************************************************************/
 
 
 nsXPCComponentsBase::nsXPCComponentsBase(XPCWrappedNativeScope* aScope)
     :   mScope(aScope)
 {
--- a/js/xpconnect/tests/chrome/test_evalInSandbox.xul
+++ b/js/xpconnect/tests/chrome/test_evalInSandbox.xul
@@ -72,20 +72,24 @@ https://bugzilla.mozilla.org/show_bug.cg
 
         sandbox =
           new Cu.Sandbox(win, { sandboxPrototype: win, wantXrays: false } );
 
         checkCrossOriginSandbox(sandbox);
 
         ok(Cu.evalInSandbox("('foo' in this.document);", sandbox),
            "can see expandos");
-        ok(("foo" in Cu.evalInSandbox("this.document", sandbox)),
-           "must see expandos in wrappers returned from the sandbox");
+        ok(!("foo" in Cu.evalInSandbox("this.document", sandbox)),
+           "must not see expandos in wrappers returned from the sandbox");
+        ok(("foo" in Cu.waiveXrays(Cu.evalInSandbox("this.document", sandbox))),
+           "must see expandos in waived wrappers returned from the sandbox");
 
-        ok(("foo" in sandbox.document),
+        ok(!("foo" in sandbox.document),
+           "must not see expandos in wrappers obtained from the sandbox");
+        ok("foo" in Cu.waiveXrays(sandbox.document),
            "must see expandos in wrappers obtained from the sandbox");
 
         testDone();
       }
 
       function checkSameOrigin(ifr) {
         var win = ifr.contentWindow;
         var sandbox =
--- a/js/xpconnect/tests/unit/test_bug845862.js
+++ b/js/xpconnect/tests/unit/test_bug845862.js
@@ -1,11 +1,7 @@
 function run_test() {
-  // We rely on the crazy "wantXrays:false also causes values return from the
-  // sandbox to be waived" behavior, because it's the simplest way to get
-  // waivers out of the sandbox (which has no native objects). :-(
-  var sb = new Cu.Sandbox('http://www.example.com', {wantXrays: false});
+  var sb = new Cu.Sandbox('http://www.example.com');
   Cu.evalInSandbox("this.foo = {}; Object.defineProperty(foo, 'bar', {get: function() {return {};}});", sb);
-  Assert.ok(sb.foo != XPCNativeWrapper(sb.foo), "sb.foo is waived");
-  var desc = Object.getOwnPropertyDescriptor(sb.foo, 'bar');
+  var desc = Object.getOwnPropertyDescriptor(Cu.waiveXrays(sb.foo), 'bar');
   var b = desc.get();
   Assert.ok(b != XPCNativeWrapper(b), "results from accessor descriptors are waived");
 }
--- a/layout/base/MobileViewportManager.cpp
+++ b/layout/base/MobileViewportManager.cpp
@@ -113,17 +113,17 @@ void
 MobileViewportManager::ResolutionUpdated()
 {
   MVM_LOG("%p: resolution updated\n", this);
   if (!mPainted) {
     // Save the value, so our default zoom calculation
     // can take it into account later on.
     SetRestoreResolution(mPresShell->GetResolution());
   }
-  RefreshSPCSPS();
+  RefreshVisualViewportSize();
 }
 
 NS_IMETHODIMP
 MobileViewportManager::HandleEvent(dom::Event* event)
 {
   nsAutoString type;
   event->GetType(type);
 
@@ -280,18 +280,18 @@ MobileViewportManager::UpdateResolution(
     mPresShell->SetResolutionAndScaleTo(res.scale);
   }
 
   return ViewTargetAs<ScreenPixel>(cssToDev * res / ParentLayerToLayerScale(1),
     PixelCastJustification::ScreenIsParentLayerForRoot);
 }
 
 void
-MobileViewportManager::UpdateSPCSPS(const ScreenIntSize& aDisplaySize,
-                                    const CSSToScreenScale& aZoom)
+MobileViewportManager::UpdateVisualViewportSize(const ScreenIntSize& aDisplaySize,
+                                                const CSSToScreenScale& aZoom)
 {
   ScreenSize compositionSize(aDisplaySize);
   ScreenMargin scrollbars =
     LayoutDeviceMargin::FromAppUnits(
       nsLayoutUtils::ScrollbarAreaToExcludeFromCompositionBoundsFor(
         mPresShell->GetRootScrollFrame()),
       mPresShell->GetPresContext()->AppUnitsPerDevPixel())
     // Scrollbars are not subject to resolution scaling, so LD pixels =
@@ -327,35 +327,35 @@ MobileViewportManager::UpdateDisplayPort
     nsLayoutUtils::SetDisplayPortBaseIfNotSet(root->GetContent(), displayportBase);
     nsIScrollableFrame* scrollable = do_QueryFrame(root);
     nsLayoutUtils::CalculateAndSetDisplayPortMargins(scrollable,
       nsLayoutUtils::RepaintMode::DoNotRepaint);
   }
 }
 
 void
-MobileViewportManager::RefreshSPCSPS()
+MobileViewportManager::RefreshVisualViewportSize()
 {
   // This function is a subset of RefreshViewportSize, and only updates the
-  // SPCSPS.
+  // visual viewport size.
 
   if (!gfxPrefs::APZAllowZooming()) {
     return;
   }
 
   ScreenIntSize displaySize = ViewAs<ScreenPixel>(
     mDisplaySize, PixelCastJustification::LayoutDeviceIsScreenForBounds);
 
   CSSToLayoutDeviceScale cssToDev =
       mPresShell->GetPresContext()->CSSToDevPixelScale();
   LayoutDeviceToLayerScale res(mPresShell->GetResolution());
   CSSToScreenScale zoom = ViewTargetAs<ScreenPixel>(cssToDev * res / ParentLayerToLayerScale(1),
     PixelCastJustification::ScreenIsParentLayerForRoot);
 
-  UpdateSPCSPS(displaySize, zoom);
+  UpdateVisualViewportSize(displaySize, zoom);
 }
 
 void
 MobileViewportManager::RefreshViewportSize(bool aForceAdjustResolution)
 {
   // This function gets called by the various triggers that may result in a
   // change of the CSS viewport. In some of these cases (e.g. the meta-viewport
   // tag changes) we want to update the resolution and in others (e.g. the full
@@ -409,17 +409,17 @@ MobileViewportManager::RefreshViewportSi
   // various APZ properties (the zoom and some things that might depend on it)
   MVM_LOG("%p: Updating properties because %d || %d\n", this,
     mIsFirstPaint, mMobileViewportSize != viewport);
 
   if (gfxPrefs::APZAllowZooming()) {
     CSSToScreenScale zoom = UpdateResolution(viewportInfo, displaySize, viewport,
       displayWidthChangeRatio);
     MVM_LOG("%p: New zoom is %f\n", this, zoom.scale);
-    UpdateSPCSPS(displaySize, zoom);
+    UpdateVisualViewportSize(displaySize, zoom);
   }
   if (gfxPlatform::AsyncPanZoomEnabled()) {
     UpdateDisplayPortMargins();
   }
 
   CSSSize oldSize = mMobileViewportSize;
 
   // Update internal state.
--- a/layout/base/MobileViewportManager.h
+++ b/layout/base/MobileViewportManager.h
@@ -48,32 +48,32 @@ private:
   void SetRestoreResolution(float aResolution);
 
 public:
   /* Notify the MobileViewportManager that a reflow was requested in the
    * presShell.*/
   void RequestReflow();
 
   /* Notify the MobileViewportManager that the resolution on the presShell was
-   * updated, and the SPCSPS needs to be updated. */
+   * updated, and the visual viewport size needs to be updated. */
   void ResolutionUpdated();
 
 private:
   ~MobileViewportManager();
 
   /* Called to compute the initial viewport on page load or before-first-paint,
    * whichever happens first. */
   void SetInitialViewport();
 
   /* Main helper method to update the CSS viewport and any other properties that
    * need updating. */
   void RefreshViewportSize(bool aForceAdjustResolution);
 
-  /* Secondary main helper method to update just the SPCSPS. */
-  void RefreshSPCSPS();
+  /* Secondary main helper method to update just the visual viewport size. */
+  void RefreshVisualViewportSize();
 
   /* Helper to clamp the given zoom by the min/max in the viewport info. */
   mozilla::CSSToScreenScale ClampZoom(const mozilla::CSSToScreenScale& aZoom,
                                       const nsViewportInfo& aViewportInfo);
 
   /* Helper to update the given resolution according to changed display and viewport widths. */
   mozilla::LayoutDeviceToLayerScale
   ScaleResolutionWithDisplayWidth(const mozilla::LayoutDeviceToLayerScale& aRes,
@@ -82,19 +82,18 @@ private:
                                   const mozilla::CSSSize& aOldViewport);
 
   /* Updates the presShell resolution and returns the new zoom. */
   mozilla::CSSToScreenScale UpdateResolution(const nsViewportInfo& aViewportInfo,
                                              const mozilla::ScreenIntSize& aDisplaySize,
                                              const mozilla::CSSSize& aViewport,
                                              const mozilla::Maybe<float>& aDisplayWidthChangeRatio);
 
-  /* Updates the scroll-position-clamping scrollport size */
-  void UpdateSPCSPS(const mozilla::ScreenIntSize& aDisplaySize,
-                    const mozilla::CSSToScreenScale& aZoom);
+  void UpdateVisualViewportSize(const mozilla::ScreenIntSize& aDisplaySize,
+                                const mozilla::CSSToScreenScale& aZoom);
 
   /* Updates the displayport margins for the presShell's root scrollable frame */
   void UpdateDisplayPortMargins();
 
   nsCOMPtr<nsIDocument> mDocument;
   nsIPresShell* MOZ_NON_OWNING_REF mPresShell; // raw ref since the presShell owns this
   nsCOMPtr<mozilla::dom::EventTarget> mEventTarget;
   bool mIsFirstPaint;
--- a/layout/generic/nsBlockFrame.cpp
+++ b/layout/generic/nsBlockFrame.cpp
@@ -281,16 +281,22 @@ RecordReflowStatus(bool aChildIsBlock, c
   // Log updates to the status that yield different values
   if (record[index] != newS) {
     record[index] = newS;
     printf("record(%d): %02x %02x\n", index, record[0], record[1]);
   }
 }
 #endif
 
+static nscoord
+ResolveTextIndent(const nsStyleCoord& aStyle, nscoord aPercentageBasis)
+{
+  return nsLayoutUtils::ResolveToLength<false>(aStyle, aPercentageBasis);
+}
+
 NS_DECLARE_FRAME_PROPERTY_WITH_DTOR_NEVER_CALLED(OverflowLinesProperty,
                                                  nsBlockFrame::FrameLines)
 NS_DECLARE_FRAME_PROPERTY_FRAMELIST(OverflowOutOfFlowsProperty)
 NS_DECLARE_FRAME_PROPERTY_FRAMELIST(PushedFloatProperty)
 NS_DECLARE_FRAME_PROPERTY_FRAMELIST(OutsideBulletProperty)
 NS_DECLARE_FRAME_PROPERTY_WITHOUT_DTOR(InsideBulletProperty, nsBulletFrame)
 NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(BlockEndEdgeOfChildrenProperty, nscoord)
 
@@ -802,25 +808,18 @@ nsBlockFrame::GetMinISize(gfxContext *aR
       if (line->IsBlock()) {
         data.ForceBreak();
         data.mCurrentLine = nsLayoutUtils::IntrinsicForContainer(aRenderingContext,
                         line->mFirstChild, nsLayoutUtils::MIN_ISIZE);
         data.ForceBreak();
       } else {
         if (!curFrame->GetPrevContinuation() &&
             line == curFrame->LinesBegin()) {
-          // Only add text-indent if it has no percentages; using a
-          // percentage basis of 0 unconditionally would give strange
-          // behavior for calc(10%-3px).
-          const nsStyleCoord &indent = StyleText()->mTextIndent;
-          if (indent.ConvertsToLength())
-            data.mCurrentLine += indent.ComputeCoordPercentCalc(0);
+          data.mCurrentLine += ::ResolveTextIndent(StyleText()->mTextIndent, 0);
         }
-        // XXX Bug NNNNNN Should probably handle percentage text-indent.
-
         data.mLine = &line;
         data.SetLineContainer(curFrame);
         nsIFrame *kid = line->mFirstChild;
         for (int32_t i = 0, i_end = line->GetChildCount(); i != i_end;
              ++i, kid = kid->GetNextSibling()) {
           kid->AddInlineMinISize(aRenderingContext, &data);
         }
       }
@@ -901,30 +900,23 @@ nsBlockFrame::GetPrefISize(gfxContext *a
         }
         data.ForceBreak(breakType);
         data.mCurrentLine = nsLayoutUtils::IntrinsicForContainer(aRenderingContext,
                         line->mFirstChild, nsLayoutUtils::PREF_ISIZE);
         data.ForceBreak();
       } else {
         if (!curFrame->GetPrevContinuation() &&
             line == curFrame->LinesBegin()) {
-          // Only add text-indent if it has no percentages; using a
-          // percentage basis of 0 unconditionally would give strange
-          // behavior for calc(10%-3px).
-          const nsStyleCoord &indent = StyleText()->mTextIndent;
-          if (indent.ConvertsToLength()) {
-            nscoord length = indent.ToLength();
-            if (length != 0) {
-              data.mCurrentLine += length;
-              data.mLineIsEmpty = false;
-            }
+          nscoord indent = ::ResolveTextIndent(StyleText()->mTextIndent, 0);
+          data.mCurrentLine += indent;
+          // XXXmats should the test below be indent > 0?
+          if (indent != nscoord(0)) {
+            data.mLineIsEmpty = false;
           }
         }
-        // XXX Bug NNNNNN Should probably handle percentage text-indent.
-
         data.mLine = &line;
         data.SetLineContainer(curFrame);
         nsIFrame *kid = line->mFirstChild;
         for (int32_t i = 0, i_end = line->GetChildCount(); i != i_end;
              ++i, kid = kid->GetNextSibling()) {
           kid->AddInlinePrefISize(aRenderingContext, &data);
         }
       }
@@ -979,26 +971,18 @@ nsBlockFrame::GetPrefWidthTightBounds(gf
         rv = line->mFirstChild->GetPrefWidthTightBounds(aRenderingContext,
                                                         &childX, &childXMost);
         NS_ENSURE_SUCCESS(rv, rv);
         *aX = std::min(*aX, childX);
         *aXMost = std::max(*aXMost, childXMost);
       } else {
         if (!curFrame->GetPrevContinuation() &&
             line == curFrame->LinesBegin()) {
-          // Only add text-indent if it has no percentages; using a
-          // percentage basis of 0 unconditionally would give strange
-          // behavior for calc(10%-3px).
-          const nsStyleCoord &indent = StyleText()->mTextIndent;
-          if (indent.ConvertsToLength()) {
-            data.mCurrentLine += indent.ComputeCoordPercentCalc(0);
-          }
+          data.mCurrentLine += ::ResolveTextIndent(StyleText()->mTextIndent, 0);
         }
-        // XXX Bug NNNNNN Should probably handle percentage text-indent.
-
         data.mLine = &line;
         data.SetLineContainer(curFrame);
         nsIFrame *kid = line->mFirstChild;
         for (int32_t i = 0, i_end = line->GetChildCount(); i != i_end;
              ++i, kid = kid->GetNextSibling()) {
           rv = kid->GetPrefWidthTightBounds(aRenderingContext, &childX,
                                             &childXMost);
           NS_ENSURE_SUCCESS(rv, rv);
--- a/layout/reftests/w3c-css/submitted/values3/calc-text-indent-intrinsic-1-ref.html
+++ b/layout/reftests/w3c-css/submitted/values3/calc-text-indent-intrinsic-1-ref.html
@@ -7,16 +7,16 @@
 <style type="text/css">
 
 body > div { margin: 0 0 1px 0; background: blue; color: white; height: 5px }
 
 </style>
 </head>
 <body>
 
-<div style="width: 10px"></div>
+<div style="width: 7px"></div>
 <div style="width: 57px"></div>
-<div style="width: 10px"></div>
+<div style="width: 60px"></div>
 <div style="width: 10px"></div>
 <div style="width: 60px"></div>
 <div style="width: 10px"></div>
 </body>
 </html>
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4603,16 +4603,20 @@ pref("toolkit.zoomManager.zoomValues", "
 // before it starts to discard already displayed frames and redecode them as
 // necessary.
 pref("image.animated.decode-on-demand.threshold-kb", 20480);
 
 // The minimum number of frames we want to have buffered ahead of an
 // animation's currently displayed frame.
 pref("image.animated.decode-on-demand.batch-size", 6);
 
+// Whether we should generate full frames at decode time or partial frames which
+// are combined at display time (historical behavior and default).
+pref("image.animated.generate-full-frames", false);
+
 // Resume an animated image from the last displayed frame rather than
 // advancing when out of view.
 pref("image.animated.resume-from-last-displayed", true);
 
 // Maximum number of surfaces for an image before entering "factor of 2" mode.
 // This in addition to the number of "native" sizes of an image. A native size
 // is a size for which we can decode a frame without up or downscaling. Most
 // images only have 1, but some (i.e. ICOs) may have multiple frames for the
--- a/netwerk/dns/nsEffectiveTLDService.cpp
+++ b/netwerk/dns/nsEffectiveTLDService.cpp
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // This service reads a file of rules describing TLD-like domain names.  For a
 // complete description of the expected file format and parsing rules, see
 // http://wiki.mozilla.org/Gecko:Effective_TLD_Service
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/HashFunctions.h"
+#include "mozilla/Maybe.h"
 #include "mozilla/MemoryReporting.h"
 
 #include "MainThreadUtils.h"
 #include "nsEffectiveTLDService.h"
 #include "nsIIDNService.h"
 #include "nsNetUtil.h"
 #include "prnetdb.h"
 #include "nsIURI.h"
@@ -209,27 +210,30 @@ nsEffectiveTLDService::GetBaseDomainInte
   // Check if we're dealing with an IPv4/IPv6 hostname, and return
   PRNetAddr addr;
   PRStatus result = PR_StringToNetAddr(aHostname.get(), &addr);
   if (result == PR_SUCCESS)
     return NS_ERROR_HOST_IS_IP_ADDRESS;
 
   // Lookup in the cache if this is a normal query. This is restricted to
   // main thread-only as the cache is not thread-safe.
-  TLDCacheEntry* entry = nullptr;
+  Maybe<TldCache::Entry> entry;
   if (aAdditionalParts == 1 && NS_IsMainThread()) {
-    if (LookupForAdd(aHostname, &entry)) {
+    auto p = mMruTable.Lookup(aHostname);
+    if (p) {
       // There was a match, just return the cached value.
-      aBaseDomain = entry->mBaseDomain;
+      aBaseDomain = p.Data().mBaseDomain;
       if (trailingDot) {
         aBaseDomain.Append('.');
       }
 
       return NS_OK;
     }
+
+    entry = Some(p);
   }
 
   // Walk up the domain tree, most specific to least specific,
   // looking for matches at each level.  Note that a given level may
   // have multiple attributes (e.g. IsWild() and IsNormal()).
   const char *prevDomain = nullptr;
   const char *currDomain = aHostname.get();
   const char *nextDot = strchr(currDomain, '.');
@@ -305,18 +309,17 @@ nsEffectiveTLDService::GetBaseDomainInte
 
   if (aAdditionalParts != 0)
     return NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS;
 
   aBaseDomain = Substring(iter, end);
 
   // Update the MRU table if in use.
   if (entry) {
-    entry->mHost = aHostname;
-    entry->mBaseDomain = aBaseDomain;
+    entry->Set(TLDCacheEntry{aHostname, nsCString(aBaseDomain)});
   }
 
   // add on the trailing dot, if applicable
   if (trailingDot)
     aBaseDomain.Append('.');
 
   return NS_OK;
 }
@@ -332,26 +335,16 @@ nsEffectiveTLDService::NormalizeHostname
     if (NS_FAILED(rv))
       return rv;
   }
 
   ToLowerCase(aHostname);
   return NS_OK;
 }
 
-bool
-nsEffectiveTLDService::LookupForAdd(const nsACString& aHost, TLDCacheEntry** aEntry)
-{
-  MOZ_ASSERT(NS_IsMainThread());
-
-  const uint32_t hash = HashString(aHost.BeginReading(), aHost.Length());
-  *aEntry = &mMruTable[hash % kTableSize];
-  return (*aEntry)->mHost == aHost;
-}
-
 NS_IMETHODIMP
 nsEffectiveTLDService::HasRootDomain(const nsACString& aInput,
                                      const nsACString& aHost,
                                      bool* aResult)
 {
   if (NS_WARN_IF(!aResult)) {
     return NS_ERROR_FAILURE;
   }
--- a/netwerk/dns/nsEffectiveTLDService.h
+++ b/netwerk/dns/nsEffectiveTLDService.h
@@ -3,22 +3,24 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef EffectiveTLDService_h
 #define EffectiveTLDService_h
 
 #include "nsIEffectiveTLDService.h"
 
+#include "nsHashKeys.h"
 #include "nsIMemoryReporter.h"
 #include "nsString.h"
 #include "nsCOMPtr.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Dafsa.h"
 #include "mozilla/MemoryReporting.h"
+#include "mozilla/MruCache.h"
 
 class nsIIDNService;
 
 class nsEffectiveTLDService final
   : public nsIEffectiveTLDService
   , public nsIMemoryReporter
 {
 public:
@@ -50,27 +52,25 @@ private:
   // We use a small most recently used cache to compensate for DAFSA lookups
   // being slightly slower than a binary search on a larger table of strings.
   //
   // We first check the cache for a matching result and avoid a DAFSA lookup
   // if a match is found. Otherwise we lookup the domain in the DAFSA and then
   // cache the result. During standard browsing the same domains are repeatedly
   // fed into |GetBaseDomainInternal| so this ends up being an effective
   // mitigation getting about a 99% hit rate with four tabs open.
-  //
-  // A size of 31 is used rather than a more logical power-of-two such as 32
-  // since it is a prime number and provides fewer collisions when when used
-  // with our hash algorithms.
-  static const uint32_t kTableSize = 31;
-  TLDCacheEntry mMruTable[kTableSize];
+  struct TldCache :
+    public mozilla::MruCache<nsACString, TLDCacheEntry, TldCache>
+  {
+    static mozilla::HashNumber Hash(const nsACString& aKey)
+    {
+      return mozilla::HashString(aKey);
+    }
+    static bool Match(const nsACString& aKey, const TLDCacheEntry& aVal)
+    {
+      return aKey == aVal.mHost;
+    }
+  };
 
-  /**
-   * Performs a lookup on the MRU table and provides a pointer to the hash
-   * entry that matched or should be used for adding this host.
-   *
-   * @param aHost The host to lookup.
-   * @param aEntry Out param, the entry in the MRU table to use.
-   * @return True if a match was found, false if there was a miss.
-   */
-  inline bool LookupForAdd(const nsACString& aHost, TLDCacheEntry** aEntry);
+  TldCache mMruTable;
 };
 
 #endif // EffectiveTLDService_h
--- a/security/certverifier/moz.build
+++ b/security/certverifier/moz.build
@@ -55,23 +55,21 @@ DIRS += [
 ]
 
 TEST_DIRS += [
     'tests/gtest',
 ]
 
 if CONFIG['CC_TYPE'] == 'clang-cl':
     # -Wall on clang-cl maps to -Weverything, which turns on way too
-    # much, so we're using -W4 instead, which is mapped to clang's
-    # -Wall -Wextra.
-    CXXFLAGS += ['-W4']
-else:
-    CXXFLAGS += ['-Wall']
+    # much, so we're passing through -Wall using -Xclang.
+    CXXFLAGS += ['-Xclang']
+CXXFLAGS += ['-Wall']
 
-if CONFIG['CC_TYPE'] in ('msvc', 'clang-cl'):
+if CONFIG['CC_TYPE'] == 'msvc':
     # -Wall with Visual C++ enables too many problematic warnings
     CXXFLAGS += [
         '-wd4324', # structure was padded due to __declspec(align())
         '-wd4355', # 'this' used in base member initializer list
         '-wd4464', # relative include path contains '..'
         '-wd4480', # nonstandard extension used: specifying underlying type for
                    # enum 'enum'
         '-wd4481', # nonstandard extension used: override specifier 'keyword'
@@ -93,16 +91,23 @@ if CONFIG['CC_TYPE'] in ('msvc', 'clang-
                    # interpreted as a digraph anyway, we can disable the
                    # warning.)
         '-wd4640', # construction of local static object is not thread-safe
         '-wd4710', # 'function': function not inlined
         '-wd4711', # function 'function' selected for inline expansion
         '-wd4820', # 'bytes' bytes padding added after construct 'member_name'
     ]
 
+    # Disable Spectre diagnostics only if optimization is disabled.
+    if not CONFIG['MOZ_OPTIMIZE']:
+        CXXFLAGS += [
+            '-wd5045', # Compiler will insert Spectre mitigation for memory
+                       # load if /Qspectre switch specified
+        ]
+
     # MSVC 2010's headers trigger these
     CXXFLAGS += [
         '-wd4548', # expression before comma has no effect; ...
         '-wd4668', # 'symbol' is not defined as a preprocessor macro...
         '-wd4987', # nonstandard extension used
     ]
 
     # MSVC 2015 triggers these
@@ -124,17 +129,17 @@ if CONFIG['CC_TYPE'] in ('msvc', 'clang-
 
     # Gecko headers aren't warning-free enough for us to enable these warnings
     CXXFLAGS += [
         '-wd4100', # 'symbol' : unreferenced formal parameter
         '-wd4127', # conditional expression is constant
         '-wd4946', # reinterpret_cast used between related types
     ]
 
-if CONFIG['CC_TYPE'] in ('clang', 'gcc'):
+if CONFIG['CC_TYPE'] in ('clang', 'clang-cl', 'gcc'):
     CXXFLAGS += [
         '-Wextra',
         '-Wunreachable-code',
     ]
 
     # Gecko headers aren't warning-free enough for us to enable these warnings.
     CXXFLAGS += [
         '-Wno-unused-parameter',
--- a/security/pkix/test/gtest/moz.build
+++ b/security/pkix/test/gtest/moz.build
@@ -65,11 +65,17 @@ elif CONFIG['CC_TYPE'] == 'msvc':
     '-wd4625', # copy constructor could not be generated.
     '-wd4626', # assugment operator could not be generated.
     '-wd4640', # construction of local static object is not thread safe.
 
     # This is intended as a temporary hack to support building with VS2015.
     # declaration of '*' hides class member
     '-wd4458',
   ]
+  # Disable Spectre diagnostics only if optimization is disabled.
+  if not CONFIG['MOZ_OPTIMIZE']:
+    CXXFLAGS += [
+      '-wd5045', # Compiler will insert Spectre mitigation for memory load if
+                 # /Qspectre switch specified
+    ]
 
 if CONFIG['CC_TYPE'] == 'clang-cl':
     AllowCompilerWarnings()  # workaround for bug 1090497
--- a/testing/marionette/evaluate.js
+++ b/testing/marionette/evaluate.js
@@ -448,17 +448,18 @@ sandbox.create = function(window, princi
  * @return {Sandbox}
  *     The created sandbox.
  */
 sandbox.createMutable = function(window) {
   let opts = {
     wantComponents: false,
     wantXrays: false,
   };
-  return sandbox.create(window, null, opts);
+  // Note: We waive Xrays here to match potentially-accidental old behavior.
+  return Cu.waiveXrays(sandbox.create(window, null, opts));
 };
 
 sandbox.createSystemPrincipal = function(window) {
   let principal = Cc["@mozilla.org/systemprincipal;1"]
       .createInstance(Ci.nsIPrincipal);
   return sandbox.create(window, principal);
 };
 
--- a/testing/mozbase/mozinfo/mozinfo/mozinfo.py
+++ b/testing/mozbase/mozinfo/mozinfo/mozinfo.py
@@ -61,17 +61,19 @@ def get_windows_version():
 
 # get system information
 info = {'os': unknown,
         'processor': unknown,
         'version': unknown,
         'os_version': unknown,
         'bits': unknown,
         'has_sandbox': unknown,
-        'webrender': bool(os.environ.get("MOZ_WEBRENDER", False))}
+        'webrender': bool(os.environ.get("MOZ_WEBRENDER", False)),
+        'automation': bool(os.environ.get("MOZ_AUTOMATION", False)),
+        }
 (system, node, release, version, machine, processor) = platform.uname()
 (bits, linkage) = platform.architecture()
 
 # get os information and related data
 if system in ["Microsoft", "Windows"]:
     info['os'] = 'win'
     # There is a Python bug on Windows to determine platform values
     # http://bugs.python.org/issue7860
--- a/testing/mozbase/mozrunner/tests/manifest.ini
+++ b/testing/mozbase/mozrunner/tests/manifest.ini
@@ -1,10 +1,12 @@
 [DEFAULT]
 subsuite = mozbase, os == "linux"
-skip-if = python == 3
+# We skip these tests in automated Windows builds because they trigger crashes
+# in sh.exe; see bug 1489277.
+skip-if = python == 3 || (automation && os == "win")
 [test_crash.py]
 [test_interactive.py]
 [test_start.py]
 [test_states.py]
 [test_stop.py]
 [test_threads.py]
 [test_wait.py]
--- a/testing/web-platform/meta/content-security-policy/inside-worker/dedicated-script.html.ini
+++ b/testing/web-platform/meta/content-security-policy/inside-worker/dedicated-script.html.ini
@@ -1,35 +1,18 @@
 [dedicated-script.html]
   expected: ERROR
-  [`eval()` blocked in blob:]
-    expected: FAIL
-
-  [`setTimeout([string\])` blocked in blob:]
-    expected: FAIL
 
   [Cross-origin `importScripts()` blocked in http:]
     expected: FAIL
 
   [Cross-origin `importScripts()` blocked in http:?pipe=sub|header(Content-Security-Policy,script-src%20*)]
     expected: FAIL
 
   [Cross-origin `importScripts()` blocked in http:?pipe=sub|header(Content-Security-Policy,default-src%20*)]
     expected: FAIL
 
   [`eval()` blocked in http:]
     expected: FAIL
 
   [`setTimeout([string\])` blocked in http:]
     expected: FAIL
 
-  [`eval()` blocked in http:?pipe=sub|header(Content-Security-Policy,default-src%20*)]
-    expected: FAIL
-
-  [`eval()` blocked in http:?pipe=sub|header(Content-Security-Policy,script-src%20*)]
-    expected: FAIL
-
-  [`setTimeout([string\])` blocked in http:?pipe=sub|header(Content-Security-Policy,default-src%20*)]
-    expected: FAIL
-
-  [`setTimeout([string\])` blocked in http:?pipe=sub|header(Content-Security-Policy,script-src%20*)]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/content-security-policy/inside-worker/shared-script.html.ini
+++ /dev/null
@@ -1,13 +0,0 @@
-[shared-script.html]
-  [`eval()` blocked in http:?pipe=sub|header(Content-Security-Policy,script-src%20%27self%27]
-    expected: FAIL
-
-  [`setTimeout([string\])` blocked in http:?pipe=sub|header(Content-Security-Policy,script-src%20%27self%27]
-    expected: FAIL
-
-  [`eval()` blocked in http:?pipe=sub|header(Content-Security-Policy,default-src%20%27self%27]
-    expected: FAIL
-
-  [`setTimeout([string\])` blocked in http:?pipe=sub|header(Content-Security-Policy,default-src%20%27self%27]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/content-security-policy/script-src/script-src-1_4_2.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[script-src-1_4_2.html]
-  [Unsafe eval ran in Function() constructor.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/content-security-policy/script-src/worker-set-timeout-blocked.sub.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[worker-set-timeout-blocked.sub.html]
-  [Expecting alerts: ["setTimeout blocked"\]]
-    expected: FAIL
-
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/css/vendor-imports/mozilla/mozilla-central-reftests/values3/calc-text-indent-intrinsic-1.html.ini
@@ -0,0 +1,2 @@
+[calc-text-indent-intrinsic-1.html]
+  expected: FAIL
--- a/toolkit/actors/ControllersChild.jsm
+++ b/toolkit/actors/ControllersChild.jsm
@@ -14,18 +14,17 @@ class ControllersChild extends ActorChil
       case "ControllerCommands:Do":
         if (this.docShell.isCommandEnabled(message.data))
           this.docShell.doCommand(message.data);
         break;
 
       case "ControllerCommands:DoWithParams":
         var data = message.data;
         if (this.docShell.isCommandEnabled(data.cmd)) {
-          var params = Cc["@mozilla.org/embedcomp/command-params;1"].
-                       createInstance(Ci.nsICommandParams);
+          var params = Cu.createCommandParams();
           for (var name in data.params) {
             var value = data.params[name];
             if (value.type == "long") {
               params.setLongValue(name, parseInt(value.value));
             } else {
               throw Cr.NS_ERROR_NOT_IMPLEMENTED;
             }
           }
--- a/toolkit/components/antitracking/AntiTrackingCommon.cpp
+++ b/toolkit/components/antitracking/AntiTrackingCommon.cpp
@@ -25,16 +25,17 @@
 #include "nsIPrincipal.h"
 #include "nsIScriptError.h"
 #include "nsIURI.h"
 #include "nsIURL.h"
 #include "nsIWebProgressListener.h"
 #include "nsNetUtil.h"
 #include "nsPIDOMWindow.h"
 #include "nsScriptSecurityManager.h"
+#include "nsSandboxFlags.h"
 #include "prtime.h"
 
 #define ANTITRACKING_PERM_KEY "3rdPartyStorage"
 
 using namespace mozilla;
 using mozilla::dom::ContentChild;
 
 static LazyLogModule gAntiTrackingLog("AntiTracking");
@@ -59,16 +60,23 @@ namespace {
 
 bool
 GetParentPrincipalAndTrackingOrigin(nsGlobalWindowInner* a3rdPartyTrackingWindow,
                                     nsIPrincipal** aTopLevelStoragePrincipal,
                                     nsACString& aTrackingOrigin)
 {
   MOZ_ASSERT(nsContentUtils::IsTrackingResourceWindow(a3rdPartyTrackingWindow));
 
+  nsIDocument* doc = a3rdPartyTrackingWindow->GetDocument();
+  // Make sure storage access isn't disabled
+  if (doc && ((doc->GetSandboxFlags() & SANDBOXED_STORAGE_ACCESS) != 0 ||
+              nsContentUtils::IsInPrivateBrowsing(doc))) {
+    return false;
+  }
+
   // Now we need the principal and the origin of the parent window.
   nsCOMPtr<nsIPrincipal> topLevelStoragePrincipal =
     a3rdPartyTrackingWindow->GetTopLevelStorageAreaPrincipal();
   if (NS_WARN_IF(!topLevelStoragePrincipal)) {
     return false;
   }
 
   // Let's take the principal and the origin of the tracker.
--- a/toolkit/components/antitracking/test/browser/browser.ini
+++ b/toolkit/components/antitracking/test/browser/browser.ini
@@ -33,8 +33,11 @@ support-files = server.sjs
 [browser_onBeforeRequestNotificationForTrackingResources.js]
 [browser_onModifyRequestNotificationForTrackingResources.js]
 [browser_permissionInNormalWindows.js]
 [browser_permissionInPrivateWindows.js]
 [browser_subResources.js]
 support-files = subResources.sjs
 [browser_script.js]
 support-files = tracker.js
+[browser_storageAccessPrivateWindow.js]
+[browser_storageAccessSandboxed.js]
+[browser_storageAccessWithHeuristics.js]
--- a/toolkit/components/antitracking/test/browser/browser_blockingCookies.js
+++ b/toolkit/components/antitracking/test/browser/browser_blockingCookies.js
@@ -38,8 +38,96 @@ AntiTracking.runTest("Set/Get Cookies",
   },
 
   // Cleanup callback
   async _ => {
     await new Promise(resolve => {
       Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
     });
   });
+
+AntiTracking.runTest("Cookies and Storage Access API",
+  // Blocking callback
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+    is(document.cookie, "", "No cookies for me");
+    document.cookie = "name=value";
+    is(document.cookie, "", "No cookies for me");
+
+    await fetch("server.sjs").then(r => r.text()).then(text => {
+      is(text, "cookie-not-present", "We should not have cookies");
+    });
+    // Let's do it twice.
+    await fetch("server.sjs").then(r => r.text()).then(text => {
+      is(text, "cookie-not-present", "We should not have cookies");
+    });
+
+    is(document.cookie, "", "Still no cookies for me");
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+    is(document.cookie, "", "No cookies for me");
+    document.cookie = "name=value";
+    is(document.cookie, "name=value", "I have the cookies!");
+  },
+
+  // Non blocking callback
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    is(document.cookie, "", "No cookies for me");
+
+    await fetch("server.sjs").then(r => r.text()).then(text => {
+      is(text, "cookie-not-present", "We should not have cookies");
+    });
+
+    document.cookie = "name=value";
+    ok(document.cookie.includes("name=value"), "Some cookies for me");
+    ok(document.cookie.includes("foopy=1"), "Some cookies for me");
+
+    await fetch("server.sjs").then(r => r.text()).then(text => {
+      is(text, "cookie-present", "We should have cookies");
+    });
+
+    ok(document.cookie.length, "Some Cookies for me");
+
+    hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+    // For non-tracking windows, calling the API is a no-op
+    ok(document.cookie.length, "Still some Cookies for me");
+    ok(document.cookie.includes("name=value"), "Some cookies for me");
+    ok(document.cookie.includes("foopy=1"), "Some cookies for me");
+  },
+
+  // Cleanup callback
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  null, false, false);
--- a/toolkit/components/antitracking/test/browser/browser_blockingIndexedDb.js
+++ b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDb.js
@@ -36,40 +36,245 @@ AntiTracking.runTest("IndexedDB in worke
     ok(blob, "Blob has been created");
 
     let blobURL = URL.createObjectURL(blob);
     ok(blobURL, "Blob URL has been created");
 
     let worker = new Worker(blobURL);
     ok(worker, "Worker has been created");
 
-    await new Promise(resolve => {
+    await new Promise((resolve, reject) => {
       worker.onmessage = function(e) {
-        resolve();
+        if (e) {
+          resolve();
+        } else {
+          reject();
+        }
       };
     });
   },
   async _ => {
     function nonBlockCode() {
       indexedDB.open("test", "1");
-      postMessage(false);
+      postMessage(true);
     }
 
     let blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]);
     ok(blob, "Blob has been created");
 
     let blobURL = URL.createObjectURL(blob);
     ok(blobURL, "Blob URL has been created");
 
     let worker = new Worker(blobURL);
     ok(worker, "Worker has been created");
 
-    await new Promise(resolve => {
+    await new Promise((resolve, reject) => {
       worker.onmessage = function(e) {
-        resolve();
+        if (e) {
+          resolve();
+        } else {
+          reject();
+        }
       };
     });
   },
   async _ => {
     await new Promise(resolve => {
       Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
     });
   });
+
+AntiTracking.runTest("IndexedDB and Storage Access API",
+  // blocking callback
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+    try {
+      indexedDB.open("test", "1");
+      ok(false, "IDB should be blocked");
+    } catch (e) {
+      ok(true, "IDB should be blocked");
+      is(e.name, "SecurityError", "We want a security error message.");
+    }
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    indexedDB.open("test", "1");
+    ok(true, "IDB should be allowed");
+  },
+  // non-blocking callback
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+    indexedDB.open("test", "1");
+    ok(true, "IDB should be allowed");
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    // For non-tracking windows, calling the API is a no-op
+    indexedDB.open("test", "1");
+    ok(true, "IDB should be allowed");
+  },
+  // Cleanup callback
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  null, false, false);
+
+AntiTracking.runTest("IndexedDB in workers and Storage Access API",
+  async _ => {
+    function blockCode() {
+      try {
+        indexedDB.open("test", "1");
+        postMessage(false);
+      } catch (e) {
+        postMessage(e.name == "SecurityError");
+      }
+    }
+    function nonBlockCode() {
+      indexedDB.open("test", "1");
+      postMessage(true);
+    }
+
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    let blob = new Blob([blockCode.toString() + "; blockCode();"]);
+    ok(blob, "Blob has been created");
+
+    let blobURL = URL.createObjectURL(blob);
+    ok(blobURL, "Blob URL has been created");
+
+    let worker = new Worker(blobURL);
+    ok(worker, "Worker has been created");
+
+    await new Promise((resolve, reject) => {
+      worker.onmessage = function(e) {
+        if (e) {
+          resolve();
+        } else {
+          reject();
+        }
+      };
+    });
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]);
+    ok(blob, "Blob has been created");
+
+    blobURL = URL.createObjectURL(blob);
+    ok(blobURL, "Blob URL has been created");
+
+    worker = new Worker(blobURL);
+    ok(worker, "Worker has been created");
+
+    await new Promise((resolve, reject) => {
+      worker.onmessage = function(e) {
+        if (e) {
+          resolve();
+        } else {
+          reject();
+        }
+      };
+    });
+  },
+  async _ => {
+    function nonBlockCode() {
+      indexedDB.open("test", "1");
+      postMessage(true);
+    }
+
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    let blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]);
+    ok(blob, "Blob has been created");
+
+    let blobURL = URL.createObjectURL(blob);
+    ok(blobURL, "Blob URL has been created");
+
+    let worker = new Worker(blobURL);
+    ok(worker, "Worker has been created");
+
+    await new Promise((resolve, reject) => {
+      worker.onmessage = function(e) {
+        if (e) {
+          resolve();
+        } else {
+          reject();
+        }
+      };
+    });
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    // For non-tracking windows, calling the API is a no-op
+
+    worker = new Worker(blobURL);
+    ok(worker, "Worker has been created");
+
+    await new Promise((resolve, reject) => {
+      worker.onmessage = function(e) {
+        if (e) {
+          resolve();
+        } else {
+          reject();
+        }
+      };
+    });
+  },
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  null, false, false);
--- a/toolkit/components/antitracking/test/browser/browser_blockingMessaging.js
+++ b/toolkit/components/antitracking/test/browser/browser_blockingMessaging.js
@@ -33,19 +33,23 @@ AntiTracking.runTest("BroadcastChannel i
     ok(blob, "Blob has been created");
 
     let blobURL = URL.createObjectURL(blob);
     ok(blobURL, "Blob URL has been created");
 
     let worker = new Worker(blobURL);
     ok(worker, "Worker has been created");
 
-    await new Promise(resolve => {
+    await new Promise((resolve, reject) => {
       worker.onmessage = function(e) {
-        resolve();
+        if (e) {
+          resolve();
+        } else {
+          reject();
+        }
       };
     });
   },
   async _ => {
     function nonBlockingCode() {
       new BroadcastChannel("hello");
       postMessage(true);
     }
@@ -54,19 +58,214 @@ AntiTracking.runTest("BroadcastChannel i
     ok(blob, "Blob has been created");
 
     let blobURL = URL.createObjectURL(blob);
     ok(blobURL, "Blob URL has been created");
 
     let worker = new Worker(blobURL);
     ok(worker, "Worker has been created");
 
-    await new Promise(resolve => {
+    await new Promise((resolve, reject) => {
       worker.onmessage = function(e) {
-        resolve();
+        if (e) {
+          resolve();
+        } else {
+          reject();
+        }
       };
     });
   },
   async _ => {
     await new Promise(resolve => {
       Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
     });
   });
+
+AntiTracking.runTest("BroadcastChannel and Storage Access API",
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+    try {
+      new BroadcastChannel("hello");
+      ok(false, "BroadcastChannel cannot be used!");
+    } catch (e) {
+      ok(true, "BroadcastChannel cannot be used!");
+      is(e.name, "SecurityError", "We want a security error message.");
+    }
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+    new BroadcastChannel("hello");
+    ok(true, "BroadcastChannel can be used");
+  },
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+    new BroadcastChannel("hello");
+    ok(true, "BroadcastChanneli can be used");
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+    new BroadcastChannel("hello");
+    ok(true, "BroadcastChannel can be used");
+  },
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  null, false, false);
+
+AntiTracking.runTest("BroadcastChannel in workers and Storage Access API",
+  async _ => {
+    function blockingCode() {
+      try {
+        new BroadcastChannel("hello");
+        postMessage(false);
+      } catch (e) {
+        postMessage(e.name == "SecurityError");
+      }
+    }
+    function nonBlockingCode() {
+      new BroadcastChannel("hello");
+      postMessage(true);
+    }
+
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    let blob = new Blob([blockingCode.toString() + "; blockingCode();"]);
+    ok(blob, "Blob has been created");
+
+    let blobURL = URL.createObjectURL(blob);
+    ok(blobURL, "Blob URL has been created");
+
+    let worker = new Worker(blobURL);
+    ok(worker, "Worker has been created");
+
+    await new Promise((resolve, reject) => {
+      worker.onmessage = function(e) {
+        if (e) {
+          resolve();
+        } else {
+          reject();
+        }
+      };
+    });
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    blob = new Blob([nonBlockingCode.toString() + "; nonBlockingCode();"]);
+    ok(blob, "Blob has been created");
+
+    blobURL = URL.createObjectURL(blob);
+    ok(blobURL, "Blob URL has been created");
+
+    worker = new Worker(blobURL);
+    ok(worker, "Worker has been created");
+
+    await new Promise((resolve, reject) => {
+      worker.onmessage = function(e) {
+        if (e) {
+          resolve();
+        } else {
+          reject();
+        }
+      };
+    });
+  },
+  async _ => {
+    function nonBlockingCode() {
+      new BroadcastChannel("hello");
+      postMessage(true);
+    }
+
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    let blob = new Blob([nonBlockingCode.toString() + "; nonBlockingCode();"]);
+    ok(blob, "Blob has been created");
+
+    let blobURL = URL.createObjectURL(blob);
+    ok(blobURL, "Blob URL has been created");
+
+    let worker = new Worker(blobURL);
+    ok(worker, "Worker has been created");
+
+    await new Promise((resolve, reject) => {
+      worker.onmessage = function(e) {
+        if (e) {
+          resolve();
+        } else {
+          reject();
+        }
+      };
+    });
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    // For non-tracking windows, calling the API is a no-op
+
+    worker = new Worker(blobURL);
+    ok(worker, "Worker has been created");
+
+    await new Promise((resolve, reject) => {
+      worker.onmessage = function(e) {
+        if (e) {
+          resolve();
+        } else {
+          reject();
+        }
+      };
+    });
+  },
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  null, false, false);
--- a/toolkit/components/antitracking/test/browser/browser_blockingStorage.js
+++ b/toolkit/components/antitracking/test/browser/browser_blockingStorage.js
@@ -31,8 +31,127 @@ AntiTracking.runTest("sessionStorage",
     await new Promise(resolve => {
       Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
     });
   },
   [],
   true,
   true,
   false);
+
+AntiTracking.runTest("localStorage and Storage Access API",
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    try {
+      localStorage.foo = 42;
+      ok(false, "LocalStorage cannot be used!");
+    } catch (e) {
+      ok(true, "LocalStorage cannot be used!");
+      is(e.name, "SecurityError", "We want a security error message.");
+    }
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    localStorage.foo = 42;
+    ok(true, "LocalStorage is allowed");
+  },
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    localStorage.foo = 42;
+    ok(true, "LocalStorage is allowed");
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    // For non-tracking windows, calling the API is a no-op
+    localStorage.foo = 42;
+    ok(true, "LocalStorage is allowed");
+  },
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  null, false, false);
+
+AntiTracking.runTest("sessionStorage and Storage Access API",
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    sessionStorage.foo = 42;
+    ok(true, "SessionStorage is always allowed");
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    sessionStorage.foo = 42;
+    ok(true, "SessionStorage is allowed after calling the storage access API too");
+  },
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    sessionStorage.foo = 42;
+    ok(true, "SessionStorage is always allowed");
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    // For non-tracking windows, calling the API is a no-op
+    sessionStorage.foo = 42;
+    ok(true, "SessionStorage is allowed after calling the storage access API too");
+  },
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  null, false, false);
--- a/toolkit/components/antitracking/test/browser/browser_blockingWorkers.js
+++ b/toolkit/components/antitracking/test/browser/browser_blockingWorkers.js
@@ -1,8 +1,10 @@
+requestLongerTimeout(4);
+
 AntiTracking.runTest("SharedWorkers",
   async _ => {
     try {
       new SharedWorker("a.js", "foo");
       ok(false, "SharedWorker cannot be used!");
     } catch (e) {
       ok(true, "SharedWorker cannot be used!");
       is(e.name, "SecurityError", "We want a security error message.");
@@ -15,17 +17,17 @@ AntiTracking.runTest("SharedWorkers",
   async _ => {
     await new Promise(resolve => {
       Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
     });
   });
 
 AntiTracking.runTest("ServiceWorkers",
   async _ => {
-    await navigator.serviceWorker.register("empty.js", { scope: "/" }).then(
+    await navigator.serviceWorker.register("empty.js").then(
       _ => { ok(false, "ServiceWorker cannot be used!"); },
       _ => { ok(true, "ServiceWorker cannot be used!"); });
   },
   null,
   async _ => {
     await new Promise(resolve => {
       Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
     });
@@ -45,8 +47,213 @@ AntiTracking.runTest("DOM Cache",
       _ => { ok(true, "DOM Cache can be used!"); },
       _ => { ok(false, "DOM Cache can be used!"); });
   },
   async _ => {
     await new Promise(resolve => {
       Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
     });
   });
+
+AntiTracking.runTest("SharedWorkers and Storage Access API",
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    try {
+      new SharedWorker("a.js", "foo");
+      ok(false, "SharedWorker cannot be used!");
+    } catch (e) {
+      ok(true, "SharedWorker cannot be used!");
+      is(e.name, "SecurityError", "We want a security error message.");
+    }
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    new SharedWorker("a.js", "foo");
+    ok(true, "SharedWorker is allowed");
+  },
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    new SharedWorker("a.js", "foo");
+    ok(true, "SharedWorker is allowed");
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    // For non-tracking windows, calling the API is a no-op
+    new SharedWorker("a.js", "foo");
+    ok(true, "SharedWorker is allowed");
+  },
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  null, false, false);
+
+AntiTracking.runTest("ServiceWorkers and Storage Access API",
+  async _ => {
+    await SpecialPowers.pushPrefEnv({"set": [
+       ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+       ["dom.serviceWorkers.enabled", true],
+       ["dom.serviceWorkers.testing.enabled", true],
+    ]});
+
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    await navigator.serviceWorker.register("empty.js").then(
+      _ => { ok(false, "ServiceWorker cannot be used!"); },
+      _ => { ok(true, "ServiceWorker cannot be used!"); });
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    await navigator.serviceWorker.register("empty.js").then(
+      reg => { ok(true, "ServiceWorker can be used!"); return reg; },
+      _ => { ok(false, "ServiceWorker cannot be used! " + _); }).then(
+      reg => reg.unregister(),
+      _ => { ok(false, "unregister failed"); });
+  },
+  async _ => {
+    await SpecialPowers.pushPrefEnv({"set": [
+       ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+       ["dom.serviceWorkers.enabled", true],
+       ["dom.serviceWorkers.testing.enabled", true],
+    ]});
+
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    await navigator.serviceWorker.register("empty.js").then(
+      reg => { ok(true, "ServiceWorker can be used!"); return reg; },
+      _ => { ok(false, "ServiceWorker cannot be used!"); }).then(
+      reg => reg.unregister(),
+      _ => { ok(false, "unregister failed"); });
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    // For non-tracking windows, calling the API is a no-op
+    await navigator.serviceWorker.register("empty.js").then(
+      reg => { ok(true, "ServiceWorker can be used!"); return reg; },
+      _ => { ok(false, "ServiceWorker cannot be used!"); }).then(
+      reg => reg.unregister(),
+      _ => { ok(false, "unregister failed"); });
+  },
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  [["dom.serviceWorkers.exemptFromPerDomainMax", true],
+   ["dom.serviceWorkers.enabled", true],
+   ["dom.serviceWorkers.testing.enabled", true]],
+  false, false);
+
+AntiTracking.runTest("DOM Cache and Storage Access API",
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    await caches.open("wow").then(
+      _ => { ok(false, "DOM Cache cannot be used!"); },
+      _ => { ok(true, "DOM Cache cannot be used!"); });
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    await caches.open("wow").then(
+      _ => { ok(true, "DOM Cache can be used!"); },
+      _ => { ok(false, "DOM Cache can be used!"); });
+  },
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    await caches.open("wow").then(
+      _ => { ok(true, "DOM Cache can be used!"); },
+      _ => { ok(false, "DOM Cache can be used!"); });
+
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    try {
+      p = document.requestStorageAccess();
+    } finally {
+      helper.destruct();
+    }
+    await p;
+
+    hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Now has storage access");
+
+    // For non-tracking windows, calling the API is a no-op
+    await caches.open("wow").then(
+      _ => { ok(true, "DOM Cache can be used!"); },
+      _ => { ok(false, "DOM Cache can be used!"); });
+  },
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  null, false, false);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessPrivateWindow.js
@@ -0,0 +1,36 @@
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+AntiTracking.runTest("Storage Access API called in a private window",
+  // blocking callback
+  async _ => {
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    let threw = false;
+    try {
+      p = document.requestStorageAccess();
+    } catch (e) {
+      threw = true;
+    } finally {
+      helper.destruct();
+    }
+    ok(!threw, "requestStorageAccess should not throw");
+    threw = false;
+    try {
+      await p;
+    } catch (e) {
+      threw = true;
+    }
+    ok(threw, "requestStorageAccess shouldn't be available");
+  },
+
+  null, // non-blocking callback
+  null, // cleanup function
+  [["dom.storage_access.enabled", true]], // extra prefs
+  false, // no window open test
+  false, // no user-interaction test
+  false, // no blocking notifications
+  true, // run in private window
+  null // iframe sandbox
+);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed.js
@@ -0,0 +1,168 @@
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+AntiTracking.runTest("Storage Access API called in a sandboxed iframe",
+  // blocking callback
+  async _ => {
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    let threw = false;
+    try {
+      p = document.requestStorageAccess();
+    } catch (e) {
+      threw = true;
+    } finally {
+      helper.destruct();
+    }
+    ok(!threw, "requestStorageAccess should not throw");
+    threw = false;
+    try {
+      await p;
+    } catch (e) {
+      threw = true;
+    }
+    ok(threw, "requestStorageAccess shouldn't be available");
+  },
+
+  null, // non-blocking callback
+  null, // cleanup function
+  [["dom.storage_access.enabled", true]], // extra prefs
+  false, // no window open test
+  false, // no user-interaction test
+  false, // no blocking notifications
+  false, // run in normal window
+  "allow-scripts allow-same-origin"
+);
+
+AntiTracking.runTest("Storage Access API called in a sandboxed iframe with" +
+                     " allow-storage-access-by-user-activation",
+  // blocking callback
+  async _ => {
+    let dwu = SpecialPowers.getDOMWindowUtils(window);
+    let helper = dwu.setHandlingUserInput(true);
+
+    let p;
+    let threw = false;
+    try {
+      p = document.requestStorageAccess();
+    } catch (e) {
+      threw = true;
+    } finally {
+      helper.destruct();
+    }
+    ok(!threw, "requestStorageAccess should not throw");
+    threw = false;
+    try {
+      await p;
+    } catch (e) {
+      threw = true;
+    }
+    ok(!threw, "requestStorageAccess should be available");
+  },
+
+  null, // non-blocking callback
+  null, // cleanup function
+  [["dom.storage_access.enabled", true]], // extra prefs
+  false, // no window open test
+  false, // no user-interaction test
+  true, // expect blocking notifications
+  false, // run in normal window
+  "allow-scripts allow-same-origin allow-storage-access-by-user-activation"
+);
+
+AntiTracking.runTest("Verify that sandboxed contexts don't get the saved permission",
+  // blocking callback
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    try {
+      localStorage.foo = 42;
+      ok(false, "LocalStorage cannot be used!");
+    } catch (e) {
+      ok(true, "LocalStorage cannot be used!");
+      is(e.name, "SecurityError", "We want a security error message.");
+    }
+  },
+
+  null, // non-blocking callback
+  null, // cleanup function
+  [["dom.storage_access.enabled", true]], // extra prefs
+  false, // no window open test
+  false, // no user-interaction test
+  false, // no blocking notifications
+  false, // run in normal window
+  "allow-scripts allow-same-origin"
+);
+
+AntiTracking.runTest("Verify that sandboxed contexts with" +
+                     " allow-storage-access-by-user-activation get the" +
+                     " saved permission",
+  // blocking callback
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Has storage access");
+
+    localStorage.foo = 42;
+    ok(true, "LocalStorage can be used!");
+  },
+
+  null, // non-blocking callback
+  null, // cleanup function
+  [["dom.storage_access.enabled", true]], // extra prefs
+  false, // no window open test
+  false, // no user-interaction test
+  false, // no blocking notifications
+  false, // run in normal window
+  "allow-scripts allow-same-origin allow-storage-access-by-user-activation"
+);
+
+AntiTracking.runTest("Verify that private browsing contexts don't get the saved permission",
+  // blocking callback
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(!hasAccess, "Doesn't yet have storage access");
+
+    try {
+      localStorage.foo = 42;
+      ok(false, "LocalStorage cannot be used!");
+    } catch (e) {
+      ok(true, "LocalStorage cannot be used!");
+      is(e.name, "SecurityError", "We want a security error message.");
+    }
+  },
+
+  null, // non-blocking callback
+  null, // cleanup function
+  [["dom.storage_access.enabled", true]], // extra prefs
+  false, // no window open test
+  false, // no user-interaction test
+  false, // no blocking notifications
+  true, // run in private window
+  null // iframe sandbox
+);
+
+AntiTracking.runTest("Verify that non-sandboxed contexts get the" +
+                     " saved permission",
+  // blocking callback
+  async _ => {
+    let hasAccess = await document.hasStorageAccess();
+    ok(hasAccess, "Has storage access");
+
+    localStorage.foo = 42;
+    ok(true, "LocalStorage can be used!");
+  },
+
+  null, // non-blocking callback
+  // cleanup function
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  [["dom.storage_access.enabled", true]], // extra prefs
+  false, // no window open test
+  false, // no user-interaction test
+  false // no blocking notifications
+);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessWithHeuristics.js
@@ -0,0 +1,198 @@
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+add_task(async function() {
+  info("Starting subResources test");
+
+  await SpecialPowers.flushPrefEnv();
+  await SpecialPowers.pushPrefEnv({"set": [
+    ["dom.storage_access.enabled", true],
+    ["browser.contentblocking.enabled", true],
+    ["browser.contentblocking.ui.enabled", true],
+    ["browser.contentblocking.rejecttrackers.ui.enabled", true],
+    ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER],
+    ["privacy.trackingprotection.enabled", false],
+    ["privacy.trackingprotection.pbmode.enabled", false],
+    ["privacy.trackingprotection.annotate_channels", true],
+  ]});
+
+  await UrlClassifierTestUtils.addTestTrackers();
+});
+
+add_task(async function testWindowOpenHeuristic() {
+  info("Starting window.open() heuristic test...");
+
+  info("Creating a new tab");
+  let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+  gBrowser.selectedTab = tab;
+
+  let browser = gBrowser.getBrowserForTab(tab);
+  await BrowserTestUtils.browserLoaded(browser);
+
+  info("Loading tracking scripts");
+  await ContentTask.spawn(browser, {
+                                     page: TEST_3RD_PARTY_PAGE_WO,
+                                   }, async obj => {
+    let msg = {};
+    msg.blockingCallback = (async _ => {
+      let hasAccess = await document.hasStorageAccess();
+      ok(!hasAccess, "Doesn't yet have storage access");
+    }).toString();
+
+    msg.nonBlockingCallback = (async _ => {
+      let hasAccess = await document.hasStorageAccess();
+      ok(hasAccess, "Now obtained storage access");
+    }).toString();
+
+    info("Checking if storage access is denied");
+    await new content.Promise(resolve => {
+      let ifr = content.document.createElement("iframe");
+      ifr.onload = function() {
+        info("Sending code to the 3rd party content");
+        ifr.contentWindow.postMessage(msg, "*");
+      };
+
+      content.addEventListener("message", function msg(event) {
+        if (event.data.type == "finish") {
+          content.removeEventListener("message", msg);
+          resolve();
+          return;
+        }
+
+        if (event.data.type == "ok") {
+          ok(event.data.what, event.data.msg);
+          return;
+        }
+
+        if (event.data.type == "info") {
+          info(event.data.msg);
+          return;
+        }
+
+        ok(false, "Unknown message");
+      });
+
+      content.document.body.appendChild(ifr);
+      ifr.src = obj.page;
+    });
+  });
+
+  info("Removing the tab");
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function() {
+  info("Cleaning up.");
+  await new Promise(resolve => {
+    Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+  });
+});
+
+add_task(async function testUserInteractionHeuristic() {
+  info("Starting user interaction heuristic test...");
+
+  info("Creating a new tab");
+  let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+  gBrowser.selectedTab = tab;
+
+  let browser = gBrowser.getBrowserForTab(tab);
+  await BrowserTestUtils.browserLoaded(browser);
+
+  info("Loading tracking scripts");
+  await ContentTask.spawn(browser, {
+                                     page: TEST_3RD_PARTY_PAGE_UI,
+                                     popup: TEST_POPUP_PAGE,
+                                   }, async obj => {
+    let msg = {};
+    msg.blockingCallback = (async _ => {
+      let hasAccess = await document.hasStorageAccess();
+      ok(!hasAccess, "Doesn't yet have storage access");
+    }).toString();
+
+    msg.nonBlockingCallback = (async _ => {
+      let hasAccess = await document.hasStorageAccess();
+      ok(hasAccess, "Now obtained storage access");
+    }).toString();
+
+    info("Checking if storage access is denied");
+
+    let ifr = content.document.createElement("iframe");
+    let loading = new content.Promise(resolve => { ifr.onload = resolve; });
+    content.document.body.appendChild(ifr);
+    ifr.src = obj.page;
+    await loading;
+
+    info("The 3rd party content should not have access to first party storage.");
+    await new content.Promise(resolve => {
+      content.addEventListener("message", function msg(event) {
+        if (event.data.type == "finish") {
+          content.removeEventListener("message", msg);
+          resolve();
+          return;
+        }
+
+        if (event.data.type == "ok") {
+          ok(event.data.what, event.data.msg);
+          return;
+        }
+
+        if (event.data.type == "info") {
+          info(event.data.msg);
+          return;
+        }
+
+        ok(false, "Unknown message");
+      });
+      ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*");
+    });
+
+    let windowClosed = new content.Promise(resolve => {
+      Services.ww.registerNotification(function notification(aSubject, aTopic, aData) {
+        if (aTopic == "domwindowclosed") {
+          Services.ww.unregisterNotification(notification);
+          resolve();
+        }
+      });
+    });
+
+    info("Opening a window from the iframe.");
+    ifr.contentWindow.open(obj.popup);
+
+    info("Let's wait for the window to be closed");
+    await windowClosed;
+
+    info("The 3rd party content should have access to first party storage.");
+    await new content.Promise(resolve => {
+      content.addEventListener("message", function msg(event) {
+        if (event.data.type == "finish") {
+          content.removeEventListener("message", msg);
+          resolve();
+          return;
+        }
+
+        if (event.data.type == "ok") {
+          ok(event.data.what, event.data.msg);
+          return;
+        }
+
+        if (event.data.type == "info") {
+          info(event.data.msg);
+          return;
+        }
+
+        ok(false, "Unknown message");
+      });
+      ifr.contentWindow.postMessage({ callback: msg.nonBlockingCallback }, "*");
+    });
+  });
+
+  info("Removing the tab");
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function() {
+  info("Cleaning up.");
+  await new Promise(resolve => {
+    Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+  });
+});
+
--- a/toolkit/components/antitracking/test/browser/head.js
+++ b/toolkit/components/antitracking/test/browser/head.js
@@ -20,27 +20,28 @@ var gFeatures = undefined;
 
 let {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
 
 requestLongerTimeout(2);
 
 this.AntiTracking = {
   runTest(name, callbackTracking, callbackNonTracking, cleanupFunction, extraPrefs,
           windowOpenTest = true, userInteractionTest = true, expectedBlockingNotifications = true,
-          runInPrivateWindow = false) {
+          runInPrivateWindow = false, iframeSandbox = null) {
     // Here we want to test that a 3rd party context is simply blocked.
     this._createTask({
       name,
       cookieBehavior: BEHAVIOR_REJECT_TRACKER,
       blockingByContentBlocking: true,
       allowList: false,
       callback: callbackTracking,
       extraPrefs,
       expectedBlockingNotifications,
       runInPrivateWindow,
+      iframeSandbox,
     });
     this._createCleanupTask(cleanupFunction);
 
     if (callbackNonTracking) {
       let runExtraTests = true;
       let options = {};
       if (typeof callbackNonTracking == "object") {
         callbackNonTracking = callbackNonTracking.callback;
@@ -76,136 +77,148 @@ this.AntiTracking = {
           name,
           cookieBehavior: BEHAVIOR_ACCEPT,
           blockingByContentBlocking: true,
           allowList: false,
           callback: callbackNonTracking,
           extraPrefs: [],
           expectedBlockingNotifications: false,
           runInPrivateWindow,
+          iframeSandbox,
         });
         this._createCleanupTask(cleanupFunction);
 
         this._createTask({
           name,
           cookieBehavior: BEHAVIOR_REJECT_FOREIGN,
           blockingByContentBlocking: false,
           allowList: false,
           callback: callbackNonTracking,
           extraPrefs: [],
           expectedBlockingNotifications: false,
           runInPrivateWindow,
+          iframeSandbox,
         });
         this._createCleanupTask(cleanupFunction);
 
         this._createTask({
           name,
           cookieBehavior: BEHAVIOR_REJECT_TRACKER,
           blockingByContentBlocking: false,
           allowList: false,
           callback: callbackNonTracking,
           extraPrefs: [],
           expectedBlockingNotifications: false,
           runInPrivateWindow,
+          iframeSandbox,
         });
         this._createCleanupTask(cleanupFunction);
 
         this._createTask({
           name,
           cookieBehavior: BEHAVIOR_REJECT_FOREIGN,
           blockingByContentBlocking: false,
           allowList: true,
           callback: callbackNonTracking,
           extraPrefs: [],
           expectedBlockingNotifications: false,
           runInPrivateWindow,
+          iframeSandbox,
         });
         this._createCleanupTask(cleanupFunction);
 
         this._createTask({
           name,
           cookieBehavior: BEHAVIOR_REJECT_TRACKER,
           blockingByContentBlocking: false,
           allowList: true,
           callback: callbackNonTracking,
           extraPrefs: [],
           expectedBlockingNotifications: false,
           runInPrivateWindow,
+          iframeSandbox,
         });
         this._createCleanupTask(cleanupFunction);
 
         this._createTask({
           name,
           cookieBehavior: BEHAVIOR_ACCEPT,
           blockingByContentBlocking: false,
           allowList: false,
           callback: callbackNonTracking,
           extraPrefs: [],
           expectedBlockingNotifications: false,
           runInPrivateWindow,
+          iframeSandbox,
         });
         this._createCleanupTask(cleanupFunction);
 
         // Try testing using the allow list with both reject foreign and reject tracker cookie behaviors
         this._createTask({
           name,
           cookieBehavior: BEHAVIOR_REJECT_FOREIGN,
           blockingByContentBlocking: true,
           allowList: true,
           callback: callbackNonTracking,
           extraPrefs: [],
           expectedBlockingNotifications: false,
           runInPrivateWindow,
+          iframeSandbox,
         });
         this._createCleanupTask(cleanupFunction);
 
         this._createTask({
           name,
           cookieBehavior: BEHAVIOR_REJECT_TRACKER,
           blockingByContentBlocking: true,
           allowList: true,
           callback: callbackNonTracking,
           extraPrefs: [],
           expectedBlockingNotifications: false,
           runInPrivateWindow,
+          iframeSandbox,
         });
         this._createCleanupTask(cleanupFunction);
       } else {
         this._createTask({
           name,
           cookieBehavior: options.cookieBehavior,
           blockingByContentBlocking: options.blockingByContentBlocking,
           allowList: options.blockingByAllowList,
           callback: callbackNonTracking,
           extraPrefs: [],
           expectedBlockingNotifications: false,
           runInPrivateWindow,
+          iframeSandbox,
         });
         this._createCleanupTask(cleanupFunction);
       }
 
       // Phase 2: Here we want to test that a third-party context doesn't
       // get blocked with when the same origin is opened through window.open().
       if (windowOpenTest) {
-        this._createWindowOpenTask(name, callbackTracking, callbackNonTracking, runInPrivateWindow, extraPrefs);
+        this._createWindowOpenTask(name, callbackTracking, callbackNonTracking,
+                                   runInPrivateWindow, iframeSandbox, extraPrefs);
         this._createCleanupTask(cleanupFunction);
       }
 
       // Phase 3: Here we want to test that a third-party context doesn't
       // get blocked with user interaction present
       if (userInteractionTest) {
-        this._createUserInteractionTask(name, callbackTracking, callbackNonTracking, runInPrivateWindow, extraPrefs);
+        this._createUserInteractionTask(name, callbackTracking, callbackNonTracking,
+                                        runInPrivateWindow, iframeSandbox, extraPrefs);
         this._createCleanupTask(cleanupFunction);
       }
     }
   },
 
   async _setupTest(win, cookieBehavior, blockingByContentBlocking, extraPrefs) {
     await SpecialPowers.flushPrefEnv();
     await SpecialPowers.pushPrefEnv({"set": [
+      ["dom.storage_access.enabled", true],
       ["browser.contentblocking.enabled", blockingByContentBlocking],
       ["network.cookie.cookieBehavior", cookieBehavior],
       ["privacy.trackingprotection.enabled", false],
       ["privacy.trackingprotection.pbmode.enabled", false],
       ["privacy.trackingprotection.annotate_channels", cookieBehavior != BEHAVIOR_ACCEPT],
       [win.ContentBlocking.prefIntroCount, win.ContentBlocking.MAX_INTROS],
       ["browser.fastblock.enabled", false], // prevent intermittent failures
     ]});
@@ -217,17 +230,18 @@ this.AntiTracking = {
     await UrlClassifierTestUtils.addTestTrackers();
   },
 
   _createTask(options) {
     add_task(async function() {
       info("Starting " + (options.cookieBehavior != BEHAVIOR_ACCEPT ? "blocking" : "non-blocking") + " cookieBehavior (" + options.cookieBehavior + ") and " +
                          (options.blockingByContentBlocking ? "blocking" : "non-blocking") + " contentBlocking with" +
                          (options.allowList ? "" : "out") + " allow list test " + options.name +
-                         " running in a " + (options.runInPrivateWindow ? "private" : "normal") + " window");
+                         " running in a " + (options.runInPrivateWindow ? "private" : "normal") + " window " +
+                         " with iframe sandbox set to " + options.iframeSandbox);
 
       let win = window;
       if (options.runInPrivateWindow) {
         win = OpenBrowserWindow({private: true});
         await TestUtils.topicObserved("browser-delayed-startup-finished");
       }
 
       await AntiTracking._setupTest(win, options.cookieBehavior,
@@ -257,24 +271,28 @@ this.AntiTracking = {
 
         // The previous function reloads the browser, so wait for it to load again!
         await BrowserTestUtils.browserLoaded(browser);
       }
 
       info("Creating a 3rd party content");
       await ContentTask.spawn(browser,
                               { page: TEST_3RD_PARTY_PAGE,
-                                callback: options.callback.toString() },
+                                callback: options.callback.toString(),
+                                iframeSandbox: options.iframeSandbox },
                               async function(obj) {
         await new content.Promise(resolve => {
           let ifr = content.document.createElement("iframe");
           ifr.onload = function() {
             info("Sending code to the 3rd party content");
             ifr.contentWindow.postMessage(obj.callback, "*");
           };
+          if (typeof obj.iframeSandbox == "string") {
+            ifr.setAttribute("sandbox", obj.iframeSandbox);
+          }
 
           content.addEventListener("message", function msg(event) {
             if (event.data.type == "finish") {
               content.removeEventListener("message", msg);
               resolve();
               return;
             }
 
@@ -321,17 +339,18 @@ this.AntiTracking = {
     add_task(async function() {
       info("Cleaning up.");
       if (cleanupFunction) {
         await cleanupFunction();
       }
     });
   },
 
-  _createWindowOpenTask(name, blockingCallback, nonBlockingCallback, runInPrivateWindow, extraPrefs) {
+  _createWindowOpenTask(name, blockingCallback, nonBlockingCallback, runInPrivateWindow,
+                        iframeSandbox, extraPrefs) {
     add_task(async function() {
       info("Starting window-open test " + name);
 
       let win = window;
       if (runInPrivateWindow) {
         win = OpenBrowserWindow({private: true});
         await TestUtils.topicObserved("browser-delayed-startup-finished");
       }
@@ -350,24 +369,28 @@ this.AntiTracking = {
         pageURL += "?noopener";
       }
 
       info("Creating a 3rd party content");
       await ContentTask.spawn(browser,
                               { page: pageURL,
                                 blockingCallback: blockingCallback.toString(),
                                 nonBlockingCallback: nonBlockingCallback.toString(),
+                                iframeSandbox,
                               },
                               async function(obj) {
         await new content.Promise(resolve => {
           let ifr = content.document.createElement("iframe");
           ifr.onload = function() {
             info("Sending code to the 3rd party content");
             ifr.contentWindow.postMessage(obj, "*");
           };
+          if (typeof obj.iframeSandbox == "string") {
+            ifr.setAttribute("sandbox", obj.iframeSandbox);
+          }
 
           content.addEventListener("message", function msg(event) {
             if (event.data.type == "finish") {
               content.removeEventListener("message", msg);
               resolve();
               return;
             }
 
@@ -393,17 +416,18 @@ this.AntiTracking = {
       BrowserTestUtils.removeTab(tab);
 
       if (runInPrivateWindow) {
         win.close();
       }
     });
   },
 
-  _createUserInteractionTask(name, blockingCallback, nonBlockingCallback, runInPrivateWindow, extraPrefs) {
+  _createUserInteractionTask(name, blockingCallback, nonBlockingCallback,
+                             runInPrivateWindow, iframeSandbox, extraPrefs) {
     add_task(async function() {
       info("Starting user-interaction test " + name);
 
       let win = window;
       if (runInPrivateWindow) {
         win = OpenBrowserWindow({private: true});
         await TestUtils.topicObserved("browser-delayed-startup-finished");
       }
@@ -418,20 +442,24 @@ this.AntiTracking = {
       await BrowserTestUtils.browserLoaded(browser);
 
       info("Creating a 3rd party content");
       await ContentTask.spawn(browser,
                               { page: TEST_3RD_PARTY_PAGE_UI,
                                 popup: TEST_POPUP_PAGE,
                                 blockingCallback: blockingCallback.toString(),
                                 nonBlockingCallback: nonBlockingCallback.toString(),
+                                iframeSandbox,
                               },
                               async function(obj) {
         let ifr = content.document.createElement("iframe");
         let loading = new content.Promise(resolve => { ifr.onload = resolve; });
+        if (typeof obj.iframeSandbox == "string") {
+          ifr.setAttribute("sandbox", obj.iframeSandbox);
+        }
         content.document.body.appendChild(ifr);
         ifr.src = obj.page;
         await loading;
 
         info("The 3rd party content should not have access to first party storage.");
         await new content.Promise(resolve => {
           content.addEventListener("message", function msg(event) {
             if (event.data.type == "finish") {
--- a/toolkit/mozapps/extensions/LightweightThemeManager.jsm
+++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm
@@ -73,35 +73,16 @@ var _fallbackThemeData = null;
 
 // Holds whether or not the default theme should display in dark mode. This is
 // typically the case when the OS has a dark system appearance.
 var _defaultThemeIsInDarkMode = false;
 // Holds the dark theme to be used if the OS has a dark system appearance and
 // the default theme is selected.
 var _defaultDarkThemeID = null;
 
-// Convert from the old storage format (in which the order of usedThemes
-// was combined with isThemeSelected to determine which theme was selected)
-// to the new one (where a selectedThemeID determines which theme is selected).
-(function() {
-  let wasThemeSelected = _prefs.getBoolPref("isThemeSelected", false);
-
-  if (wasThemeSelected) {
-    _prefs.clearUserPref("isThemeSelected");
-    let themes = [];
-    try {
-      themes = JSON.parse(_prefs.getStringPref("usedThemes"));
-    } catch (e) { }
-
-    if (Array.isArray(themes) && themes[0]) {
-      _prefs.setCharPref("selectedThemeID", themes[0].id);
-    }
-  }
-})();
-
 var LightweightThemeManager = {
   get name() {
     return "LightweightThemeManager";
   },
 
   set fallbackThemeData(data) {
     if (data && Object.getOwnPropertyNames(data).length) {
       _fallbackThemeData = Object.assign({}, data);
--- a/toolkit/xre/nsEmbeddingModule.cpp
+++ b/toolkit/xre/nsEmbeddingModule.cpp
@@ -5,34 +5,30 @@
 
 #include "mozilla/ModuleUtils.h"
 #include "nsDialogParamBlock.h"
 #include "nsWindowWatcher.h"
 #include "nsAppStartupNotifier.h"
 #include "nsFind.h"
 #include "nsWebBrowserFind.h"
 #include "nsWebBrowserPersist.h"
-#include "nsCommandParams.h"
-#include "nsCommandGroup.h"
 #include "nsNetCID.h"
 #include "nsEmbedCID.h"
 
 #ifdef NS_PRINTING
 #include "nsPrintingPromptService.h"
 #include "nsPrintingProxy.h"
 #endif
 
 
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsWindowWatcher, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsAppStartupNotifier)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsFind)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsWebBrowserFind)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsWebBrowserPersist)
-NS_GENERIC_FACTORY_CONSTRUCTOR(nsCommandParams)
-NS_GENERIC_FACTORY_CONSTRUCTOR(nsControllerCommandGroup)
 
 #ifdef MOZ_XUL
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsDialogParamBlock)
 #ifdef NS_PRINTING
 NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsPrintingPromptService,
                                          nsPrintingPromptService::GetSingleton)
 #ifdef PROXY_PRINTING
 NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsPrintingProxy,
@@ -47,18 +43,16 @@ NS_DEFINE_NAMED_CID(NS_DIALOGPARAMBLOCK_
 NS_DEFINE_NAMED_CID(NS_PRINTINGPROMPTSERVICE_CID);
 #endif
 #endif
 NS_DEFINE_NAMED_CID(NS_WINDOWWATCHER_CID);
 NS_DEFINE_NAMED_CID(NS_FIND_CID);
 NS_DEFINE_NAMED_CID(NS_WEB_BROWSER_FIND_CID);
 NS_DEFINE_NAMED_CID(NS_APPSTARTUPNOTIFIER_CID);
 NS_DEFINE_NAMED_CID(NS_WEBBROWSERPERSIST_CID);
-NS_DEFINE_NAMED_CID(NS_COMMAND_PARAMS_CID);
-NS_DEFINE_NAMED_CID(NS_CONTROLLER_COMMAND_GROUP_CID);
 
 static const mozilla::Module::CIDEntry kEmbeddingCIDs[] = {
 #ifdef MOZ_XUL
     { &kNS_DIALOGPARAMBLOCK_CID, false, nullptr, nsDialogParamBlockConstructor },
 #ifdef NS_PRINTING
 
 #ifdef PROXY_PRINTING
     { &kNS_PRINTINGPROMPTSERVICE_CID, false, nullptr, nsPrintingPromptServiceConstructor,
@@ -70,35 +64,31 @@ static const mozilla::Module::CIDEntry k
 #endif
 #endif
 #endif
     { &kNS_WINDOWWATCHER_CID, false, nullptr, nsWindowWatcherConstructor },
     { &kNS_FIND_CID, false, nullptr, nsFindConstructor },
     { &kNS_WEB_BROWSER_FIND_CID, false, nullptr, nsWebBrowserFindConstructor },
     { &kNS_APPSTARTUPNOTIFIER_CID, false, nullptr, nsAppStartupNotifierConstructor },
     { &kNS_WEBBROWSERPERSIST_CID, false, nullptr, nsWebBrowserPersistConstructor },
-    { &kNS_COMMAND_PARAMS_CID, false, nullptr, nsCommandParamsConstructor },
-    { &kNS_CONTROLLER_COMMAND_GROUP_CID, false, nullptr, nsControllerCommandGroupConstructor },
     { nullptr }
 };
 
 static const mozilla::Module::ContractIDEntry kEmbeddingContracts[] = {
 #ifdef MOZ_XUL
     { NS_DIALOGPARAMBLOCK_CONTRACTID, &kNS_DIALOGPARAMBLOCK_CID },
 #ifdef NS_PRINTING
     { NS_PRINTINGPROMPTSERVICE_CONTRACTID, &kNS_PRINTINGPROMPTSERVICE_CID },
 #endif
 #endif
     { NS_WINDOWWATCHER_CONTRACTID, &kNS_WINDOWWATCHER_CID },
     { NS_FIND_CONTRACTID, &kNS_FIND_CID },
     { NS_WEB_BROWSER_FIND_CONTRACTID, &kNS_WEB_BROWSER_FIND_CID },
     { NS_APPSTARTUPNOTIFIER_CONTRACTID, &kNS_APPSTARTUPNOTIFIER_CID },
     { NS_WEBBROWSERPERSIST_CONTRACTID, &kNS_WEBBROWSERPERSIST_CID },
-    { NS_COMMAND_PARAMS_CONTRACTID, &kNS_COMMAND_PARAMS_CID },
-    { NS_CONTROLLER_COMMAND_GROUP_CONTRACTID, &kNS_CONTROLLER_COMMAND_GROUP_CID },
     { nullptr }
 };
 
 static const mozilla::Module kEmbeddingModule = {
     mozilla::Module::kVersion,
     kEmbeddingCIDs,
     kEmbeddingContracts
 };
--- a/widget/nsBaseWidget.cpp
+++ b/widget/nsBaseWidget.cpp
@@ -2482,19 +2482,18 @@ nsIWidget::GetEditCommands(nsIWidget::Na
   MOZ_ASSERT(aCommands.IsEmpty());
 }
 
 already_AddRefed<nsIBidiKeyboard>
 nsIWidget::CreateBidiKeyboard()
 {
   if (XRE_IsContentProcess()) {
     return CreateBidiKeyboardContentProcess();
-  } else {
-    return CreateBidiKeyboardInner();
   }
+  return CreateBidiKeyboardInner();
 }
 
 #ifdef ANDROID
 already_AddRefed<nsIBidiKeyboard>
 nsIWidget::CreateBidiKeyboardInner()
 {
   // no bidi keyboard implementation
   return nullptr;
--- a/xpcom/base/nsCycleCollector.cpp
+++ b/xpcom/base/nsCycleCollector.cpp
@@ -159,18 +159,18 @@
 #include "mozilla/DebugOnly.h"
 #include "mozilla/HashFunctions.h"
 #include "mozilla/HashTable.h"
 #include "mozilla/HoldDropJSObjects.h"
 /* This must occur *after* base/process_util.h to avoid typedefs conflicts. */
 #include "mozilla/LinkedList.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/Move.h"
+#include "mozilla/MruCache.h"
 #include "mozilla/SegmentedVector.h"
-#include "mozilla/Variant.h"
 
 #include "nsCycleCollectionParticipant.h"
 #include "nsCycleCollectionNoteRootCallback.h"
 #include "nsDeque.h"
 #include "nsExceptionHandler.h"
 #include "nsCycleCollector.h"
 #include "nsThreadUtils.h"
 #include "nsXULAppAPI.h"
@@ -2089,52 +2089,26 @@ private:
   nsCycleCollectionParticipant* mJSParticipant;
   nsCycleCollectionParticipant* mJSZoneParticipant;
   nsCString mNextEdgeName;
   RefPtr<nsCycleCollectorLogger> mLogger;
   bool mMergeZones;
   nsAutoPtr<NodePool::Enumerator> mCurrNode;
   uint32_t mNoteChildCount;
 
-  class GraphCache
+  struct PtrInfoCache : public MruCache<void*, PtrInfo*, PtrInfoCache, 491>
   {
-  public:
-    // This either returns a pointer if present, or an index, if it isn't.
-    Variant<PtrInfo*, uint32_t> GetEntryOrIndex(void* aPtr)
-    {
-      uint32_t hash = mozilla::HashGeneric(aPtr);
-      uint32_t index = hash % kCacheSize;
-      PtrInfo* result = mCache[index];
-      if (result && result->mPointer == aPtr) {
-        return AsVariant(result);
-      }
-
-      return AsVariant(index);
-    }
-
-    void Add(uint32_t aIndex, PtrInfo* aPtrInfo)
+    static HashNumber Hash(const void* aKey) { return HashGeneric(aKey); }
+    static bool Match(const void* aKey, const PtrInfo* aVal)
     {
-      mCache[aIndex] = aPtrInfo;
+      return aVal->mPointer == aKey;
     }
-
-    void Remove(void* aPtr)
-    {
-      uint32_t hash = mozilla::HashGeneric(aPtr);
-      uint32_t index = hash % kCacheSize;
-      PtrInfo* pinfo = mCache[index];
-      if (pinfo && pinfo->mPointer == aPtr) {
-        mCache[index] = nullptr;
-      }
-    }
-  private:
-    const static uint32_t kCacheSize = 491;
-    PtrInfo* mCache[kCacheSize] = {0};
   };
 
-  GraphCache mGraphCache;
+  PtrInfoCache mGraphCache;
 
 public:
   CCGraphBuilder(CCGraph& aGraph,
                  CycleCollectorResults& aResults,
                  CycleCollectedJSRuntime* aCCRuntime,
                  nsCycleCollectorLogger* aLogger,
                  bool aMergeZones);
   virtual ~CCGraphBuilder();
@@ -2282,24 +2256,22 @@ CCGraphBuilder::~CCGraphBuilder()
 
 PtrInfo*
 CCGraphBuilder::AddNode(void* aPtr, nsCycleCollectionParticipant* aParticipant)
 {
   if (mGraph.mOutOfMemory) {
     return nullptr;
   }
 
-  Variant<PtrInfo*, uint32_t> cacheVariant = mGraphCache.GetEntryOrIndex(aPtr);
-  if (cacheVariant.is<PtrInfo*>()) {
-    MOZ_ASSERT(cacheVariant.as<PtrInfo*>()->mParticipant == aParticipant,
+  PtrInfoCache::Entry cached = mGraphCache.Lookup(aPtr);
+  if (cached) {
+    MOZ_ASSERT(cached.Data()->mParticipant == aParticipant,
                "nsCycleCollectionParticipant shouldn't change!");
-    return cacheVariant.as<PtrInfo*>();
-  }
-
-  MOZ_ASSERT(cacheVariant.is<uint32_t>());
+    return cached.Data();
+  }
 
   PtrInfo* result;
   auto p = mGraph.mPtrInfoMap.lookupForAdd(aPtr);
   if (!p) {
     // New entry
     result = mNodeBuilder.Add(aPtr, aParticipant);
     if (!result) {
       return nullptr;
@@ -2314,17 +2286,17 @@ CCGraphBuilder::AddNode(void* aPtr, nsCy
     }
 
   } else {
     result = *p;
     MOZ_ASSERT(result->mParticipant == aParticipant,
                "nsCycleCollectionParticipant shouldn't change!");
   }
 
-  mGraphCache.Add(cacheVariant.as<uint32_t>(), result);
+  cached.Set(result);
 
   return result;
 }
 
 bool
 CCGraphBuilder::AddPurpleRoot(void* aRoot, nsCycleCollectionParticipant* aParti)
 {
   ToParticipant(aRoot, &aParti);
new file mode 100644
--- /dev/null
+++ b/xpcom/ds/MruCache.h
@@ -0,0 +1,183 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_MruCache_h
+#define mozilla_MruCache_h
+
+#include <cstdint>
+#include <type_traits>
+#include <utility>
+
+#include "mozilla/Attributes.h"
+#include "mozilla/HashFunctions.h"
+
+namespace mozilla {
+
+namespace detail {
+
+// Helper struct for checking if a value is empty.
+//
+// `IsNotEmpty` will return true if `Value` is not a pointer type or if the
+// pointer value is not null.
+template <typename Value, bool IsPtr = std::is_pointer<Value>::value>
+struct EmptyChecker
+{
+  static bool IsNotEmpty(const Value&) { return true; }
+};
+// Template specialization for the `IsPtr == true` case.
+template <typename Value>
+struct EmptyChecker<Value, true>
+{
+  static bool IsNotEmpty(const Value& aVal) { return aVal != nullptr; }
+};
+
+} // namespace detail
+
+// Provides a most recently used cache that can be used as a layer on top of
+// a larger container where lookups can be expensive. The default size is 31,
+// which as a prime number provides a better distrubution of cached entries.
+//
+// Users are expected to provide a `Cache` class that defines two required
+// methods:
+//   - A method for providing the hash of a key:
+//
+//     static HashNumber Hash(const KeyType& aKey)
+//
+//   - A method for matching a key to a value, for pointer types the value
+//     is guaranteed not to be null.
+//
+//     static bool Match(const KeyType& aKey, const ValueType& aVal)
+//
+// For example:
+//    class MruExample : public MruCache<void*, PtrInfo*, MruExample>
+//    {
+//      static HashNumber Hash(const KeyType& aKey)
+//      {
+//        return HashGeneric(aKey);
+//      }
+//      static Match(const KeyType& aKey, const ValueType& aVal)
+//      {
+//        return aVal->mPtr == aKey;
+//      }
+//    };
+template <class Key, class Value, class Cache, size_t Size=31>
+class MruCache
+{
+  // Best distribution is achieved with a prime number. Ideally the closest
+  // to a power of two will be the most efficient use of memory. This
+  // assertion is pretty weak, but should catch the common inclination to
+  // use a power-of-two.
+  static_assert(Size % 2 != 0, "Use a prime number");
+
+  // This is a stronger assertion but significantly limits the values to just
+  // those close to a power-of-two value.
+  //static_assert(Size == 7 || Size == 13 || Size == 31 || Size == 61 ||
+  //              Size == 127 || Size == 251 || Size == 509 || Size == 1021,
+  //              "Use a prime number less than 1024");
+
+public:
+  using KeyType = Key;
+  using ValueType = Value;
+
+  MruCache() = default;
+  MruCache(const MruCache&) = delete;
+  MruCache(const MruCache&&) = delete;
+
+  // Inserts the given value into the cache. Potentially overwrites an
+  // existing entry.
+  template <typename U>
+  void Put(const KeyType& aKey, U&& aVal)
+  {
+    *RawEntry(aKey) = std::forward<U>(aVal);
+  }
+
+  // Removes the given entry if it is in the cache.
+  void Remove(const KeyType& aKey)
+  {
+    Lookup(aKey).Remove();
+  }
+
+  // Clears all cached entries and resets them to a default value.
+  void Clear()
+  {
+    for (ValueType& val : mCache) {
+      val = ValueType{};
+    }
+  }
+
+  // Helper that holds an entry that matched a lookup key. Usage:
+  //
+  //    auto p = mCache.Lookup(aKey);
+  //    if (p) {
+  //      return p.Data();
+  //    }
+  //
+  //    auto foo = new Foo();
+  //    mTable.Insert(aKey, foo);
+  //    p.Set(foo);
+  //    return foo;
+  class Entry
+  {
+  public:
+    Entry(ValueType* aEntry, bool aMatch)
+        : mEntry(aEntry)
+        , mMatch(aMatch)
+    {
+      MOZ_ASSERT(mEntry);
+    }
+
+    explicit operator bool() const { return mMatch; }
+
+    ValueType& Data() const
+    {
+      MOZ_ASSERT(mMatch);
+      return *mEntry;
+    }
+
+    template<typename U>
+    void Set(U&& aValue)
+    {
+      mMatch = true;
+      Data() = std::forward<U>(aValue);
+    }
+
+    void Remove()
+    {
+      if (mMatch) {
+        Data() = ValueType{};
+        mMatch = false;
+      }
+    }
+
+  private:
+    ValueType* mEntry; // Location of the entry in the cache.
+    bool mMatch;       // Whether the value matched.
+  };
+
+  // Retrieves an entry from the cache. Can be used to test if an entry is
+  // present, update the entry to a new value, or remove the entry if one was
+  // matched.
+  Entry Lookup(const KeyType& aKey)
+  {
+    using EmptyChecker = detail::EmptyChecker<ValueType>;
+
+    auto entry = RawEntry(aKey);
+    bool match = EmptyChecker::IsNotEmpty(*entry) && Cache::Match(aKey, *entry);
+    return Entry(entry, match);
+  }
+
+private:
+    MOZ_ALWAYS_INLINE ValueType* RawEntry(const KeyType& aKey)
+    {
+      return &mCache[Cache::Hash(aKey) % Size];
+    }
+
+    ValueType mCache[Size] = {};
+};
+
+} // namespace mozilla
+
+#endif // mozilla_mrucache_h
--- a/xpcom/ds/StaticAtoms.py
+++ b/xpcom/ds/StaticAtoms.py
@@ -69,16 +69,18 @@ STATIC_ATOMS = [
     Atom("allowfullscreen", "allowfullscreen"),
     Atom("allowmodals", "allow-modals"),
     Atom("alloworientationlock", "allow-orientation-lock"),
     Atom("allowpaymentrequest", "allowpaymentrequest"),
     Atom("allowpointerlock", "allow-pointer-lock"),
     Atom("allowpopupstoescapesandbox", "allow-popups-to-escape-sandbox"),
     Atom("allowpopups", "allow-popups"),
     Atom("allowpresentation", "allow-presentation"),
+    Atom("allowstorageaccessbyuseractivatetion",
+         "allow-storage-access-by-user-activation"),
     Atom("allowsameorigin", "allow-same-origin"),
     Atom("allowscripts", "allow-scripts"),
     Atom("allowscriptstoclose", "allowscriptstoclose"),
     Atom("allowtopnavigation", "allow-top-navigation"),
     Atom("allowuntrusted", "allowuntrusted"),
     Atom("alt", "alt"),
     Atom("alternate", "alternate"),
     Atom("always", "always"),
--- a/xpcom/ds/moz.build
+++ b/xpcom/ds/moz.build
@@ -82,16 +82,17 @@ EXPORTS += [
 
 EXPORTS.mozilla += [
     'ArenaAllocator.h',
     'ArenaAllocatorExtensions.h',
     'ArrayIterator.h',
     'AtomArray.h',
     'Dafsa.h',
     'IncrementalTokenizer.h',
+    'MruCache.h',
     'Observer.h',
     'SimpleEnumerator.h',
     'StickyTimeDuration.h',
     'Tokenizer.h',
 ]
 
 UNIFIED_SOURCES += [
     'Dafsa.cpp',
--- a/xpcom/ds/nsAtomTable.cpp
+++ b/xpcom/ds/nsAtomTable.cpp
@@ -3,16 +3,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 "mozilla/Assertions.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/HashFunctions.h"
 #include "mozilla/MemoryReporting.h"
+#include "mozilla/MruCache.h"
 #include "mozilla/Mutex.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/Sprintf.h"
 #include "mozilla/Unused.h"
 
 #include "nsAtom.h"
 #include "nsAtomTable.h"
 #include "nsAutoPtr.h"
@@ -216,19 +217,27 @@ struct AtomTableKey
 struct AtomTableEntry : public PLDHashEntryHdr
 {
   // These references are either to dynamic atoms, in which case they are
   // non-owning, or they are to static atoms, which aren't really refcounted.
   // See the comment at the top of this file for more details.
   nsAtom* MOZ_NON_OWNING_REF mAtom;
 };
 
-#define RECENTLY_USED_MAIN_THREAD_ATOM_CACHE_SIZE 31
-static nsAtom*
-  sRecentlyUsedMainThreadAtoms[RECENTLY_USED_MAIN_THREAD_ATOM_CACHE_SIZE] = {};
+struct AtomCache : public MruCache<AtomTableKey, nsAtom*, AtomCache>
+{
+  static HashNumber Hash(const AtomTableKey& aKey) { return aKey.mHash; }
+  static bool Match(const AtomTableKey& aKey, const nsAtom* aVal)
+  {
+    MOZ_ASSERT(aKey.mUTF16String);
+    return aVal->Equals(aKey.mUTF16String, aKey.mLength);
+  }
+};
+
+static AtomCache sRecentlyUsedMainThreadAtoms;
 
 // In order to reduce locking contention for concurrent atomization, we segment
 // the atom table into N subtables, each with a separate lock. If the hash
 // values we use to select the subtable are evenly distributed, this reduces the
 // probability of contention by a factor of N. See bug 1440824.
 //
 // NB: This is somewhat similar to the technique used by Java's
 // ConcurrentHashTable.
@@ -413,19 +422,17 @@ nsAtomTable::AddSizeOfIncludingThis(Mall
     MutexAutoLock lock(table.mLock);
     table.AddSizeOfExcludingThisLocked(aMallocSizeOf, aSizes);
   }
 }
 
 void nsAtomTable::GC(GCKind aKind)
 {
   MOZ_ASSERT(NS_IsMainThread());
-  for (uint32_t i = 0; i < RECENTLY_USED_MAIN_THREAD_ATOM_CACHE_SIZE; ++i) {
-    sRecentlyUsedMainThreadAtoms[i] = nullptr;
-  }
+  sRecentlyUsedMainThreadAtoms.Clear();
 
   // Note that this is effectively an incremental GC, since only one subtable
   // is locked at a time.
   for (auto& table: mSubTables) {
     MutexAutoLock lock(table.mLock);
     table.GCLocked(aKind);
   }
 
@@ -765,42 +772,36 @@ NS_Atomize(const nsAString& aUTF16String
 
 already_AddRefed<nsAtom>
 nsAtomTable::AtomizeMainThread(const nsAString& aUTF16String)
 {
   MOZ_ASSERT(NS_IsMainThread());
   RefPtr<nsAtom> retVal;
   uint32_t hash;
   AtomTableKey key(aUTF16String.Data(), aUTF16String.Length(), &hash);
-  uint32_t index = hash % RECENTLY_USED_MAIN_THREAD_ATOM_CACHE_SIZE;
-  nsAtom* atom = sRecentlyUsedMainThreadAtoms[index];
-  if (atom) {
-    uint32_t length = atom->GetLength();
-    if (length == key.mLength &&
-        (memcmp(atom->GetUTF16String(),
-                key.mUTF16String, length * sizeof(char16_t)) == 0)) {
-      retVal = atom;
-      return retVal.forget();
-    }
+  auto p = sRecentlyUsedMainThreadAtoms.Lookup(key);
+  if (p) {
+    retVal = p.Data();
+    return retVal.forget();
   }
 
   nsAtomSubTable& table = SelectSubTable(key);
   MutexAutoLock lock(table.mLock);
   AtomTableEntry* he = table.Add(key);
 
   if (he->mAtom) {
     retVal = he->mAtom;
   } else {
     RefPtr<nsAtom> newAtom =
       dont_AddRef(nsDynamicAtom::Create(aUTF16String, hash));
     he->mAtom = newAtom;
     retVal = newAtom.forget();
   }
 
-  sRecentlyUsedMainThreadAtoms[index] = he->mAtom;
+  p.Set(retVal);
   return retVal.forget();
 }
 
 already_AddRefed<nsAtom>
 NS_AtomizeMainThread(const nsAString& aUTF16String)
 {
   MOZ_ASSERT(gAtomTable);
   return gAtomTable->AtomizeMainThread(aUTF16String);
new file mode 100644
--- /dev/null
+++ b/xpcom/tests/gtest/TestMruCache.cpp
@@ -0,0 +1,393 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "gtest/gtest.h"
+
+#include "mozilla/MruCache.h"
+#include "nsString.h"
+
+using namespace mozilla;
+
+// A few MruCache implementations to use during testing.
+struct IntMap : public MruCache<int, int, IntMap>
+{
+  static HashNumber Hash(const KeyType& aKey) { return aKey - 1; }
+  static bool Match(const KeyType& aKey, const ValueType& aVal) { return aKey == aVal; }
+};
+
+struct UintPtrMap : public MruCache<uintptr_t, int*, UintPtrMap>
+{
+  static HashNumber Hash(const KeyType& aKey) { return aKey - 1; }
+  static bool Match(const KeyType& aKey, const ValueType& aVal) { return aKey == (KeyType)aVal; }
+};
+
+struct StringStruct
+{
+  nsCString mKey;
+  nsCString mOther;
+};
+
+struct StringStructMap : public MruCache<nsCString, StringStruct, StringStructMap>
+{
+  static HashNumber Hash(const KeyType& aKey) { return *aKey.BeginReading() - 1; }
+  static bool Match(const KeyType& aKey, const ValueType& aVal) { return aKey == aVal.mKey; }
+};
+
+// Helper for emulating convertable holders such as RefPtr.
+template <typename T>
+struct Convertable
+{
+  T mItem;
+  operator T() const { return mItem; }
+};
+
+// Helper to create a StringStructMap key.
+nsCString MakeStringKey(char aKey)
+{
+  nsCString key;
+  key.Append(aKey);
+  return key;
+}
+
+TEST(MruCache, TestNullChecker)
+{
+  using mozilla::detail::EmptyChecker;
+
+  {
+    int test = 0;
+    EXPECT_TRUE(EmptyChecker<decltype(test)>::IsNotEmpty(test));
+
+    test = 42;
+    EXPECT_TRUE(EmptyChecker<decltype(test)>::IsNotEmpty(test));
+  }
+
+  {
+    const char* test = "abc";
+    EXPECT_TRUE(EmptyChecker<decltype(test)>::IsNotEmpty(test));
+
+    test = nullptr;
+    EXPECT_FALSE(EmptyChecker<decltype(test)>::IsNotEmpty(test));
+  }
+
+  {
+    int foo = 42;
+    int* test = &foo;
+    EXPECT_TRUE(EmptyChecker<decltype(test)>::IsNotEmpty(test));
+
+    test = nullptr;
+    EXPECT_FALSE(EmptyChecker<decltype(test)>::IsNotEmpty(test));
+  }
+}
+
+TEST(MruCache, TestEmptyCache)
+{
+  {
+    // Test a basic empty cache.
+    IntMap mru;
+
+    // Make sure the default values are set.
+    for (int i = 1; i < 32; i++) {
+      auto p = mru.Lookup(i);
+
+      // Shouldn't be found.
+      EXPECT_FALSE(p);
+    }
+  }
+
+  {
+    // Test an empty cache with pointer values.
+    UintPtrMap mru;
+
+    // Make sure the default values are set.
+    for (uintptr_t i = 1; i < 32; i++) {
+      auto p = mru.Lookup(i);
+
+      // Shouldn't be found.
+      EXPECT_FALSE(p);
+    }
+  }
+
+  {
+    // Test an empty cache with more complex structure.
+    StringStructMap mru;
+
+    // Make sure the default values are set.
+    for (char i = 1; i < 32; i++) {
+      const nsCString key = MakeStringKey(i);
+      auto p = mru.Lookup(key);
+
+      // Shouldn't be found.
+      EXPECT_FALSE(p);
+    }
+  }
+}
+