Merge m-i to m-c, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Sun, 30 Oct 2016 10:55:00 -0700
changeset 363056 e3279760cd977aac30bd9e8032d3ee71f55d2a67
parent 363034 6a79c696da8bd7210cd4354293e1f9929f8dc1e8 (current diff)
parent 363055 fdea89bad933375fd650bc2cd8fee6dc860de9bb (diff)
child 363057 970b0b749ebd570ccbd4423f26d6b9a8b8e671aa
child 363060 709e4c09a28a4332f6e302a8c756b879be8c8320
child 363064 e017b947cea08b7bdd21007e95bd13e14ad59103
child 363069 708de5d681d113649e8fac2a10a4a0c0eae8be43
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-beta@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone52.0a1
first release with
nightly linux32
e3279760cd97 / 52.0a1 / 20161031030202 / files
nightly linux64
e3279760cd97 / 52.0a1 / 20161031030202 / files
nightly mac
e3279760cd97 / 52.0a1 / 20161031030202 / files
nightly win32
e3279760cd97 / 52.0a1 / 20161031030202 / files
nightly win64
e3279760cd97 / 52.0a1 / 20161031030202 / 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 m-i to m-c, a=merge
dom/base/moz.build
dom/bindings/Bindings.conf
dom/tests/mochitest/general/test_interfaces.html
dom/webidl/moz.build
layout/base/nsRefreshDriver.cpp
layout/base/nsRefreshDriver.h
modules/libpref/init/all.js
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -3800,35 +3800,58 @@ const BrowserSearch = {
   /**
    * Helper to record a search with Telemetry.
    *
    * Telemetry records only search counts and nothing pertaining to the search itself.
    *
    * @param engine
    *        (nsISearchEngine) The engine handling the search.
    * @param source
-   *        (string) Where the search originated from. See the FHR
-   *        SearchesProvider for allowed values.
-   * @param selection [optional]
-   *        ({index: The selected index, kind: "key" or "mouse"}) If
-   *        the search was a suggested search, this indicates where the
-   *        item was in the suggestion list and how the user selected it.
+   *        (string) Where the search originated from. See BrowserUsageTelemetry for
+   *        allowed values.
+   * @param details [optional]
+   *        An optional parameter passed to |BrowserUsageTelemetry.recordSearch|.
+   *        See its documentation for allowed options.
+   *        Additionally, if the search was a suggested search, |details.selection|
+   *        indicates where the item was in the suggestion list and how the user
+   *        selected it: {selection: {index: The selected index, kind: "key" or "mouse"}}
    */
-  recordSearchInTelemetry: function (engine, source, selection) {
-    BrowserUITelemetry.countSearchEvent(source, null, selection);
+  recordSearchInTelemetry: function (engine, source, details={}) {
+    BrowserUITelemetry.countSearchEvent(source, null, details.selection);
     try {
-      BrowserUsageTelemetry.recordSearch(engine, source);
+      BrowserUsageTelemetry.recordSearch(engine, source, details);
     } catch (ex) {
       Cu.reportError(ex);
     }
   },
 
+  /**
+   * Helper to record a one-off search with Telemetry.
+   *
+   * Telemetry records only search counts and nothing pertaining to the search itself.
+   *
+   * @param engine
+   *        (nsISearchEngine) The engine handling the search.
+   * @param source
+   *        (string) Where the search originated from. See BrowserUsageTelemetry for
+   *        allowed values.
+   * @param type
+   *        (string) Indicates how the user selected the search item.
+   * @param where
+   *        (string) Where was the search link opened (e.g. new tab, current tab, ..).
+   */
   recordOneoffSearchInTelemetry: function (engine, source, type, where) {
     let id = this._getSearchEngineId(engine) + "." + source;
     BrowserUITelemetry.countOneoffSearchEvent(id, type, where);
+    try {
+      const details = {type, isOneOff: true};
+      BrowserUsageTelemetry.recordSearch(engine, source, details);
+    } catch (ex) {
+      Cu.reportError(ex);
+    }
   }
 };
 
 XPCOMUtils.defineConstant(this, "BrowserSearch", BrowserSearch);
 
 function FillHistoryMenu(aParent) {
   // Lazily add the hover listeners on first showing and never remove them
   if (!aParent.hasStatusListener) {
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -448,22 +448,27 @@ file, You can obtain one at http://mozil
                   return;
                 }
                 break;
               case "searchengine":
                 if (selectedOneOff && selectedOneOff.engine) {
                   // Replace the engine with the selected one-off engine.
                   action.params.engineName = selectedOneOff.engine.name;
                 }
+                const actionDetails = {
+                  isSuggestion: !!action.params.searchSuggestion,
+                  isAlias: !!action.params.alias
+                };
                 [url, postData] = this._recordSearchEngineLoad(
                   action.params.engineName,
                   action.params.searchSuggestion || action.params.searchQuery,
                   event,
                   where,
-                  openUILinkParams
+                  openUILinkParams,
+                  actionDetails
                 );
                 break;
             }
             this._loadURL(url, browser, postData, where, openUILinkParams,
                           matchLastLocationChange, mayInheritPrincipal);
             return;
           }
 
@@ -584,24 +589,37 @@ file, You can obtain one at http://mozil
       </method>
 
       <method name="_recordSearchEngineLoad">
         <parameter name="engineOrEngineName"/>
         <parameter name="query"/>
         <parameter name="event"/>
         <parameter name="openUILinkWhere"/>
         <parameter name="openUILinkParams"/>
+        <parameter name="searchActionDetails"/>
         <body><![CDATA[
           let engine =
             typeof(engineOrEngineName) == "string" ?
               Services.search.getEngineByName(engineOrEngineName) :
               engineOrEngineName;
-          BrowserSearch.recordSearchInTelemetry(engine, "urlbar");
-          this.popup.oneOffSearchButtons
+          let isOneOff = this.popup.oneOffSearchButtons
               .maybeRecordTelemetry(event, openUILinkWhere, openUILinkParams);
+          // Infer the type of the even which triggered the search.
+          let eventType = "unknown";
+          if (event instanceof KeyboardEvent) {
+            eventType = "key";
+          } else if (event instanceof MouseEvent) {
+            eventType = "mouse";
+          }
+          // Augment the search action details object.
+          let details = searchActionDetails || {};
+          details.isOneOff = isOneOff;
+          details.type = eventType;
+
+          BrowserSearch.recordSearchInTelemetry(engine, "urlbar", details);
           let submission = engine.getSubmission(query, null, "keyword");
           return [submission.uri.spec, submission.postData];
         ]]></body>
       </method>
 
       <method name="maybeCanonizeURL">
         <parameter name="aTriggeringEvent"/>
         <parameter name="aUrl"/>
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -368,22 +368,22 @@
         <parameter name="aEngine"/>
         <parameter name="aWhere"/>
         <parameter name="aParams"/>
         <body><![CDATA[
           var textBox = this._textbox;
           var textValue = textBox.value;
 
           let selection = this.telemetrySearchDetails;
-          this.doSearch(textValue, aWhere, aEngine, aParams);
+          let oneOffRecorded = false;
 
           if (!selection || (selection.index == -1)) {
-            let recorded = this.textbox.popup.oneOffButtons
-                               .maybeRecordTelemetry(aEvent, aWhere, aParams);
-            if (!recorded) {
+            oneOffRecorded = this.textbox.popup.oneOffButtons
+                                 .maybeRecordTelemetry(aEvent, aWhere, aParams);
+            if (!oneOffRecorded) {
               let source = "unknown";
               let type = "unknown";
               let target = aEvent.originalTarget;
               if (aEvent instanceof KeyboardEvent) {
                 type = "key";
               } else if (aEvent instanceof MouseEvent) {
                 type = "mouse";
                 if (target.classList.contains("search-panel-header") ||
@@ -398,26 +398,30 @@
               if (!aEngine) {
                 aEngine = this.currentEngine;
               }
               BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type,
                                                           aWhere);
             }
           }
 
+          // This is a one-off search only if oneOffRecorded is true.
+          this.doSearch(textValue, aWhere, aEngine, aParams, oneOffRecorded);
+
           if (aWhere == "tab" && aParams && aParams.inBackground)
             this.focus();
         ]]></body>
       </method>
 
       <method name="doSearch">
         <parameter name="aData"/>
         <parameter name="aWhere"/>
         <parameter name="aEngine"/>
         <parameter name="aParams"/>
+        <parameter name="aOneOff"/>
         <body><![CDATA[
           var textBox = this._textbox;
 
           // Save the current value in the form history
           if (aData && !PrivateBrowsingUtils.isWindowPrivate(window) && this.FormHistory.enabled) {
             this.FormHistory.update(
               { op : "bump",
                 fieldname : textBox.getAttribute("autocompletesearchparam"),
@@ -429,17 +433,23 @@
 
           let engine = aEngine || this.currentEngine;
           var submission = engine.getSubmission(aData, null, "searchbar");
           let telemetrySearchDetails = this.telemetrySearchDetails;
           this.telemetrySearchDetails = null;
           if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
             telemetrySearchDetails = null;
           }
-          BrowserSearch.recordSearchInTelemetry(engine, "searchbar", telemetrySearchDetails);
+          // If we hit here, we come either from a one-off, a plain search or a suggestion.
+          const details = {
+            isOneOff: aOneOff,
+            isSuggestion: (!aOneOff && telemetrySearchDetails),
+            selection: telemetrySearchDetails
+          };
+          BrowserSearch.recordSearchInTelemetry(engine, "searchbar", details);
           // null parameter below specifies HTML response for search
           let params = {
             postData: submission.postData,
           };
           if (aParams) {
             for (let key in aParams) {
               params[key] = aParams[key];
             }
--- a/browser/modules/BrowserUsageTelemetry.jsm
+++ b/browser/modules/BrowserUsageTelemetry.jsm
@@ -37,16 +37,22 @@ const UNFILTERED_URI_COUNT_SCALAR_NAME =
 const KNOWN_SEARCH_SOURCES = [
   "abouthome",
   "contextmenu",
   "newtab",
   "searchbar",
   "urlbar",
 ];
 
+const KNOWN_ONEOFF_SOURCES = [
+  "oneoff-urlbar",
+  "oneoff-searchbar",
+  "unknown", // Edge case: this is the searchbar (see bug 1195733 comment 7).
+];
+
 function getOpenTabsAndWinsCounts() {
   let tabCount = 0;
   let winCount = 0;
 
   let browserEnum = Services.wm.getEnumerator("navigator:browser");
   while (browserEnum.hasMoreElements()) {
     let win = browserEnum.getNext();
     winCount++;
@@ -241,30 +247,120 @@ let BrowserUsageTelemetry = {
 
   /**
    * The main entry point for recording search related Telemetry. This includes
    * search counts and engagement measurements.
    *
    * Telemetry records only search counts per engine and action origin, but
    * nothing pertaining to the search contents themselves.
    *
-   * @param engine
-   *        (nsISearchEngine) The engine handling the search.
-   * @param source
-   *        (string) Where the search originated from. See
-   *        KNOWN_SEARCH_SOURCES for allowed values.
+   * @param {nsISearchEngine} engine
+   *        The engine handling the search.
+   * @param {String} source
+   *        Where the search originated from. See KNOWN_SEARCH_SOURCES for allowed
+   *        values.
+   * @param {Object} [details] Options object.
+   * @param {Boolean} [details.isOneOff=false]
+   *        true if this event was generated by a one-off search.
+   * @param {Boolean} [details.isSuggestion=false]
+   *        true if this event was generated by a suggested search.
+   * @param {Boolean} [details.isAlias=false]
+   *        true if this event was generated by a search using an alias.
+   * @param {Object} [details.type=null]
+   *        The object describing the event that triggered the search.
    * @throws if source is not in the known sources list.
    */
-  recordSearch(engine, source) {
-    if (!KNOWN_SEARCH_SOURCES.includes(source)) {
-      throw new Error("Unknown source for search: " + source);
+  recordSearch(engine, source, details={}) {
+    const isOneOff = !!details.isOneOff;
+
+    if (isOneOff) {
+      if (!KNOWN_ONEOFF_SOURCES.includes(source)) {
+        throw new Error("Unknown source for one-off search: " + source);
+      }
+    } else {
+      if (!KNOWN_SEARCH_SOURCES.includes(source)) {
+        throw new Error("Unknown source for search: " + source);
+      }
+      let countId = getSearchEngineId(engine) + "." + source;
+      Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").add(countId);
     }
 
-    let countId = getSearchEngineId(engine) + "." + source;
-    Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").add(countId);
+    // Dispatch the search signal to other handlers.
+    this._handleSearchAction(source, details);
+  },
+
+  _handleSearchAction(source, details) {
+    switch (source) {
+      case "urlbar":
+      case "oneoff-urlbar":
+      case "searchbar":
+      case "oneoff-searchbar":
+      case "unknown": // Edge case: this is the searchbar (see bug 1195733 comment 7).
+        this._handleSearchAndUrlbar(source, details);
+        break;
+      case "abouthome":
+        Services.telemetry.keyedScalarAdd("browser.engagement.navigation.about_home",
+                                          "search_enter", 1);
+        break;
+      case "newtab":
+        Services.telemetry.keyedScalarAdd("browser.engagement.navigation.about_newtab",
+                                          "search_enter", 1);
+        break;
+      case "contextmenu":
+        Services.telemetry.keyedScalarAdd("browser.engagement.navigation.contextmenu",
+                                          "search", 1);
+        break;
+    }
+  },
+
+  /**
+   * This function handles the "urlbar", "urlbar-oneoff", "searchbar" and
+   * "searchbar-oneoff" sources.
+   */
+  _handleSearchAndUrlbar(source, details) {
+    // We want "urlbar" and "urlbar-oneoff" (and similar cases) to go in the same
+    // scalar, but in a different key.
+
+    // When using one-offs in the searchbar we get an "unknown" source. See bug
+    // 1195733 comment 7 for the context. Fix-up the label here.
+    const plainSourceName =
+      (source === "unknown") ? "searchbar" : source.replace("oneoff-", "");
+    const scalarName = "browser.engagement.navigation." + plainSourceName;
+
+    const isOneOff = !!details.isOneOff;
+    if (isOneOff) {
+      // We will receive a signal from the "urlbar"/"searchbar" even when the
+      // search came from "oneoff-urlbar". That's because both signals
+      // are propagated from search.xml. Skip it if that's the case.
+      // Moreover, we skip the "unknown" source that comes from the searchbar
+      // when performing searches from the default search engine. See bug 1195733
+      // comment 7 for context.
+      if (["urlbar", "searchbar", "unknown"].includes(source)) {
+        return;
+      }
+
+      // If that's a legit one-off search signal, increment the scalar using the
+      // relative key.
+      Services.telemetry.keyedScalarAdd(scalarName, "search_oneoff", 1);
+      return;
+    }
+
+    // The search was not a one-off. It was a search with the default search engine.
+    if (details.isSuggestion) {
+      // It came from a suggested search, so count it as such.
+      Services.telemetry.keyedScalarAdd(scalarName, "search_suggestion", 1);
+      return;
+    } else if (details.isAlias) {
+      // This one came from a search that used an alias.
+      Services.telemetry.keyedScalarAdd(scalarName, "search_alias", 1);
+      return;
+    }
+
+    // The search signal was generated by typing something and pressing enter.
+    Services.telemetry.keyedScalarAdd(scalarName, "search_enter", 1);
   },
 
   /**
    * This gets called shortly after the SessionStore has finished restoring
    * windows and tabs. It counts the open tabs and adds listeners to all the
    * windows.
    */
   _setupAfterRestore() {
--- a/browser/modules/ContentSearch.jsm
+++ b/browser/modules/ContentSearch.jsm
@@ -250,17 +250,17 @@ this.ContentSearch = {
     } else {
       let params = {
         postData: submission.postData,
         inBackground: Services.prefs.getBoolPref("browser.tabs.loadInBackground"),
       };
       win.openUILinkIn(submission.uri.spec, where, params);
     }
     win.BrowserSearch.recordSearchInTelemetry(engine, data.healthReportKey,
-                                              data.selection || null);
+                                              { selection: data.selection });
     return;
   },
 
   getSuggestions: Task.async(function* (engineName, searchString, browser, remoteTimeout=null) {
     let engine = Services.search.getEngineByName(engineName);
     if (!engine) {
       throw new Error("Unknown engine name: " + engineName);
     }
--- a/browser/modules/test/browser.ini
+++ b/browser/modules/test/browser.ini
@@ -22,9 +22,22 @@ support-files =
   ../../components/uitour/test/uitour.html
   ../../components/uitour/UITour-lib.js
 [browser_taskbar_preview.js]
 skip-if = os != "win"
 [browser_UnsubmittedCrashHandler.js]
 run-if = crashreporter
 [browser_UsageTelemetry.js]
 [browser_UsageTelemetry_private_and_restore.js]
+[browser_UsageTelemetry_urlbar.js]
+support-files =
+  usageTelemetrySearchSuggestions.sjs
+  usageTelemetrySearchSuggestions.xml
+[browser_UsageTelemetry_searchbar.js]
+support-files =
+  usageTelemetrySearchSuggestions.sjs
+  usageTelemetrySearchSuggestions.xml
+[browser_UsageTelemetry_content.js]
+[browser_UsageTelemetry_content_aboutHome.js]
+# Disabled for intermittent failures.
+# Re-enabling this test is tracked in bug 1313825.
+skip-if = true
 [browser_urlBar_zoom.js]
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_UsageTelemetry_content.js
@@ -0,0 +1,96 @@
+"use strict";
+
+const BASE_PROBE_NAME = "browser.engagement.navigation.";
+const SCALAR_CONTEXT_MENU = BASE_PROBE_NAME + "contextmenu";
+const SCALAR_ABOUT_NEWTAB = BASE_PROBE_NAME + "about_newtab";
+
+add_task(function* setup() {
+  // Create two new search engines. Mark one as the default engine, so
+  // the test don't crash. We need to engines for this test as the searchbar
+  // in content doesn't display the default search engine among the one-off engines.
+  Services.search.addEngineWithDetails("MozSearch", "", "mozalias", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+
+  Services.search.addEngineWithDetails("MozSearch2", "", "mozalias2", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+
+  // Make the first engine the default search engine.
+  let engineDefault = Services.search.getEngineByName("MozSearch");
+  let originalEngine = Services.search.currentEngine;
+  Services.search.currentEngine = engineDefault;
+
+  // Move the second engine at the beginning of the one-off list.
+  let engineOneOff = Services.search.getEngineByName("MozSearch2");
+  Services.search.moveEngine(engineOneOff, 0);
+
+  // Make sure to restore the engine once we're done.
+  registerCleanupFunction(function* () {
+    Services.search.currentEngine = originalEngine;
+    Services.search.removeEngine(engineDefault);
+    Services.search.removeEngine(engineOneOff);
+  });
+});
+
+add_task(function* test_context_menu() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  // Open a new tab with a page containing some text.
+  let tab =
+    yield BrowserTestUtils.openNewForegroundTab(gBrowser, "data:text/plain;charset=utf8,test%20search");
+
+  info("Select all the text in the page.");
+  yield ContentTask.spawn(tab.linkedBrowser, "", function*() {
+    return new Promise(resolve => {
+      content.document.addEventListener("selectionchange", () => resolve(), { once: true });
+      content.document.getSelection().selectAllChildren(content.document.body);
+    });
+  });
+
+  info("Open the context menu.");
+  let contextMenu = document.getElementById("contentAreaContextMenu");
+  let popupPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+  BrowserTestUtils.synthesizeMouseAtCenter("body", { type: "contextmenu", button: 2 },
+                                           gBrowser.selectedBrowser);
+  yield popupPromise;
+
+  info("Click on search.");
+  let searchItem = contextMenu.getElementsByAttribute("id", "context-searchselect")[0];
+  searchItem.click();
+
+  info("Validate the search counts.");
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_CONTEXT_MENU, "search", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_CONTEXT_MENU]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  contextMenu.hidePopup();
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* test_about_newtab() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab", false);
+  yield ContentTask.spawn(tab.linkedBrowser, null, function* () {
+    yield ContentTaskUtils.waitForCondition(() => !content.document.hidden);
+  });
+
+  info("Trigger a simple serch, just text + enter.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield typeInSearchField(tab.linkedBrowser, "test query", "newtab-search-text");
+  yield BrowserTestUtils.synthesizeKey("VK_RETURN", {}, tab.linkedBrowser);
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_ABOUT_NEWTAB, "search_enter", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_ABOUT_NEWTAB]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  yield BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_UsageTelemetry_content_aboutHome.js
@@ -0,0 +1,64 @@
+"use strict";
+
+const SCALAR_ABOUT_HOME = "browser.engagement.navigation.about_home";
+
+add_task(function* setup() {
+  // about:home uses IndexedDB. However, the test finishes too quickly and doesn't
+  // allow it enougth time to save. So it throws. This disables all the uncaught
+  // exception in this file and that's the reason why we split about:home tests
+  // out of the other UsageTelemetry files.
+  ignoreAllUncaughtExceptions();
+
+  // Create two new search engines. Mark one as the default engine, so
+  // the test don't crash. We need to engines for this test as the searchbar
+  // in content doesn't display the default search engine among the one-off engines.
+  Services.search.addEngineWithDetails("MozSearch", "", "mozalias", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+
+  Services.search.addEngineWithDetails("MozSearch2", "", "mozalias2", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+
+  // Make the first engine the default search engine.
+  let engineDefault = Services.search.getEngineByName("MozSearch");
+  let originalEngine = Services.search.currentEngine;
+  Services.search.currentEngine = engineDefault;
+
+  // Move the second engine at the beginning of the one-off list.
+  let engineOneOff = Services.search.getEngineByName("MozSearch2");
+  Services.search.moveEngine(engineOneOff, 0);
+
+  // Make sure to restore the engine once we're done.
+  registerCleanupFunction(function* () {
+    Services.search.currentEngine = originalEngine;
+    Services.search.removeEngine(engineDefault);
+    Services.search.removeEngine(engineOneOff);
+  });
+});
+
+add_task(function* test_abouthome_simpleQuery() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home");
+  yield new Promise(resolve => {
+    tab.linkedBrowser.addEventListener("AboutHomeLoadSnippetsCompleted", function loadListener(event) {
+      tab.linkedBrowser.removeEventListener("AboutHomeLoadSnippetsCompleted", loadListener, true);
+      resolve();
+    }, true, true);
+  });
+
+  info("Trigger a simple serch, just test + enter.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield typeInSearchField(tab.linkedBrowser, "test query", "searchText");
+  yield BrowserTestUtils.synthesizeKey("VK_RETURN", {}, tab.linkedBrowser);
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_ABOUT_HOME, "search_enter", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_ABOUT_HOME]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  yield BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_UsageTelemetry_searchbar.js
@@ -0,0 +1,162 @@
+"use strict";
+
+const SCALAR_SEARCHBAR = "browser.engagement.navigation.searchbar";
+
+let searchInSearchbar = Task.async(function* (inputText) {
+  let win = window;
+  yield new Promise(r => waitForFocus(r, win));
+  let sb = BrowserSearch.searchBar;
+  // Write the search query in the searchbar.
+  sb.focus();
+  sb.value = inputText;
+  //sb.textbox.openPopup();
+  sb.textbox.controller.startSearch(inputText);
+  // Wait for the popup to show.
+  yield BrowserTestUtils.waitForEvent(sb.textbox.popup, "popupshown");
+  // And then for the search to complete.
+  yield BrowserTestUtils.waitForCondition(() => sb.textbox.controller.searchStatus >=
+                                                Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH,
+                                          "The search in the searchbar must complete.");
+});
+
+/**
+ * Click one of the entries in the search suggestion popup.
+ *
+ * @param {String} entryName
+ *        The name of the elemet to click on.
+ */
+function clickSearchbarSuggestion(entryName) {
+  let popup = BrowserSearch.searchBar.textbox.popup;
+  let column = popup.tree.columns[0];
+
+  for (let rowID = 0; rowID < popup.tree.view.rowCount; rowID++) {
+    const suggestion = popup.tree.view.getValueAt(rowID, column);
+    if (suggestion !== entryName) {
+      continue;
+    }
+
+    // Make sure the suggestion is visible, just in case.
+    let tbo = popup.tree.treeBoxObject;
+    tbo.ensureRowIsVisible(rowID);
+    // Calculate the click coordinates.
+    let rect = tbo.getCoordsForCellItem(rowID, column, "text");
+    let x = rect.x + rect.width / 2;
+    let y = rect.y + rect.height / 2;
+    // Simulate the click.
+    EventUtils.synthesizeMouse(popup.tree.body, x, y, {},
+                               popup.tree.ownerGlobal);
+    break;
+  }
+}
+
+add_task(function* setup() {
+  // Create two new search engines. Mark one as the default engine, so
+  // the test don't crash. We need to engines for this test as the searchbar
+  // doesn't display the default search engine among the one-off engines.
+  Services.search.addEngineWithDetails("MozSearch", "", "mozalias", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+
+  Services.search.addEngineWithDetails("MozSearch2", "", "mozalias2", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+
+  // Make the first engine the default search engine.
+  let engineDefault = Services.search.getEngineByName("MozSearch");
+  let originalEngine = Services.search.currentEngine;
+  Services.search.currentEngine = engineDefault;
+
+  // Move the second engine at the beginning of the one-off list.
+  let engineOneOff = Services.search.getEngineByName("MozSearch2");
+  Services.search.moveEngine(engineOneOff, 0);
+
+  // Make sure to restore the engine once we're done.
+  registerCleanupFunction(function* () {
+    Services.search.currentEngine = originalEngine;
+    Services.search.removeEngine(engineDefault);
+    Services.search.removeEngine(engineOneOff);
+  });
+});
+
+add_task(function* test_plainQuery() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+  info("Simulate entering a simple search.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield searchInSearchbar("simple query");
+  EventUtils.sendKey("return");
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_SEARCHBAR, "search_enter", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_SEARCHBAR]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* test_oneOff() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+  info("Perform a one-off search using the first engine.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield searchInSearchbar("query");
+
+  info("Pressing Alt+Down to highlight the first one off engine.");
+  EventUtils.synthesizeKey("VK_DOWN", { altKey: true });
+  EventUtils.sendKey("return");
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_SEARCHBAR, "search_oneoff", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_SEARCHBAR]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* test_suggestion() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  // Create an engine to generate search suggestions and add it as default
+  // for this test.
+  const url = getRootDirectory(gTestPath) + "usageTelemetrySearchSuggestions.xml";
+  let suggestionEngine = yield new Promise((resolve, reject) => {
+    Services.search.addEngine(url, null, "", false, {
+      onSuccess(engine) { resolve(engine) },
+      onError() { reject() }
+    });
+  });
+
+  let previousEngine = Services.search.currentEngine;
+  Services.search.currentEngine = suggestionEngine;
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+  info("Perform a one-off search using the first engine.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield searchInSearchbar("query");
+  info("Clicking the searchbar suggestion.");
+  clickSearchbarSuggestion("queryfoo");
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_SEARCHBAR, "search_suggestion", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_SEARCHBAR]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  Services.search.currentEngine = previousEngine;
+  Services.search.removeEngine(suggestionEngine);
+  yield BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_UsageTelemetry_urlbar.js
@@ -0,0 +1,171 @@
+"use strict";
+
+const SCALAR_URLBAR = "browser.engagement.navigation.urlbar";
+
+// The preference to enable suggestions in the urlbar.
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+// The name of the search engine used to generate suggestions.
+const SUGGESTION_ENGINE_NAME = "browser_UsageTelemetry usageTelemetrySearchSuggestions.xml";
+
+let searchInAwesomebar = Task.async(function* (inputText, win=window) {
+  yield new Promise(r => waitForFocus(r, win));
+  // Write the search query in the urlbar.
+  win.gURLBar.focus();
+  win.gURLBar.value = inputText;
+  win.gURLBar.controller.startSearch(inputText);
+  // Wait for the popup to show.
+  yield BrowserTestUtils.waitForEvent(win.gURLBar.popup, "popupshown");
+  // And then for the search to complete.
+  yield BrowserTestUtils.waitForCondition(() => win.gURLBar.controller.searchStatus >=
+                                                Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
+});
+
+/**
+ * Click one of the entries in the urlbar suggestion popup.
+ *
+ * @param {String} entryName
+ *        The name of the elemet to click on.
+ */
+function clickURLBarSuggestion(entryName) {
+  // The entry in the suggestion list should follow the format:
+  // "<search term> <engine name> Search"
+  const expectedSuggestionName = entryName + " " + SUGGESTION_ENGINE_NAME + " Search";
+  for (let child of gURLBar.popup.richlistbox.children) {
+    if (child.label === expectedSuggestionName) {
+      // This entry is the search suggestion we're looking for.
+      child.click();
+      return;
+    }
+  }
+}
+
+add_task(function* setup() {
+  // Create a new search engine.
+  Services.search.addEngineWithDetails("MozSearch", "", "mozalias", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+
+  // Make it the default search engine.
+  let engine = Services.search.getEngineByName("MozSearch");
+  let originalEngine = Services.search.currentEngine;
+  Services.search.currentEngine = engine;
+
+  // And the first one-off engine.
+  Services.search.moveEngine(engine, 0);
+
+  // Enable search suggestions in the urlbar.
+  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+
+  // Make sure to restore the engine once we're done.
+  registerCleanupFunction(function* () {
+    Services.search.currentEngine = originalEngine;
+    Services.search.removeEngine(engine);
+    Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF, true);
+  });
+});
+
+add_task(function* test_simpleQuery() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+  info("Simulate entering a simple search.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield searchInAwesomebar("simple query");
+  EventUtils.sendKey("return");
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_URLBAR, "search_enter", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* test_searchAlias() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+  info("Search using a search alias.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield searchInAwesomebar("mozalias query");
+  EventUtils.sendKey("return");
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_URLBAR, "search_alias", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* test_oneOff() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+  info("Perform a one-off search using the first engine.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield searchInAwesomebar("query");
+
+  info("Pressing Alt+Down to take us to the first one-off engine.");
+  EventUtils.synthesizeKey("VK_DOWN", { altKey: true });
+  EventUtils.sendKey("return");
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_URLBAR, "search_oneoff", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* test_suggestion() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  // Create an engine to generate search suggestions and add it as default
+  // for this test.
+  const url = getRootDirectory(gTestPath) + "usageTelemetrySearchSuggestions.xml";
+  let suggestionEngine = yield new Promise((resolve, reject) => {
+    Services.search.addEngine(url, null, "", false, {
+      onSuccess(engine) { resolve(engine) },
+      onError() { reject() }
+    });
+  });
+
+  let previousEngine = Services.search.currentEngine;
+  Services.search.currentEngine = suggestionEngine;
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+  info("Perform a one-off search using the first engine.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield searchInAwesomebar("query");
+  info("Clicking the urlbar suggestion.");
+  clickURLBarSuggestion("queryfoo");
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_URLBAR, "search_suggestion", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  Services.search.currentEngine = previousEngine;
+  Services.search.removeEngine(suggestionEngine);
+  yield BrowserTestUtils.removeTab(tab);
+});
--- a/browser/modules/test/head.js
+++ b/browser/modules/test/head.js
@@ -27,8 +27,53 @@ function waitForConditionPromise(conditi
   return defer.promise;
 }
 
 function waitForCondition(condition, nextTest, errorMsg) {
   waitForConditionPromise(condition, errorMsg).then(nextTest, (reason) => {
     ok(false, reason + (reason.stack ? "\n" + reason.stack : ""));
   });
 }
+
+/**
+ * Checks if the snapshotted keyed scalars contain the expected
+ * data.
+ *
+ * @param {Object} scalars
+ *        The snapshot of the keyed scalars.
+ * @param {String} scalarName
+ *        The name of the keyed scalar to check.
+ * @param {String} key
+ *        The key that must be within the keyed scalar.
+ * @param {String|Boolean|Number} expectedValue
+ *        The expected value for the provided key in the scalar.
+ */
+function checkKeyedScalar(scalars, scalarName, key, expectedValue) {
+  Assert.ok(scalarName in scalars,
+            scalarName + " must be recorded.");
+  Assert.ok(key in scalars[scalarName],
+            scalarName + " must contain the '" + key + "' key.");
+  Assert.ok(scalars[scalarName][key], expectedValue,
+            scalarName + "['" + key + "'] must contain the expected value");
+}
+
+/**
+ * An utility function to write some text in the search input box
+ * in a content page.
+ * @param {Object} browser
+ *        The browser that contains the content.
+ * @param {String} text
+ *        The string to write in the search field.
+ * @param {String} fieldName
+ *        The name of the field to write to.
+ */
+let typeInSearchField = Task.async(function* (browser, text, fieldName) {
+  yield ContentTask.spawn(browser, { fieldName, text }, function* ({fieldName, text}) {
+    // Avoid intermittent failures.
+    if (fieldName === "searchText") {
+      content.wrappedJSObject.gContentSearchController.remoteTimeout = 5000;
+    }
+    // Put the focus on the search box.
+    let searchInput = content.document.getElementById(fieldName);
+    searchInput.focus();
+    searchInput.value = text;
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/usageTelemetrySearchSuggestions.sjs
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(req, resp) {
+  let suffixes = ["foo", "bar"];
+  let data = [req.queryString, suffixes.map(s => req.queryString + s)];
+  resp.setHeader("Content-Type", "application/json", false);
+  resp.write(JSON.stringify(data));
+}
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/usageTelemetrySearchSuggestions.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_UsageTelemetry usageTelemetrySearchSuggestions.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/modules/test/usageTelemetrySearchSuggestions.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://example.com" rel="searchform"/>
+</SearchPlugin>
--- a/devtools/client/devtools-startup.js
+++ b/devtools/client/devtools-startup.js
@@ -193,22 +193,23 @@ DevToolsStartup.prototype = {
       dump("Unable to start debugger server on " + portOrPath + ": " + e);
     }
 
     if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
       cmdLine.preventDefault = true;
     }
   },
 
+  /* eslint-disable max-len */
   helpInfo: "  --jsconsole        Open the Browser Console.\n" +
             "  --jsdebugger       Open the Browser Toolbox.\n" +
             "  --devtools         Open DevTools on initial load.\n" +
-            "  --start-debugger-server [ws:][ <port> | <path> ] " +
-            "Start the debugger server on a TCP port or " +
-            "Unix domain socket path.  Defaults to TCP port 6000. " +
-            "Use WebSocket protocol if ws: prefix is specified.\n",
+            "  --start-debugger-server [ws:][ <port> | <path> ] Start the debugger server on\n" +
+            "                     a TCP port or Unix domain socket path. Defaults to TCP port\n" +
+            "                     6000. Use WebSocket protocol if ws: prefix is specified.\n",
+  /* eslint-disable max-len */
 
   classID: Components.ID("{9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]),
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(
   [DevToolsStartup]);
new file mode 100644
--- /dev/null
+++ b/dom/base/DOMIntersectionObserver.cpp
@@ -0,0 +1,458 @@
+/* -*- 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 "DOMIntersectionObserver.h"
+#include "nsCSSParser.h"
+#include "nsCSSPropertyID.h"
+#include "nsIFrame.h"
+#include "nsContentUtils.h"
+#include "nsLayoutUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserverEntry)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserverEntry)
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DOMIntersectionObserverEntry, mOwner,
+                                      mRootBounds, mBoundingClientRect,
+                                      mIntersectionRect, mTarget)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserver)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+  NS_INTERFACE_MAP_ENTRY(DOMIntersectionObserver)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserver)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserver)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(DOMIntersectionObserver)
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(DOMIntersectionObserver)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DOMIntersectionObserver)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoot)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DOMIntersectionObserver)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+already_AddRefed<DOMIntersectionObserver>
+DOMIntersectionObserver::Constructor(const mozilla::dom::GlobalObject& aGlobal,
+                                     mozilla::dom::IntersectionCallback& aCb,
+                                     mozilla::ErrorResult& aRv)
+{
+  return Constructor(aGlobal, aCb, IntersectionObserverInit(), aRv);
+}
+
+already_AddRefed<DOMIntersectionObserver>
+DOMIntersectionObserver::Constructor(const mozilla::dom::GlobalObject& aGlobal,
+                                     mozilla::dom::IntersectionCallback& aCb,
+                                     const mozilla::dom::IntersectionObserverInit& aOptions,
+                                     mozilla::ErrorResult& aRv)
+{
+  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal.GetAsSupports());
+  if (!window) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+  RefPtr<DOMIntersectionObserver> observer =
+    new DOMIntersectionObserver(window.forget(), aCb);
+
+  observer->mRoot = aOptions.mRoot;
+
+  if (!observer->SetRootMargin(aOptions.mRootMargin)) {
+    aRv.ThrowDOMException(NS_ERROR_DOM_SYNTAX_ERR,
+      NS_LITERAL_CSTRING("rootMargin must be specified in pixels or percent."));
+    return nullptr;
+  }
+
+  if (aOptions.mThreshold.IsDoubleSequence()) {
+    const mozilla::dom::Sequence<double>& thresholds = aOptions.mThreshold.GetAsDoubleSequence();
+    observer->mThresholds.SetCapacity(thresholds.Length());
+    for (const auto& thresh : thresholds) {
+      if (thresh < 0.0 || thresh > 1.0) {
+        aRv.ThrowTypeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
+        return nullptr;
+      }
+      observer->mThresholds.AppendElement(thresh);
+    }
+    observer->mThresholds.Sort();
+  } else {
+    double thresh = aOptions.mThreshold.GetAsDouble();
+    if (thresh < 0.0 || thresh > 1.0) {
+      aRv.ThrowTypeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
+      return nullptr;
+    }
+    observer->mThresholds.AppendElement(thresh);
+  }
+
+  return observer.forget();
+}
+
+bool
+DOMIntersectionObserver::SetRootMargin(const nsAString& aString)
+{
+  // By not passing a CSS Loader object we make sure we don't parse in quirks
+  // mode so that pixel/percent and unit-less values will be differentiated.
+  nsCSSParser parser(nullptr);
+  nsCSSValue value;
+  if (!parser.ParseMarginString(aString, nullptr, 0, value, true)) {
+    return false;
+  }
+
+  mRootMargin = value.GetRectValue();
+
+  for (uint32_t i = 0; i < ArrayLength(nsCSSRect::sides); ++i) {
+    nsCSSValue value = mRootMargin.*nsCSSRect::sides[i];
+    if (!(value.IsPixelLengthUnit() || value.IsPercentLengthUnit())) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+void
+DOMIntersectionObserver::GetRootMargin(mozilla::dom::DOMString& aRetVal)
+{
+  mRootMargin.AppendToString(eCSSProperty_DOM, aRetVal, nsCSSValue::eNormalized);
+}
+
+void
+DOMIntersectionObserver::GetThresholds(nsTArray<double>& aRetVal)
+{
+  aRetVal = mThresholds;
+}
+
+void
+DOMIntersectionObserver::Observe(Element& aTarget)
+{
+  if (mObservationTargets.Contains(&aTarget)) {
+    return;
+  }
+  aTarget.RegisterIntersectionObserver(this);
+  mObservationTargets.PutEntry(&aTarget);
+  Connect();
+}
+
+void
+DOMIntersectionObserver::Unobserve(Element& aTarget)
+{
+  if (!mObservationTargets.Contains(&aTarget)) {
+    return;
+  }
+  if (mObservationTargets.Count() == 1) {
+    Disconnect();
+    return;
+  }
+  aTarget.UnregisterIntersectionObserver(this);
+  mObservationTargets.RemoveEntry(&aTarget);
+}
+
+void
+DOMIntersectionObserver::Connect()
+{
+  if (mConnected) {
+    return;
+  }
+  nsIDocument* document = mOwner->GetExtantDoc();
+  document->AddIntersectionObserver(this);
+  mConnected = true;
+}
+
+void
+DOMIntersectionObserver::Disconnect()
+{
+  if (!mConnected) {
+    return;
+  }
+  for (auto iter = mObservationTargets.Iter(); !iter.Done(); iter.Next()) {
+    Element* target = iter.Get()->GetKey();
+    target->UnregisterIntersectionObserver(this);
+  }
+  mObservationTargets.Clear();
+  nsIDocument* document = mOwner->GetExtantDoc();
+  document->RemoveIntersectionObserver(this);
+  mConnected = false;
+}
+
+void
+DOMIntersectionObserver::TakeRecords(nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal)
+{
+  aRetVal.SwapElements(mQueuedEntries);
+  mQueuedEntries.Clear();
+}
+
+static bool
+CheckSimilarOrigin(nsINode* aNode1, nsINode* aNode2)
+{
+  nsIPrincipal* principal1 = aNode1->NodePrincipal();
+  nsIPrincipal* principal2 = aNode2->NodePrincipal();
+  nsAutoCString baseDomain1;
+  nsAutoCString baseDomain2;
+
+  nsresult rv = principal1->GetBaseDomain(baseDomain1);
+  if (NS_FAILED(rv)) {
+    return principal1 == principal2;
+  }
+
+  rv = principal2->GetBaseDomain(baseDomain2);
+  if (NS_FAILED(rv)) {
+    return principal1 == principal2;
+  }
+
+  return baseDomain1 == baseDomain2;
+}
+
+static Maybe<nsRect>
+EdgeInclusiveIntersection(const nsRect& aRect, const nsRect& aOtherRect)
+{
+  nscoord left = std::max(aRect.x, aOtherRect.x);
+  nscoord top = std::max(aRect.y, aOtherRect.y);
+  nscoord right = std::min(aRect.XMost(), aOtherRect.XMost());
+  nscoord bottom = std::min(aRect.YMost(), aOtherRect.YMost());
+  if (left > right || top > bottom) {
+    return Nothing();
+  }
+  return Some(nsRect(left, top, right - left, bottom - top));
+}
+
+void
+DOMIntersectionObserver::Update(nsIDocument* aDocument, DOMHighResTimeStamp time)
+{
+  Element* root;
+  nsIFrame* rootFrame;
+  nsRect rootRect;
+
+  if (mRoot) {
+    root = mRoot;
+    rootFrame = root->GetPrimaryFrame();
+    if (rootFrame) {
+      if (rootFrame->GetType() == nsGkAtoms::scrollFrame) {
+        nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
+        rootRect = nsLayoutUtils::TransformFrameRectToAncestor(
+          rootFrame,
+          rootFrame->GetContentRectRelativeToSelf(),
+          scrollFrame->GetScrolledFrame());
+      } else {
+        rootRect = nsLayoutUtils::GetAllInFlowRectsUnion(rootFrame,
+          nsLayoutUtils::GetContainingBlockForClientRect(rootFrame),
+          nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
+      }
+    }
+  } else {
+    nsCOMPtr<nsIPresShell> presShell = aDocument->GetShell();
+    if (presShell) {
+      rootFrame = presShell->GetRootScrollFrame();
+      nsPresContext* presContext = rootFrame->PresContext();
+      while (!presContext->IsRootContentDocument()) {
+        presContext = rootFrame->PresContext()->GetParentPresContext();
+        rootFrame = presContext->PresShell()->GetRootScrollFrame();
+      }
+      root = rootFrame->GetContent()->AsElement();
+      nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
+      rootRect = scrollFrame->GetScrollPortRect();
+    }
+  }
+
+  nsMargin rootMargin;
+  NS_FOR_CSS_SIDES(side) {
+    nscoord basis = side == NS_SIDE_TOP || side == NS_SIDE_BOTTOM ?
+      rootRect.height : rootRect.width;
+    nsCSSValue value = mRootMargin.*nsCSSRect::sides[side];
+    nsStyleCoord coord;
+    if (value.IsPixelLengthUnit()) {
+      coord.SetCoordValue(value.GetPixelLength());
+    } else if (value.IsPercentLengthUnit()) {
+      coord.SetPercentValue(value.GetPercentValue());
+    } else {
+      MOZ_ASSERT_UNREACHABLE("invalid length unit");
+    }
+    rootMargin.Side(side) = nsLayoutUtils::ComputeCBDependentValue(basis, coord);
+  }
+
+  for (auto iter = mObservationTargets.Iter(); !iter.Done(); iter.Next()) {
+    Element* target = iter.Get()->GetKey();
+    nsIFrame* targetFrame = target->GetPrimaryFrame();
+    nsRect targetRect;
+    Maybe<nsRect> intersectionRect;
+
+    if (rootFrame && targetFrame) {
+      // If mRoot is set we are testing intersection with a container element
+      // instead of the implicit root.
+      if (mRoot) {
+        // Skip further processing of this target if it is not in the same
+        // Document as the intersection root, e.g. if root is an element of
+        // the main document and target an element from an embedded iframe.
+        if (target->GetComposedDoc() != root->GetComposedDoc()) {
+          continue;
+        }
+        // Skip further processing of this target if is not a descendant of the
+        // intersection root in the containing block chain. E.g. this would be
+        // the case if the target is in a position:absolute element whose
+        // containing block is an ancestor of root.
+        if (!nsLayoutUtils::IsAncestorFrameCrossDoc(rootFrame, targetFrame)) {
+          continue;
+        }
+      }
+
+      targetRect = nsLayoutUtils::GetAllInFlowRectsUnion(
+        targetFrame,
+        nsLayoutUtils::GetContainingBlockForClientRect(targetFrame),
+        nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS
+      );
+      intersectionRect = Some(targetFrame->GetVisualOverflowRect());
+
+      nsIFrame* containerFrame = nsLayoutUtils::GetCrossDocParentFrame(targetFrame);
+      while (containerFrame && containerFrame != rootFrame) {
+        if (containerFrame->GetType() == nsGkAtoms::scrollFrame) {
+          nsIScrollableFrame* scrollFrame = do_QueryFrame(containerFrame);
+          nsRect subFrameRect = scrollFrame->GetScrollPortRect();
+          nsRect intersectionRectRelativeToContainer =
+            nsLayoutUtils::TransformFrameRectToAncestor(targetFrame,
+                                                        intersectionRect.value(),
+                                                        containerFrame);
+          intersectionRect = EdgeInclusiveIntersection(intersectionRectRelativeToContainer,
+                                                       subFrameRect);
+          if (!intersectionRect) {
+            break;
+          }
+          targetFrame = containerFrame;
+        }
+
+        // TODO: Apply clip-path.
+
+        containerFrame = nsLayoutUtils::GetCrossDocParentFrame(containerFrame);
+      }
+    }
+
+    nsRect rootIntersectionRect = rootRect;
+    bool isInSimilarOriginBrowsingContext = CheckSimilarOrigin(root, target);
+
+    if (isInSimilarOriginBrowsingContext) {
+      rootIntersectionRect.Inflate(rootMargin);
+    }
+
+    if (intersectionRect.isSome()) {
+      nsRect intersectionRectRelativeToRoot =
+        nsLayoutUtils::TransformFrameRectToAncestor(
+          targetFrame,
+          intersectionRect.value(),
+          nsLayoutUtils::GetContainingBlockForClientRect(rootFrame)
+      );
+      intersectionRect = EdgeInclusiveIntersection(
+        intersectionRectRelativeToRoot,
+        rootIntersectionRect
+      );
+      if (intersectionRect.isSome()) {
+        intersectionRect = Some(nsLayoutUtils::TransformFrameRectToAncestor(
+          nsLayoutUtils::GetContainingBlockForClientRect(rootFrame),
+          intersectionRect.value(),
+          targetFrame->PresContext()->PresShell()->GetRootScrollFrame()
+        ));
+      }
+    }
+
+    double targetArea = targetRect.width * targetRect.height;
+    double intersectionArea = !intersectionRect ?
+      0 : intersectionRect->width * intersectionRect->height;
+    double intersectionRatio = targetArea > 0.0 ? intersectionArea / targetArea : 0.0;
+
+    size_t threshold = -1;
+    if (intersectionRatio > 0.0) {
+      if (intersectionRatio >= 1.0) {
+        intersectionRatio = 1.0;
+        threshold = mThresholds.Length();
+      } else {
+        for (size_t k = 0; k < mThresholds.Length(); ++k) {
+          if (mThresholds[k] <= intersectionRatio) {
+            threshold = k + 1;
+          } else {
+            break;
+          }
+        }
+      }
+    } else if (intersectionRect.isSome()) {
+      threshold = 0;
+    }
+
+    if (target->UpdateIntersectionObservation(this, threshold)) {
+      QueueIntersectionObserverEntry(
+        target, time,
+        isInSimilarOriginBrowsingContext ? Some(rootIntersectionRect) : Nothing(),
+        targetRect, intersectionRect, intersectionRatio
+      );
+    }
+  }
+}
+
+void
+DOMIntersectionObserver::QueueIntersectionObserverEntry(Element* aTarget,
+                                                        DOMHighResTimeStamp time,
+                                                        const Maybe<nsRect>& aRootRect,
+                                                        const nsRect& aTargetRect,
+                                                        const Maybe<nsRect>& aIntersectionRect,
+                                                        double aIntersectionRatio)
+{
+  RefPtr<DOMRect> rootBounds;
+  if (aRootRect.isSome()) {
+    rootBounds = new DOMRect(this);
+    rootBounds->SetLayoutRect(aRootRect.value());
+  }
+  RefPtr<DOMRect> boundingClientRect = new DOMRect(this);
+  boundingClientRect->SetLayoutRect(aTargetRect);
+  RefPtr<DOMRect> intersectionRect = new DOMRect(this);
+  if (aIntersectionRect.isSome()) {
+    intersectionRect->SetLayoutRect(aIntersectionRect.value());
+  }
+  RefPtr<DOMIntersectionObserverEntry> entry = new DOMIntersectionObserverEntry(
+    this,
+    time,
+    rootBounds.forget(),
+    boundingClientRect.forget(),
+    intersectionRect.forget(),
+    aTarget, aIntersectionRatio);
+  mQueuedEntries.AppendElement(entry.forget());
+}
+
+void
+DOMIntersectionObserver::Notify()
+{
+  if (!mQueuedEntries.Length()) {
+    return;
+  }
+  mozilla::dom::Sequence<mozilla::OwningNonNull<DOMIntersectionObserverEntry>> entries;
+  if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) {
+    for (uint32_t i = 0; i < mQueuedEntries.Length(); ++i) {
+      RefPtr<DOMIntersectionObserverEntry> next = mQueuedEntries[i];
+      *entries.AppendElement(mozilla::fallible) = next;
+    }
+  }
+  mQueuedEntries.Clear();
+  mCallback->Call(this, entries, *this);
+}
+
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/base/DOMIntersectionObserver.h
@@ -0,0 +1,179 @@
+/* -*- 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 DOMIntersectionObserver_h
+#define DOMIntersectionObserver_h
+
+#include "mozilla/dom/IntersectionObserverBinding.h"
+#include "nsCSSValue.h"
+#include "nsTArray.h"
+
+using mozilla::dom::DOMRect;
+using mozilla::dom::Element;
+
+namespace mozilla {
+namespace dom {
+
+class DOMIntersectionObserver;
+
+class DOMIntersectionObserverEntry final : public nsISupports,
+                                           public nsWrapperCache
+{
+  ~DOMIntersectionObserverEntry() {}
+
+public:
+  DOMIntersectionObserverEntry(nsISupports* aOwner,
+                               DOMHighResTimeStamp aTime,
+                               RefPtr<DOMRect> aRootBounds,
+                               RefPtr<DOMRect> aBoundingClientRect,
+                               RefPtr<DOMRect> aIntersectionRect,
+                               Element* aTarget,
+                               double aIntersectionRatio)
+  : mOwner(aOwner),
+    mTime(aTime),
+    mRootBounds(aRootBounds),
+    mBoundingClientRect(aBoundingClientRect),
+    mIntersectionRect(aIntersectionRect),
+    mTarget(aTarget),
+    mIntersectionRatio(aIntersectionRatio)
+  {
+  }
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(DOMIntersectionObserverEntry)
+
+  nsISupports* GetParentObject() const
+  {
+    return mOwner;
+  }
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
+  {
+    return mozilla::dom::IntersectionObserverEntryBinding::Wrap(aCx, this, aGivenProto);
+  }
+
+  DOMHighResTimeStamp Time()
+  {
+    return mTime;
+  }
+
+  DOMRect* GetRootBounds()
+  {
+    return mRootBounds;
+  }
+
+  DOMRect* BoundingClientRect()
+  {
+    return mBoundingClientRect;
+  }
+
+  DOMRect* IntersectionRect()
+  {
+    return mIntersectionRect;
+  }
+
+  double IntersectionRatio()
+  {
+    return mIntersectionRatio;
+  }
+
+  Element* Target()
+  {
+    return mTarget;
+  }
+
+protected:
+  nsCOMPtr<nsISupports> mOwner;
+  DOMHighResTimeStamp   mTime;
+  RefPtr<DOMRect>       mRootBounds;
+  RefPtr<DOMRect>       mBoundingClientRect;
+  RefPtr<DOMRect>       mIntersectionRect;
+  RefPtr<Element>       mTarget;
+  double                mIntersectionRatio;
+};
+
+#define NS_DOM_INTERSECTION_OBSERVER_IID \
+{ 0x8570a575, 0xe303, 0x4d18, \
+  { 0xb6, 0xb1, 0x4d, 0x2b, 0x49, 0xd8, 0xef, 0x94 } }
+
+class DOMIntersectionObserver final : public nsISupports,
+                                      public nsWrapperCache
+{
+  virtual ~DOMIntersectionObserver() { }
+
+public:
+  DOMIntersectionObserver(already_AddRefed<nsPIDOMWindowInner>&& aOwner,
+                          mozilla::dom::IntersectionCallback& aCb)
+  : mOwner(aOwner), mCallback(&aCb), mConnected(false)
+  {
+  }
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(DOMIntersectionObserver)
+  NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOM_INTERSECTION_OBSERVER_IID)
+
+  static already_AddRefed<DOMIntersectionObserver>
+  Constructor(const mozilla::dom::GlobalObject& aGlobal,
+              mozilla::dom::IntersectionCallback& aCb,
+              mozilla::ErrorResult& aRv);
+  static already_AddRefed<DOMIntersectionObserver>
+  Constructor(const mozilla::dom::GlobalObject& aGlobal,
+              mozilla::dom::IntersectionCallback& aCb,
+              const mozilla::dom::IntersectionObserverInit& aOptions,
+              mozilla::ErrorResult& aRv);
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
+  {
+    return mozilla::dom::IntersectionObserverBinding::Wrap(aCx, this, aGivenProto);
+  }
+
+  nsISupports* GetParentObject() const
+  {
+    return mOwner;
+  }
+
+  Element* GetRoot() const {
+    return mRoot;
+  }
+
+  void GetRootMargin(mozilla::dom::DOMString& aRetVal);
+  void GetThresholds(nsTArray<double>& aRetVal);
+  void Observe(Element& aTarget);
+  void Unobserve(Element& aTarget);
+
+  void Disconnect();
+  void TakeRecords(nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal);
+
+  mozilla::dom::IntersectionCallback* IntersectionCallback() { return mCallback; }
+
+  bool SetRootMargin(const nsAString& aString);
+
+  void Update(nsIDocument* aDocument, DOMHighResTimeStamp time);
+  void Notify();
+
+protected:
+  void Connect();
+  void QueueIntersectionObserverEntry(Element* aTarget,
+                                      DOMHighResTimeStamp time,
+                                      const Maybe<nsRect>& aRootRect,
+                                      const nsRect& aTargetRect,
+                                      const Maybe<nsRect>& aIntersectionRect,
+                                      double aIntersectionRatio);
+
+  nsCOMPtr<nsPIDOMWindowInner>                    mOwner;
+  RefPtr<mozilla::dom::IntersectionCallback>      mCallback;
+  RefPtr<Element>                                 mRoot;
+  nsCSSRect                                       mRootMargin;
+  nsTArray<double>                                mThresholds;
+  nsTHashtable<nsPtrHashKey<Element>>             mObservationTargets;
+  nsTArray<RefPtr<DOMIntersectionObserverEntry>>  mQueuedEntries;
+  bool                                            mConnected;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(DOMIntersectionObserver, NS_DOM_INTERSECTION_OBSERVER_IID)
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -144,16 +144,17 @@
 #include "mozilla/dom/KeyframeEffectBinding.h"
 #include "mozilla/dom/WindowBinding.h"
 #include "mozilla/dom/ElementBinding.h"
 #include "mozilla/dom/VRDisplay.h"
 #include "mozilla/IntegerPrintfMacros.h"
 #include "mozilla/Preferences.h"
 #include "nsComputedDOMStyle.h"
 #include "nsDOMStringMap.h"
+#include "DOMIntersectionObserver.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 nsIAtom*
 nsIContent::DoGetID() const
 {
   MOZ_ASSERT(HasID(), "Unexpected call");
@@ -3881,8 +3882,49 @@ Element::ClearDataset()
 {
   nsDOMSlots *slots = GetExistingDOMSlots();
 
   MOZ_ASSERT(slots && slots->mDataset,
              "Slots should exist and dataset should not be null.");
   slots->mDataset = nullptr;
 }
 
+nsTArray<Element::nsDOMSlots::IntersectionObserverRegistration>*
+Element::RegisteredIntersectionObservers()
+{
+  nsDOMSlots* slots = DOMSlots();
+  return &slots->mRegisteredIntersectionObservers;
+}
+
+void
+Element::RegisterIntersectionObserver(DOMIntersectionObserver* aObserver)
+{
+  RegisteredIntersectionObservers()->AppendElement(
+    nsDOMSlots::IntersectionObserverRegistration { aObserver, -1 });
+}
+
+void
+Element::UnregisterIntersectionObserver(DOMIntersectionObserver* aObserver)
+{
+  nsTArray<nsDOMSlots::IntersectionObserverRegistration>* observers =
+    RegisteredIntersectionObservers();
+  for (uint32_t i = 0; i < observers->Length(); ++i) {
+    nsDOMSlots::IntersectionObserverRegistration reg = observers->ElementAt(i);
+    if (reg.observer == aObserver) {
+      observers->RemoveElementAt(i);
+      break;
+    }
+  }
+}
+
+bool
+Element::UpdateIntersectionObservation(DOMIntersectionObserver* aObserver, int32_t aThreshold)
+{
+  nsTArray<nsDOMSlots::IntersectionObserverRegistration>* observers =
+    RegisteredIntersectionObservers();
+  for (auto& reg : *observers) {
+    if (reg.observer == aObserver && reg.previousThreshold != aThreshold) {
+      reg.previousThreshold = aThreshold;
+      return true;
+    }
+  }
+  return false;
+}
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -34,16 +34,17 @@
 #include "nsAttrValue.h"
 #include "mozilla/EventForwards.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/DOMTokenListSupportedTokens.h"
 #include "mozilla/dom/WindowBinding.h"
 #include "mozilla/dom/ElementBinding.h"
 #include "mozilla/dom/Nullable.h"
 #include "Units.h"
+#include "DOMIntersectionObserver.h"
 
 class nsIFrame;
 class nsIDOMMozNamedAttrMap;
 class nsIURI;
 class nsIScrollableFrame;
 class nsAttrValueOrString;
 class nsContentList;
 class nsDOMTokenList;
@@ -56,16 +57,17 @@ class nsDocument;
 class nsDOMStringMap;
 
 namespace mozilla {
 class DeclarationBlock;
 namespace dom {
   struct AnimationFilter;
   struct ScrollIntoViewOptions;
   struct ScrollToOptions;
+  class DOMIntersectionObserver;
   class ElementOrCSSPseudoElement;
   class UnrestrictedDoubleOrKeyframeAnimationOptions;
 } // namespace dom
 } // namespace mozilla
 
 
 already_AddRefed<nsContentList>
 NS_GetContentList(nsINode* aRootNode,
@@ -1144,16 +1146,20 @@ public:
    * sorts of elements expose it to JS as a .dataset property
    */
   // Getter, to be called from bindings.
   already_AddRefed<nsDOMStringMap> Dataset();
   // Callback for destructor of dataset to ensure to null out our weak pointer
   // to it.
   void ClearDataset();
 
+  void RegisterIntersectionObserver(DOMIntersectionObserver* aObserver);
+  void UnregisterIntersectionObserver(DOMIntersectionObserver* aObserver);
+  bool UpdateIntersectionObservation(DOMIntersectionObserver* aObserver, int32_t threshold);
+
 protected:
   /*
    * Named-bools for use with SetAttrAndNotify to make call sites easier to
    * read.
    */
   static const bool kFireMutationEvent           = true;
   static const bool kDontFireMutationEvent       = false;
   static const bool kNotifyDocumentObservers     = true;
@@ -1357,16 +1363,18 @@ protected:
    * the value of xlink:show, converted to a suitably equivalent named target
    * (e.g. _blank).
    */
   virtual void GetLinkTarget(nsAString& aTarget);
 
   nsDOMTokenList* GetTokenList(nsIAtom* aAtom,
                                const DOMTokenListSupportedTokenArray aSupportedTokens = nullptr);
 
+  nsTArray<nsDOMSlots::IntersectionObserverRegistration>* RegisteredIntersectionObservers();
+
 private:
   /**
    * Get this element's client area rect in app units.
    * @return the frame's client area
    */
   nsRect GetClientAreaRect();
 
   nsIScrollableFrame* GetScrollFrame(nsIFrame **aStyledFrame = nullptr,
--- a/dom/base/FragmentOrElement.h
+++ b/dom/base/FragmentOrElement.h
@@ -32,16 +32,17 @@ class nsIDocument;
 class nsDOMStringMap;
 class nsIURI;
 
 namespace mozilla {
 namespace css {
 class Declaration;
 } // namespace css
 namespace dom {
+class DOMIntersectionObserver;
 class Element;
 } // namespace dom
 } // namespace mozilla
 
 /**
  * A class that implements nsIWeakReference
  */
 
@@ -339,16 +340,26 @@ public:
      * XBL binding installed on the lement.
      */
     nsCOMPtr<nsIContent> mXBLInsertionParent;
 
     /**
      * Web components custom element data.
      */
     RefPtr<CustomElementData> mCustomElementData;
+
+    /**
+     * Registered Intersection Observers on the element.
+     */
+    struct IntersectionObserverRegistration {
+      DOMIntersectionObserver* observer;
+      int32_t previousThreshold;
+    };
+
+    nsTArray<IntersectionObserverRegistration> mRegisteredIntersectionObservers;
   };
 
 protected:
   void GetMarkup(bool aIncludeSelf, nsAString& aMarkup);
   void SetInnerHTMLInternal(const nsAString& aInnerHTML, ErrorResult& aError);
 
   // Override from nsINode
   virtual nsINode::nsSlots* CreateSlots() override;
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -1269,25 +1269,25 @@ Navigator::SendBeacon(const nsAString& a
 
   nsCOMPtr<nsIURI> uri;
   nsresult rv = nsContentUtils::NewURIWithDocumentCharset(
                   getter_AddRefs(uri),
                   aUrl,
                   doc,
                   doc->GetDocBaseURI());
   if (NS_FAILED(rv)) {
-    aRv.Throw(NS_ERROR_DOM_URL_MISMATCH_ERR);
+    aRv.ThrowTypeError<MSG_INVALID_URL>(aUrl);
     return false;
   }
 
-  // Explicitly disallow loading data: URIs
-  bool isDataScheme = false;
-  rv = uri->SchemeIs("data", &isDataScheme);
-  if (NS_FAILED(rv) || isDataScheme) {
-    aRv.Throw(NS_ERROR_CONTENT_BLOCKED);
+  // Spec disallows any schemes save for HTTP/HTTPs
+  bool isValidScheme;
+  if (!(NS_SUCCEEDED(uri->SchemeIs("http", &isValidScheme)) && isValidScheme) &&
+      !(NS_SUCCEEDED(uri->SchemeIs("https", &isValidScheme)) && isValidScheme)) {
+    aRv.ThrowTypeError<MSG_INVALID_URL_SCHEME>( NS_LITERAL_STRING("Beacon"), aUrl);
     return false;
   }
 
   nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL |
     nsIChannel::LOAD_CLASSIFY_URI;
 
   // No need to use CORS for sendBeacon unless it's a BLOB
   nsSecurityFlags securityFlags = (!aData.IsNull() && aData.Value().IsBlob())
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -162,16 +162,17 @@ EXPORTS.mozilla.dom += [
     'DirectionalityUtils.h',
     'DocGroup.h',
     'DocumentFragment.h',
     'DocumentType.h',
     'DOMCursor.h',
     'DOMError.h',
     'DOMException.h',
     'DOMImplementation.h',
+    'DOMIntersectionObserver.h',
     'DOMMatrix.h',
     'DOMParser.h',
     'DOMPoint.h',
     'DOMQuad.h',
     'DOMRect.h',
     'DOMRequest.h',
     'DOMStringList.h',
     'DOMTokenListSupportedTokens.h',
@@ -366,16 +367,18 @@ UNIFIED_SOURCES += [
 
 if CONFIG['MOZ_WEBRTC']:
     UNIFIED_SOURCES += [
         'nsDOMDataChannel.cpp',
     ]
 
 # these files couldn't be in UNIFIED_SOURCES for now for reasons given below:
 SOURCES += [
+    # Several conflicts with other bindings.
+    'DOMIntersectionObserver.cpp',
     # Because of OS X headers.
     'nsContentUtils.cpp',
     # this file doesn't like windows.h
     'nsDOMWindowUtils.cpp',
     # Conflicts with windows.h's definition of SendMessage.
     'nsFrameMessageManager.cpp',
     # This file has a #error "Never include windows.h in this file!"
     'nsGlobalWindow.cpp',
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -1452,16 +1452,18 @@ nsDocument::~nsDocument()
   mInDestructor = true;
   mInUnlinkOrDeletion = true;
 
   mozilla::DropJSObjects(this);
 
   // Clear mObservers to keep it in sync with the mutationobserver list
   mObservers.Clear();
 
+  mIntersectionObservers.Clear();
+
   if (mStyleSheetSetList) {
     mStyleSheetSetList->Disconnect();
   }
 
   if (mAnimationController) {
     mAnimationController->Disconnect();
   }
 
@@ -1721,16 +1723,18 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildrenCollection)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnonymousContents)
 
   // Traverse all our nsCOMArrays.
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStyleSheets)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOnDemandBuiltInUASheets)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPreloadingImages)
 
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIntersectionObservers)
+
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSubImportLinks)
 
   for (uint32_t i = 0; i < tmp->mFrameRequestCallbacks.Length(); ++i) {
     NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mFrameRequestCallbacks[i]");
     cb.NoteXPCOMChild(tmp->mFrameRequestCallbacks[i].mCallback);
   }
 
   // Traverse animation components
@@ -1807,16 +1811,18 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ns
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mImportManager)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSubImportLinks)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mFontFaceSet)
 
   tmp->mParentDocument = nullptr;
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPreloadingImages)
 
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mIntersectionObservers)
+
   tmp->ClearAllBoxObjects();
 
   if (tmp->mListenerManager) {
     tmp->mListenerManager->Disconnect();
     tmp->UnsetFlags(NODE_HAS_LISTENERMANAGER);
     tmp->mListenerManager = nullptr;
   }
 
@@ -12357,16 +12363,61 @@ nsDocument::ReportUseCounters()
 
           Telemetry::Accumulate(id, 1);
         }
       }
     }
   }
 }
 
+void
+nsDocument::AddIntersectionObserver(DOMIntersectionObserver* aObserver)
+{
+  NS_ASSERTION(mIntersectionObservers.IndexOf(aObserver) == nsTArray<int>::NoIndex,
+               "Intersection observer already in the list");
+  mIntersectionObservers.AppendElement(aObserver);
+}
+
+void
+nsDocument::RemoveIntersectionObserver(DOMIntersectionObserver* aObserver)
+{
+  mIntersectionObservers.RemoveElement(aObserver);
+}
+
+void
+nsDocument::UpdateIntersectionObservations()
+{
+  DOMHighResTimeStamp time = 0;
+  if (nsPIDOMWindowInner* window = GetInnerWindow()) {
+    Performance* perf = window->GetPerformance();
+    if (perf) {
+      time = perf->Now();
+    }
+  }
+  for (const auto& observer : mIntersectionObservers) {
+    observer->Update(this, time);
+  }
+}
+
+void
+nsDocument::ScheduleIntersectionObserverNotification()
+{
+  nsCOMPtr<nsIRunnable> notification = NewRunnableMethod(this,
+    &nsDocument::NotifyIntersectionObservers);
+  NS_DispatchToCurrentThread(notification);
+}
+
+void
+nsDocument::NotifyIntersectionObservers()
+{
+  for (const auto& observer : mIntersectionObservers) {
+    observer->Notify();
+  }
+}
+
 XPathEvaluator*
 nsIDocument::XPathEvaluator()
 {
   if (!mXPathEvaluator) {
     mXPathEvaluator = new dom::XPathEvaluator(this);
   }
   return mXPathEvaluator;
 }
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -64,16 +64,17 @@
 #include "nsDataHashtable.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/Attributes.h"
 #include "nsIDOMXPathEvaluator.h"
 #include "jsfriendapi.h"
 #include "ImportManager.h"
 #include "mozilla/LinkedList.h"
 #include "CustomElementRegistry.h"
+#include "mozilla/dom/Performance.h"
 
 #define XML_DECLARATION_BITS_DECLARATION_EXISTS   (1 << 0)
 #define XML_DECLARATION_BITS_ENCODING_EXISTS      (1 << 1)
 #define XML_DECLARATION_BITS_STANDALONE_EXISTS    (1 << 2)
 #define XML_DECLARATION_BITS_STANDALONE_YES       (1 << 3)
 
 
 class nsDOMStyleSheetSetList;
@@ -92,16 +93,18 @@ class nsPIBoxObject;
 
 namespace mozilla {
 class EventChainPreVisitor;
 namespace dom {
 class BoxObject;
 class ImageTracker;
 struct LifecycleCallbacks;
 class CallbackFunction;
+class DOMIntersectionObserver;
+class Performance;
 
 struct FullscreenRequest : public LinkedListElement<FullscreenRequest>
 {
   explicit FullscreenRequest(Element* aElement);
   FullscreenRequest(const FullscreenRequest&) = delete;
   ~FullscreenRequest();
 
   Element* GetElement() const { return mElement; }
@@ -768,16 +771,25 @@ public:
   // for radio group
   nsRadioGroupStruct* GetRadioGroup(const nsAString& aName) const;
   nsRadioGroupStruct* GetOrCreateRadioGroup(const nsAString& aName);
 
   virtual nsViewportInfo GetViewportInfo(const mozilla::ScreenIntSize& aDisplaySize) override;
 
   void ReportUseCounters();
 
+  virtual void AddIntersectionObserver(
+    mozilla::dom::DOMIntersectionObserver* aObserver) override;
+  virtual void RemoveIntersectionObserver(
+    mozilla::dom::DOMIntersectionObserver* aObserver) override;
+  virtual void UpdateIntersectionObservations() override;
+  virtual void ScheduleIntersectionObserverNotification() override;
+  virtual void NotifyIntersectionObservers() override;
+
+
 private:
   void AddOnDemandBuiltInUASheet(mozilla::StyleSheet* aSheet);
   nsRadioGroupStruct* GetRadioGroupInternal(const nsAString& aName) const;
   void SendToConsole(nsCOMArray<nsISecurityConsoleMessage>& aMessages);
 
 public:
   // nsIDOMNode
   NS_FORWARD_NSIDOMNODE_TO_NSINODE_OVERRIDABLE
@@ -1320,16 +1332,19 @@ protected:
 
   nsTArray<RefPtr<mozilla::StyleSheet>> mStyleSheets;
   nsTArray<RefPtr<mozilla::StyleSheet>> mOnDemandBuiltInUASheets;
   nsTArray<RefPtr<mozilla::StyleSheet>> mAdditionalSheets[AdditionalSheetTypeCount];
 
   // Array of observers
   nsTObserverArray<nsIDocumentObserver*> mObservers;
 
+  // Array of intersection observers
+  nsTArray<RefPtr<mozilla::dom::DOMIntersectionObserver>> mIntersectionObservers;
+
   // Tracker for animations that are waiting to start.
   // nullptr until GetOrCreatePendingAnimationTracker is called.
   RefPtr<mozilla::PendingAnimationTracker> mPendingAnimationTracker;
 
   // Weak reference to the scope object (aka the script global object)
   // that, unlike mScriptGlobalObject, is never unset once set. This
   // is a weak reference to avoid leaks due to circular references.
   nsWeakPtr mScopeObject;
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -123,16 +123,17 @@ class BoxObject;
 class CDATASection;
 class Comment;
 struct CustomElementDefinition;
 class DocGroup;
 class DocumentFragment;
 class DocumentTimeline;
 class DocumentType;
 class DOMImplementation;
+class DOMIntersectionObserver;
 class DOMStringList;
 class Element;
 struct ElementCreationOptions;
 struct ElementRegistrationOptions;
 class Event;
 class EventTarget;
 class FontFaceSet;
 class FrameRequestCallback;
@@ -2842,16 +2843,25 @@ public:
   void ReportHasScrollLinkedEffect();
   bool HasScrollLinkedEffect() const
   {
     return mHasScrollLinkedEffect;
   }
 
   mozilla::dom::DocGroup* GetDocGroup();
 
+  virtual void AddIntersectionObserver(
+    mozilla::dom::DOMIntersectionObserver* aObserver) = 0;
+  virtual void RemoveIntersectionObserver(
+    mozilla::dom::DOMIntersectionObserver* aObserver) = 0;
+  
+  virtual void UpdateIntersectionObservations() = 0;
+  virtual void ScheduleIntersectionObserverNotification() = 0;
+  virtual void NotifyIntersectionObservers() = 0;
+
 protected:
   bool GetUseCounter(mozilla::UseCounter aUseCounter)
   {
     return mUseCounters[aUseCounter];
   }
 
   void SetChildDocumentUseCounter(mozilla::UseCounter aUseCounter)
   {
new file mode 100644
--- /dev/null
+++ b/dom/base/test/intersectionobserver_iframe.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<style>
+#target5 {
+        position: absolute;
+        top: 0px;
+        left: 0px;
+        width: 20px;
+        height: 20px;
+        background: #f00;
+}
+</style>
+<body>
+<div id="target5"></div>
+<script>
+        var io = new IntersectionObserver(function (records) {
+                window.parent.postMessage(records[0].rootBounds == null, 'http://mochi.test:8888');
+        }, {});
+        io.observe(document.getElementById("target5"));
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/base/test/intersectionobserver_window.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<style>
+#target5 {
+        position: absolute;
+        top: 0px;
+        left: 0px;
+        width: 20px;
+        height: 20px;
+        background: #f00;
+}
+</style>
+<body>
+<div id="target"></div>
+<script>
+        var io = new IntersectionObserver(function(records) {
+          var viewportWidth =
+              document.documentElement.clientWidth || document.body.clientWidth;
+          var viewportHeight =
+              document.documentElement.clientHeight || document.body.clientHeight;
+          var passed = records.length === 1 &&
+                       records[0].rootBounds.top === 0 &&
+                       records[0].rootBounds.left === 0 &&
+                       records[0].rootBounds.right === viewportWidth &&
+                       records[0].rootBounds.width === viewportWidth &&
+                       records[0].rootBounds.bottom === viewportHeight &&
+                       records[0].rootBounds.height === viewportHeight;
+          window.opener.postMessage(passed, '*');
+        });
+        io.observe(document.getElementById("target"));
+</script>
+</body>
+</html>
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -231,16 +231,18 @@ support-files =
   websocket_helpers.js
   websocket_tests.js
   !/dom/html/test/form_submit_server.sjs
   !/dom/security/test/cors/file_CrossSiteXHR_server.sjs
   !/image/test/mochitest/blue.png
   !/dom/xhr/tests/file_XHRSendData.sjs
   script_bug1238440.js
   file_blobURL_expiring.html
+  intersectionobserver_iframe.html
+  intersectionobserver_window.html
 
 [test_anchor_area_referrer.html]
 [test_anchor_area_referrer_changing.html]
 [test_anchor_area_referrer_invalid.html]
 [test_anchor_area_referrer_rel.html]
 [test_anonymousContent_api.html]
 [test_anonymousContent_append_after_reflow.html]
 [test_anonymousContent_canvas.html]
@@ -692,16 +694,17 @@ skip-if = e10s || os != 'linux' || build
 [test_htmlcopyencoder.xhtml]
 [test_iframe_referrer.html]
 [test_iframe_referrer_changing.html]
 [test_iframe_referrer_invalid.html]
 [test_Image_constructor.html]
 [test_img_referrer.html]
 [test_innersize_scrollport.html]
 [test_integer_attr_with_leading_zero.html]
+[test_intersectionobservers.html]
 [test_ipc_messagemanager_blob.html]
 [test_link_prefetch.html]
 skip-if = !e10s # Track Bug 1281415
 [test_link_stylesheet.html]
 [test_messagemanager_targetchain.html]
 [test_meta_viewport0.html]
 skip-if = (os != 'b2g' && os != 'android')    # meta-viewport tag support is mobile-only
 [test_meta_viewport1.html]
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_intersectionobservers.html
@@ -0,0 +1,1214 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1243846
+
+Some tests ported from IntersectionObserver/polyfill/intersection-observer-test.html
+
+Original license header:
+
+Copyright 2016 Google Inc. All Rights Reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1243846</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="next()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1243846">Mozilla Bug 1243846</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+  SpecialPowers.setIntPref("layout.visibility.min-notify-intersection-observers-interval-ms", 0);
+
+  var tests = [];
+  var curDescribeMsg = '';
+  var curItMsg = '';
+
+  function beforeEach_fn() { };
+  function afterEach_fn() { };
+
+  function before(fn) {
+    fn();
+  }
+
+  function beforeEach(fn) {
+    beforeEach_fn = fn;
+  }
+
+  function afterEach(fn) {
+    afterEach_fn = fn;
+  }
+
+  function it(msg, fn) {
+    tests.push({
+      msg: `${msg} [${curDescribeMsg}]`,
+      fn: fn
+    });
+  }
+
+  var callbacks = [];
+  function callDelayed(fn, delay) {
+    callbacks.push({
+      fn: fn,
+      time: +new Date() + delay
+    });
+  }
+
+  requestAnimationFrame(function tick() {
+    var i = callbacks.length;
+    while (i--) {
+      var cb = callbacks[i];
+      if (+new Date() >= cb.time) {
+        SimpleTest.executeSoon(cb.fn);
+        callbacks.splice(i, 1);
+      }
+    }
+    requestAnimationFrame(tick);
+  });
+
+  function expect(val) {
+    return {
+      to: {
+        throwException: function (regexp) {
+          try {
+            val();
+            ok(false, `${curItMsg} - an exception should have beeen thrown`);
+          } catch (e) {
+            ok(regexp.test(e), `${curItMsg} - supplied regexp should match thrown exception`);
+          }
+        },
+        get be() {
+          var fn = function (expected) {
+            is(val, expected, curItMsg);
+          };
+          fn.ok = function () {
+            ok(val, curItMsg);
+          };
+          fn.greaterThan = function (other) {
+            ok(val > other, `${curItMsg} - ${val} should be greater than ${other}`);
+          };
+          fn.lessThan = function (other) {
+            ok(val < other, `${curItMsg} - ${val} should be less than ${other}`);
+          };
+          return fn;
+        },
+        eql: function (expected) {
+          if (Array.isArray(expected)) {
+            if (!Array.isArray(val)) {
+              ok(false, curItMsg, `${curItMsg} - should be an array,`);
+              return;
+            }
+            is(val.length, expected.length, curItMsg, `${curItMsg} - arrays should be the same length`);
+            if (expected.length != val.length) {
+              return;
+            }
+            for (var i = 0; i < expected.length; i++) {
+              is(val[i], expected[i], `${curItMsg} - array elements at position ${i} should be equal`);
+              if (expected[i] != val[i]) {
+                return;
+              }
+            }
+            ok(true);
+          }
+        },
+      }
+    }
+  }
+
+  function describe(msg, fn) {
+    curDescribeMsg = msg;
+    fn();
+    curDescribeMsg = '';
+  }
+
+  function next() {
+    var test = tests.shift();
+    if (test) {
+      console.log(test.msg);
+      curItMsg = test.msg;
+      var fn = test.fn;
+      beforeEach_fn();
+      if (fn.length) {
+        fn(function () {
+          afterEach_fn();
+          next();
+        });
+      } else {
+        fn();
+        afterEach_fn();
+        next();
+      }
+    } else {
+      SimpleTest.finish();
+    }
+  }
+
+  var sinon = {
+    spy: function () {
+      var callbacks = [];
+      var fn = function () {
+        fn.callCount++;
+        fn.lastCall = { args: arguments };
+        if (callbacks.length) {
+          callbacks.shift()();
+        }
+      };
+      fn.callCount = 0;
+      fn.lastCall = { args: [] };
+      fn.waitForNotification = (fn) => {
+        callbacks.push(fn);
+      };
+      return fn;
+    }
+  };
+
+  var ASYNC_TIMEOUT = 300;
+
+
+  var io;
+  var noop = function() {};
+
+
+  // References to DOM elements, which are accessible to any test
+  // and reset prior to each test so state isn't shared.
+  var rootEl;
+  var grandParentEl;
+  var parentEl;
+  var targetEl1;
+  var targetEl2;
+  var targetEl3;
+  var targetEl4;
+  var targetEl5;
+
+
+  describe('IntersectionObserver', function() {
+
+    before(function() {
+
+    });
+
+
+    beforeEach(function() {
+      addStyles();
+      addFixtures();
+    });
+
+
+    afterEach(function() {
+      if (io && 'disconnect' in io) io.disconnect();
+      io = null;
+
+      window.onmessage = null;
+
+      removeStyles();
+      removeFixtures();
+    });
+
+
+    describe('constructor', function() {
+
+      it('throws when callback is not a function', function() {
+        expect(function() {
+          io = new IntersectionObserver(null);
+        }).to.throwException(/.*/i);
+      });
+
+
+      it('instantiates root correctly', function() {
+        io = new IntersectionObserver(noop);
+        expect(io.root).to.be(null);
+
+        io = new IntersectionObserver(noop, {root: rootEl});
+        expect(io.root).to.be(rootEl);
+      });
+
+
+      it('throws when root is not an Element', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop, {root: 'foo'});
+        }).to.throwException(/.*/i);
+      });
+
+
+      it('instantiates rootMargin correctly', function() {
+        io = new IntersectionObserver(noop, {rootMargin: '10px'});
+        expect(io.rootMargin).to.be('10px 10px 10px 10px');
+
+        io = new IntersectionObserver(noop, {rootMargin: '10px -5%'});
+        expect(io.rootMargin).to.be('10px -5% 10px -5%');
+
+        io = new IntersectionObserver(noop, {rootMargin: '10px 20% 0px'});
+        expect(io.rootMargin).to.be('10px 20% 0px 20%');
+
+        io = new IntersectionObserver(noop, {rootMargin: '0px 0px -5% 5px'});
+        expect(io.rootMargin).to.be('0px 0px -5% 5px');
+      });
+
+
+      it('throws when rootMargin is not in pixels or percent', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop, {rootMargin: 'auto'});
+        }).to.throwException(/pixels.*percent/i);
+      });
+
+
+      it('instantiates thresholds correctly', function() {
+        io = new IntersectionObserver(noop);
+        expect(io.thresholds).to.eql([0]);
+
+        io = new IntersectionObserver(noop, {threshold: 0.5});
+        expect(io.thresholds).to.eql([0.5]);
+
+        io = new IntersectionObserver(noop, {threshold: [0.25, 0.5, 0.75]});
+        expect(io.thresholds).to.eql([0.25, 0.5, 0.75]);
+
+        io = new IntersectionObserver(noop, {threshold: [1, .5, 0]});
+        expect(io.thresholds).to.eql([0, .5, 1]);
+      });
+
+      it('throws when a threshold value is not between 0 and 1', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop, {threshold: [0, -1]});
+        }).to.throwException(/threshold/i);
+      });
+
+      it('throws when a threshold value is not a number', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop, {threshold: "foo"});
+        }).to.throwException(/.*/i);
+      });
+
+    });
+
+
+    describe('observe', function() {
+
+      it('throws when target is not an Element', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop);
+          io.observe(null);
+        }).to.throwException(/.*/i);
+      });
+
+
+      it('triggers if target intersects when observing begins', function(done) {
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].intersectionRatio).to.be(1);
+          done();
+        }, {root: rootEl});
+        io.observe(targetEl1);
+      });
+
+
+      it('triggers with the correct arguments', function(done) {
+        io = new IntersectionObserver(function(records, observer) {
+          expect(records.length).to.be(1);
+          expect(records[0] instanceof IntersectionObserverEntry).to.be.ok();
+          expect(observer).to.be(io);
+          expect(this).to.be(io);
+          done();
+        }, {root: rootEl});
+        io.observe(targetEl1);
+      });
+
+
+      it('does not trigger if target does not intersect when observing begins',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        targetEl2.style.top = '-40px';
+        io.observe(targetEl2);
+        callDelayed(function() {
+          expect(spy.callCount).to.be(0);
+          done();
+        }, ASYNC_TIMEOUT);
+      });
+
+
+      it('does not trigger if target is not a descendant of the intersection root in the containing block chain',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: parentEl});
+
+        parentEl.style.position = 'static';
+        io.observe(targetEl2);
+        callDelayed(function() {
+          expect(spy.callCount).to.be(0);
+          done();
+        }, ASYNC_TIMEOUT);
+      });
+
+      it('triggers if target or root becomes invisible',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            io.observe(targetEl1);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.display = 'none';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.display = 'block';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            rootEl.style.display = 'none';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(4);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            rootEl.style.display = 'block';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(5);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+        ], done);
+      });
+
+
+      it('handles container elements with non-visible overflow',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            io.observe(targetEl1);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.left = '-40px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            parentEl.style.overflow = 'visible';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+      });
+
+
+      it('observes one target at a single threshold correctly', function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl, threshold: 0.5});
+
+        runSequence([
+          function(done) {
+            targetEl1.style.left = '-5px';
+            io.observe(targetEl1);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be.greaterThan(0.5);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.left = '-15px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be.lessThan(0.5);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.left = '-25px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(2);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.left = '-10px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0.5);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+
+      });
+
+
+      it('observes multiple targets at multiple thresholds correctly',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {
+          root: rootEl,
+          threshold: [1, 0.5, 0]
+        });
+
+        runSequence([
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '-15px';
+            targetEl2.style.top = '-5px';
+            targetEl2.style.left = '0px';
+            targetEl3.style.top = '0px';
+            targetEl3.style.left = '205px';
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.25);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(0.75);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '-5px';
+            targetEl2.style.top = '-15px';
+            targetEl2.style.left = '0px';
+            targetEl3.style.top = '0px';
+            targetEl3.style.left = '195px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(3);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.75);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(0.25);
+              expect(records[2].target).to.be(targetEl3);
+              expect(records[2].intersectionRatio).to.be(0.25);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '5px';
+            targetEl2.style.top = '-25px';
+            targetEl2.style.left = '0px';
+            targetEl3.style.top = '0px';
+            targetEl3.style.left = '185px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(3);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(0);
+              expect(records[2].target).to.be(targetEl3);
+              expect(records[2].intersectionRatio).to.be(0.75);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '15px';
+            targetEl2.style.top = '-35px';
+            targetEl2.style.left = '0px';
+            targetEl3.style.top = '0px';
+            targetEl3.style.left = '175px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(4);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].target).to.be(targetEl3);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+      });
+
+
+      it('handles rootMargin properly', function(done) {
+
+        parentEl.style.overflow = 'visible';
+        targetEl1.style.top = '0px';
+        targetEl1.style.left = '-20px';
+        targetEl2.style.top = '-20px';
+        targetEl2.style.left = '0px';
+        targetEl3.style.top = '0px';
+        targetEl3.style.left = '200px';
+        targetEl4.style.top = '180px';
+        targetEl4.style.left = '180px';
+
+        runSequence([
+          function(done) {
+            io = new IntersectionObserver(function(records) {
+              records = sortRecords(records);
+              expect(records.length).to.be(4);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(.5);
+              expect(records[2].target).to.be(targetEl3);
+              expect(records[2].intersectionRatio).to.be(.5);
+              expect(records[3].target).to.be(targetEl4);
+              expect(records[3].intersectionRatio).to.be(1);
+              io.disconnect();
+              done();
+            }, {root: rootEl, rootMargin: '10px'});
+
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+          },
+          function(done) {
+            io = new IntersectionObserver(function(records) {
+              records = sortRecords(records);
+              expect(records.length).to.be(3);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.5);
+              expect(records[1].target).to.be(targetEl3);
+              expect(records[1].intersectionRatio).to.be(0.5);
+              expect(records[2].target).to.be(targetEl4);
+              expect(records[2].intersectionRatio).to.be(0.5);
+              io.disconnect();
+              done();
+            }, {root: rootEl, rootMargin: '-10px 10%'});
+
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+          },
+          function(done) {
+            io = new IntersectionObserver(function(records) {
+              records = sortRecords(records);
+              expect(records.length).to.be(2);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.5);
+              expect(records[1].target).to.be(targetEl4);
+              expect(records[1].intersectionRatio).to.be(0.5);
+              io.disconnect();
+              done();
+            }, {root: rootEl, rootMargin: '-5% -2.5% 0px'});
+
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+          },
+          function(done) {
+            io = new IntersectionObserver(function(records) {
+              records = sortRecords(records);
+              expect(records.length).to.be(3);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.5);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(0.5);
+              expect(records[2].target).to.be(targetEl4);
+              expect(records[2].intersectionRatio).to.be(0.25);
+              io.disconnect();
+              done();
+            }, {root: rootEl, rootMargin: '5% -2.5% -10px -190px'});
+
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+          }
+        ], done);
+      });
+
+
+      it('handles targets on the boundary of root', function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '-21px';
+            targetEl2.style.top = '-20px';
+            targetEl2.style.left = '0px';
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl2);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '-20px';
+            targetEl2.style.top = '-21px';
+            targetEl2.style.left = '0px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[1].intersectionRatio).to.be(0);
+              expect(records[1].target).to.be(targetEl2);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '-20px';
+            targetEl1.style.left = '200px';
+            targetEl2.style.top = '200px';
+            targetEl2.style.left = '200px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl2);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl3.style.top = '20px';
+            targetEl3.style.left = '-20px';
+            targetEl4.style.top = '-20px';
+            targetEl4.style.left = '20px';
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(4);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl3);
+              expect(records[1].intersectionRatio).to.be(0);
+              expect(records[1].target).to.be(targetEl4);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+
+      });
+
+
+      it('handles zero-size targets within the root coordinate space',
+          function(done) {
+
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].intersectionRatio).to.be(0);
+          done();
+        }, {root: rootEl});
+
+        targetEl1.style.top = '0px';
+        targetEl1.style.left = '0px';
+        targetEl1.style.width = '0px';
+        targetEl1.style.height = '0px';
+        io.observe(targetEl1);
+      });
+
+
+      it('handles root/target elements not yet in the DOM', function(done) {
+
+        rootEl.parentNode.removeChild(rootEl);
+        targetEl1.parentNode.removeChild(targetEl1);
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            io.observe(targetEl1);
+            callDelayed(done, 0);
+          },
+          function(done) {
+            document.getElementById('fixtures').appendChild(rootEl);
+            callDelayed(function() {
+              expect(spy.callCount).to.be(0);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            parentEl.insertBefore(targetEl1, targetEl2);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[0].target).to.be(targetEl1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            grandParentEl.parentNode.removeChild(grandParentEl);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            rootEl.appendChild(targetEl1);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[0].target).to.be(targetEl1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            rootEl.parentNode.removeChild(rootEl);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(4);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl1);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+      });
+
+
+      it('handles sub-root element scrolling', function(done) {
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].intersectionRatio).to.be(1);
+          done();
+        }, {root: rootEl});
+
+        io.observe(targetEl3);
+        callDelayed(function() {
+          parentEl.scrollLeft = 40;
+        }, 0);
+      });
+
+
+      it('supports CSS transitions and transforms', function(done) {
+
+        targetEl1.style.top = '220px';
+        targetEl1.style.left = '220px';
+
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].intersectionRatio).to.be(1);
+          done();
+        }, {root: rootEl, threshold: [1]});
+
+        io.observe(targetEl1);
+        callDelayed(function() {
+          targetEl1.style.transform = 'translateX(-40px) translateY(-40px)';
+        }, 0);
+      });
+
+
+      it('uses the viewport when no root is specified', function(done) {
+        window.onmessage = function (e) {
+          expect(e.data).to.be.ok();
+          win.close();
+          done();
+        };
+
+        var win = window.open("intersectionobserver_window.html");
+      });
+
+    });
+
+    describe('observe subframe', function () {
+      
+      it('should not trigger if target and root are not in the same document',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        targetEl4.onload = function () {
+          targetEl5 = targetEl4.contentDocument.getElementById('target5');
+          io.observe(targetEl5);
+          callDelayed(function() {
+            expect(spy.callCount).to.be(0);
+            done();
+          }, ASYNC_TIMEOUT);
+        }
+
+        targetEl4.src = "intersectionobserver_iframe.html";
+      
+      });
+
+      it('boundingClientRect matches target.getBoundingClientRect() for an element inside an iframe',
+          function(done) {
+
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].boundingClientRect.top, targetEl5.getBoundingClientRect().top);
+          expect(records[0].boundingClientRect.left, targetEl5.getBoundingClientRect().left);
+          expect(records[0].boundingClientRect.width, targetEl5.getBoundingClientRect().width);
+          expect(records[0].boundingClientRect.height, targetEl5.getBoundingClientRect().height);
+          done();
+        }, {threshold: [1]});
+
+        targetEl4.onload = function () {
+          targetEl5 = targetEl4.contentDocument.getElementById('target5');
+          io.observe(targetEl5);
+        }
+
+        targetEl4.src = "intersectionobserver_iframe.html";
+      });
+
+      it('rootBounds should is set to null for cross-origin observations', function(done) {
+
+        window.onmessage = function (e) {
+          expect(e.data).to.be.ok();
+          done();
+        };
+
+        targetEl4.src = "http://example.org/tests/dom/base/test/intersectionobserver_iframe.html";
+
+      });
+    
+    });
+
+    describe('takeRecords', function() {
+
+      it('supports getting records before the callback is invoked',
+          function(done) {
+
+        var lastestRecords = [];
+        io = new IntersectionObserver(function(records) {
+          lastestRecords = lastestRecords.concat(records);
+        }, {root: rootEl});
+        io.observe(targetEl1);
+
+        window.requestAnimationFrame && requestAnimationFrame(function() {
+          lastestRecords = lastestRecords.concat(io.takeRecords());
+        });
+
+        callDelayed(function() {
+          expect(lastestRecords.length).to.be(1);
+          expect(lastestRecords[0].intersectionRatio).to.be(1);
+          done();
+        }, ASYNC_TIMEOUT);
+      });
+
+    });
+
+
+    describe('unobserve', function() {
+
+      it('removes targets from the internal store', function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '0px';
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            io.unobserve(targetEl1);
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '-40px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].target).to.be(targetEl2);
+              expect(records[0].intersectionRatio).to.be(0);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            io.unobserve(targetEl2);
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '0px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(2);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+
+      });
+
+    });
+
+    describe('disconnect', function() {
+
+      it('removes all targets and stops listening for changes', function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '0px';
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            io.disconnect();
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '-40px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+
+      });
+
+    });
+
+  });
+
+
+  /**
+   * Runs a sequence of function and when finished invokes the done callback.
+   * Each function in the sequence is invoked with its own done function and
+   * it should call that function once it's complete.
+   * @param {Array<Function>} functions An array of async functions.
+   * @param {Function} done A final callback to be invoked once all function
+   *     have run.
+   */
+  function runSequence(functions, done) {
+    var next = functions.shift();
+    if (next) {
+      next(function() {
+        runSequence(functions, done);
+      });
+    } else {
+      done && done();
+    }
+  }
+
+
+  /**
+   * Sorts an array of records alphebetically by ascending ID. Since the current
+   * native implementation doesn't sort change entries by `observe` order, we do
+   * that ourselves for the non-polyfill case. Since all tests call observe
+   * on targets in sequential order, this should always match.
+   * https://crbug.com/613679
+   * @param {Array<IntersectionObserverEntry>} entries The entries to sort.
+   * @return {Array<IntersectionObserverEntry>} The sorted array.
+   */
+  function sortRecords(entries) {
+    entries = entries.sort(function(a, b) {
+      return a.target.id < b.target.id ? -1 : 1;
+    });
+    return entries;
+  }
+
+
+  /**
+   * Adds the common styles used by all tests to the page.
+   */
+  function addStyles() {
+    var styles = document.createElement('style');
+    styles.id = 'styles';
+    document.documentElement.appendChild(styles);
+
+    var cssText =
+        '#root {' +
+        '  position: relative;' +
+        '  width: 400px;' +
+        '  height: 200px;' +
+        '  background: #eee' +
+        '}' +
+        '#grand-parent {' +
+        '  position: relative;' +
+        '  width: 200px;' +
+        '  height: 200px;' +
+        '}' +
+        '#parent {' +
+        '  position: absolute;' +
+        '  top: 0px;' +
+        '  left: 200px;' +
+        '  overflow: hidden;' +
+        '  width: 200px;' +
+        '  height: 200px;' +
+        '  background: #ddd;' +
+        '}' +
+        '#target1, #target2, #target3, #target4 {' +
+        '  position: absolute;' +
+        '  top: 0px;' +
+        '  left: 0px;' +
+        '  width: 20px;' +
+        '  height: 20px;' +
+        '  transform: translateX(0px) translateY(0px);' +
+        '  transition: transform .5s;' +
+        '  background: #f00;' +
+        '  border: none;' +
+        '}';
+
+    styles.innerHTML = cssText;
+  }
+
+
+  /**
+   * Adds the DOM fixtures used by all tests to the page and assigns them to
+   * global variables so they can be referenced within the tests.
+   */
+  function addFixtures() {
+    var fixtures = document.createElement('div');
+    fixtures.id = 'fixtures';
+
+    fixtures.innerHTML =
+        '<div id="root">' +
+        '  <div id="grand-parent">' +
+        '    <div id="parent">' +
+        '      <div id="target1"></div>' +
+        '      <div id="target2"></div>' +
+        '      <div id="target3"></div>' +
+        '      <iframe id="target4"></iframe>' +
+        '    </div>' +
+        '  </div>' +
+        '</div>';
+
+    document.body.appendChild(fixtures);
+
+    rootEl = document.getElementById('root');
+    grandParentEl = document.getElementById('grand-parent');
+    parentEl = document.getElementById('parent');
+    targetEl1 = document.getElementById('target1');
+    targetEl2 = document.getElementById('target2');
+    targetEl3 = document.getElementById('target3');
+    targetEl4 = document.getElementById('target4');
+  }
+
+
+  /**
+   * Removes the common styles from the page.
+   */
+  function removeStyles() {
+    var styles = document.getElementById('styles');
+    styles.parentNode.removeChild(styles);
+  }
+
+
+  /**
+   * Removes the DOM fixtures from the page and resets the global references.
+   */
+  function removeFixtures() {
+    var fixtures = document.getElementById('fixtures');
+    fixtures.parentNode.removeChild(fixtures);
+
+    rootEl = null;
+    grandParentEl = null;
+    parentEl = null;
+    targetEl1 = null;
+    targetEl2 = null;
+    targetEl3 = null;
+    targetEl4 = null;
+  }
+
+  SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+<div id="log">
+</div>
+</body>
+</html>
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -576,16 +576,25 @@ DOMInterfaces = {
     'wrapperCache': False,
 },
 
 'InputStream': {
     'nativeType': 'nsIInputStream',
     'notflattened': True
 },
 
+'IntersectionObserver': {
+    'nativeType': 'mozilla::dom::DOMIntersectionObserver',
+},
+
+'IntersectionObserverEntry': {
+    'nativeType': 'mozilla::dom::DOMIntersectionObserverEntry',
+    'headerFile': 'DOMIntersectionObserver.h',
+},
+
 'KeyEvent': {
     'concrete': False
 },
 
 'KeyframeEffect': {
     'implicitJSContext': { 'setterOnly': [ 'spacing' ] }
 },
 
--- a/dom/bindings/Errors.msg
+++ b/dom/bindings/Errors.msg
@@ -98,8 +98,9 @@ MSG_DEF(MSG_SW_UPDATE_BAD_REGISTRATION, 
 MSG_DEF(MSG_INVALID_DURATION_ERROR, 1, JSEXN_TYPEERR, "Invalid duration '{0}'.")
 MSG_DEF(MSG_INVALID_EASING_ERROR, 1, JSEXN_TYPEERR, "Invalid easing '{0}'.")
 MSG_DEF(MSG_INVALID_SPACING_MODE_ERROR, 1, JSEXN_TYPEERR, "Invalid spacing '{0}'.")
 MSG_DEF(MSG_USELESS_SETTIMEOUT, 1, JSEXN_TYPEERR, "Useless {0} call (missing quotes around argument?)")
 MSG_DEF(MSG_TOKENLIST_NO_SUPPORTED_TOKENS, 2, JSEXN_TYPEERR, "{0} attribute of <{1}> does not define any supported tokens")
 MSG_DEF(MSG_CACHE_STREAM_CLOSED, 0, JSEXN_TYPEERR, "Response body is a cache file stream that has already been closed.")
 MSG_DEF(MSG_TIME_VALUE_OUT_OF_RANGE, 1, JSEXN_TYPEERR, "{0} is outside the supported range for time values.")
 MSG_DEF(MSG_ONLY_IF_CACHED_WITHOUT_SAME_ORIGIN, 1, JSEXN_TYPEERR, "Request mode '{0}' was used, but request cache mode 'only-if-cached' can only be used with request mode 'same-origin'.")
+MSG_DEF(MSG_THRESHOLD_RANGE_ERROR, 0, JSEXN_RANGEERR, "Threshold values must all be in the range [0, 1].")
--- a/dom/indexedDB/ActorsParent.cpp
+++ b/dom/indexedDB/ActorsParent.cpp
@@ -19728,17 +19728,17 @@ DatabaseOperationBase::IndexDataValuesFr
   }
 
   for (uint32_t idxIndex = 0; idxIndex < count; idxIndex++) {
     const IndexUpdateInfo& updateInfo = aUpdateInfos[idxIndex];
     const int64_t& indexId = updateInfo.indexId();
     const Key& key = updateInfo.value();
     const Key& sortKey = updateInfo.localizedValue();
 
-    bool unique;
+    bool unique = false;
     MOZ_ALWAYS_TRUE(aUniqueIndexTable.Get(indexId, &unique));
 
     IndexDataValue idv(indexId, unique, key, sortKey);
 
     MOZ_ALWAYS_TRUE(
       aIndexValues.InsertElementSorted(idv, fallible));
   }
 
--- a/dom/tests/mochitest/beacon/test_beacon.html
+++ b/dom/tests/mochitest/beacon/test_beacon.html
@@ -17,23 +17,35 @@ https://bugzilla.mozilla.org/show_bug.cg
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 SimpleTest.waitForExplicitFinish();
 SpecialPowers.pushPrefEnv({'set': [["beacon.enabled", true]]}, beginTest);
 
 function beginTest() {
   var threw;
-  try {
-    is(false, navigator.sendBeacon("ftp://example.com", "0"));
-    threw = false;
-  } catch (ex) {
-    threw = true;
+  for(let scheme of ["bad", "ftp", "data"]) {
+    try {
+      is(false, navigator.sendBeacon(`${scheme}://example.com`, "0"));
+      threw = false;
+    } catch (ex) {
+      threw = true;
+    }
+    ok(threw, `sendBeacon not supported for ${scheme} scheme.`);
   }
-  ok(threw, "sendBeacon not supported for non ftp calls.");
+
+  for(let scheme of ["http", "https"]) {
+    try {
+      is(false, navigator.sendBeacon(`${scheme}://invalid:URL`, "0"));
+      threw = false;
+    } catch (ex) {
+      threw = true;
+    }
+    ok(threw, `sendBeacon not supported for invalid ${scheme} URLs.`);
+  }
 
   try {
     is(false, navigator.sendBeacon());
     threw = false;
   } catch (e) {
     threw = true;
   }
   ok(threw, "sendBeacon needs more parameters.");
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -608,16 +608,20 @@ var interfaceNamesInGlobalScope =
     {name: "ImageCaptureErrorEvent", disabled: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "ImageData",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "InputEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "InstallTrigger",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    "IntersectionObserver",
+// IMPORTANT: Do not change this list without review from a DOM peer!
+    "IntersectionObserverEntry",
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "KeyEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "KeyboardEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "KeyframeEffectReadOnly", release: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "KeyframeEffect", release: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/webidl/DOMRect.webidl
+++ b/dom/webidl/DOMRect.webidl
@@ -24,9 +24,16 @@ interface DOMRectReadOnly {
     readonly attribute unrestricted double x;
     readonly attribute unrestricted double y;
     readonly attribute unrestricted double width;
     readonly attribute unrestricted double height;
     readonly attribute unrestricted double top;
     readonly attribute unrestricted double right;
     readonly attribute unrestricted double bottom;
     readonly attribute unrestricted double left;
-};
\ No newline at end of file
+};
+
+dictionary DOMRectInit {
+    unrestricted double x = 0;
+    unrestricted double y = 0;
+    unrestricted double width = 0;
+    unrestricted double height = 0;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/IntersectionObserver.webidl
@@ -0,0 +1,59 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * The origin of this IDL file is
+ * https://wicg.github.io/IntersectionObserver/
+ */
+
+[ProbablyShortLivingObject]
+interface IntersectionObserverEntry {
+  [Constant]
+  readonly attribute DOMHighResTimeStamp time;
+  [Constant]
+  readonly attribute DOMRectReadOnly? rootBounds;
+  [Constant]
+  readonly attribute DOMRectReadOnly boundingClientRect;
+  [Constant]
+  readonly attribute DOMRectReadOnly intersectionRect;
+  [Constant]
+  readonly attribute double intersectionRatio;
+  [Constant]
+  readonly attribute Element target;
+};
+
+[Constructor(IntersectionCallback intersectionCallback,
+             optional IntersectionObserverInit options)]
+interface IntersectionObserver {
+  [Constant]
+  readonly attribute Element? root;
+  [Constant]
+  readonly attribute DOMString rootMargin;
+  [Constant,Cached]
+  readonly attribute sequence<double> thresholds;
+  void observe(Element target);
+  void unobserve(Element target);
+  void disconnect();
+  sequence<IntersectionObserverEntry> takeRecords();
+
+  [ChromeOnly]
+  readonly attribute IntersectionCallback intersectionCallback;
+};
+
+callback IntersectionCallback =
+  void (sequence<IntersectionObserverEntry> entries, IntersectionObserver observer);
+
+dictionary IntersectionObserverEntryInit {
+  required DOMHighResTimeStamp time;
+  required DOMRectInit rootBounds;
+  required DOMRectInit boundingClientRect;
+  required DOMRectInit intersectionRect;
+  required Element target;
+};
+
+dictionary IntersectionObserverInit {
+  Element?  root = null;
+  DOMString rootMargin = "0px";
+  (double or sequence<double>) threshold = 0;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -276,16 +276,17 @@ WEBIDL_FILES = [
     'ImageCapture.webidl',
     'ImageData.webidl',
     'ImageDocument.webidl',
     'InputEvent.webidl',
     'InputMethod.webidl',
     'InputPort.webidl',
     'InputPortManager.webidl',
     'InspectorUtils.webidl',
+    'IntersectionObserver.webidl',
     'IterableIterator.webidl',
     'KeyAlgorithm.webidl',
     'KeyboardEvent.webidl',
     'KeyEvent.webidl',
     'KeyframeAnimationOptions.webidl',
     'KeyframeEffect.webidl',
     'KeyIdsInitData.webidl',
     'LegacyQueryInterface.webidl',
--- a/gfx/src/nsDeviceContext.cpp
+++ b/gfx/src/nsDeviceContext.cpp
@@ -327,16 +327,28 @@ nsDeviceContext::Init(nsIWidget *aWidget
 
     return rv;
 }
 
 // XXX This is only for printing. We should make that obvious in the name.
 already_AddRefed<gfxContext>
 nsDeviceContext::CreateRenderingContext()
 {
+  return CreateRenderingContextCommon(/* not a reference context */ false);
+}
+
+already_AddRefed<gfxContext>
+nsDeviceContext::CreateReferenceRenderingContext()
+{
+  return CreateRenderingContextCommon(/* a reference context */ true);
+}
+
+already_AddRefed<gfxContext>
+nsDeviceContext::CreateRenderingContextCommon(bool aWantReferenceContext)
+{
     MOZ_ASSERT(IsPrinterContext());
     MOZ_ASSERT(mWidth > 0 && mHeight > 0);
 
     RefPtr<PrintTarget> printingTarget = mPrintTarget;
 #ifdef XP_MACOSX
     // CreateRenderingContext() can be called (on reflow) after EndPage()
     // but before BeginPage().  On OS X (and only there) mPrintTarget
     // will in this case be null, because OS X printing surfaces are
@@ -347,18 +359,23 @@ nsDeviceContext::CreateRenderingContext(
       printingTarget = mCachedPrintTarget;
     }
 #endif
 
     // This will usually be null, depending on the pref print.print_via_parent.
     RefPtr<DrawEventRecorder> recorder;
     mDeviceContextSpec->GetDrawEventRecorder(getter_AddRefs(recorder));
 
-    RefPtr<gfx::DrawTarget> dt =
-      printingTarget->MakeDrawTarget(gfx::IntSize(mWidth, mHeight), recorder);
+    RefPtr<gfx::DrawTarget> dt;
+    if (aWantReferenceContext) {
+      dt = printingTarget->GetReferenceDrawTarget();
+    } else {
+      dt = printingTarget->MakeDrawTarget(gfx::IntSize(mWidth, mHeight), recorder);
+    }
+
     if (!dt || !dt->IsValid()) {
       gfxCriticalNote
         << "Failed to create draw target in device context sized "
         << mWidth << "x" << mHeight << " and pointers "
         << hexa(mPrintTarget) << " and " << hexa(printingTarget);
       return nullptr;
     }
 
--- a/gfx/src/nsDeviceContext.h
+++ b/gfx/src/nsDeviceContext.h
@@ -66,16 +66,24 @@ public:
      * Create a rendering context and initialize it.  Only call this
      * method on device contexts that were initialized for printing.
      *
      * @return the new rendering context (guaranteed to be non-null)
      */
     already_AddRefed<gfxContext> CreateRenderingContext();
 
     /**
+     * Create a reference rendering context and initialize it.  Only call this
+     * method on device contexts that were initialized for printing.
+     *
+     * @return the new rendering context.
+     */
+    already_AddRefed<gfxContext> CreateReferenceRenderingContext();
+
+    /**
      * Gets the number of app units in one CSS pixel; this number is global,
      * not unique to each device context.
      */
     static int32_t AppUnitsPerCSSPixel() { return mozilla::AppUnitsPerCSSPixel(); }
 
     /**
      * Gets the number of app units in one device pixel; this number
      * is usually a factor of AppUnitsPerCSSPixel(), although that is
@@ -257,16 +265,23 @@ public:
     bool IsPrinterContext();
 
     mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScale();
 
 private:
     // Private destructor, to discourage deletion outside of Release():
     ~nsDeviceContext();
 
+    /**
+     * Implementation shared by CreateRenderingContext and
+     * CreateReferenceRenderingContext.
+     */
+    already_AddRefed<gfxContext>
+    CreateRenderingContextCommon(bool aWantReferenceContext);
+
     void SetDPI(double* aScale = nullptr);
     void ComputeClientRectUsingScreen(nsRect *outRect);
     void ComputeFullAreaUsingScreen(nsRect *outRect);
     void FindScreen(nsIScreen **outScreen);
 
     // Return false if the surface is not right
     bool CalcPrintingSize();
     void UpdateAppUnitsForFullZoom();
--- a/gfx/thebes/PrintTarget.cpp
+++ b/gfx/thebes/PrintTarget.cpp
@@ -11,16 +11,20 @@
 
 namespace mozilla {
 namespace gfx {
 
 PrintTarget::PrintTarget(cairo_surface_t* aCairoSurface, const IntSize& aSize)
   : mCairoSurface(aCairoSurface)
   , mSize(aSize)
   , mIsFinished(false)
+#ifdef DEBUG
+  , mHasActivePage(false)
+#endif
+
 {
 #if 0
   // aCairoSurface is null when our PrintTargetThebes subclass's ctor calls us.
   // Once PrintTargetThebes is removed, enable this assertion.
   MOZ_ASSERT(aCairoSurface && !cairo_surface_status(aCairoSurface),
              "CreateOrNull factory methods should not call us without a "
              "valid cairo_surface_t*");
 #endif
@@ -47,16 +51,20 @@ PrintTarget::~PrintTarget()
 
 already_AddRefed<DrawTarget>
 PrintTarget::MakeDrawTarget(const IntSize& aSize,
                             DrawEventRecorder* aRecorder)
 {
   MOZ_ASSERT(mCairoSurface,
              "We shouldn't have been constructed without a cairo surface");
 
+  // This should not be called outside of BeginPage()/EndPage() calls since
+  // some backends can only provide a valid DrawTarget at that time.
+  MOZ_ASSERT(mHasActivePage, "We can't guarantee a valid DrawTarget");
+
   if (cairo_surface_status(mCairoSurface)) {
     return nullptr;
   }
 
   // Note than aSize may not be the same as mSize (the size of mCairoSurface).
   // See the comments in our header.  If the sizes are different a clip will
   // be applied to mCairoSurface.
   RefPtr<DrawTarget> dt =
@@ -71,16 +79,46 @@ PrintTarget::MakeDrawTarget(const IntSiz
       return nullptr;
     }
   }
 
   return dt.forget();
 }
 
 already_AddRefed<DrawTarget>
+PrintTarget::GetReferenceDrawTarget()
+{
+  if (!mRefDT) {
+    IntSize size(1, 1);
+
+    cairo_surface_t* surface =
+      cairo_surface_create_similar(mCairoSurface,
+                                   cairo_surface_get_content(mCairoSurface),
+                                   size.width, size.height);
+
+    if (cairo_surface_status(surface)) {
+      return nullptr;
+    }
+
+    RefPtr<DrawTarget> dt =
+      Factory::CreateDrawTargetForCairoSurface(surface, size);
+
+    // The DT addrefs the surface, so we need drop our own reference to it:
+    cairo_surface_destroy(surface);
+
+    if (!dt || !dt->IsValid()) {
+      return nullptr;
+    }
+
+    mRefDT = dt.forget();
+  }
+  return do_AddRef(mRefDT);
+}
+
+already_AddRefed<DrawTarget>
 PrintTarget::CreateRecordingDrawTarget(DrawEventRecorder* aRecorder,
                                        DrawTarget* aDrawTarget)
 {
   MOZ_ASSERT(aRecorder);
   MOZ_ASSERT(aDrawTarget);
 
   RefPtr<DrawTarget> dt;
 
--- a/gfx/thebes/PrintTarget.h
+++ b/gfx/thebes/PrintTarget.h
@@ -32,22 +32,32 @@ public:
   virtual nsresult BeginPrinting(const nsAString& aTitle,
                                  const nsAString& aPrintToFileName) {
     return NS_OK;
   }
   virtual nsresult EndPrinting() {
     return NS_OK;
   }
   virtual nsresult AbortPrinting() {
+#ifdef DEBUG
+    mHasActivePage = false;
+#endif
     return NS_OK;
   }
   virtual nsresult BeginPage() {
+#ifdef DEBUG
+    MOZ_ASSERT(!mHasActivePage, "Missing EndPage() call");
+    mHasActivePage = true;
+#endif
     return NS_OK;
   }
   virtual nsresult EndPage() {
+#ifdef DEBUG
+    mHasActivePage = false;
+#endif
     return NS_OK;
   }
 
   /**
    * Releases the resources used by this PrintTarget.  Typically this should be
    * called after calling EndPrinting().  Calling this more than once is
    * allowed, but subsequent calls are a no-op.
    *
@@ -73,16 +83,20 @@ public:
    * failure.
    *
    * If aRecorder is passed a recording DrawTarget will be created instead of
    * the type of DrawTarget that would normally be returned for a particular
    * subclass of this class.  This argument is only intended to be used in
    * the e10s content process if printing output can't otherwise be transfered
    * over to the parent process using the normal DrawTarget type.
    *
+   * NOTE: this should only be called between BeginPage()/EndPage() calls, and
+   * the returned DrawTarget should not be drawn to after EndPage() has been
+   * called.
+   *
    * XXX For consistency with the old code this takes a size parameter even
    * though we already have the size passed to our subclass's CreateOrNull
    * factory methods.  The size passed to the factory method comes from
    * nsIDeviceContextSpec::MakePrintTarget overrides, whereas the size
    * passed to us comes from nsDeviceContext::CreateRenderingContext.  In at
    * least one case (nsDeviceContextSpecAndroid::MakePrintTarget) these are
    * different.  At some point we should align the two sources and get rid of
    * this method's size parameter.
@@ -106,29 +120,41 @@ public:
    *
    * TODO: Consider adding a SetDPI method that calls
    * cairo_surface_set_fallback_resolution.
    */
   virtual already_AddRefed<DrawTarget>
   MakeDrawTarget(const IntSize& aSize,
                  DrawEventRecorder* aRecorder = nullptr);
 
+  /**
+   * Returns a reference DrawTarget. Unlike MakeDrawTarget, this method is not
+   * restricted to being called between BeginPage()/EndPage() calls, and the
+   * returned DrawTarget it is still valid to use after EndPage() has been
+   * called.
+   */
+  virtual already_AddRefed<DrawTarget> GetReferenceDrawTarget();
+
 protected:
 
   // Only created via subclass's constructors
   explicit PrintTarget(cairo_surface_t* aCairoSurface, const IntSize& aSize);
 
   // Protected because we're refcounted
   virtual ~PrintTarget();
 
   already_AddRefed<DrawTarget>
   CreateRecordingDrawTarget(DrawEventRecorder* aRecorder,
                             DrawTarget* aDrawTarget);
 
   cairo_surface_t* mCairoSurface;
+  RefPtr<DrawTarget> mRefDT; // reference DT
   IntSize mSize;
   bool mIsFinished;
+#ifdef DEBUG
+  bool mHasActivePage;
+#endif
 };
 
 } // namespace gfx
 } // namespace mozilla
 
 #endif /* MOZILLA_GFX_PRINTTARGET_H */
--- a/gfx/thebes/PrintTargetThebes.cpp
+++ b/gfx/thebes/PrintTargetThebes.cpp
@@ -31,32 +31,50 @@ PrintTargetThebes::PrintTargetThebes(gfx
   , mGfxSurface(aSurface)
 {
 }
 
 already_AddRefed<DrawTarget>
 PrintTargetThebes::MakeDrawTarget(const IntSize& aSize,
                                   DrawEventRecorder* aRecorder)
 {
+  // This should not be called outside of BeginPage()/EndPage() calls since
+  // some backends can only provide a valid DrawTarget at that time.
+  MOZ_ASSERT(mHasActivePage, "We can't guarantee a valid DrawTarget");
+
   RefPtr<gfx::DrawTarget> dt =
     gfxPlatform::GetPlatform()->CreateDrawTargetForSurface(mGfxSurface, aSize);
   if (!dt || !dt->IsValid()) {
     return nullptr;
   }
 
   if (aRecorder) {
     dt = CreateRecordingDrawTarget(aRecorder, dt);
     if (!dt || !dt->IsValid()) {
       return nullptr;
     }
   }
 
   return dt.forget();
 }
 
+already_AddRefed<DrawTarget>
+PrintTargetThebes::GetReferenceDrawTarget()
+{
+  if (!mRefDT) {
+    RefPtr<gfx::DrawTarget> dt =
+      gfxPlatform::GetPlatform()->CreateDrawTargetForSurface(mGfxSurface, mSize);
+    if (!dt || !dt->IsValid()) {
+      return nullptr;
+    }
+    mRefDT = dt->CreateSimilarDrawTarget(IntSize(1,1), dt->GetFormat());
+  }
+  return do_AddRef(mRefDT);
+}
+
 nsresult
 PrintTargetThebes::BeginPrinting(const nsAString& aTitle,
                                  const nsAString& aPrintToFileName)
 {
   return mGfxSurface->BeginPrinting(aTitle, aPrintToFileName);
 }
 
 nsresult
--- a/gfx/thebes/PrintTargetThebes.h
+++ b/gfx/thebes/PrintTargetThebes.h
@@ -36,16 +36,18 @@ public:
   virtual nsresult BeginPage() override;
   virtual nsresult EndPage() override;
   virtual void Finish() override;
 
   virtual already_AddRefed<DrawTarget>
   MakeDrawTarget(const IntSize& aSize,
                  DrawEventRecorder* aRecorder = nullptr) override;
 
+  virtual already_AddRefed<DrawTarget> GetReferenceDrawTarget() final;
+
 private:
 
   // Only created via CreateOrNull
   explicit PrintTargetThebes(gfxASurface* aSurface);
 
   RefPtr<gfxASurface> mGfxSurface;
 };
 
--- a/js/src/builtin/Promise.cpp
+++ b/js/src/builtin/Promise.cpp
@@ -25,151 +25,573 @@ using namespace js;
 static double
 MillisecondsSinceStartup()
 {
     auto now = mozilla::TimeStamp::Now();
     bool ignored;
     return (now - mozilla::TimeStamp::ProcessCreation(ignored)).ToMilliseconds();
 }
 
-enum ResolutionFunctionSlots {
-    ResolutionFunctionSlot_Promise = 0,
-    ResolutionFunctionSlot_OtherFunction,
+#define PROMISE_HANDLER_IDENTITY 0
+#define PROMISE_HANDLER_THROWER  1
+
+enum ResolutionMode {
+    ResolveMode,
+    RejectMode
+};
+
+enum ResolveFunctionSlots {
+    ResolveFunctionSlot_Promise = 0,
+    ResolveFunctionSlot_RejectFunction,
+};
+
+enum RejectFunctionSlots {
+    RejectFunctionSlot_Promise = ResolveFunctionSlot_Promise,
+    RejectFunctionSlot_ResolveFunction,
+    RejectFunctionSlot_PromiseAllData = RejectFunctionSlot_ResolveFunction,
+};
+
+enum PromiseAllResolveElementFunctionSlots {
+    PromiseAllResolveElementFunctionSlot_Data = 0,
+    PromiseAllResolveElementFunctionSlot_ElementIndex,
+};
+
+enum ReactionJobSlots {
+    ReactionJobSlot_ReactionRecord = 0,
+};
+
+enum ThenableJobSlots {
+    ThenableJobSlot_Handler = 0,
+    ThenableJobSlot_JobData,
+};
+
+enum ThenableJobDataIndices {
+    ThenableJobDataIndex_Promise = 0,
+    ThenableJobDataIndex_Thenable,
+    ThenableJobDataLength,
+};
+
+enum PromiseAllDataHolderSlots {
+    PromiseAllDataHolderSlot_Promise = 0,
+    PromiseAllDataHolderSlot_RemainingElements,
+    PromiseAllDataHolderSlot_ValuesArray,
+    PromiseAllDataHolderSlot_ResolveFunction,
+    PromiseAllDataHolderSlots,
 };
 
-// ES2016, 25.4.1.8.
-static MOZ_MUST_USE bool
-TriggerPromiseReactions(JSContext* cx, HandleValue reactionsVal, JS::PromiseState state,
-                        HandleValue valueOrReason)
+class PromiseAllDataHolder : public NativeObject
 {
-    RootedObject reactions(cx, &reactionsVal.toObject());
-
-    AutoIdVector keys(cx);
-    if (!GetPropertyKeys(cx, reactions, JSITER_OWNONLY, &keys))
+  public:
+    static const Class class_;
+    JSObject* promiseObj() { return &getFixedSlot(PromiseAllDataHolderSlot_Promise).toObject(); }
+    JSObject* resolveObj() {
+        return getFixedSlot(PromiseAllDataHolderSlot_ResolveFunction).toObjectOrNull();
+    }
+    Value valuesArray() { return getFixedSlot(PromiseAllDataHolderSlot_ValuesArray); }
+    int32_t remainingCount() {
+        return getFixedSlot(PromiseAllDataHolderSlot_RemainingElements).toInt32();
+    }
+    int32_t increaseRemainingCount() {
+        int32_t remainingCount = getFixedSlot(PromiseAllDataHolderSlot_RemainingElements).toInt32();
+        remainingCount++;
+        setFixedSlot(PromiseAllDataHolderSlot_RemainingElements, Int32Value(remainingCount));
+        return remainingCount;
+    }
+    int32_t decreaseRemainingCount() {
+        int32_t remainingCount = getFixedSlot(PromiseAllDataHolderSlot_RemainingElements).toInt32();
+        remainingCount--;
+        setFixedSlot(PromiseAllDataHolderSlot_RemainingElements, Int32Value(remainingCount));
+        return remainingCount;
+    }
+};
+
+const Class PromiseAllDataHolder::class_ = {
+    "PromiseAllDataHolder",
+    JSCLASS_HAS_RESERVED_SLOTS(PromiseAllDataHolderSlots)
+};
+
+static PromiseAllDataHolder*
+NewPromiseAllDataHolder(JSContext* cx, HandleObject resultPromise, HandleValue valuesArray,
+                        HandleObject resolve)
+{
+    Rooted<PromiseAllDataHolder*> dataHolder(cx, NewObjectWithClassProto<PromiseAllDataHolder>(cx));
+    if (!dataHolder)
+        return nullptr;
+
+    assertSameCompartment(cx, resultPromise);
+    assertSameCompartment(cx, valuesArray);
+    assertSameCompartment(cx, resolve);
+
+    dataHolder->setFixedSlot(PromiseAllDataHolderSlot_Promise, ObjectValue(*resultPromise));
+    dataHolder->setFixedSlot(PromiseAllDataHolderSlot_RemainingElements, Int32Value(1));
+    dataHolder->setFixedSlot(PromiseAllDataHolderSlot_ValuesArray, valuesArray);
+    dataHolder->setFixedSlot(PromiseAllDataHolderSlot_ResolveFunction, ObjectOrNullValue(resolve));
+    return dataHolder;
+}
+
+static MOZ_MUST_USE bool RunResolutionFunction(JSContext *cx, HandleObject resolutionFun,
+                                               HandleValue result, ResolutionMode mode,
+                                               HandleObject promiseObj);
+
+// ES2016, 25.4.1.1.1, Steps 1.a-b.
+// Extracting all of this internal spec algorithm into a helper function would
+// be tedious, so the check in step 1 and the entirety of step 2 aren't
+// included.
+static bool
+AbruptRejectPromise(JSContext *cx, CallArgs& args, HandleObject promiseObj, HandleObject reject)
+{
+    // Step 1.a.
+    RootedValue reason(cx);
+    if (!GetAndClearException(cx, &reason))
+        return false;
+
+    if (!RunResolutionFunction(cx, reject, reason, RejectMode, promiseObj))
         return false;
-    MOZ_ASSERT(keys.length() > 0, "Reactions list should be created lazily");
-
-    RootedPropertyName handlerName(cx);
-    handlerName = state == JS::PromiseState::Fulfilled
-                  ? cx->names().fulfillHandler
-                  : cx->names().rejectHandler;
-
-    // Each reaction is an internally-created object with the structure:
-    // {
-    //   promise: [the promise this reaction resolves],
-    //   resolve: [the `resolve` callback content code provided],
-    //   reject:  [the `reject` callback content code provided],
-    //   fulfillHandler: [the internal handler that fulfills the promise]
-    //   rejectHandler: [the internal handler that rejects the promise]
-    //   incumbentGlobal: [an object from the global that was incumbent when
-    //                     the reaction was created]
-    // }
-    RootedValue val(cx);
-    RootedObject reaction(cx);
-    RootedValue handler(cx);
-    RootedObject promise(cx);
-    RootedObject resolve(cx);
-    RootedObject reject(cx);
-    RootedObject objectFromIncumbentGlobal(cx);
-
-    for (size_t i = 0; i < keys.length(); i++) {
-        if (!GetProperty(cx, reactions, reactions, keys[i], &val))
-            return false;
-        reaction = &val.toObject();
-
-        if (!GetProperty(cx, reaction, reaction, cx->names().promise, &val))
-            return false;
-
-        // The jsapi function AddPromiseReactions can add reaction records
-        // without a `promise` object. See comment for EnqueuePromiseReactions
-        // in Promise.js.
-        promise = val.toObjectOrNull();
-
-        if (!GetProperty(cx, reaction, reaction, handlerName, &handler))
-            return false;
-
-#ifdef DEBUG
-        if (handler.isNumber()) {
-            MOZ_ASSERT(handler.toNumber() == PROMISE_HANDLER_IDENTITY ||
-                       handler.toNumber() == PROMISE_HANDLER_THROWER);
-        } else {
-            MOZ_ASSERT(handler.toObject().isCallable());
-        }
-#endif
-
-        if (!GetProperty(cx, reaction, reaction, cx->names().resolve, &val))
-            return false;
-        resolve = &val.toObject();
-        MOZ_ASSERT(IsCallable(resolve));
-
-        if (!GetProperty(cx, reaction, reaction, cx->names().reject, &val))
-            return false;
-        reject = &val.toObject();
-        MOZ_ASSERT(IsCallable(reject));
-
-        if (!GetProperty(cx, reaction, reaction, cx->names().incumbentGlobal, &val))
-            return false;
-        objectFromIncumbentGlobal = val.toObjectOrNull();
-
-        if (!EnqueuePromiseReactionJob(cx, handler, valueOrReason, resolve, reject, promise,
-                                       objectFromIncumbentGlobal))
-        {
-            return false;
-        }
+
+    // Step 1.b.
+    args.rval().setObject(*promiseObj);
+    return true;
+}
+
+enum ReactionRecordSlots {
+    ReactionRecordSlot_Promise = 0,
+    ReactionRecordSlot_OnFulfilled,
+    ReactionRecordSlot_OnRejected,
+    ReactionRecordSlot_Resolve,
+    ReactionRecordSlot_Reject,
+    ReactionRecordSlot_IncumbentGlobalObject,
+    ReactionRecordSlot_Flags,
+    ReactionRecordSlot_HandlerArg,
+    ReactionRecordSlots,
+};
+
+#define REACTION_FLAG_RESOLVED                  0x1
+#define REACTION_FLAG_FULFILLED                 0x2
+#define REACTION_FLAG_IGNORE_DEFAULT_RESOLUTION 0x4
+
+// ES2016, 25.4.1.2.
+class PromiseReactionRecord : public NativeObject
+{
+  public:
+    static const Class class_;
+
+    JSObject* promise() { return getFixedSlot(ReactionRecordSlot_Promise).toObjectOrNull(); }
+    int32_t flags() { return getFixedSlot(ReactionRecordSlot_Flags).toInt32(); }
+    JS::PromiseState targetState() {
+        int32_t flags = this->flags();
+        if (!(flags & REACTION_FLAG_RESOLVED))
+            return JS::PromiseState::Pending;
+        return flags & REACTION_FLAG_FULFILLED
+               ? JS::PromiseState::Fulfilled
+               : JS::PromiseState::Rejected;
+    }
+    void setTargetState(JS::PromiseState state) {
+        int32_t flags = this->flags();
+        MOZ_ASSERT(!(flags & REACTION_FLAG_RESOLVED));
+        MOZ_ASSERT(state != JS::PromiseState::Pending, "Can't revert a reaction to pending.");
+        flags |= REACTION_FLAG_RESOLVED;
+        if (state == JS::PromiseState::Fulfilled)
+            flags |= REACTION_FLAG_FULFILLED;
+        setFixedSlot(ReactionRecordSlot_Flags, Int32Value(flags));
+    }
+    Value handler() {
+        MOZ_ASSERT(targetState() != JS::PromiseState::Pending);
+        uint32_t slot = targetState() == JS::PromiseState::Fulfilled
+                        ? ReactionRecordSlot_OnFulfilled
+                        : ReactionRecordSlot_OnRejected;
+        return getFixedSlot(slot);
+    }
+    Value handlerArg() {
+        MOZ_ASSERT(targetState() != JS::PromiseState::Pending);
+        return getFixedSlot(ReactionRecordSlot_HandlerArg);
     }
+    void setHandlerArg(Value& arg) {
+        MOZ_ASSERT(targetState() == JS::PromiseState::Pending);
+        setFixedSlot(ReactionRecordSlot_HandlerArg, arg);
+    }
+    JSObject* incumbentGlobalObject() {
+        return getFixedSlot(ReactionRecordSlot_IncumbentGlobalObject).toObjectOrNull();
+    }
+};
+
+const Class PromiseReactionRecord::class_ = {
+    "PromiseReactionRecord",
+    JSCLASS_HAS_RESERVED_SLOTS(ReactionRecordSlots)
+};
+
+static void
+AddPromiseFlags(PromiseObject& promise, int32_t flag)
+{
+    int32_t flags = promise.getFixedSlot(PromiseSlot_Flags).toInt32();
+    promise.setFixedSlot(PromiseSlot_Flags, Int32Value(flags | flag));
+}
+
+static bool
+PromiseHasAnyFlag(PromiseObject& promise, int32_t flag)
+{
+    return promise.getFixedSlot(PromiseSlot_Flags).toInt32() & flag;
+}
+
+static bool ResolvePromiseFunction(JSContext* cx, unsigned argc, Value* vp);
+static bool RejectPromiseFunction(JSContext* cx, unsigned argc, Value* vp);
+
+// ES2016, 25.4.1.3.
+static MOZ_MUST_USE bool
+CreateResolvingFunctions(JSContext* cx, HandleValue promise,
+                         MutableHandleValue resolveVal,
+                         MutableHandleValue rejectVal)
+{
+    RootedAtom funName(cx, cx->names().empty);
+    RootedFunction resolve(cx, NewNativeFunction(cx, ResolvePromiseFunction, 1, funName,
+                                                 gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
+    if (!resolve)
+        return false;
+
+    RootedFunction reject(cx, NewNativeFunction(cx, RejectPromiseFunction, 1, funName,
+                                                 gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
+    if (!reject)
+        return false;
+
+    // TODO: remove this once all self-hosted promise code is gone.
+    // The resolving functions are trusted, so self-hosted code should be able
+    // to call them using callFunction instead of callContentFunction.
+    resolve->setFlags(resolve->flags() | JSFunction::SELF_HOSTED);
+    reject->setFlags(reject->flags() | JSFunction::SELF_HOSTED);
+
+    resolve->setExtendedSlot(ResolveFunctionSlot_Promise, promise);
+    resolve->setExtendedSlot(ResolveFunctionSlot_RejectFunction, ObjectValue(*reject));
+
+    reject->setExtendedSlot(RejectFunctionSlot_Promise, promise);
+    reject->setExtendedSlot(RejectFunctionSlot_ResolveFunction, ObjectValue(*resolve));
+
+    resolveVal.setObject(*resolve);
+    rejectVal.setObject(*reject);
 
     return true;
 }
 
+static void ClearResolutionFunctionSlots(JSFunction* resolutionFun);
+static MOZ_MUST_USE bool RejectMaybeWrappedPromise(JSContext *cx, HandleObject promiseObj,
+                                                   HandleValue reason);
+
+// ES2016, 25.4.1.3.1.
+static bool
+RejectPromiseFunction(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    RootedFunction reject(cx, &args.callee().as<JSFunction>());
+    RootedValue reasonVal(cx, args.get(0));
+
+    // Steps 1-2.
+    RootedValue promiseVal(cx, reject->getExtendedSlot(RejectFunctionSlot_Promise));
+
+    // Steps 3-4.
+    // If the Promise isn't available anymore, it has been resolved and the
+    // reference to it removed to make it eligible for collection.
+    if (promiseVal.isUndefined()) {
+        args.rval().setUndefined();
+        return true;
+    }
+
+    RootedObject promise(cx, &promiseVal.toObject());
+
+    // In some cases the Promise reference on the resolution function won't
+    // have been removed during resolution, so we need to check that here,
+    // too.
+    if (promise->is<PromiseObject>() &&
+        PromiseHasAnyFlag(promise->as<PromiseObject>(), PROMISE_FLAG_RESOLVED))
+    {
+        args.rval().setUndefined();
+        return true;
+    }
+
+    // Step 5.
+    // Here, we only remove the Promise reference from the resolution
+    // functions. Actually marking it as fulfilled/rejected happens later.
+    ClearResolutionFunctionSlots(reject);
+
+    // Step 6.
+    bool result = RejectMaybeWrappedPromise(cx, promise, reasonVal);
+    if (result)
+        args.rval().setUndefined();
+    return result;
+}
+
+static MOZ_MUST_USE bool FulfillMaybeWrappedPromise(JSContext *cx, HandleObject promiseObj,
+                                                    HandleValue value_);
+
+static MOZ_MUST_USE bool EnqueuePromiseResolveThenableJob(JSContext* cx,
+                                                          HandleValue promiseToResolve,
+                                                          HandleValue thenable,
+                                                          HandleValue thenVal);
+
+// ES2016, 25.4.1.3.2, steps 7-13.
+static MOZ_MUST_USE bool
+ResolvePromiseInternal(JSContext* cx, HandleObject promise, HandleValue resolutionVal)
+{
+    // Step 7.
+    if (!resolutionVal.isObject())
+        return FulfillMaybeWrappedPromise(cx, promise, resolutionVal);
+
+    RootedObject resolution(cx, &resolutionVal.toObject());
+
+    // Step 8.
+    RootedValue thenVal(cx);
+    bool status = GetProperty(cx, resolution, resolution, cx->names().then, &thenVal);
+
+    // Step 9.
+    if (!status) {
+        RootedValue error(cx);
+        if (!GetAndClearException(cx, &error))
+            return false;
+
+        return RejectMaybeWrappedPromise(cx, promise, error);
+    }
+
+    // Step 10 (implicit).
+
+    // Step 11.
+    if (!IsCallable(thenVal))
+        return FulfillMaybeWrappedPromise(cx, promise, resolutionVal);
+
+    // Step 12.
+    RootedValue promiseVal(cx, ObjectValue(*promise));
+    if (!EnqueuePromiseResolveThenableJob(cx, promiseVal, resolutionVal, thenVal))
+        return false;
+
+    // Step 13.
+    return true;
+}
+
+// ES2016, 25.4.1.3.2.
+static bool
+ResolvePromiseFunction(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    RootedFunction resolve(cx, &args.callee().as<JSFunction>());
+    RootedValue resolutionVal(cx, args.get(0));
+
+    // Steps 1-2.
+    RootedValue promiseVal(cx, resolve->getExtendedSlot(ResolveFunctionSlot_Promise));
+
+    // Steps 3-4.
+    // We use the reference to the reject function as a signal for whether
+    // the resolve or reject function was already called, at which point
+    // the references on each of the functions are cleared.
+    if (!resolve->getExtendedSlot(ResolveFunctionSlot_RejectFunction).isObject()) {
+        args.rval().setUndefined();
+        return true;
+    }
+
+    RootedObject promise(cx, &promiseVal.toObject());
+
+    // In some cases the Promise reference on the resolution function won't
+    // have been removed during resolution, so we need to check that here,
+    // too.
+    if (promise->is<PromiseObject>() &&
+        PromiseHasAnyFlag(promise->as<PromiseObject>(), PROMISE_FLAG_RESOLVED))
+    {
+        args.rval().setUndefined();
+        return true;
+    }
+
+    // Step 5.
+    // Here, we only remove the Promise reference from the resolution
+    // functions. Actually marking it as fulfilled/rejected happens later.
+    ClearResolutionFunctionSlots(resolve);
+
+    // Step 6.
+    if (resolutionVal == promiseVal) {
+        // Step 6.a.
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_CANNOT_RESOLVE_PROMISE_WITH_ITSELF);
+        RootedValue selfResolutionError(cx);
+        bool status = GetAndClearException(cx, &selfResolutionError);
+        MOZ_ASSERT(status);
+
+        // Step 6.b.
+        status = RejectMaybeWrappedPromise(cx, promise, selfResolutionError);
+        if (status)
+            args.rval().setUndefined();
+        return status;
+    }
+
+    bool status = ResolvePromiseInternal(cx, promise, resolutionVal);
+    if (status)
+        args.rval().setUndefined();
+    return status;
+}
+
+static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp);
+
+/**
+ * Tells the embedding to enqueue a Promise reaction job, based on
+ * three parameters:
+ * reactionObj - The reaction record.
+ * handlerArg_ - The first and only argument to pass to the handler invoked by
+ *              the job. This will be stored on the reaction record.
+ * targetState - The PromiseState this reaction job targets. This decides
+ *               whether the onFulfilled or onRejected handler is called.
+ */
+MOZ_MUST_USE static bool
+EnqueuePromiseReactionJob(JSContext* cx, HandleObject reactionObj,
+                          HandleValue handlerArg_, JS::PromiseState targetState)
+{
+    // The reaction might have been stored on a Promise from another
+    // compartment, which means it would've been wrapped in a CCW.
+    // To properly handle that case here, unwrap it and enter its
+    // compartment, where the job creation should take place anyway.
+    Rooted<PromiseReactionRecord*> reaction(cx);
+    RootedValue handlerArg(cx, handlerArg_);
+    mozilla::Maybe<AutoCompartment> ac;
+    if (IsWrapper(reactionObj)) {
+        RootedObject unwrappedReactionObj(cx, UncheckedUnwrap(reactionObj));
+        if (!unwrappedReactionObj)
+            return false;
+        ac.emplace(cx, unwrappedReactionObj);
+        reaction = &unwrappedReactionObj->as<PromiseReactionRecord>();
+        if (!cx->compartment()->wrap(cx, &handlerArg))
+            return false;
+    } else {
+        reaction = &reactionObj->as<PromiseReactionRecord>();
+    }
+
+    // Must not enqueue a reaction job more than once.
+    MOZ_ASSERT(reaction->targetState() == JS::PromiseState::Pending);
+
+    assertSameCompartment(cx, handlerArg);
+    reaction->setHandlerArg(handlerArg.get());
+
+    RootedValue reactionVal(cx, ObjectValue(*reaction));
+
+    reaction->setTargetState(targetState);
+    RootedValue handler(cx, reaction->handler());
+
+    // If we have a handler callback, we enter that handler's compartment so
+    // that the promise reaction job function is created in that compartment.
+    // That guarantees that the embedding ends up with the right entry global.
+    // This is relevant for some html APIs like fetch that derive information
+    // from said global.
+    mozilla::Maybe<AutoCompartment> ac2;
+    if (handler.isObject()) {
+        RootedObject handlerObj(cx, &handler.toObject());
+
+        // The unwrapping has to be unchecked because we specifically want to
+        // be able to use handlers with wrappers that would only allow calls.
+        // E.g., it's ok to have a handler from a chrome compartment in a
+        // reaction to a content compartment's Promise instance.
+        handlerObj = UncheckedUnwrap(handlerObj);
+        MOZ_ASSERT(handlerObj);
+        ac2.emplace(cx, handlerObj);
+
+        // We need to wrap the reaction to store it on the job function.
+        if (!cx->compartment()->wrap(cx, &reactionVal))
+            return false;
+    }
+
+    // Create the JS function to call when the job is triggered.
+    RootedAtom funName(cx, cx->names().empty);
+    RootedFunction job(cx, NewNativeFunction(cx, PromiseReactionJob, 0, funName,
+                                             gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
+    if (!job)
+        return false;
+
+    // Store the reaction on the reaction job.
+    job->setExtendedSlot(ReactionJobSlot_ReactionRecord, reactionVal);
+
+    // When using JS::AddPromiseReactions, no actual promise is created, so we
+    // might not have one here.
+    // Additionally, we might have an object here that isn't an instance of
+    // Promise. This can happen if content overrides the value of
+    // Promise[@@species] (or invokes Promise#then on a Promise subclass
+    // instance with a non-default @@species value on the constructor) with a
+    // function that returns objects that're not Promise (subclass) instances.
+    // In that case, we just pretend we didn't have an object in the first
+    // place.
+    // If after all this we do have an object, wrap it in case we entered the
+    // handler's compartment above, because we should pass objects from a
+    // single compartment to the enqueuePromiseJob callback.
+    RootedObject promise(cx, reaction->promise());
+    if (promise && promise->is<PromiseObject>()) {
+      if (!cx->compartment()->wrap(cx, &promise))
+          return false;
+    }
+
+    // Using objectFromIncumbentGlobal, we can derive the incumbent global by
+    // unwrapping and then getting the global. This is very convoluted, but
+    // much better than having to store the original global as a private value
+    // because we couldn't wrap it to store it as a normal JS value.
+    RootedObject global(cx);
+    RootedObject objectFromIncumbentGlobal(cx, reaction->incumbentGlobalObject());
+    if (objectFromIncumbentGlobal) {
+        objectFromIncumbentGlobal = CheckedUnwrap(objectFromIncumbentGlobal);
+        MOZ_ASSERT(objectFromIncumbentGlobal);
+        global = &objectFromIncumbentGlobal->global();
+    }
+
+    // Note: the global we pass here might be from a different compartment
+    // than job and promise. While it's somewhat unusual to pass objects
+    // from multiple compartments, in this case we specifically need the
+    // global to be unwrapped because wrapping and unwrapping aren't
+    // necessarily symmetric for globals.
+    return cx->runtime()->enqueuePromiseJob(cx, job, promise, global);
+}
+
+static MOZ_MUST_USE bool TriggerPromiseReactions(JSContext* cx, HandleValue reactionsVal,
+                                                 JS::PromiseState state, HandleValue valueOrReason);
+
 // ES2016, Commoned-out implementation of 25.4.1.4. and 25.4.1.7.
 static MOZ_MUST_USE bool
 ResolvePromise(JSContext* cx, Handle<PromiseObject*> promise, HandleValue valueOrReason,
                JS::PromiseState state)
 {
     // Step 1.
     MOZ_ASSERT(promise->state() == JS::PromiseState::Pending);
     MOZ_ASSERT(state == JS::PromiseState::Fulfilled || state == JS::PromiseState::Rejected);
 
     // Step 2.
     // We only have one list of reactions for both resolution types. So
     // instead of getting the right list of reactions, we determine the
     // resolution type to retrieve the right information from the
     // reaction records.
-    RootedValue reactionsVal(cx, promise->getFixedSlot(PROMISE_REACTIONS_OR_RESULT_SLOT));
-
-    // Step 3-5.
+    RootedValue reactionsVal(cx, promise->getFixedSlot(PromiseSlot_ReactionsOrResult));
+
+    // Steps 3-5.
     // The same slot is used for the reactions list and the result, so setting
     // the result also removes the reactions list.
-    promise->setFixedSlot(PROMISE_REACTIONS_OR_RESULT_SLOT, valueOrReason);
+    promise->setFixedSlot(PromiseSlot_ReactionsOrResult, valueOrReason);
 
     // Step 6.
-    int32_t flags = promise->getFixedSlot(PROMISE_FLAGS_SLOT).toInt32();
+    int32_t flags = promise->getFixedSlot(PromiseSlot_Flags).toInt32();
     flags |= PROMISE_FLAG_RESOLVED;
     if (state == JS::PromiseState::Fulfilled)
         flags |= PROMISE_FLAG_FULFILLED;
-    promise->setFixedSlot(PROMISE_FLAGS_SLOT, Int32Value(flags));
+    promise->setFixedSlot(PromiseSlot_Flags, Int32Value(flags));
 
     // Also null out the resolve/reject functions so they can be GC'd.
-    promise->setFixedSlot(PROMISE_RESOLVE_FUNCTION_SLOT, UndefinedValue());
+    promise->setFixedSlot(PromiseSlot_RejectFunction, UndefinedValue());
 
     // Now that everything else is done, do the things the debugger needs.
     // Step 7 of RejectPromise implemented in onSettled.
     promise->onSettled(cx);
 
     // Step 7 of FulfillPromise.
     // Step 8 of RejectPromise.
     if (reactionsVal.isObject())
         return TriggerPromiseReactions(cx, reactionsVal, state, valueOrReason);
 
     return true;
 }
 
 // ES2016, 25.4.1.4.
 static MOZ_MUST_USE bool
-FulfillMaybeWrappedPromise(JSContext *cx, HandleObject promiseObj, HandleValue value_) {
+FulfillMaybeWrappedPromise(JSContext *cx, HandleObject promiseObj, HandleValue value_)
+{
     Rooted<PromiseObject*> promise(cx);
     RootedValue value(cx, value_);
 
     mozilla::Maybe<AutoCompartment> ac;
     if (!IsProxy(promiseObj)) {
         promise = &promiseObj->as<PromiseObject>();
     } else {
         if (JS_IsDeadWrapper(promiseObj)) {
@@ -182,19 +604,135 @@ FulfillMaybeWrappedPromise(JSContext *cx
             return false;
     }
 
     MOZ_ASSERT(promise->state() == JS::PromiseState::Pending);
 
     return ResolvePromise(cx, promise, value, JS::PromiseState::Fulfilled);
 }
 
+static bool GetCapabilitiesExecutor(JSContext* cx, unsigned argc, Value* vp);
+static bool PromiseConstructor(JSContext* cx, unsigned argc, Value* vp);
+static MOZ_MUST_USE PromiseObject* CreatePromiseObjectInternal(JSContext* cx,
+                                                               HandleObject proto = nullptr,
+                                                               bool protoIsWrapped = false,
+                                                               bool informDebugger = true);
+
+enum GetCapabilitiesExecutorSlots {
+    GetCapabilitiesExecutorSlots_Resolve,
+    GetCapabilitiesExecutorSlots_Reject
+};
+
+// ES2016, 25.4.1.5.
+static MOZ_MUST_USE bool
+NewPromiseCapability(JSContext* cx, HandleObject C, MutableHandleObject promise,
+                     MutableHandleObject resolve, MutableHandleObject reject,
+                     bool canOmitResolutionFunctions)
+{
+    RootedValue cVal(cx, ObjectValue(*C));
+
+    // Steps 1-2.
+    if (!IsConstructor(C)) {
+        ReportValueError(cx, JSMSG_NOT_CONSTRUCTOR, -1, cVal, nullptr);
+        return false;
+    }
+
+    // If we'd call the original Promise constructor and know that the
+    // resolve/reject functions won't ever escape to content, we can skip
+    // creating and calling the executor function and instead return a Promise
+    // marked as having default resolve/reject functions.
+    //
+    // This can't be used in Promise.all and Promise.race because we have to
+    // pass the reject (and resolve, in the race case) function to thenables
+    // in the list passed to all/race, which (potentially) means exposing them
+    // to content.
+    if (canOmitResolutionFunctions && IsNativeFunction(cVal, PromiseConstructor)) {
+        promise.set(CreatePromiseObjectInternal(cx));
+        if (!promise)
+            return false;
+        AddPromiseFlags(promise->as<PromiseObject>(), PROMISE_FLAG_DEFAULT_RESOLVE_FUNCTION |
+                                                      PROMISE_FLAG_DEFAULT_REJECT_FUNCTION);
+
+        return true;
+    }
+
+    // Step 3 (omitted).
+
+    // Step 4.
+    RootedAtom funName(cx, cx->names().empty);
+    RootedFunction executor(cx, NewNativeFunction(cx, GetCapabilitiesExecutor, 2, funName,
+                                                  gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
+    if (!executor)
+        return false;
+
+    // Step 5 (omitted).
+
+    // Step 6.
+    FixedConstructArgs<1> cargs(cx);
+    cargs[0].setObject(*executor);
+    if (!Construct(cx, cVal, cargs, cVal, promise))
+        return false;
+
+    // Step 7.
+    RootedValue resolveVal(cx, executor->getExtendedSlot(GetCapabilitiesExecutorSlots_Resolve));
+    if (!IsCallable(resolveVal)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_PROMISE_RESOLVE_FUNCTION_NOT_CALLABLE);
+        return false;
+    }
+
+    // Step 8.
+    RootedValue rejectVal(cx, executor->getExtendedSlot(GetCapabilitiesExecutorSlots_Reject));
+    if (!IsCallable(rejectVal)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_PROMISE_REJECT_FUNCTION_NOT_CALLABLE);
+        return false;
+    }
+
+    // Step 9 (well, the equivalent for all of promiseCapabilities' fields.)
+    resolve.set(&resolveVal.toObject());
+    reject.set(&rejectVal.toObject());
+
+    // Step 10.
+    return true;
+}
+
+// ES2016, 25.4.1.5.1.
+static bool
+GetCapabilitiesExecutor(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    RootedFunction F(cx, &args.callee().as<JSFunction>());
+
+    // Steps 1-2 (implicit).
+
+    // Steps 3-4.
+    if (!F->getExtendedSlot(GetCapabilitiesExecutorSlots_Resolve).isUndefined() ||
+        !F->getExtendedSlot(GetCapabilitiesExecutorSlots_Reject).isUndefined())
+    {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_PROMISE_CAPABILITY_HAS_SOMETHING_ALREADY);
+        return false;
+    }
+
+    // Step 5.
+    F->setExtendedSlot(GetCapabilitiesExecutorSlots_Resolve, args.get(0));
+
+    // Step 6.
+    F->setExtendedSlot(GetCapabilitiesExecutorSlots_Reject, args.get(1));
+
+    // Step 7.
+    args.rval().setUndefined();
+    return true;
+}
+
 // ES2016, 25.4.1.7.
 static MOZ_MUST_USE bool
-RejectMaybeWrappedPromise(JSContext *cx, HandleObject promiseObj, HandleValue reason_) {
+RejectMaybeWrappedPromise(JSContext *cx, HandleObject promiseObj, HandleValue reason_)
+{
     Rooted<PromiseObject*> promise(cx);
     RootedValue reason(cx, reason_);
 
     mozilla::Maybe<AutoCompartment> ac;
     if (!IsProxy(promiseObj)) {
         promise = &promiseObj->as<PromiseObject>();
     } else {
         if (JS_IsDeadWrapper(promiseObj)) {
@@ -225,182 +763,328 @@ RejectMaybeWrappedPromise(JSContext *cx,
         }
     }
 
     MOZ_ASSERT(promise->state() == JS::PromiseState::Pending);
 
     return ResolvePromise(cx, promise, reason, JS::PromiseState::Rejected);
 }
 
+// ES2016, 25.4.1.8.
+static MOZ_MUST_USE bool
+TriggerPromiseReactions(JSContext* cx, HandleValue reactionsVal, JS::PromiseState state,
+                        HandleValue valueOrReason)
+{
+    RootedObject reactions(cx, &reactionsVal.toObject());
+    RootedObject reaction(cx);
+
+    if (reactions->is<PromiseReactionRecord>() || IsWrapper(reactions))
+        return EnqueuePromiseReactionJob(cx, reactions, valueOrReason, state);
+
+    RootedNativeObject reactionsList(cx, &reactions->as<NativeObject>());
+    size_t reactionsCount = reactionsList->getDenseInitializedLength();
+    MOZ_ASSERT(reactionsCount > 1, "Reactions list should be created lazily");
+
+    for (size_t i = 0; i < reactionsCount; i++) {
+        reaction = &reactionsList->getDenseElement(i).toObject();
+        if (!EnqueuePromiseReactionJob(cx, reaction, valueOrReason, state))
+            return false;
+    }
+
+    return true;
+}
+
+// ES2016, 25.4.2.1.
+/**
+ * Callback triggering the fulfill/reject reaction for a resolved Promise,
+ * to be invoked by the embedding during its processing of the Promise job
+ * queue.
+ *
+ * See http://www.ecma-international.org/ecma-262/7.0/index.html#sec-jobs-and-job-queues
+ *
+ * A PromiseReactionJob is set as the native function of an extended
+ * JSFunction object, with all information required for the job's
+ * execution stored in in a reaction record in its first extended slot.
+ */
+static bool
+PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    RootedFunction job(cx, &args.callee().as<JSFunction>());
+
+    RootedObject reactionObj(cx, &job->getExtendedSlot(ReactionJobSlot_ReactionRecord).toObject());
+
+    // To ensure that the embedding ends up with the right entry global, we're
+    // guaranteeing that the reaction job function gets created in the same
+    // compartment as the handler function. That's not necessarily the global
+    // that the job was triggered from, though.
+    // We can find the triggering global via the job's reaction record. To go
+    // back, we check if the reaction is a wrapper and if so, unwrap it and
+    // enter its compartment.
+    mozilla::Maybe<AutoCompartment> ac;
+    if (IsWrapper(reactionObj)) {
+        reactionObj = UncheckedUnwrap(reactionObj);
+        ac.emplace(cx, reactionObj);
+    }
+
+    // Steps 1-2.
+    Rooted<PromiseReactionRecord*> reaction(cx, &reactionObj->as<PromiseReactionRecord>());
+
+    // Step 3.
+    RootedValue handlerVal(cx, reaction->handler());
+
+    RootedValue argument(cx, reaction->handlerArg());
+
+    RootedValue handlerResult(cx);
+    ResolutionMode resolutionMode = ResolveMode;
+
+    // Steps 4-6.
+    if (handlerVal.isNumber()) {
+        int32_t handlerNum = int32_t(handlerVal.toNumber());
+
+        // Step 4.
+        if (handlerNum == PROMISE_HANDLER_IDENTITY) {
+            handlerResult = argument;
+        } else {
+            // Step 5.
+            MOZ_ASSERT(handlerNum == PROMISE_HANDLER_THROWER);
+            resolutionMode = RejectMode;
+            handlerResult = argument;
+        }
+    } else {
+        // Step 6.
+        FixedInvokeArgs<1> args2(cx);
+        args2[0].set(argument);
+        if (!Call(cx, handlerVal, UndefinedHandleValue, args2, &handlerResult)) {
+            resolutionMode = RejectMode;
+            // Not much we can do about uncatchable exceptions, so just bail
+            // for those.
+            if (!cx->isExceptionPending() || !GetAndClearException(cx, &handlerResult))
+                return false;
+        }
+    }
+
+    // Steps 7-9.
+    size_t hookSlot = resolutionMode == RejectMode
+                      ? ReactionRecordSlot_Reject
+                      : ReactionRecordSlot_Resolve;
+    RootedObject callee(cx, reaction->getFixedSlot(hookSlot).toObjectOrNull());
+    RootedObject promiseObj(cx, reaction->promise());
+    if (!RunResolutionFunction(cx, callee, handlerResult, resolutionMode, promiseObj))
+        return false;
+
+    args.rval().setUndefined();
+    return true;
+}
+
+// ES2016, 25.4.2.2.
+/**
+ * Callback for resolving a thenable, to be invoked by the embedding during
+ * its processing of the Promise job queue.
+ *
+ * See http://www.ecma-international.org/ecma-262/7.0/index.html#sec-jobs-and-job-queues
+ *
+ * A PromiseResolveThenableJob is set as the native function of an extended
+ * JSFunction object, with all information required for the job's
+ * execution stored in the function's extended slots.
+ *
+ * Usage of the function's extended slots is as follows:
+ * ThenableJobSlot_Handler: The handler to use as the Promise reaction.
+ *                          This can be PROMISE_HANDLER_IDENTITY,
+ *                          PROMISE_HANDLER_THROWER, or a callable. In the
+ *                          latter case, it's guaranteed to be an object
+ *                          from the same compartment as the
+ *                          PromiseReactionJob.
+ * ThenableJobSlot_JobData: JobData - a, potentially CCW-wrapped, dense list
+ *                          containing data required for proper execution of
+ *                          the reaction.
+ *
+ * The JobData list has the following entries:
+ * ThenableJobDataSlot_Promise: The Promise to resolve using the given
+ *                              thenable.
+ * ThenableJobDataSlot_Thenable: The thenable to use as the receiver when
+ *                               calling the `then` function.
+ */
+static bool
+PromiseResolveThenableJob(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    RootedFunction job(cx, &args.callee().as<JSFunction>());
+    RootedValue then(cx, job->getExtendedSlot(ThenableJobSlot_Handler));
+    MOZ_ASSERT(!IsWrapper(&then.toObject()));
+    RootedNativeObject jobArgs(cx, &job->getExtendedSlot(ThenableJobSlot_JobData)
+                                    .toObject().as<NativeObject>());
+
+    RootedValue promise(cx, jobArgs->getDenseElement(ThenableJobDataIndex_Promise));
+    RootedValue thenable(cx, jobArgs->getDenseElement(ThenableJobDataIndex_Thenable));
+
+    // Step 1.
+    RootedValue resolveVal(cx);
+    RootedValue rejectVal(cx);
+    if (!CreateResolvingFunctions(cx, promise, &resolveVal, &rejectVal))
+        return false;
+
+    // Step 2.
+    FixedInvokeArgs<2> args2(cx);
+    args2[0].set(resolveVal);
+    args2[1].set(rejectVal);
+
+    RootedValue rval(cx);
+
+    // In difference to the usual pattern, we return immediately on success.
+    if (Call(cx, then, thenable, args2, &rval))
+        return true;
+
+    if (!GetAndClearException(cx, &rval))
+        return false;
+
+    FixedInvokeArgs<1> rejectArgs(cx);
+    rejectArgs[0].set(rval);
+
+    return Call(cx, rejectVal, UndefinedHandleValue, rejectArgs, &rval);
+}
+
+/**
+ * Tells the embedding to enqueue a Promise resolve thenable job, based on
+ * three parameters:
+ * promiseToResolve_ - The promise to resolve, obviously.
+ * thenable_ - The thenable to resolve the Promise with.
+ * thenVal - The `then` function to invoke with the `thenable` as the receiver.
+ */
+static MOZ_MUST_USE bool
+EnqueuePromiseResolveThenableJob(JSContext* cx, HandleValue promiseToResolve_,
+                                 HandleValue thenable_, HandleValue thenVal)
+{
+    // Need to re-root these to enable wrapping them below.
+    RootedValue promiseToResolve(cx, promiseToResolve_);
+    RootedValue thenable(cx, thenable_);
+
+    // We enter the `then` callable's compartment so that the job function is
+    // created in that compartment.
+    // That guarantees that the embedding ends up with the right entry global.
+    // This is relevant for some html APIs like fetch that derive information
+    // from said global.
+    RootedObject then(cx, CheckedUnwrap(&thenVal.toObject()));
+    AutoCompartment ac(cx, then);
+
+    RootedAtom funName(cx, cx->names().empty);
+    RootedFunction job(cx, NewNativeFunction(cx, PromiseResolveThenableJob, 0, funName,
+                                             gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
+    if (!job)
+        return false;
+
+    // Store the `then` function on the callback.
+    job->setExtendedSlot(ThenableJobSlot_Handler, ObjectValue(*then));
+
+    // Create a dense array to hold the data needed for the reaction job to
+    // work.
+    // See the doc comment for PromiseResolveThenableJob for the layout.
+    RootedArrayObject data(cx, NewDenseFullyAllocatedArray(cx, ThenableJobDataLength));
+    if (!data ||
+        data->ensureDenseElements(cx, 0, ThenableJobDataLength) != DenseElementResult::Success)
+    {
+        return false;
+    }
+
+    // Wrap and set the `promiseToResolve` argument.
+    if (!cx->compartment()->wrap(cx, &promiseToResolve))
+        return false;
+    data->setDenseElement(ThenableJobDataIndex_Promise, promiseToResolve);
+    // At this point the promise is guaranteed to be wrapped into the job's
+    // compartment.
+    RootedObject promise(cx, &promiseToResolve.toObject());
+
+    // Wrap and set the `thenable` argument.
+    MOZ_ASSERT(thenable.isObject());
+    if (!cx->compartment()->wrap(cx, &thenable))
+        return false;
+    data->setDenseElement(ThenableJobDataIndex_Thenable, thenable);
+
+    // Store the data array on the reaction job.
+    job->setExtendedSlot(ThenableJobSlot_JobData, ObjectValue(*data));
+
+    RootedObject incumbentGlobal(cx, cx->runtime()->getIncumbentGlobal(cx));
+    return cx->runtime()->enqueuePromiseJob(cx, job, promise, incumbentGlobal);
+}
+
+static MOZ_MUST_USE bool
+AddPromiseReaction(JSContext* cx, Handle<PromiseObject*> promise, HandleValue onFulfilled,
+                   HandleValue onRejected, HandleObject dependentPromise,
+                   HandleObject resolve, HandleObject reject, HandleObject incumbentGlobal);
+
+static MOZ_MUST_USE bool
+AddPromiseReaction(JSContext* cx, Handle<PromiseObject*> promise,
+                   Handle<PromiseReactionRecord*> reaction);
+
+static MOZ_MUST_USE bool BlockOnPromise(JSContext* cx, HandleObject promise,
+                                        HandleObject blockedPromise,
+                                        HandleValue onFulfilled, HandleValue onRejected);
+
+static JSFunction*
+GetResolveFunctionFromReject(JSFunction* reject)
+{
+    MOZ_ASSERT(reject->maybeNative() == RejectPromiseFunction);
+    Value resolveFunVal = reject->getExtendedSlot(RejectFunctionSlot_ResolveFunction);
+    if (IsNativeFunction(resolveFunVal, ResolvePromiseFunction))
+        return &resolveFunVal.toObject().as<JSFunction>();
+
+    PromiseAllDataHolder* resolveFunObj = &resolveFunVal.toObject().as<PromiseAllDataHolder>();
+    return &resolveFunObj->resolveObj()->as<JSFunction>();
+}
+
+static JSFunction*
+GetResolveFunctionFromPromise(PromiseObject* promise)
+{
+    Value rejectFunVal = promise->getFixedSlot(PromiseSlot_RejectFunction);
+    if (rejectFunVal.isUndefined())
+        return nullptr;
+    JSObject* rejectFunObj = &rejectFunVal.toObject();
+    if (IsWrapper(rejectFunObj))
+        rejectFunObj = CheckedUnwrap(rejectFunObj);
+
+    if (!rejectFunObj->is<JSFunction>())
+        return nullptr;
+
+    JSFunction* rejectFun = &rejectFunObj->as<JSFunction>();
+
+    // Only the original RejectPromiseFunction has a reference to the resolve
+    // function.
+    if (rejectFun->maybeNative() != &RejectPromiseFunction)
+        return nullptr;
+
+    return GetResolveFunctionFromReject(rejectFun);
+}
+
 static void
 ClearResolutionFunctionSlots(JSFunction* resolutionFun)
 {
-    JSFunction* otherFun = &resolutionFun->getExtendedSlot(ResolutionFunctionSlot_OtherFunction)
-                           .toObject().as<JSFunction>();
-    resolutionFun->setExtendedSlot(ResolutionFunctionSlot_Promise, UndefinedValue());
-    otherFun->setExtendedSlot(ResolutionFunctionSlot_Promise, UndefinedValue());
-
-    // Also reset the reference to the resolve function so it can be collected.
-    resolutionFun->setExtendedSlot(ResolutionFunctionSlot_OtherFunction, UndefinedValue());
-    otherFun->setExtendedSlot(ResolutionFunctionSlot_OtherFunction, UndefinedValue());
-}
-// ES2016, 25.4.1.3.1.
-static bool
-RejectPromiseFunction(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-
-    RootedFunction reject(cx, &args.callee().as<JSFunction>());
-    RootedValue reasonVal(cx, args.get(0));
-
-    // Steps 1-2.
-    RootedValue promiseVal(cx, reject->getExtendedSlot(ResolutionFunctionSlot_Promise));
-
-    // Steps 3-4.
-    // We use the existence of the Promise a a signal for whether it was
-    // already resolved. Upon resolution, it's reset to `undefined`.
-    if (promiseVal.isUndefined()) {
-        args.rval().setUndefined();
-        return true;
+    JSFunction* resolve;
+    JSFunction* reject;
+    if (resolutionFun->maybeNative() == ResolvePromiseFunction) {
+        resolve = resolutionFun;
+        reject = &resolutionFun->getExtendedSlot(ResolveFunctionSlot_RejectFunction)
+                  .toObject().as<JSFunction>();
+    } else {
+        resolve = GetResolveFunctionFromReject(resolutionFun);
+        reject = resolutionFun;
     }
 
-    RootedObject promise(cx, &promiseVal.toObject());
-
-    // Step 5.
-    ClearResolutionFunctionSlots(reject);
-
-    // Step 6.
-    bool result = RejectMaybeWrappedPromise(cx, promise, reasonVal);
-    if (result)
-        args.rval().setUndefined();
-    return result;
-}
-
-// ES2016, 25.4.1.3.2, steps 7-13.
-static bool
-ResolvePromiseInternal(JSContext* cx, HandleObject promise, HandleValue resolutionVal)
-{
-    // Step 7.
-    if (!resolutionVal.isObject())
-        return FulfillMaybeWrappedPromise(cx, promise, resolutionVal);
-
-    RootedObject resolution(cx, &resolutionVal.toObject());
-
-    // Step 8.
-    RootedValue thenVal(cx);
-    bool status = GetProperty(cx, resolution, resolution, cx->names().then, &thenVal);
-
-    // Step 9.
-    if (!status) {
-        RootedValue error(cx);
-        if (!GetAndClearException(cx, &error))
-            return false;
-
-        return RejectMaybeWrappedPromise(cx, promise, error);
-    }
-
-    // Step 10 (implicit).
-
-    // Step 11.
-    if (!IsCallable(thenVal)) {
-        return FulfillMaybeWrappedPromise(cx, promise, resolutionVal);
-    }
-
-    // Step 12.
-    RootedValue promiseVal(cx, ObjectValue(*promise));
-    if (!EnqueuePromiseResolveThenableJob(cx, promiseVal, resolutionVal, thenVal))
-        return false;
-
-    // Step 13.
-    return true;
+    resolve->setExtendedSlot(ResolveFunctionSlot_Promise, UndefinedValue());
+    resolve->setExtendedSlot(ResolveFunctionSlot_RejectFunction, UndefinedValue());
+
+    reject->setExtendedSlot(RejectFunctionSlot_Promise, UndefinedValue());
+    reject->setExtendedSlot(RejectFunctionSlot_ResolveFunction, UndefinedValue());
 }
 
-// ES2016, 25.4.1.3.2.
-static bool
-ResolvePromiseFunction(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-
-    RootedFunction resolve(cx, &args.callee().as<JSFunction>());
-    RootedValue resolutionVal(cx, args.get(0));
-
-    // Steps 1-2.
-    RootedValue promiseVal(cx, resolve->getExtendedSlot(ResolutionFunctionSlot_Promise));
-
-    // Steps 3-4.
-    // We use the existence of the Promise a a signal for whether it was
-    // already resolved. Upon resolution, it's reset to `undefined`.
-    if (promiseVal.isUndefined()) {
-        args.rval().setUndefined();
-        return true;
-    }
-
-    RootedObject promise(cx, &promiseVal.toObject());
-
-    // Step 5.
-    ClearResolutionFunctionSlots(resolve);
-
-    // Step 6.
-    if (resolutionVal == promiseVal) {
-        // Step 6.a.
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                  JSMSG_CANNOT_RESOLVE_PROMISE_WITH_ITSELF);
-        RootedValue selfResolutionError(cx);
-        bool status = GetAndClearException(cx, &selfResolutionError);
-        MOZ_ASSERT(status);
-
-        // Step 6.b.
-        status = RejectMaybeWrappedPromise(cx, promise, selfResolutionError);
-        if (status)
-            args.rval().setUndefined();
-        return status;
-    }
-
-    bool status = ResolvePromiseInternal(cx, promise, resolutionVal);
-    if (status)
-        args.rval().setUndefined();
-    return status;
-}
-
-// ES2016, 25.4.1.3.
-static MOZ_MUST_USE bool
-CreateResolvingFunctions(JSContext* cx, HandleValue promise,
-                         MutableHandleValue resolveVal,
-                         MutableHandleValue rejectVal)
-{
-    RootedAtom funName(cx, cx->names().empty);
-    RootedFunction resolve(cx, NewNativeFunction(cx, ResolvePromiseFunction, 1, funName,
-                                                 gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
-    if (!resolve)
-        return false;
-
-    RootedFunction reject(cx, NewNativeFunction(cx, RejectPromiseFunction, 1, funName,
-                                                 gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
-    if (!reject)
-        return false;
-
-    // The resolving functions are trusted, so self-hosted code should be able
-    // to call them using callFunction instead of callContentFunction.
-    resolve->setFlags(resolve->flags() | JSFunction::SELF_HOSTED);
-    reject->setFlags(reject->flags() | JSFunction::SELF_HOSTED);
-
-    resolve->setExtendedSlot(ResolutionFunctionSlot_Promise, promise);
-    resolve->setExtendedSlot(ResolutionFunctionSlot_OtherFunction, ObjectValue(*reject));
-
-    reject->setExtendedSlot(ResolutionFunctionSlot_Promise, promise);
-    reject->setExtendedSlot(ResolutionFunctionSlot_OtherFunction, ObjectValue(*resolve));
-
-    resolveVal.setObject(*resolve);
-    rejectVal.setObject(*reject);
-
-    return true;
-}
-
-static PromiseObject*
-CreatePromiseObjectInternal(JSContext* cx, HandleObject proto, bool protoIsWrapped)
+// ES2016, 25.4.3.1. steps 3-7.
+static MOZ_MUST_USE PromiseObject*
+CreatePromiseObjectInternal(JSContext* cx, HandleObject proto /* = nullptr */,
+                            bool protoIsWrapped /* = false */, bool informDebugger /* = true */)
 {
     // Step 3.
     Rooted<PromiseObject*> promise(cx);
     // Enter the unwrapped proto's compartment, if that's different from
     // the current one.
     // All state stored in a Promise's fixed slots must be created in the
     // same compartment, so we get all of that out of the way here.
     // (Except for the resolution functions, which are created below.)
@@ -408,466 +1092,44 @@ CreatePromiseObjectInternal(JSContext* c
     if (protoIsWrapped)
         ac.emplace(cx, proto);
 
     promise = NewObjectWithClassProto<PromiseObject>(cx, proto);
     if (!promise)
         return nullptr;
 
     // Step 4.
-    promise->setFixedSlot(PROMISE_FLAGS_SLOT, Int32Value(0));
+    promise->setFixedSlot(PromiseSlot_Flags, Int32Value(0));
 
     // Steps 5-6.
     // Omitted, we allocate our single list of reaction records lazily.
 
     // Step 7.
     // Implicit, the handled flag is unset by default.
 
     // Store an allocation stack so we can later figure out what the
     // control flow was for some unexpected results. Frightfully expensive,
     // but oh well.
     RootedObject stack(cx);
     if (cx->options().asyncStack() || cx->compartment()->isDebuggee()) {
         if (!JS::CaptureCurrentStack(cx, &stack, JS::StackCapture(JS::AllFrames())))
             return nullptr;
     }
-    promise->setFixedSlot(PROMISE_ALLOCATION_SITE_SLOT, ObjectOrNullValue(stack));
-    promise->setFixedSlot(PROMISE_ALLOCATION_TIME_SLOT,
-                          DoubleValue(MillisecondsSinceStartup()));
+    promise->setFixedSlot(PromiseSlot_AllocationSite, ObjectOrNullValue(stack));
+    promise->setFixedSlot(PromiseSlot_AllocationTime, DoubleValue(MillisecondsSinceStartup()));
+
+    // Let the Debugger know about this Promise.
+    if (informDebugger)
+        JS::dbg::onNewPromise(cx, promise);
 
     return promise;
 }
 
-/**
- * Unforgeable version of ES2016, 25.4.4.4, Promise.reject.
- */
-/* static */ JSObject*
-PromiseObject::unforgeableReject(JSContext* cx, HandleValue value)
-{
-    // Steps 1-2 (omitted).
-
-    // Roughly step 3.
-    Rooted<PromiseObject*> promise(cx, CreatePromiseObjectInternal(cx, nullptr, false));
-    if (!promise)
-        return nullptr;
-
-    // Let the Debugger know about this Promise.
-    JS::dbg::onNewPromise(cx, promise);
-
-    // Roughly step 4.
-    if (!ResolvePromise(cx, promise, value, JS::PromiseState::Rejected))
-        return nullptr;
-
-    // Step 5.
-    return promise;
-}
-
-/**
- * Unforgeable version of ES2016, 25.4.4.5, Promise.resolve.
- */
-/* static */ JSObject*
-PromiseObject::unforgeableResolve(JSContext* cx, HandleValue value)
-{
-    // Steps 1-2 (omitted).
-
-    // Step 3.
-    if (value.isObject()) {
-        JSObject* obj = &value.toObject();
-        if (IsWrapper(obj))
-            obj = CheckedUnwrap(obj);
-        // Instead of getting the `constructor` property, do an unforgeable
-        // check.
-        if (obj && obj->is<PromiseObject>())
-            return obj;
-    }
-
-    // Step 4.
-    Rooted<PromiseObject*> promise(cx, CreatePromiseObjectInternal(cx, nullptr, false));
-    if (!promise)
-        return nullptr;
-
-    // Let the Debugger know about this Promise.
-    JS::dbg::onNewPromise(cx, promise);
-
-    // Steps 5.
-    if (!ResolvePromiseInternal(cx, promise, value))
-        return nullptr;
-
-    // Step 6.
-    return promise;
-}
-
-// ES6, 25.4.1.5.1.
-/* static */ bool
-GetCapabilitiesExecutor(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-    RootedFunction F(cx, &args.callee().as<JSFunction>());
-
-    // Steps 1-2 (implicit).
-
-    // Steps 3-4.
-    if (!F->getExtendedSlot(0).isUndefined() || !F->getExtendedSlot(1).isUndefined()) {
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                  JSMSG_PROMISE_CAPABILITY_HAS_SOMETHING_ALREADY);
-        return false;
-    }
-
-    // Step 5.
-    F->setExtendedSlot(0, args.get(0));
-
-    // Step 6.
-    F->setExtendedSlot(1, args.get(1));
-
-    // Step 7.
-    args.rval().setUndefined();
-    return true;
-}
-
-// ES2016, 25.4.1.5.
-// Creates PromiseCapability records, see 25.4.1.1.
-static bool
-NewPromiseCapability(JSContext* cx, HandleObject C, MutableHandleObject promise,
-                     MutableHandleObject resolve, MutableHandleObject reject)
-{
-    RootedValue cVal(cx, ObjectValue(*C));
-
-    // Steps 1-2.
-    if (!IsConstructor(C)) {
-        ReportValueError(cx, JSMSG_NOT_CONSTRUCTOR, -1, cVal, nullptr);
-        return false;
-    }
-
-    // Step 3 (omitted).
-
-    // Step 4.
-    RootedAtom funName(cx, cx->names().empty);
-    RootedFunction executor(cx, NewNativeFunction(cx, GetCapabilitiesExecutor, 2, funName,
-                                                  gc::AllocKind::FUNCTION_EXTENDED));
-    if (!executor)
-        return false;
-
-    // Step 5 (omitted).
-
-    // Step 6.
-    FixedConstructArgs<1> cargs(cx);
-    cargs[0].setObject(*executor);
-    if (!Construct(cx, cVal, cargs, cVal, promise))
-        return false;
-
-    // Step 7.
-    RootedValue resolveVal(cx, executor->getExtendedSlot(0));
-    if (!IsCallable(resolveVal)) {
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                  JSMSG_PROMISE_RESOLVE_FUNCTION_NOT_CALLABLE);
-        return false;
-    }
-
-    // Step 8.
-    RootedValue rejectVal(cx, executor->getExtendedSlot(1));
-    if (!IsCallable(rejectVal)) {
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                  JSMSG_PROMISE_REJECT_FUNCTION_NOT_CALLABLE);
-        return false;
-    }
-
-    // Step 9 (well, the equivalent for all of promiseCapabilities' fields.)
-    resolve.set(&resolveVal.toObject());
-    reject.set(&rejectVal.toObject());
-
-    // Step 10.
-    return true;
-}
-
-enum ResolveOrRejectMode {
-    ResolveMode,
-    RejectMode
-};
-
+// ES2016, 25.4.3.1.
 static bool
-CommonStaticResolveRejectImpl(JSContext* cx, unsigned argc, Value* vp, ResolveOrRejectMode mode)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-    RootedValue x(cx, args.get(0));
-
-    // Steps 1-2.
-    if (!args.thisv().isObject()) {
-        const char* msg = mode == ResolveMode
-                          ? "Receiver of Promise.resolve call"
-                          : "Receiver of Promise.reject call";
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT, msg);
-        return false;
-    }
-    RootedValue cVal(cx, args.thisv());
-    RootedObject C(cx, &cVal.toObject());
-
-    // Step 3 of Resolve.
-    if (mode == ResolveMode && x.isObject()) {
-        RootedObject xObj(cx, &x.toObject());
-        bool isPromise = false;
-        if (xObj->is<PromiseObject>()) {
-            isPromise = true;
-        } else if (IsWrapper(xObj)) {
-            // Treat instances of Promise from other compartments as Promises
-            // here, too.
-            // It's important to do the GetProperty for the `constructor`
-            // below through the wrapper, because wrappers can change the
-            // outcome, so instead of unwrapping and then performing the
-            // GetProperty, just check here and then operate on the original
-            // object again.
-            RootedObject unwrappedObject(cx, CheckedUnwrap(xObj));
-            if (unwrappedObject && unwrappedObject->is<PromiseObject>())
-                isPromise = true;
-        }
-        if (isPromise) {
-            RootedValue ctorVal(cx);
-            if (!GetProperty(cx, xObj, xObj, cx->names().constructor, &ctorVal))
-                return false;
-            if (ctorVal == cVal) {
-                args.rval().set(x);
-                return true;
-            }
-        }
-    }
-
-    // Steps 4-5 of Resolve, 3-4 of Reject.
-    RootedObject promiseCtor(cx);
-    if (!GetBuiltinConstructor(cx, JSProto_Promise, &promiseCtor))
-        return false;
-    RootedObject promise(cx);
-
-    // If the current constructor is the original Promise constructor, we can
-    // optimize things by skipping the creation and invocation of the resolve
-    // and reject callbacks, directly creating and resolving the new Promise.
-    if (promiseCtor == C) {
-        // Roughly step 4 of Resolve, 3 of Reject.
-        promise = CreatePromiseObjectInternal(cx, nullptr, false);
-        if (!promise)
-            return false;
-
-        // Let the Debugger know about this Promise.
-        JS::dbg::onNewPromise(cx, promise);
-
-        // Roughly step 5 of Resolve.
-        if (mode == ResolveMode) {
-            if (!ResolvePromiseInternal(cx, promise, x))
-                return false;
-        } else {
-            // Roughly step 4 of Reject.
-            Rooted<PromiseObject*> promiseObj(cx, &promise->as<PromiseObject>());
-            if (!ResolvePromise(cx, promiseObj, x, JS::PromiseState::Rejected))
-                return false;
-        }
-    } else {
-        // Step 4 of Resolve, 3 of Reject.
-        RootedObject resolveFun(cx);
-        RootedObject rejectFun(cx);
-        if (!NewPromiseCapability(cx, C, &promise, &resolveFun, &rejectFun))
-            return false;
-
-        // Step 5 of Resolve, 4 of Reject.
-        FixedInvokeArgs<1> args2(cx);
-        args2[0].set(x);
-        RootedValue calleeOrRval(cx, ObjectValue(mode == ResolveMode ? *resolveFun : *rejectFun));
-        if (!Call(cx, calleeOrRval, UndefinedHandleValue, args2, &calleeOrRval))
-            return false;
-    }
-
-    // Step 6 of Resolve, 4 of Reject.
-    args.rval().setObject(*promise);
-    return true;
-}
-
-/**
- * ES2016, 25.4.4.4, Promise.reject.
- */
-static bool
-Promise_reject(JSContext* cx, unsigned argc, Value* vp)
-{
-    return CommonStaticResolveRejectImpl(cx, argc, vp, RejectMode);
-}
-
-/**
- * ES2016, 25.4.4.5, Promise.resolve.
- */
-static bool
-Promise_resolve(JSContext* cx, unsigned argc, Value* vp)
-{
-    return CommonStaticResolveRejectImpl(cx, argc, vp, ResolveMode);
-}
-
-// ES2016, February 12 draft, 25.4.3.1. steps 3-11.
-/* static */
-PromiseObject*
-PromiseObject::create(JSContext* cx, HandleObject executor, HandleObject proto /* = nullptr */)
-{
-    MOZ_ASSERT(executor->isCallable());
-
-    RootedObject usedProto(cx, proto);
-    bool wrappedProto = false;
-    // If the proto is wrapped, that means the current function is running
-    // with a different compartment active from the one the Promise instance
-    // is to be created in.
-    // See the comment in PromiseConstructor for details.
-    if (proto && IsWrapper(proto)) {
-        wrappedProto = true;
-        usedProto = CheckedUnwrap(proto);
-        if (!usedProto)
-            return nullptr;
-    }
-
-
-    // Steps 3-7.
-    Rooted<PromiseObject*> promise(cx, CreatePromiseObjectInternal(cx, usedProto, wrappedProto));
-    if (!promise)
-        return nullptr;
-
-    RootedValue promiseVal(cx, ObjectValue(*promise));
-    if (wrappedProto && !cx->compartment()->wrap(cx, &promiseVal))
-        return nullptr;
-
-    // Step 8.
-    // The resolving functions are created in the compartment active when the
-    // (maybe wrapped) Promise constructor was called. They contain checks and
-    // can unwrap the Promise if required.
-    RootedValue resolveVal(cx);
-    RootedValue rejectVal(cx);
-    if (!CreateResolvingFunctions(cx, promiseVal, &resolveVal, &rejectVal))
-        return nullptr;
-
-    // Need to wrap the resolution functions before storing them on the Promise.
-    if (wrappedProto) {
-        AutoCompartment ac(cx, promise);
-        RootedValue wrappedResolveVal(cx, resolveVal);
-        if (!cx->compartment()->wrap(cx, &wrappedResolveVal))
-            return nullptr;
-        promise->setFixedSlot(PROMISE_RESOLVE_FUNCTION_SLOT, wrappedResolveVal);
-    } else {
-        promise->setFixedSlot(PROMISE_RESOLVE_FUNCTION_SLOT, resolveVal);
-    }
-
-    // Step 9.
-    bool success;
-    {
-        FixedInvokeArgs<2> args(cx);
-
-        args[0].set(resolveVal);
-        args[1].set(rejectVal);
-
-        RootedValue calleeOrRval(cx, ObjectValue(*executor));
-        success = Call(cx, calleeOrRval, UndefinedHandleValue, args, &calleeOrRval);
-    }
-
-    // Step 10.
-    if (!success) {
-        RootedValue exceptionVal(cx);
-        // Not much we can do about uncatchable exceptions, so just bail
-        // for those.
-        if (!cx->isExceptionPending() || !GetAndClearException(cx, &exceptionVal))
-            return nullptr;
-
-        FixedInvokeArgs<1> args(cx);
-
-        args[0].set(exceptionVal);
-
-        // |rejectVal| is unused after this, so we can safely write to it.
-        if (!Call(cx, rejectVal, UndefinedHandleValue, args, &rejectVal))
-            return nullptr;
-    }
-
-    // Let the Debugger know about this Promise.
-    JS::dbg::onNewPromise(cx, promise);
-
-    // Step 11.
-    return promise;
-}
-
-namespace {
-// Generator used by PromiseObject::getID.
-mozilla::Atomic<uint64_t> gIDGenerator(0);
-} // namespace
-
-double
-PromiseObject::lifetime()
-{
-    return MillisecondsSinceStartup() - allocationTime();
-}
-
-uint64_t
-PromiseObject::getID()
-{
-    Value idVal(getReservedSlot(PROMISE_ID_SLOT));
-    if (idVal.isUndefined()) {
-        idVal.setDouble(++gIDGenerator);
-        setReservedSlot(PROMISE_ID_SLOT, idVal);
-    }
-    return uint64_t(idVal.toNumber());
-}
-
-/**
- * Returns all promises that directly depend on this one. That means those
- * created by calling `then` on this promise, or the promise returned by
- * `Promise.all(iterable)` or `Promise.race(iterable)`, with this promise
- * being a member of the passed-in `iterable`.
- *
- * Per spec, we should have separate lists of reaction records for the
- * fulfill and reject cases. As an optimization, we have only one of those,
- * containing the required data for both cases. So we just walk that list
- * and extract the dependent promises from all reaction records.
- */
-bool
-PromiseObject::dependentPromises(JSContext* cx, MutableHandle<GCVector<Value>> values)
-{
-    if (state() != JS::PromiseState::Pending)
-        return true;
-
-    RootedValue reactionsVal(cx, getReservedSlot(PROMISE_REACTIONS_OR_RESULT_SLOT));
-    if (reactionsVal.isNullOrUndefined())
-        return true;
-    RootedObject reactions(cx, &reactionsVal.toObject());
-
-    AutoIdVector keys(cx);
-    if (!GetPropertyKeys(cx, reactions, JSITER_OWNONLY, &keys))
-        return false;
-
-    if (keys.length() == 0)
-        return true;
-
-    if (!values.growBy(keys.length()))
-        return false;
-
-    // Each reaction is an internally-created object with the structure:
-    // {
-    //   promise: [the promise this reaction resolves],
-    //   resolve: [the `resolve` callback content code provided],
-    //   reject:  [the `reject` callback content code provided],
-    //   fulfillHandler: [the internal handler that fulfills the promise]
-    //   rejectHandler: [the internal handler that rejects the promise]
-    //   incumbentGlobal: [an object from the global that was incumbent when
-    //                     the reaction was created]
-    // }
-    //
-    // In the following loop we collect the `capabilities.promise` values for
-    // each reaction.
-    for (size_t i = 0; i < keys.length(); i++) {
-        MutableHandleValue val = values[i];
-        if (!GetProperty(cx, reactions, reactions, keys[i], val))
-            return false;
-        RootedObject reaction(cx, &val.toObject());
-        if (!GetProperty(cx, reaction, reaction, cx->names().promise, val))
-            return false;
-    }
-
-    return true;
-}
-
-namespace js {
-
-// ES6, 25.4.3.1.
-bool
 PromiseConstructor(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     // Step 1.
     if (!ThrowIfNotConstructing(cx, args, "Promise"))
         return false;
 
@@ -938,49 +1200,1267 @@ PromiseConstructor(JSContext* cx, unsign
 
     // Step 11.
     args.rval().setObject(*promise);
     if (needsWrapping)
         return cx->compartment()->wrap(cx, args.rval());
     return true;
 }
 
-
+// ES2016, 25.4.3.1. steps 3-11.
+/* static */ PromiseObject*
+PromiseObject::create(JSContext* cx, HandleObject executor, HandleObject proto /* = nullptr */)
+{
+    MOZ_ASSERT(executor->isCallable());
+
+    RootedObject usedProto(cx, proto);
+    bool wrappedProto = false;
+    // If the proto is wrapped, that means the current function is running
+    // with a different compartment active from the one the Promise instance
+    // is to be created in.
+    // See the comment in PromiseConstructor for details.
+    if (proto && IsWrapper(proto)) {
+        wrappedProto = true;
+        usedProto = CheckedUnwrap(proto);
+        if (!usedProto)
+            return nullptr;
+    }
+
+
+    // Steps 3-7.
+    Rooted<PromiseObject*> promise(cx, CreatePromiseObjectInternal(cx, usedProto, wrappedProto,
+                                                                   false));
+    if (!promise)
+        return nullptr;
+
+    RootedValue promiseVal(cx, ObjectValue(*promise));
+    if (wrappedProto && !cx->compartment()->wrap(cx, &promiseVal))
+        return nullptr;
+
+    // Step 8.
+    // The resolving functions are created in the compartment active when the
+    // (maybe wrapped) Promise constructor was called. They contain checks and
+    // can unwrap the Promise if required.
+    RootedValue resolveVal(cx);
+    RootedValue rejectVal(cx);
+    if (!CreateResolvingFunctions(cx, promiseVal, &resolveVal, &rejectVal))
+        return nullptr;
+
+    // Need to wrap the resolution functions before storing them on the Promise.
+    if (wrappedProto) {
+        AutoCompartment ac(cx, promise);
+        RootedValue wrappedRejectVal(cx, rejectVal);
+        if (!cx->compartment()->wrap(cx, &wrappedRejectVal))
+            return nullptr;
+        promise->setFixedSlot(PromiseSlot_RejectFunction, wrappedRejectVal);
+    } else {
+        promise->setFixedSlot(PromiseSlot_RejectFunction, rejectVal);
+    }
+
+    // Step 9.
+    bool success;
+    {
+        FixedInvokeArgs<2> args(cx);
+
+        args[0].set(resolveVal);
+        args[1].set(rejectVal);
+
+        RootedValue calleeOrRval(cx, ObjectValue(*executor));
+        success = Call(cx, calleeOrRval, UndefinedHandleValue, args, &calleeOrRval);
+    }
+
+    // Step 10.
+    if (!success) {
+        RootedValue exceptionVal(cx);
+        // Not much we can do about uncatchable exceptions, so just bail
+        // for those.
+        if (!cx->isExceptionPending() || !GetAndClearException(cx, &exceptionVal))
+            return nullptr;
+
+        FixedInvokeArgs<1> args(cx);
+
+        args[0].set(exceptionVal);
+
+        // |rejectVal| is unused after this, so we can safely write to it.
+        if (!Call(cx, rejectVal, UndefinedHandleValue, args, &rejectVal))
+            return nullptr;
+    }
+
+    // Let the Debugger know about this Promise.
+    JS::dbg::onNewPromise(cx, promise);
+
+    // Step 11.
+    return promise;
+}
+
+static MOZ_MUST_USE bool PerformPromiseAll(JSContext *cx, JS::ForOfIterator& iterator,
+                                           HandleObject C, HandleObject promiseObj,
+                                           HandleObject resolve, HandleObject reject);
+
+// ES2016, 25.4.4.1.
+static bool
+Promise_static_all(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    RootedValue iterable(cx, args.get(0));
+
+    // Step 2 (reordered).
+    RootedValue CVal(cx, args.thisv());
+    if (!CVal.isObject()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT,
+                                  "Receiver of Promise.all call");
+        return false;
+    }
+
+    // Step 1.
+    RootedObject C(cx, &CVal.toObject());
+
+    // Step 3.
+    RootedObject resultPromise(cx);
+    RootedObject resolve(cx);
+    RootedObject reject(cx);
+    if (!NewPromiseCapability(cx, C, &resultPromise, &resolve, &reject, false))
+        return false;
+
+    // Steps 4-5.
+    JS::ForOfIterator iter(cx);
+    if (!iter.init(iterable, JS::ForOfIterator::AllowNonIterable))
+        return AbruptRejectPromise(cx, args, resultPromise, reject);
+
+    if (!iter.valueIsIterable()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_ITERABLE,
+                                  "Argument of Promise.all");
+        return AbruptRejectPromise(cx, args, resultPromise, reject);
+    }
+
+    // Step 6 (implicit).
+
+    // Step 7.
+    bool result = PerformPromiseAll(cx, iter, C, resultPromise, resolve, reject);
+
+    // Step 8.
+    if (!result) {
+        // Step 8.a.
+        // TODO: implement iterator closing.
+
+        // Step 8.b.
+        return AbruptRejectPromise(cx, args, resultPromise, reject);
+    }
+
+    // Step 9.
+    args.rval().setObject(*resultPromise);
+    return true;
+}
+
+static MOZ_MUST_USE bool PerformPromiseThen(JSContext* cx, Handle<PromiseObject*> promise,
+                                            HandleValue onFulfilled_, HandleValue onRejected_,
+                                            HandleObject resultPromise,
+                                            HandleObject resolve, HandleObject reject);
+
+static bool PromiseAllResolveElementFunction(JSContext* cx, unsigned argc, Value* vp);
+
+// Unforgeable version of ES2016, 25.4.4.1.
+MOZ_MUST_USE JSObject*
+js::GetWaitForAllPromise(JSContext* cx, const JS::AutoObjectVector& promises)
+{
+#ifdef DEBUG
+    for (size_t i = 0, len = promises.length(); i < len; i++) {
+        JSObject* obj = promises[i];
+        assertSameCompartment(cx, obj);
+        MOZ_ASSERT(UncheckedUnwrap(obj)->is<PromiseObject>());
+    }
+#endif
+
+    // Step 1.
+    RootedObject C(cx, GlobalObject::getOrCreatePromiseConstructor(cx, cx->global()));
+    if (!C)
+        return nullptr;
+
+    // Step 2 (omitted).
+
+    // Step 3.
+    RootedObject resultPromise(cx);
+    RootedObject resolve(cx);
+    RootedObject reject(cx);
+    if (!NewPromiseCapability(cx, C, &resultPromise, &resolve, &reject, false))
+        return nullptr;
+
+    // Steps 4-6 (omitted).
+
+    // Step 7.
+    // Implemented as an inlined, simplied version of ES2016 25.4.4.1.1, PerformPromiseAll.
+    {
+        uint32_t promiseCount = promises.length();
+        // Sub-steps 1-2 (omitted).
+
+        // Sub-step 3.
+        RootedNativeObject valuesArray(cx, NewDenseFullyAllocatedArray(cx, promiseCount));
+        if (!valuesArray)
+            return nullptr;
+        if (valuesArray->ensureDenseElements(cx, 0, promiseCount) != DenseElementResult::Success)
+            return nullptr;
+
+        // Sub-step 4.
+        // Create our data holder that holds all the things shared across
+        // every step of the iterator.  In particular, this holds the
+        // remainingElementsCount (as an integer reserved slot), the array of
+        // values, and the resolve function from our PromiseCapability.
+        RootedValue valuesArrayVal(cx, ObjectValue(*valuesArray));
+        Rooted<PromiseAllDataHolder*> dataHolder(cx, NewPromiseAllDataHolder(cx, resultPromise,
+                                                                             valuesArrayVal,
+                                                                             resolve));
+        if (!dataHolder)
+            return nullptr;
+        RootedValue dataHolderVal(cx, ObjectValue(*dataHolder));
+
+        // Sub-step 5 (inline in loop-header below).
+
+        // Sub-step 6.
+        for (uint32_t index = 0; index < promiseCount; index++) {
+            // Steps a-c (omitted).
+            // Step d (implemented after the loop).
+            // Steps e-g (omitted).
+
+            // Step h.
+            valuesArray->setDenseElement(index, UndefinedHandleValue);
+
+            // Step i, vastly simplified.
+            RootedObject nextPromiseObj(cx, promises[index]);
+
+            // Step j.
+            RootedFunction resolveFunc(cx, NewNativeFunction(cx, PromiseAllResolveElementFunction,
+                                                             1, nullptr,
+                                                             gc::AllocKind::FUNCTION_EXTENDED,
+                                                             GenericObject));
+            if (!resolveFunc)
+                return nullptr;
+
+            // Steps k-o.
+            resolveFunc->setExtendedSlot(PromiseAllResolveElementFunctionSlot_Data, dataHolderVal);
+            resolveFunc->setExtendedSlot(PromiseAllResolveElementFunctionSlot_ElementIndex,
+                                         Int32Value(index));
+
+            // Step p.
+            dataHolder->increaseRemainingCount();
+
+            // Step q, very roughly.
+            RootedValue resolveFunVal(cx, ObjectValue(*resolveFunc));
+            RootedValue rejectFunVal(cx, ObjectValue(*reject));
+            Rooted<PromiseObject*> nextPromise(cx);
+
+            // GetWaitForAllPromise is used internally only and must not
+            // trigger content-observable effects when registering a reaction.
+            // It's also meant to work on wrapped Promises, potentially from
+            // compartments with principals inaccessible from the current
+            // compartment. To make that work, it unwraps promises with
+            // UncheckedUnwrap,
+            nextPromise = &UncheckedUnwrap(nextPromiseObj)->as<PromiseObject>();
+
+            if (!PerformPromiseThen(cx, nextPromise, resolveFunVal, rejectFunVal,
+                                    resultPromise, nullptr, nullptr))
+            {
+                return nullptr;
+            }
+
+            // Step r (inline in loop-header).
+        }
+
+        // Sub-step d.i (implicit).
+        // Sub-step d.ii.
+        int32_t remainingCount = dataHolder->decreaseRemainingCount();
+
+        // Sub-step d.iii-iv.
+        if (remainingCount == 0) {
+            RootedValue valuesArrayVal(cx, ObjectValue(*valuesArray));
+            if (!ResolvePromiseInternal(cx, resultPromise, valuesArrayVal))
+                return nullptr;
+        }
+    }
+
+    // Step 8 (omitted).
+
+    // Step 9.
+    return resultPromise;
+}
+
+static MOZ_MUST_USE bool
+RunResolutionFunction(JSContext *cx, HandleObject resolutionFun, HandleValue result,
+                      ResolutionMode mode, HandleObject promiseObj)
+{
+    // The absence of a resolve/reject function can mean that, as an
+    // optimization, those weren't created. In that case, a flag is set on
+    // the Promise object. There are also reactions where the Promise
+    // itself is missing. For those, there's nothing left to do here.
+    assertSameCompartment(cx, resolutionFun);
+    assertSameCompartment(cx, result);
+    assertSameCompartment(cx, promiseObj);
+    if (resolutionFun) {
+        RootedValue calleeOrRval(cx, ObjectValue(*resolutionFun));
+        FixedInvokeArgs<1> resolveArgs(cx);
+        resolveArgs[0].set(result);
+        return Call(cx, calleeOrRval, UndefinedHandleValue, resolveArgs, &calleeOrRval);
+    }
+
+    if (!promiseObj)
+        return true;
+
+    Rooted<PromiseObject*> promise(cx, &promiseObj->as<PromiseObject>());
+    if (promise->state() != JS::PromiseState::Pending)
+        return true;
+
+
+    if (mode == ResolveMode) {
+        if (!PromiseHasAnyFlag(*promise, PROMISE_FLAG_DEFAULT_RESOLVE_FUNCTION))
+            return true;
+        return ResolvePromiseInternal(cx, promise, result);
+    }
+
+    if (!PromiseHasAnyFlag(*promise, PROMISE_FLAG_DEFAULT_REJECT_FUNCTION))
+        return true;
+    return RejectMaybeWrappedPromise(cx, promiseObj, result);
+
+}
+
+// ES2016, 25.4.4.1.1.
+static MOZ_MUST_USE bool
+PerformPromiseAll(JSContext *cx, JS::ForOfIterator& iterator, HandleObject C,
+                  HandleObject promiseObj, HandleObject resolve, HandleObject reject)
+{
+    RootedObject unwrappedPromiseObj(cx);
+    if (IsWrapper(promiseObj)) {
+        unwrappedPromiseObj = CheckedUnwrap(promiseObj);
+        MOZ_ASSERT(unwrappedPromiseObj);
+    }
+
+    // Step 1.
+    MOZ_ASSERT(C->isConstructor());
+    RootedValue CVal(cx, ObjectValue(*C));
+
+    // Step 2 (omitted).
+
+    // Step 3.
+    // We have to be very careful about which compartments we create things in
+    // here.  In particular, we have to maintain the invariant that anything
+    // stored in a reserved slot is same-compartment with the object whose
+    // reserved slot it's in.  But we want to create the values array in the
+    // Promise's compartment, because that array can get exposed to
+    // code that has access to the Promise (in particular code from
+    // that compartment), and that should work, even if the Promise
+    // compartment is less-privileged than our caller compartment.
+    //
+    // So the plan is as follows: Create the values array in the promise
+    // compartment.  Create the PromiseAllResolveElement function
+    // and the data holder in our current compartment.  Store a
+    // cross-compartment wrapper to the values array in the holder.  This
+    // should be OK because the only things we hand the
+    // PromiseAllResolveElement function to are the "then" calls we do and in
+    // the case when the Promise's compartment is not the current compartment
+    // those are happening over Xrays anyway, which means they get the
+    // canonical "then" function and content can't see our
+    // PromiseAllResolveElement.
+    RootedObject valuesArray(cx);
+    if (unwrappedPromiseObj) {
+        JSAutoCompartment ac(cx, unwrappedPromiseObj);
+        valuesArray = NewDenseFullyAllocatedArray(cx, 0);
+    } else {
+        valuesArray = NewDenseFullyAllocatedArray(cx, 0);
+    }
+    if (!valuesArray)
+        return false;
+
+    RootedValue valuesArrayVal(cx, ObjectValue(*valuesArray));
+    if (!cx->compartment()->wrap(cx, &valuesArrayVal))
+        return false;
+
+    // Step 4.
+    // Create our data holder that holds all the things shared across
+    // every step of the iterator.  In particular, this holds the
+    // remainingElementsCount (as an integer reserved slot), the array of
+    // values, and the resolve function from our PromiseCapability.
+    Rooted<PromiseAllDataHolder*> dataHolder(cx, NewPromiseAllDataHolder(cx, promiseObj,
+                                                                         valuesArrayVal, resolve));
+    if (!dataHolder)
+        return false;
+    RootedValue dataHolderVal(cx, ObjectValue(*dataHolder));
+
+    // Step 5.
+    uint32_t index = 0;
+
+    // Step 6.
+    RootedValue nextValue(cx);
+    RootedId indexId(cx);
+    RootedValue rejectFunVal(cx, ObjectOrNullValue(reject));
+
+    while (true) {
+        bool done;
+        // Steps a, b, c, e, f, g.
+        if (!iterator.next(&nextValue, &done))
+            return false;
+
+        // Step d.
+        if (done) {
+            // Step d.i (implicit).
+            // Step d.ii.
+            int32_t remainingCount = dataHolder->decreaseRemainingCount();
+
+            // Steps d.iii-iv.
+            if (remainingCount == 0) {
+                if (resolve) {
+                    return RunResolutionFunction(cx, resolve, valuesArrayVal, ResolveMode,
+                                                 promiseObj);
+                }
+                return ResolvePromiseInternal(cx, promiseObj, valuesArrayVal);
+            }
+
+            // We're all set for now!
+            return true;
+        }
+
+        // Step h.
+        { // Scope for the JSAutoCompartment we need to work with valuesArray.  We
+            // mostly do this for performance; we could go ahead and do the define via
+            // a cross-compartment proxy instead...
+            JSAutoCompartment ac(cx, valuesArray);
+            indexId = INT_TO_JSID(index);
+            if (!DefineProperty(cx, valuesArray, indexId, UndefinedHandleValue))
+                return false;
+        }
+
+        // Step i.
+        // Sadly, because someone could have overridden
+        // "resolve" on the canonical Promise constructor.
+        RootedValue nextPromise(cx);
+        RootedValue staticResolve(cx);
+        if (!GetProperty(cx, CVal, cx->names().resolve, &staticResolve))
+            return false;
+
+        FixedInvokeArgs<1> resolveArgs(cx);
+        resolveArgs[0].set(nextValue);
+        if (!Call(cx, staticResolve, CVal, resolveArgs, &nextPromise))
+            return false;
+
+        // Step j.
+        RootedFunction resolveFunc(cx, NewNativeFunction(cx, PromiseAllResolveElementFunction,
+                                                         1, nullptr,
+                                                         gc::AllocKind::FUNCTION_EXTENDED,
+                                                         GenericObject));
+        if (!resolveFunc)
+            return false;
+
+        // Steps k,m,n.
+        resolveFunc->setExtendedSlot(PromiseAllResolveElementFunctionSlot_Data, dataHolderVal);
+
+        // Step l.
+        resolveFunc->setExtendedSlot(PromiseAllResolveElementFunctionSlot_ElementIndex,
+                                     Int32Value(index));
+
+        // Steps o-p.
+        dataHolder->increaseRemainingCount();
+
+        // Step q.
+        RootedObject nextPromiseObj(cx, &nextPromise.toObject());
+        RootedValue resolveFunVal(cx, ObjectValue(*resolveFunc));
+        if (!BlockOnPromise(cx, nextPromiseObj, promiseObj, resolveFunVal, rejectFunVal))
+            return false;
+
+        // Step r.
+        index++;
+        MOZ_ASSERT(index > 0);
+    }
+}
+
+// ES2016, 25.4.4.1.2.
+static bool
+PromiseAllResolveElementFunction(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    RootedFunction resolve(cx, &args.callee().as<JSFunction>());
+    RootedValue xVal(cx, args.get(0));
+
+    // Step 1.
+    RootedValue dataVal(cx, resolve->getExtendedSlot(PromiseAllResolveElementFunctionSlot_Data));
+
+    // Step 2.
+    // We use the existence of the data holder as a signal for whether the
+    // Promise was already resolved. Upon resolution, it's reset to
+    // `undefined`.
+    if (dataVal.isUndefined()) {
+        args.rval().setUndefined();
+        return true;
+    }
+
+    Rooted<PromiseAllDataHolder*> data(cx, &dataVal.toObject().as<PromiseAllDataHolder>());
+
+    // Step 3.
+    resolve->setExtendedSlot(PromiseAllResolveElementFunctionSlot_Data, UndefinedValue());
+
+    // Step 4.
+    int32_t index = resolve->getExtendedSlot(PromiseAllResolveElementFunctionSlot_ElementIndex)
+                    .toInt32();
+
+    // Step 5.
+    RootedValue valuesVal(cx, data->valuesArray());
+    RootedObject valuesObj(cx, &valuesVal.toObject());
+    bool valuesListIsWrapped = false;
+    if (IsWrapper(valuesObj)) {
+        valuesListIsWrapped = true;
+        // See comment for PerformPromiseAll, step 3 for why we unwrap here.
+        valuesObj = UncheckedUnwrap(valuesObj);
+    }
+    RootedNativeObject values(cx, &valuesObj->as<NativeObject>());
+
+    // Step 6 (moved under step 10).
+    // Step 7 (moved to step 9).
+
+    // Step 8.
+    // The index is guaranteed to be initialized to `undefined`.
+    if (valuesListIsWrapped) {
+        AutoCompartment ac(cx, values);
+        if (!cx->compartment()->wrap(cx, &xVal))
+            return false;
+    }
+    values->setDenseElement(index, xVal);
+
+    // Steps 7,9.
+    uint32_t remainingCount = data->decreaseRemainingCount();
+
+    // Step 10.
+    if (remainingCount == 0) {
+        // Step 10.a. (Omitted, happened in PerformPromiseAll.)
+        // Step 10.b.
+
+        // Step 6 (Adapted to work with PromiseAllDataHolder's layout).
+        RootedObject resolveAllFun(cx, data->resolveObj());
+        RootedObject promiseObj(cx, data->promiseObj());
+        if (!resolveAllFun) {
+            if (!FulfillMaybeWrappedPromise(cx, promiseObj, valuesVal))
+                return false;
+        } else {
+            if (!RunResolutionFunction(cx, resolveAllFun, valuesVal, ResolveMode, promiseObj))
+                return false;
+        }
+    }
+
+    // Step 11.
+    args.rval().setUndefined();
+    return true;
+}
+
+static MOZ_MUST_USE bool PerformPromiseRace(JSContext *cx, JS::ForOfIterator& iterator,
+                                            HandleObject C, HandleObject promiseObj,
+                                            HandleObject resolve, HandleObject reject);
+
+// ES2016, 25.4.4.3.
+static bool
+Promise_static_race(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    RootedValue iterable(cx, args.get(0));
+
+    // Step 2 (reordered).
+    RootedValue CVal(cx, args.thisv());
+    if (!CVal.isObject()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT,
+                                  "Receiver of Promise.race call");
+        return false;
+    }
+
+    // Step 1.
+    RootedObject C(cx, &CVal.toObject());
+
+    // Step 3.
+    RootedObject resultPromise(cx);
+    RootedObject resolve(cx);
+    RootedObject reject(cx);
+    if (!NewPromiseCapability(cx, C, &resultPromise, &resolve, &reject, false))
+        return false;
+
+    // Steps 4-5.
+    JS::ForOfIterator iter(cx);
+    if (!iter.init(iterable, JS::ForOfIterator::AllowNonIterable))
+        return AbruptRejectPromise(cx, args, resultPromise, reject);
+
+    if (!iter.valueIsIterable()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_ITERABLE,
+                                  "Argument of Promise.race");
+        return AbruptRejectPromise(cx, args, resultPromise, reject);
+    }
+
+    // Step 6 (implicit).
+
+    // Step 7.
+    bool result = PerformPromiseRace(cx, iter, C, resultPromise, resolve, reject);
+
+    // Step 8.
+    if (!result) {
+        // Step 8.a.
+        // TODO: implement iterator closing.
+
+        // Step 8.b.
+        return AbruptRejectPromise(cx, args, resultPromise, reject);
+    }
+
+    // Step 9.
+    args.rval().setObject(*resultPromise);
+    return true;
+}
+
+// ES2016, 25.4.4.3.1.
+static MOZ_MUST_USE bool
+PerformPromiseRace(JSContext *cx, JS::ForOfIterator& iterator, HandleObject C,
+                   HandleObject promiseObj, HandleObject resolve, HandleObject reject)
+{
+    MOZ_ASSERT(C->isConstructor());
+    RootedValue CVal(cx, ObjectValue(*C));
+
+    RootedValue nextValue(cx);
+    RootedValue resolveFunVal(cx, ObjectOrNullValue(resolve));
+    RootedValue rejectFunVal(cx, ObjectOrNullValue(reject));
+    bool done;
+
+    while (true) {
+        // Steps a-c, e-g.
+        if (!iterator.next(&nextValue, &done))
+            return false;
+
+        // Step d.
+        if (done) {
+            // Step d.i.
+            // TODO: implement iterator closing.
+
+            // Step d.ii.
+            return true;
+        }
+
+        // Step h.
+        // Sadly, because someone could have overridden
+        // "resolve" on the canonical Promise constructor.
+        RootedValue nextPromise(cx);
+        RootedValue staticResolve(cx);
+        if (!GetProperty(cx, CVal, cx->names().resolve, &staticResolve))
+            return false;
+
+        FixedInvokeArgs<1> resolveArgs(cx);
+        resolveArgs[0].set(nextValue);
+        if (!Call(cx, staticResolve, CVal, resolveArgs, &nextPromise))
+            return false;
+
+        // Step i.
+        RootedObject nextPromiseObj(cx, &nextPromise.toObject());
+        if (!BlockOnPromise(cx, nextPromiseObj, promiseObj, resolveFunVal, rejectFunVal))
+            return false;
+    }
+
+    MOZ_ASSERT_UNREACHABLE("Shouldn't reach the end of PerformPromiseRace");
+}
+
+// ES2016, Sub-steps of 25.4.4.4 and 25.4.4.5.
+static MOZ_MUST_USE bool
+CommonStaticResolveRejectImpl(JSContext* cx, unsigned argc, Value* vp, ResolutionMode mode)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    RootedValue x(cx, args.get(0));
+
+    // Steps 1-2.
+    if (!args.thisv().isObject()) {
+        const char* msg = mode == ResolveMode
+                          ? "Receiver of Promise.resolve call"
+                          : "Receiver of Promise.reject call";
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT, msg);
+        return false;
+    }
+    RootedValue cVal(cx, args.thisv());
+    RootedObject C(cx, &cVal.toObject());
+
+    // Step 3 of Resolve.
+    if (mode == ResolveMode && x.isObject()) {
+        RootedObject xObj(cx, &x.toObject());
+        bool isPromise = false;
+        if (xObj->is<PromiseObject>()) {
+            isPromise = true;
+        } else if (IsWrapper(xObj)) {
+            // Treat instances of Promise from other compartments as Promises
+            // here, too.
+            // It's important to do the GetProperty for the `constructor`
+            // below through the wrapper, because wrappers can change the
+            // outcome, so instead of unwrapping and then performing the
+            // GetProperty, just check here and then operate on the original
+            // object again.
+            RootedObject unwrappedObject(cx, CheckedUnwrap(xObj));
+            if (unwrappedObject && unwrappedObject->is<PromiseObject>())
+                isPromise = true;
+        }
+        if (isPromise) {
+            RootedValue ctorVal(cx);
+            if (!GetProperty(cx, xObj, xObj, cx->names().constructor, &ctorVal))
+                return false;
+            if (ctorVal == cVal) {
+                args.rval().set(x);
+                return true;
+            }
+        }
+    }
+
+    // Step 4 of Resolve, 3 of Reject.
+    RootedObject promise(cx);
+    RootedObject resolveFun(cx);
+    RootedObject rejectFun(cx);
+    if (!NewPromiseCapability(cx, C, &promise, &resolveFun, &rejectFun, true))
+        return false;
+
+    // Step 5 of Resolve, 4 of Reject.
+    if (!RunResolutionFunction(cx, mode == ResolveMode ? resolveFun : rejectFun, x, mode, promise))
+        return false;
+
+    // Step 6 of Resolve, 4 of Reject.
+    args.rval().setObject(*promise);
+    return true;
+}
+
+/**
+ * ES2016, 25.4.4.4, Promise.reject.
+ */
+static bool
+Promise_reject(JSContext* cx, unsigned argc, Value* vp)
+{
+    return CommonStaticResolveRejectImpl(cx, argc, vp, RejectMode);
+}
+
+/**
+ * Unforgeable version of ES2016, 25.4.4.4, Promise.reject.
+ */
+/* static */ JSObject*
+PromiseObject::unforgeableReject(JSContext* cx, HandleValue value)
+{
+    // Steps 1-2 (omitted).
+
+    // Roughly step 3.
+    Rooted<PromiseObject*> promise(cx, CreatePromiseObjectInternal(cx));
+    if (!promise)
+        return nullptr;
+
+    // Roughly step 4.
+    if (!ResolvePromise(cx, promise, value, JS::PromiseState::Rejected))
+        return nullptr;
+
+    // Step 5.
+    return promise;
+}
+
+/**
+ * ES2016, 25.4.4.5, Promise.resolve.
+ */
+static bool
+Promise_static_resolve(JSContext* cx, unsigned argc, Value* vp)
+{
+    return CommonStaticResolveRejectImpl(cx, argc, vp, ResolveMode);
+}
+
+/**
+ * Unforgeable version of ES2016, 25.4.4.5, Promise.resolve.
+ */
+/* static */ JSObject*
+PromiseObject::unforgeableResolve(JSContext* cx, HandleValue value)
+{
+    // Steps 1-2 (omitted).
+
+    // Step 3.
+    if (value.isObject()) {
+        JSObject* obj = &value.toObject();
+        if (IsWrapper(obj))
+            obj = CheckedUnwrap(obj);
+        // Instead of getting the `constructor` property, do an unforgeable
+        // check.
+        if (obj && obj->is<PromiseObject>())
+            return obj;
+    }
+
+    // Step 4.
+    Rooted<PromiseObject*> promise(cx, CreatePromiseObjectInternal(cx));
+    if (!promise)
+        return nullptr;
+
+    // Steps 5.
+    if (!ResolvePromiseInternal(cx, promise, value))
+        return nullptr;
+
+    // Step 6.
+    return promise;
+}
+
+// ES2016, 25.4.4.6, implemented in Promise.js.
+
+// ES2016, 25.4.5.1, implemented in Promise.js.
+
+static PromiseReactionRecord*
+NewReactionRecord(JSContext* cx, HandleObject resultPromise, HandleValue onFulfilled,
+                  HandleValue onRejected, HandleObject resolve, HandleObject reject,
+                  HandleObject incumbentGlobalObject)
+{
+    Rooted<PromiseReactionRecord*> reaction(cx, NewObjectWithClassProto<PromiseReactionRecord>(cx));
+    if (!reaction)
+        return nullptr;
+
+    assertSameCompartment(cx, resultPromise);
+    assertSameCompartment(cx, onFulfilled);
+    assertSameCompartment(cx, onRejected);
+    assertSameCompartment(cx, resolve);
+    assertSameCompartment(cx, reject);
+    assertSameCompartment(cx, incumbentGlobalObject);
+
+    reaction->setFixedSlot(ReactionRecordSlot_Promise, ObjectOrNullValue(resultPromise));
+    reaction->setFixedSlot(ReactionRecordSlot_Flags, Int32Value(0));
+    reaction->setFixedSlot(ReactionRecordSlot_OnFulfilled, onFulfilled);
+    reaction->setFixedSlot(ReactionRecordSlot_OnRejected, onRejected);
+    reaction->setFixedSlot(ReactionRecordSlot_Resolve, ObjectOrNullValue(resolve));
+    reaction->setFixedSlot(ReactionRecordSlot_Reject, ObjectOrNullValue(reject));
+    reaction->setFixedSlot(ReactionRecordSlot_IncumbentGlobalObject,
+                           ObjectOrNullValue(incumbentGlobalObject));
+
+    return reaction;
+}
+
+// ES2016, 25.4.5.3., steps 3-5.
+MOZ_MUST_USE JSObject*
+js::OriginalPromiseThen(JSContext* cx, Handle<PromiseObject*> promise, HandleValue onFulfilled,
+                        HandleValue onRejected)
+{
+    RootedObject promiseObj(cx, promise);
+    if (promise->compartment() != cx->compartment()) {
+        if (!cx->compartment()->wrap(cx, &promiseObj))
+            return nullptr;
+    }
+
+    // Step 3.
+    RootedValue ctorVal(cx);
+    if (!SpeciesConstructor(cx, promiseObj, JSProto_Promise, &ctorVal))
+        return nullptr;
+    RootedObject C(cx, &ctorVal.toObject());
+
+    // Step 4.
+    RootedObject resultPromise(cx);
+    RootedObject resolve(cx);
+    RootedObject reject(cx);
+    if (!NewPromiseCapability(cx, C, &resultPromise, &resolve, &reject, true))
+        return nullptr;
+
+    // Step 5.
+    if (!PerformPromiseThen(cx, promise, onFulfilled, onRejected, resultPromise, resolve, reject))
+        return nullptr;
+
+    return resultPromise;
+}
+
+// ES2016, 25.4.5.3.
+static bool
+Promise_then(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Step 1.
+    RootedValue promiseVal(cx, args.thisv());
+
+    RootedValue onFulfilled(cx, args.get(0));
+    RootedValue onRejected(cx, args.get(1));
+
+    // Step 2.
+    if (!promiseVal.isObject()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT,
+                                  "Receiver of Promise.prototype.then call");
+        return false;
+    }
+    RootedObject promiseObj(cx, &promiseVal.toObject());
+    Rooted<PromiseObject*> promise(cx);
+
+    bool isPromise = promiseObj->is<PromiseObject>();
+    if (isPromise) {
+        promise = &promiseObj->as<PromiseObject>();
+    } else {
+        RootedObject unwrappedPromiseObj(cx, CheckedUnwrap(promiseObj));
+        if (!unwrappedPromiseObj) {
+            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNWRAP_DENIED);
+            return false;
+        }
+        if (!unwrappedPromiseObj->is<PromiseObject>()) {
+            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
+                                      "Promise", "then", "value");
+            return false;
+        }
+        promise = &unwrappedPromiseObj->as<PromiseObject>();
+    }
+
+    // Steps 3-5.
+    RootedObject resultPromise(cx, OriginalPromiseThen(cx, promise, onFulfilled, onRejected));
+    if (!resultPromise)
+        return false;
+
+    args.rval().setObject(*resultPromise);
+    return true;
+}
+
+// ES2016, 25.4.5.3.1.
+static MOZ_MUST_USE bool
+PerformPromiseThen(JSContext* cx, Handle<PromiseObject*> promise, HandleValue onFulfilled_,
+                   HandleValue onRejected_, HandleObject resultPromise,
+                   HandleObject resolve, HandleObject reject)
+{
+    // Step 1 (implicit).
+    // Step 2 (implicit).
+
+    // Step 3.
+    RootedValue onFulfilled(cx, onFulfilled_);
+    if (!IsCallable(onFulfilled))
+        onFulfilled = Int32Value(PROMISE_HANDLER_IDENTITY);
+
+    // Step 4.
+    RootedValue onRejected(cx, onRejected_);
+    if (!IsCallable(onRejected))
+        onRejected = Int32Value(PROMISE_HANDLER_THROWER);
+
+    RootedObject incumbentGlobal(cx);
+    if (!GetObjectFromIncumbentGlobal(cx, &incumbentGlobal))
+        return false;
+
+    // Step 7.
+    Rooted<PromiseReactionRecord*> reaction(cx, NewReactionRecord(cx, resultPromise,
+                                                                  onFulfilled, onRejected,
+                                                                  resolve, reject,
+                                                                  incumbentGlobal));
+
+    JS::PromiseState state = promise->state();
+    int32_t flags = promise->getFixedSlot(PromiseSlot_Flags).toInt32();
+    if (state == JS::PromiseState::Pending) {
+        // Steps 5,6 (reordered).
+        // Instead of creating separate reaction records for fulfillment and
+        // rejection, we create a combined record. All places we use the record
+        // can handle that.
+        if (!AddPromiseReaction(cx, promise, reaction))
+            return false;
+    }
+
+    // Steps 8,9.
+    else {
+        // Step 9.a.
+        MOZ_ASSERT_IF(state != JS::PromiseState::Fulfilled, state == JS::PromiseState::Rejected);
+
+        // Step 8.a. / 9.b.
+        RootedValue valueOrReason(cx, promise->getFixedSlot(PromiseSlot_ReactionsOrResult));
+
+        // We might be operating on a promise from another compartment. In
+        // that case, we need to wrap the result/reason value before using it.
+        if (!cx->compartment()->wrap(cx, &valueOrReason))
+            return false;
+
+        // Step 9.c.
+        if (state == JS::PromiseState::Rejected && !(flags & PROMISE_FLAG_HANDLED))
+            cx->runtime()->removeUnhandledRejectedPromise(cx, promise);
+
+        // Step 8.b. / 9.d.
+        if (!EnqueuePromiseReactionJob(cx, reaction, valueOrReason, state))
+            return false;
+    }
+
+    // Step 10.
+    promise->setFixedSlot(PromiseSlot_Flags, Int32Value(flags | PROMISE_FLAG_HANDLED));
+
+    // Step 11.
+    return true;
+}
+
+/**
+ * Calls |promise.then| with the provided hooks and adds |blockedPromise| to
+ * its list of dependent promises. Used by |Promise.all| and |Promise.race|.
+ *
+ * If |promise.then| is the original |Promise.prototype.then| function and
+ * the call to |promise.then| would use the original |Promise| constructor to
+ * create the resulting promise, this function skips the call to |promise.then|
+ * and thus creating a new promise that would not be observable by content.
+ */
+static MOZ_MUST_USE bool
+BlockOnPromise(JSContext* cx, HandleObject promiseObj, HandleObject blockedPromise_,
+               HandleValue onFulfilled, HandleValue onRejected)
+{
+    RootedValue thenVal(cx);
+    if (!GetProperty(cx, promiseObj, promiseObj, cx->names().then, &thenVal))
+        return false;
+
+    if (promiseObj->is<PromiseObject>() && IsNativeFunction(thenVal, Promise_then)) {
+        // |promise| is an unwrapped Promise, and |then| is the original
+        // |Promise.prototype.then|, inline it here.
+        // 25.4.5.3., step 3.
+        RootedObject PromiseCtor(cx);
+        if (!GetBuiltinConstructor(cx, JSProto_Promise, &PromiseCtor))
+            return false;
+        RootedValue PromiseCtorVal(cx, ObjectValue(*PromiseCtor));
+        RootedValue CVal(cx);
+        if (!SpeciesConstructor(cx, promiseObj, PromiseCtorVal, &CVal))
+            return false;
+        RootedObject C(cx, &CVal.toObject());
+
+        RootedObject resultPromise(cx, blockedPromise_);
+        RootedObject resolveFun(cx);
+        RootedObject rejectFun(cx);
+
+        // By default, the blocked promise is added as an extra entry to the
+        // rejected promises list.
+        bool addToDependent = true;
+
+        if (C == PromiseCtor) {
+            addToDependent = false;
+        } else {
+            // 25.4.5.3., step 4.
+            if (!NewPromiseCapability(cx, C, &resultPromise, &resolveFun, &rejectFun, true))
+                return false;
+        }
+
+        // 25.4.5.3., step 5.
+        Rooted<PromiseObject*> promise(cx, &promiseObj->as<PromiseObject>());
+        if (!PerformPromiseThen(cx, promise, onFulfilled, onRejected, resultPromise,
+                                resolveFun, rejectFun))
+        {
+            return false;
+        }
+
+        if (!addToDependent)
+            return true;
+    } else {
+        // Optimization failed, do the normal call.
+        RootedValue promiseVal(cx, ObjectValue(*promiseObj));
+        RootedValue rval(cx);
+        if (!Call(cx, thenVal, promiseVal, onFulfilled, onRejected, &rval))
+            return false;
+    }
+
+    // The object created by the |promise.then| call or the inlined version
+    // of it above is visible to content (either because |promise.then| was
+    // overridden by content and could leak it, or because a constructor
+    // other than the original value of |Promise| was used to create it).
+    // To have both that object and |blockedPromise| show up as dependent
+    // promises in the debugger, add a dummy reaction to the list of reject
+    // reactions that contains |blockedPromise|, but otherwise does nothing.
+    RootedObject unwrappedPromiseObj(cx, promiseObj);
+    RootedObject blockedPromise(cx, blockedPromise_);
+
+    mozilla::Maybe<AutoCompartment> ac;
+    if (IsWrapper(promiseObj)) {
+        unwrappedPromiseObj = CheckedUnwrap(promiseObj);
+        if (!unwrappedPromiseObj)
+            return false;
+        ac.emplace(cx, unwrappedPromiseObj);
+        if (!cx->compartment()->wrap(cx, &blockedPromise))
+            return false;
+    }
+
+    // If the object to depend on isn't a, maybe-wrapped, Promise instance,
+    // we ignore it. All this does is lose some small amount of debug
+    // information in scenarios that are highly unlikely to occur in useful
+    // code.
+    if (!unwrappedPromiseObj->is<PromiseObject>())
+        return true;
+
+    Rooted<PromiseObject*> promise(cx, &unwrappedPromiseObj->as<PromiseObject>());
+    return AddPromiseReaction(cx, promise, UndefinedHandleValue, UndefinedHandleValue,
+                              blockedPromise, nullptr, nullptr, nullptr);
+}
+
+static MOZ_MUST_USE bool
+AddPromiseReaction(JSContext* cx, Handle<PromiseObject*> promise,
+                   Handle<PromiseReactionRecord*> reaction)
+{
+    RootedValue reactionVal(cx, ObjectValue(*reaction));
+
+    // The code that creates Promise reactions can handle wrapped Promises,
+    // unwrapping them as needed. That means that the `promise` and `reaction`
+    // objects we have here aren't necessarily from the same compartment. In
+    // order to store the reaction on the promise, we have to ensure that it
+    // is properly wrapped.
+    mozilla::Maybe<AutoCompartment> ac;
+    if (promise->compartment() != cx->compartment()) {
+        ac.emplace(cx, promise);
+        if (!cx->compartment()->wrap(cx, &reactionVal))
+            return false;
+    }
+
+    // 25.4.5.3.1 steps 7.a,b.
+    RootedValue reactionsVal(cx, promise->getFixedSlot(PromiseSlot_ReactionsOrResult));
+    RootedNativeObject reactions(cx);
+
+    if (reactionsVal.isUndefined()) {
+        // If no reactions existed so far, just store the reaction record directly.
+        promise->setFixedSlot(PromiseSlot_ReactionsOrResult, reactionVal);
+        return true;
+    }
+
+    RootedObject reactionsObj(cx, &reactionsVal.toObject());
+
+    // If only a single reaction exists, it's stored directly instead of in a
+    // list. In that case, `reactionsObj` might be a wrapper, which we can
+    // always safely unwrap. It is always safe to unwrap it in that case.
+    if (IsWrapper(reactionsObj)) {
+        reactionsObj = UncheckedUnwrap(reactionsObj);
+        MOZ_ASSERT(reactionsObj->is<PromiseReactionRecord>());
+    }
+
+    if (reactionsObj->is<PromiseReactionRecord>()) {
+        // If a single reaction existed so far, create a list and store the
+        // old and the new reaction in it.
+        reactions = NewDenseFullyAllocatedArray(cx, 2);
+        if (!reactions)
+            return false;
+        if (reactions->ensureDenseElements(cx, 0, 2) != DenseElementResult::Success)
+            return false;
+
+        reactions->setDenseElement(0, reactionsVal);
+        reactions->setDenseElement(1, reactionVal);
+
+        promise->setFixedSlot(PromiseSlot_ReactionsOrResult, ObjectValue(*reactions));
+    } else {
+        // Otherwise, just store the new reaction.
+        reactions = &reactionsObj->as<NativeObject>();
+        uint32_t len = reactions->getDenseInitializedLength();
+        if (reactions->ensureDenseElements(cx, 0, len + 1) != DenseElementResult::Success)
+            return false;
+        reactions->setDenseElement(len, reactionVal);
+    }
+
+    return true;
+}
+
+static MOZ_MUST_USE bool
+AddPromiseReaction(JSContext* cx, Handle<PromiseObject*> promise, HandleValue onFulfilled,
+                   HandleValue onRejected, HandleObject dependentPromise,
+                   HandleObject resolve, HandleObject reject, HandleObject incumbentGlobal)
+{
+    if (promise->state() != JS::PromiseState::Pending)
+        return true;
+
+    Rooted<PromiseReactionRecord*> reaction(cx, NewReactionRecord(cx, dependentPromise,
+                                                                  onFulfilled, onRejected,
+                                                                  resolve, reject,
+                                                                  incumbentGlobal));
+    if (!reaction)
+        return false;
+    return AddPromiseReaction(cx, promise, reaction);
+}
+
+namespace {
+// Generator used by PromiseObject::getID.
+mozilla::Atomic<uint64_t> gIDGenerator(0);
+} // namespace
+
+double
+PromiseObject::lifetime()
+{
+    return MillisecondsSinceStartup() - allocationTime();
+}
+
+uint64_t
+PromiseObject::getID()
+{
+    Value idVal(getFixedSlot(PromiseSlot_Id));
+    if (idVal.isUndefined()) {
+        idVal.setDouble(++gIDGenerator);
+        setFixedSlot(PromiseSlot_Id, idVal);
+    }
+    return uint64_t(idVal.toNumber());
+}
+
+/**
+ * Returns all promises that directly depend on this one. That means those
+ * created by calling `then` on this promise, or the promise returned by
+ * `Promise.all(iterable)` or `Promise.race(iterable)`, with this promise
+ * being a member of the passed-in `iterable`.
+ *
+ * Per spec, we should have separate lists of reaction records for the
+ * fulfill and reject cases. As an optimization, we have only one of those,
+ * containing the required data for both cases. So we just walk that list
+ * and extract the dependent promises from all reaction records.
+ */
+bool
+PromiseObject::dependentPromises(JSContext* cx, MutableHandle<GCVector<Value>> values)
+{
+    if (state() != JS::PromiseState::Pending)
+        return true;
+
+    RootedValue reactionsVal(cx, getFixedSlot(PromiseSlot_ReactionsOrResult));
+
+    // If no reactions are pending, we don't have list and are done.
+    if (reactionsVal.isNullOrUndefined())
+        return true;
+
+    RootedNativeObject reactions(cx, &reactionsVal.toObject().as<NativeObject>());
+
+    // If only a single reaction is pending, it's stored directly.
+    if (reactions->is<PromiseReactionRecord>()) {
+        // Not all reactions have a Promise on them.
+        RootedObject promiseObj(cx, reactions->as<PromiseReactionRecord>().promise());
+        if (!promiseObj)
+            return true;
+
+        if (!values.growBy(1))
+            return false;
+
+        values[0].setObject(*promiseObj);
+        return true;
+    }
+
+    uint32_t len = reactions->getDenseInitializedLength();
+    MOZ_ASSERT(len >= 2);
+
+    size_t valuesIndex = 0;
+    Rooted<PromiseReactionRecord*> reaction(cx);
+    for (size_t i = 0; i < len; i++) {
+        reaction = &reactions->getDenseElement(i).toObject().as<PromiseReactionRecord>();
+
+        // Not all reactions have a Promise on them.
+        RootedObject promiseObj(cx, reaction->promise());
+        if (!promiseObj)
+            continue;
+        if (!values.growBy(1))
+            return false;
+
+        values[valuesIndex++].setObject(*promiseObj);
+    }
+
+    return true;
+}
 
 bool
 PromiseObject::resolve(JSContext* cx, HandleValue resolutionValue)
 {
     if (state() != JS::PromiseState::Pending)
         return true;
 
-    RootedValue funVal(cx, this->getReservedSlot(PROMISE_RESOLVE_FUNCTION_SLOT));
-    // TODO: ensure that this holds for xray'd promises. (It probably doesn't)
-    MOZ_ASSERT(funVal.toObject().is<JSFunction>());
+    RootedObject resolveFun(cx, GetResolveFunctionFromPromise(this));
+    RootedValue funVal(cx, ObjectValue(*resolveFun));
+
+    // For xray'd Promises, the resolve fun may have been created in another
+    // compartment. For the call below to work in that case, wrap the
+    // function into the current compartment.
+    if (!cx->compartment()->wrap(cx, &funVal))
+        return false;
 
     FixedInvokeArgs<1> args(cx);
-
     args[0].set(resolutionValue);
 
     RootedValue dummy(cx);
     return Call(cx, funVal, UndefinedHandleValue, args, &dummy);
 }
 
 bool
 PromiseObject::reject(JSContext* cx, HandleValue rejectionValue)
 {
     if (state() != JS::PromiseState::Pending)
         return true;
 
-    RootedValue resolveVal(cx, this->getReservedSlot(PROMISE_RESOLVE_FUNCTION_SLOT));
-    RootedFunction resolve(cx, &resolveVal.toObject().as<JSFunction>());
-    RootedValue funVal(cx, resolve->getExtendedSlot(ResolutionFunctionSlot_OtherFunction));
-    MOZ_ASSERT(funVal.toObject().is<JSFunction>());
+    RootedValue funVal(cx, this->getFixedSlot(PromiseSlot_RejectFunction));
+    MOZ_ASSERT(IsCallable(funVal));
 
     FixedInvokeArgs<1> args(cx);
-
     args[0].set(rejectionValue);
 
     RootedValue dummy(cx);
     return Call(cx, funVal, UndefinedHandleValue, args, &dummy);
 }
 
 void
 PromiseObject::onSettled(JSContext* cx)
@@ -988,382 +2468,33 @@ PromiseObject::onSettled(JSContext* cx)
     Rooted<PromiseObject*> promise(cx, this);
     RootedObject stack(cx);
     if (cx->options().asyncStack() || cx->compartment()->isDebuggee()) {
         if (!JS::CaptureCurrentStack(cx, &stack, JS::StackCapture(JS::AllFrames()))) {
             cx->clearPendingException();
             return;
         }
     }
-    promise->setFixedSlot(PROMISE_RESOLUTION_SITE_SLOT, ObjectOrNullValue(stack));
-    promise->setFixedSlot(PROMISE_RESOLUTION_TIME_SLOT, DoubleValue(MillisecondsSinceStartup()));
+    promise->setFixedSlot(PromiseSlot_ResolutionSite, ObjectOrNullValue(stack));
+    promise->setFixedSlot(PromiseSlot_ResolutionTime, DoubleValue(MillisecondsSinceStartup()));
 
     if (promise->state() == JS::PromiseState::Rejected && promise->isUnhandled())
         cx->runtime()->addUnhandledRejectedPromise(cx, promise);
 
     JS::dbg::onPromiseSettled(cx, promise);
 }
 
-enum ReactionJobSlots {
-    ReactionJobSlot_Handler = 0,
-    ReactionJobSlot_JobData,
-};
-
-enum ReactionJobDataSlots {
-    ReactionJobDataSlot_HandlerArg = 0,
-    ReactionJobDataSlot_ResolveHook,
-    ReactionJobDataSlot_RejectHook,
-    ReactionJobDataSlotsCount,
-};
-
-// ES6, 25.4.2.1.
-/**
- * Callback triggering the fulfill/reject reaction for a resolved Promise,
- * to be invoked by the embedding during its processing of the Promise job
- * queue.
- *
- * See http://www.ecma-international.org/ecma-262/6.0/index.html#sec-jobs-and-job-queues
- *
- * A PromiseReactionJob is set as the native function of an extended
- * JSFunction object, with all information required for the job's
- * execution stored in the function's extended slots.
- *
- * Usage of the function's extended slots is as follows:
- * ReactionJobSlot_Handler: The handler to use as the Promise reaction.
- *                          This can be PROMISE_HANDLER_IDENTITY,
- *                          PROMISE_HANDLER_THROWER, or a callable. In the
- *                          latter case, it's guaranteed to be an object from
- *                          the same compartment as the PromiseReactionJob.
- * ReactionJobSlot_JobData: JobData - a, potentially CCW-wrapped, dense list
- *                          containing data required for proper execution of
- *                          the reaction.
- *
- * The JobData list has the following entries:
- * ReactionJobDataSlot_HandlerArg: Value passed as argument when invoking the
- *                                 reaction handler.
- * ReactionJobDataSlot_ResolveHook: The Promise's resolve hook, invoked if the
- *                                  handler is PROMISE_HANDLER_IDENTITY or
- *                                  upon successful execution of a callable
- *                                  handler.
- *  ReactionJobDataSlot_RejectHook: The Promise's reject hook, invoked if the
- *                                  handler is PROMISE_HANDLER_THROWER or if
- *                                  execution of a callable handler aborts
- *                                  abnormally.
- */
-static bool
-PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-
-    RootedFunction job(cx, &args.callee().as<JSFunction>());
-
-    RootedValue handlerVal(cx, job->getExtendedSlot(ReactionJobSlot_Handler));
-    RootedObject jobDataObj(cx, &job->getExtendedSlot(ReactionJobSlot_JobData).toObject());
-
-    // To ensure that the embedding ends up with the right entry global, we're
-    // guaranteeing that the reaction job function gets created in the same
-    // compartment as the handler function. That's not necessarily the global
-    // that the job was triggered from, though. To be able to find the
-    // triggering global, we always create the jobArgs object in that global
-    // and wrap it into the handler's. So to go back, we check if jobArgsObj
-    // is a wrapper and if so, unwrap it, enter its compartment, and wrap
-    // the handler into that compartment.
-    //
-    // See the doc comment for PromiseReactionJob for how this information is
-    // stored.
-    mozilla::Maybe<AutoCompartment> ac;
-    if (IsWrapper(jobDataObj)) {
-        jobDataObj = UncheckedUnwrap(jobDataObj);
-        ac.emplace(cx, jobDataObj);
-        if (!cx->compartment()->wrap(cx, &handlerVal))
-            return false;
-    }
-    RootedNativeObject jobData(cx, &jobDataObj->as<NativeObject>());
-    RootedValue argument(cx, jobData->getDenseElement(ReactionJobDataSlot_HandlerArg));
-
-    // Step 1 (omitted).
-
-    // Steps 2-3.
-    RootedValue handlerResult(cx);
-    bool shouldReject = false;
-
-    // Steps 4-7.
-    if (handlerVal.isNumber()) {
-        int32_t handlerNum = int32_t(handlerVal.toNumber());
-        // Step 4.
-        if (handlerNum == PROMISE_HANDLER_IDENTITY) {
-            handlerResult = argument;
-        } else {
-            // Step 5.
-            MOZ_ASSERT(handlerNum == PROMISE_HANDLER_THROWER);
-            shouldReject = true;
-            handlerResult = argument;
-        }
-    } else {
-        // Step 6.
-        FixedInvokeArgs<1> args2(cx);
-        args2[0].set(argument);
-        if (!Call(cx, handlerVal, UndefinedHandleValue, args2, &handlerResult)) {
-            shouldReject = true;
-            // Not much we can do about uncatchable exceptions, so just bail
-            // for those.
-            if (!cx->isExceptionPending() || !GetAndClearException(cx, &handlerResult))
-                return false;
-        }
-    }
-
-    // Steps 7-9.
-    size_t hookSlot = shouldReject
-                      ? ReactionJobDataSlot_RejectHook
-                      : ReactionJobDataSlot_ResolveHook;
-    RootedObject callee(cx, &jobData->getDenseElement(hookSlot).toObject());
-
-    FixedInvokeArgs<1> args2(cx);
-    args2[0].set(handlerResult);
-    RootedValue calleeOrRval(cx, ObjectValue(*callee));
-    bool result = Call(cx, calleeOrRval, UndefinedHandleValue, args2, &calleeOrRval);
-
-    args.rval().set(calleeOrRval);
-    return result;
-}
-
-bool
-EnqueuePromiseReactionJob(JSContext* cx, HandleValue handler_, HandleValue handlerArg,
-                          HandleObject resolve, HandleObject reject,
-                          HandleObject promise_, HandleObject objectFromIncumbentGlobal_)
+MOZ_MUST_USE bool
+js::EnqueuePromiseReactions(JSContext* cx, Handle<PromiseObject*> promise,
+                            HandleObject dependentPromise,
+                            HandleValue onFulfilled, HandleValue onRejected)
 {
-    // Create a dense array to hold the data needed for the reaction job to
-    // work.
-    // See doc comment for PromiseReactionJob for layout details.
-    RootedArrayObject data(cx, NewDenseFullyAllocatedArray(cx, ReactionJobDataSlotsCount));
-    if (!data ||
-        data->ensureDenseElements(cx, 0, ReactionJobDataSlotsCount) != DenseElementResult::Success)
-    {
-        return false;
-    }
-
-    // Store the handler argument.
-    data->setDenseElement(ReactionJobDataSlot_HandlerArg, handlerArg);
-
-    // Store the resolve hook.
-    data->setDenseElement(ReactionJobDataSlot_ResolveHook, ObjectValue(*resolve));
-
-    // Store the reject hook.
-    data->setDenseElement(ReactionJobDataSlot_RejectHook, ObjectValue(*reject));
-
-    RootedValue dataVal(cx, ObjectValue(*data));
-
-    // Re-rooting because we might need to unwrap it.
-    RootedValue handler(cx, handler_);
-
-    // If we have a handler callback, we enter that handler's compartment so
-    // that the promise reaction job function is created in that compartment.
-    // That guarantees that the embedding ends up with the right entry global.
-    // This is relevant for some html APIs like fetch that derive information
-    // from said global.
-    mozilla::Maybe<AutoCompartment> ac;
-    if (handler.isObject()) {
-        RootedObject handlerObj(cx, &handler.toObject());
-
-        // The unwrapping has to be unchecked because we specifically want to
-        // be able to use handlers with wrappers that would only allow calls.
-        // E.g., it's ok to have a handler from a chrome compartment in a
-        // reaction to a content compartment's Promise instance.
-        handlerObj = UncheckedUnwrap(handlerObj);
-        MOZ_ASSERT(handlerObj);
-        ac.emplace(cx, handlerObj);
-        handler = ObjectValue(*handlerObj);
-
-        // We need to wrap the |data| array to store it on the job function.
-        if (!cx->compartment()->wrap(cx, &dataVal))
-            return false;
-    }
-
-    // Create the JS function to call when the job is triggered.
-    RootedAtom funName(cx, cx->names().empty);
-    RootedFunction job(cx, NewNativeFunction(cx, PromiseReactionJob, 0, funName,
-                                             gc::AllocKind::FUNCTION_EXTENDED));
-    if (!job)
-        return false;
-
-    // Store the handler and the data array on the reaction job.
-    job->setExtendedSlot(ReactionJobSlot_Handler, handler);
-    job->setExtendedSlot(ReactionJobSlot_JobData, dataVal);
-
-    // When using JS::AddPromiseReactions, no actual promise is created, so we
-    // might not have one here.
-    // Additionally, we might have an object here that isn't an instance of
-    // Promise. This can happen if content overrides the value of
-    // Promise[@@species] (or invokes Promise#then on a Promise subclass
-    // instance with a non-default @@species value on the constructor) with a
-    // function that returns objects that're not Promise (subclass) instances.
-    // In that case, we just pretend we didn't have an object in the first
-    // place.
-    // If after all this we do have an object, wrap it in case we entered the
-    // handler's compartment above, because we should pass objects from a
-    // single compartment to the enqueuePromiseJob callback.
-    RootedObject promise(cx);
-    if (promise_ && promise_->is<PromiseObject>()) {
-      promise = promise_;
-      if (!cx->compartment()->wrap(cx, &promise))
-          return false;
-    }
-
-    // Using objectFromIncumbentGlobal, we can derive the incumbent global by
-    // unwrapping and then getting the global. This is very convoluted, but
-    // much better than having to store the original global as a private value
-    // because we couldn't wrap it to store it as a normal JS value.
-    RootedObject global(cx);
-    RootedObject objectFromIncumbentGlobal(cx, objectFromIncumbentGlobal_);
-    if (objectFromIncumbentGlobal) {
-        objectFromIncumbentGlobal = CheckedUnwrap(objectFromIncumbentGlobal);
-        MOZ_ASSERT(objectFromIncumbentGlobal);
-        global = &objectFromIncumbentGlobal->global();
-    }
-
-    // Note: the global we pass here might be from a different compartment
-    // than job and promise. While it's somewhat unusual to pass objects
-    // from multiple compartments, in this case we specifically need the
-    // global to be unwrapped because wrapping and unwrapping aren't
-    // necessarily symmetric for globals.
-    return cx->runtime()->enqueuePromiseJob(cx, job, promise, global);
-}
-
-enum ThenableJobSlots {
-    ThenableJobSlot_Handler = 0,
-    ThenableJobSlot_JobData,
-};
-
-enum ThenableJobDataSlots {
-    ThenableJobDataSlot_Promise = 0,
-    ThenableJobDataSlot_Thenable,
-    ThenableJobDataSlotsCount,
-};
-// ES6, 25.4.2.2.
-/**
- * Callback for resolving a thenable, to be invoked by the embedding during
- * its processing of the Promise job queue.
- *
- * See http://www.ecma-international.org/ecma-262/6.0/index.html#sec-jobs-and-job-queues
- *
- * A PromiseResolveThenableJob is set as the native function of an extended
- * JSFunction object, with all information required for the job's
- * execution stored in the function's extended slots.
- *
- * Usage of the function's extended slots is as follows:
- * ThenableJobSlot_Handler: The handler to use as the Promise reaction.
- *                          This can be PROMISE_HANDLER_IDENTITY,
- *                          PROMISE_HANDLER_THROWER, or a callable. In the
- *                          latter case, it's guaranteed to be an object
- *                          from the same compartment as the
- *                          PromiseReactionJob.
- * ThenableJobSlot_JobData: JobData - a, potentially CCW-wrapped, dense list
- *                          containing data required for proper execution of
- *                          the reaction.
- *
- * The JobData list has the following entries:
- * ThenableJobDataSlot_Promise: The Promise to resolve using the given
- *                              thenable.
- * ThenableJobDataSlot_Thenable: The thenable to use as the receiver when
- *                               calling the `then` function.
- */
-static bool
-PromiseResolveThenableJob(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-
-    RootedFunction job(cx, &args.callee().as<JSFunction>());
-    RootedValue then(cx, job->getExtendedSlot(ThenableJobSlot_Handler));
-    MOZ_ASSERT(!IsWrapper(&then.toObject()));
-    RootedNativeObject jobArgs(cx, &job->getExtendedSlot(ThenableJobSlot_JobData)
-                                    .toObject().as<NativeObject>());
-
-    RootedValue promise(cx, jobArgs->getDenseElement(ThenableJobDataSlot_Promise));
-    RootedValue thenable(cx, jobArgs->getDenseElement(ThenableJobDataSlot_Thenable));
-
-    // Step 1.
-    RootedValue resolveVal(cx);
-    RootedValue rejectVal(cx);
-    if (!CreateResolvingFunctions(cx, promise, &resolveVal, &rejectVal))
-        return false;
-
-    // Step 2.
-    FixedInvokeArgs<2> args2(cx);
-    args2[0].set(resolveVal);
-    args2[1].set(rejectVal);
-
-    RootedValue rval(cx);
-
-    // In difference to the usual pattern, we return immediately on success.
-    if (Call(cx, then, thenable, args2, &rval))
-        return true;
-
-    if (!GetAndClearException(cx, &rval))
-        return false;
-
-    FixedInvokeArgs<1> rejectArgs(cx);
-    rejectArgs[0].set(rval);
-
-    return Call(cx, rejectVal, UndefinedHandleValue, rejectArgs, &rval);
-}
-
-bool
-EnqueuePromiseResolveThenableJob(JSContext* cx, HandleValue promiseToResolve_,
-                                 HandleValue thenable_, HandleValue thenVal)
-{
-    // Need to re-root these to enable wrapping them below.
-    RootedValue promiseToResolve(cx, promiseToResolve_);
-    RootedValue thenable(cx, thenable_);
-
-    // We enter the `then` callable's compartment so that the job function is
-    // created in that compartment.
-    // That guarantees that the embedding ends up with the right entry global.
-    // This is relevant for some html APIs like fetch that derive information
-    // from said global.
-    RootedObject then(cx, CheckedUnwrap(&thenVal.toObject()));
-    AutoCompartment ac(cx, then);
-
-    RootedAtom funName(cx, cx->names().empty);
-    RootedFunction job(cx, NewNativeFunction(cx, PromiseResolveThenableJob, 0, funName,
-                                             gc::AllocKind::FUNCTION_EXTENDED));
-    if (!job)
-        return false;
-
-    // Store the `then` function on the callback.
-    job->setExtendedSlot(ThenableJobSlot_Handler, ObjectValue(*then));
-
-    // Create a dense array to hold the data needed for the reaction job to
-    // work.
-    // See the doc comment for PromiseResolveThenableJob for the layout.
-    RootedArrayObject data(cx, NewDenseFullyAllocatedArray(cx, ThenableJobDataSlotsCount));
-    if (!data ||
-        data->ensureDenseElements(cx, 0, ThenableJobDataSlotsCount) != DenseElementResult::Success)
-    {
-        return false;
-    }
-
-    // Wrap and set the `promiseToResolve` argument.
-    if (!cx->compartment()->wrap(cx, &promiseToResolve))
-        return false;
-    data->setDenseElement(ThenableJobDataSlot_Promise, promiseToResolve);
-    // At this point the promise is guaranteed to be wrapped into the job's
-    // compartment.
-    RootedObject promise(cx, &promiseToResolve.toObject());
-
-    // Wrap and set the `thenable` argument.
-    MOZ_ASSERT(thenable.isObject());
-    if (!cx->compartment()->wrap(cx, &thenable))
-        return false;
-    data->setDenseElement(ThenableJobDataSlot_Thenable, thenable);
-
-    // Store the data array on the reaction job.
-    job->setExtendedSlot(ThenableJobSlot_JobData, ObjectValue(*data));
-
-    RootedObject incumbentGlobal(cx, cx->runtime()->getIncumbentGlobal(cx));
-    return cx->runtime()->enqueuePromiseJob(cx, job, promise, incumbentGlobal);
+    MOZ_ASSERT_IF(dependentPromise, dependentPromise->is<PromiseObject>());
+    return PerformPromiseThen(cx, promise, onFulfilled, onRejected, dependentPromise,
+                              nullptr, nullptr);
 }
 
 PromiseTask::PromiseTask(JSContext* cx, Handle<PromiseObject*> promise)
   : runtime_(cx),
     promise_(cx, promise)
 {}
 
 PromiseTask::~PromiseTask()
@@ -1388,40 +2519,38 @@ PromiseTask::finish(JSContext* cx)
 
 void
 PromiseTask::cancel(JSContext* cx)
 {
     MOZ_ASSERT(cx == runtime_);
     js_delete(this);
 }
 
-} // namespace js
-
 static JSObject*
 CreatePromisePrototype(JSContext* cx, JSProtoKey key)
 {
     return cx->global()->createBlankPrototype(cx, &PromiseObject::protoClass_);
 }
 
 static const JSFunctionSpec promise_methods[] = {
     JS_SELF_HOSTED_FN("catch", "Promise_catch", 1, 0),
-    JS_SELF_HOSTED_FN("then", "Promise_then", 2, 0),
+    JS_FN("then", Promise_then, 2, 0),
     JS_FS_END
 };
 
 static const JSPropertySpec promise_properties[] = {
     JS_STRING_SYM_PS(toStringTag, "Promise", JSPROP_READONLY),
     JS_PS_END
 };
 
 static const JSFunctionSpec promise_static_methods[] = {
-    JS_SELF_HOSTED_FN("all", "Promise_static_all", 1, 0),
-    JS_SELF_HOSTED_FN("race", "Promise_static_race", 1, 0),
+    JS_FN("all", Promise_static_all, 1, 0),
+    JS_FN("race", Promise_static_race, 1, 0),
     JS_FN("reject", Promise_reject, 1, 0),
-    JS_FN("resolve", Promise_resolve, 1, 0),
+    JS_FN("resolve", Promise_static_resolve, 1, 0),
     JS_FS_END
 };
 
 static const JSPropertySpec promise_static_properties[] = {
     JS_SELF_HOSTED_SYM_GET(species, "Promise_static_get_species", 0),
     JS_PS_END
 };
 
--- a/js/src/builtin/Promise.h
+++ b/js/src/builtin/Promise.h
@@ -7,110 +7,120 @@
 #ifndef builtin_Promise_h
 #define builtin_Promise_h
 
 #include "builtin/SelfHostingDefines.h"
 #include "vm/NativeObject.h"
 
 namespace js {
 
+enum PromiseSlots {
+    PromiseSlot_Flags = 0,
+    PromiseSlot_ReactionsOrResult,
+    PromiseSlot_RejectFunction,
+    PromiseSlot_AllocationSite,
+    PromiseSlot_ResolutionSite,
+    PromiseSlot_AllocationTime,
+    PromiseSlot_ResolutionTime,
+    PromiseSlot_Id,
+    PromiseSlots,
+};
+
+#define PROMISE_FLAG_RESOLVED  0x1
+#define PROMISE_FLAG_FULFILLED 0x2
+#define PROMISE_FLAG_HANDLED   0x4
+#define PROMISE_FLAG_REPORTED  0x8
+#define PROMISE_FLAG_DEFAULT_RESOLVE_FUNCTION 0x10
+#define PROMISE_FLAG_DEFAULT_REJECT_FUNCTION  0x20
+
 class AutoSetNewObjectMetadata;
 
 class PromiseObject : public NativeObject
 {
   public:
-    static const unsigned RESERVED_SLOTS = 8;
+    static const unsigned RESERVED_SLOTS = PromiseSlots;
     static const Class class_;
     static const Class protoClass_;
     static PromiseObject* create(JSContext* cx, HandleObject executor,
                                  HandleObject proto = nullptr);
 
     static JSObject* unforgeableResolve(JSContext* cx, HandleValue value);
     static JSObject* unforgeableReject(JSContext* cx, HandleValue value);
 
     JS::PromiseState state() {
-        int32_t flags = getFixedSlot(PROMISE_FLAGS_SLOT).toInt32();
+        int32_t flags = getFixedSlot(PromiseSlot_Flags).toInt32();
         if (!(flags & PROMISE_FLAG_RESOLVED)) {
             MOZ_ASSERT(!(flags & PROMISE_FLAG_FULFILLED));
             return JS::PromiseState::Pending;
         }
         if (flags & PROMISE_FLAG_FULFILLED)
             return JS::PromiseState::Fulfilled;
         return JS::PromiseState::Rejected;
     }
     Value value()  {
         MOZ_ASSERT(state() == JS::PromiseState::Fulfilled);
-        return getFixedSlot(PROMISE_REACTIONS_OR_RESULT_SLOT);
+        return getFixedSlot(PromiseSlot_ReactionsOrResult);
     }
     Value reason() {
         MOZ_ASSERT(state() == JS::PromiseState::Rejected);
-        return getFixedSlot(PROMISE_REACTIONS_OR_RESULT_SLOT);
+        return getFixedSlot(PromiseSlot_ReactionsOrResult);
     }
 
     MOZ_MUST_USE bool resolve(JSContext* cx, HandleValue resolutionValue);
     MOZ_MUST_USE bool reject(JSContext* cx, HandleValue rejectionValue);
 
     void onSettled(JSContext* cx);
 
-    double allocationTime() { return getFixedSlot(PROMISE_ALLOCATION_TIME_SLOT).toNumber(); }
-    double resolutionTime() { return getFixedSlot(PROMISE_RESOLUTION_TIME_SLOT).toNumber(); }
+    double allocationTime() { return getFixedSlot(PromiseSlot_AllocationTime).toNumber(); }
+    double resolutionTime() { return getFixedSlot(PromiseSlot_ResolutionTime).toNumber(); }
     JSObject* allocationSite() {
-        return getFixedSlot(PROMISE_ALLOCATION_SITE_SLOT).toObjectOrNull();
+        return getFixedSlot(PromiseSlot_AllocationSite).toObjectOrNull();
     }
     JSObject* resolutionSite() {
-        return getFixedSlot(PROMISE_RESOLUTION_SITE_SLOT).toObjectOrNull();
+        return getFixedSlot(PromiseSlot_ResolutionSite).toObjectOrNull();
     }
     double lifetime();
     double timeToResolution() {
         MOZ_ASSERT(state() != JS::PromiseState::Pending);
         return resolutionTime() - allocationTime();
     }
     MOZ_MUST_USE bool dependentPromises(JSContext* cx, MutableHandle<GCVector<Value>> values);
     uint64_t getID();
     bool isUnhandled() {
         MOZ_ASSERT(state() == JS::PromiseState::Rejected);
-        return !(getFixedSlot(PROMISE_FLAGS_SLOT).toInt32() & PROMISE_FLAG_HANDLED);
+        return !(getFixedSlot(PromiseSlot_Flags).toInt32() & PROMISE_FLAG_HANDLED);
     }
     void markAsReported() {
         MOZ_ASSERT(isUnhandled());
-        int32_t flags = getFixedSlot(PROMISE_FLAGS_SLOT).toInt32();
-        setFixedSlot(PROMISE_FLAGS_SLOT, Int32Value(flags | PROMISE_FLAG_REPORTED));
+        int32_t flags = getFixedSlot(PromiseSlot_Flags).toInt32();
+        setFixedSlot(PromiseSlot_Flags, Int32Value(flags | PROMISE_FLAG_REPORTED));
     }
 };
 
 /**
- * Tells the embedding to enqueue a Promise reaction job, based on six
- * parameters:
- * reaction handler - The callback to invoke for this job.
-   argument - The first and only argument to pass to the handler.
-   resolve - The Promise cabability's resolve hook, called upon normal
-             completion of the handler.
-   reject -  The Promise cabability's reject hook, called if the handler
-             throws.
-   promise - The associated Promise, or null for some internal uses.
-   objectFromIncumbentGlobal - An object from the global that was the
-                               incumbent global when the Promise reaction job
-                               was created (not enqueued). Not the global
-                               itself because unwrapping that might unwrap an
-                               inner to an outer window, which we never want
-                               to happen.
+ * Enqueues resolve/reject reactions in the given Promise's reactions lists
+ * in a content-invisible way.
+ *
+ * Used internally to implement DOM functionality.
+ *
+ * Note: the reactions pushed using this function contain a `promise` field
+ * that can contain null. That field is only ever used by devtools, which have
+ * to treat these reactions specially.
  */
-bool EnqueuePromiseReactionJob(JSContext* cx, HandleValue handler, HandleValue handlerArg,
-                               HandleObject resolve, HandleObject reject,
-                               HandleObject promise, HandleObject objectFromIncumbentGlobal);
+MOZ_MUST_USE bool
+EnqueuePromiseReactions(JSContext* cx, Handle<PromiseObject*> promise,
+                        HandleObject dependentPromise,
+                        HandleValue onFulfilled, HandleValue onRejected);
 
-/**
- * Tells the embedding to enqueue a Promise resolve thenable job, based on six
- * parameters:
- * promiseToResolve - The promise to resolve, obviously.
- * thenable - The thenable to resolve the Promise with.
- * then - The `then` function to invoke with the `thenable` as the receiver.
- */
-bool EnqueuePromiseResolveThenableJob(JSContext* cx, HandleValue promiseToResolve,
-                                      HandleValue thenable, HandleValue then);
+MOZ_MUST_USE JSObject*
+GetWaitForAllPromise(JSContext* cx, const JS::AutoObjectVector& promises);
+
+MOZ_MUST_USE JSObject*
+OriginalPromiseThen(JSContext* cx, Handle<PromiseObject*> promise, HandleValue onFulfilled,
+                    HandleValue onRejected);
 
 /**
  * A PromiseTask represents a task that can be dispatched to a helper thread
  * (via StartPromiseTask), executed (by implementing PromiseTask::execute()),
  * and then resolved back on the original JSContext owner thread.
  * Because it contains a PersistentRooted, a PromiseTask will only be destroyed
  * on the JSContext's owner thread.
  */
--- a/js/src/builtin/Promise.js
+++ b/js/src/builtin/Promise.js
@@ -1,762 +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/. */
 
-// ES6, 25.4.1.2.
-// This object is used to verify that an object is a PromiseReaction record.
-var PromiseReactionRecordProto = {__proto__: null};
-function PromiseReactionRecord(promise, resolve, reject, fulfillHandler, rejectHandler,
-                               incumbentGlobal) {
-    this.promise = promise;
-    this.resolve = resolve;
-    this.reject = reject;
-    this.fulfillHandler = fulfillHandler;
-    this.rejectHandler = rejectHandler;
-    this.incumbentGlobal = incumbentGlobal;
-}
-
-MakeConstructible(PromiseReactionRecord, PromiseReactionRecordProto);
-
-// Used to verify that an object is a PromiseCapability record.
-var PromiseCapabilityRecordProto = {__proto__: null};
-
-// ES2016, 25.4.1.3, implemented in Promise.cpp.
-
-// ES2016, 25.4.1.4, implemented in Promise.cpp.
-
-// ES2016, 25.4.1.5.
-// Creates PromiseCapability records, see 25.4.1.1.
-function NewPromiseCapability(C) {
-    // Steps 1-2.
-    if (!IsConstructor(C))
-        ThrowTypeError(JSMSG_NOT_CONSTRUCTOR, 0);
-
-    // Step 3. Replaced by individual fields, combined in step 11.
-    let resolve;
-    let reject;
-
-    // Steps 4-5.
-    // ES6, 25.4.1.5.1. Inlined here so we can use an upvar instead of a slot to
-    // store promiseCapability.
-    function GetCapabilitiesExecutor(resolve_, reject_) {
-        // Steps 1-2 (implicit).
-
-        // Steps 3-4.
-        if (resolve !== undefined || reject !== undefined)
-            ThrowTypeError(JSMSG_PROMISE_CAPABILITY_HAS_SOMETHING_ALREADY);
-        resolve = resolve_;
-        reject = reject_;
-    }
-
-    // Steps 6-7.
-    let promise = new C(GetCapabilitiesExecutor);
-
-    // Step 8.
-    if (!IsCallable(resolve))
-        ThrowTypeError(JSMSG_PROMISE_RESOLVE_FUNCTION_NOT_CALLABLE);
-
-    // Step 9.
-    if (!IsCallable(reject))
-        ThrowTypeError(JSMSG_PROMISE_REJECT_FUNCTION_NOT_CALLABLE);
-
-    // Steps 10-11.
-    return {
-        __proto__: PromiseCapabilityRecordProto,
-        promise,
-        resolve,
-        reject
-    };
-}
-
-// ES2016, 25.4.1.6, implemented in SelfHosting.cpp.
-
-// ES2016, 25.4.1.7, implemented in Promise.cpp.
-
-// ES2016, 25.4.1.8, implemented in Promise.cpp.
-
-// ES2016, 25.4.1.9, implemented in SelfHosting.cpp.
-
-// ES6, 25.4.2.1.
-function EnqueuePromiseReactionJob(reaction, jobType, argument) {
-    // Reaction records contain handlers for both fulfillment and rejection.
-    // The `jobType` parameter allows us to retrieves the right one.
-    assert(jobType === PROMISE_JOB_TYPE_FULFILL || jobType === PROMISE_JOB_TYPE_REJECT,
-           "Invalid job type");
-    _EnqueuePromiseReactionJob(reaction[jobType],
-                               argument,
-                               reaction.resolve,
-                               reaction.reject,
-                               reaction.promise,
-                               reaction.incumbentGlobal || null);
-}
-
-// ES6, 25.4.2.2. (Implemented in C++).
-
-// ES6, 25.4.3.1. (Implemented in C++).
-
-// ES2016, 25.4.4.1.
-function Promise_static_all(iterable) {
-    // Step 1.
-    let C = this;
-
-    // Step 2.
-    if (!IsObject(C))
-        ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, "Receiver of Promise.all call");
-
-    // Step 3.
-    let promiseCapability = NewPromiseCapability(C);
-
-    // Steps 4-5.
-    let iterator;
-    try {
-        iterator = GetIterator(iterable);
-    } catch (e) {
-        callContentFunction(promiseCapability.reject, undefined, e);
-        return promiseCapability.promise;
-    }
-
-    // Step 6.
-    let iteratorRecord = {__proto__: null, iterator, done: false};
-
-    // Steps 7-9.
-    try {
-        // Steps 7,9.
-        return PerformPromiseAll(iteratorRecord, C, promiseCapability);
-    } catch (e) {
-        // Step 8.a.
-        // TODO: implement iterator closing.
-
-        // Step 8.b.
-        callContentFunction(promiseCapability.reject, undefined, e);
-        return promiseCapability.promise;
-    }
-}
-
-// ES6, 25.4.4.1.1.
-function PerformPromiseAll(iteratorRecord, constructor, resultCapability) {
-    // Step 1.
-    assert(IsConstructor(constructor), "PerformPromiseAll called with non-constructor");
-
-    // Step 2.
-    assert(IsPromiseCapability(resultCapability), "Invalid promise capability record");
-
-    // Step 3.
-    // Immediately create an Array instead of a List, so we can skip step 6.d.iii.1.
-    //
-    // We might be dealing with a wrapped instance from another Realm. In that
-    // case, we want to create the `values` array in that other Realm so if
-    // it's less-privileged than the current one, code in that Realm can still
-    // work with the array.
-    let values = IsPromise(resultCapability.promise) || !IsWrappedPromise(resultCapability.promise)
-                 ? []
-                 : NewArrayInCompartment(constructor);
-    let valuesCount = 0;
-
-    // Step 4.
-    let remainingElementsCount = {value: 1};
-
-    // Step 5.
-    let index = 0;
-
-    // Step 6.
-    let iterator = iteratorRecord.iterator;
-    let next;
-    let nextValue;
-    let allPromise = resultCapability.promise;
-    while (true) {
-        try {
-            // Step 6.a.
-            next = callContentFunction(iterator.next, iterator);
-            if (!IsObject(next))
-                ThrowTypeError(JSMSG_NEXT_RETURNED_PRIMITIVE);
-        } catch (e) {
-            // Step 6.b.
-            iteratorRecord.done = true;
-
-            // Step 6.c.
-            throw (e);
-        }
-
-        // Step 6.d.
-        if (next.done) {
-            // Step 6.d.i.
-            iteratorRecord.done = true;
-
-            // Step 6.d.ii.
-            remainingElementsCount.value--;
-            assert(remainingElementsCount.value >= 0,
-                   "remainingElementsCount mustn't be negative.");
-
-            // Step 6.d.iii.
-            if (remainingElementsCount.value === 0)
-                callContentFunction(resultCapability.resolve, undefined, values);
-
-            // Step 6.d.iv.
-            return allPromise;
-        }
-        try {
-            // Step 6.e.
-            nextValue = next.value;
-        } catch (e) {
-            // Step 6.f.
-            iteratorRecord.done = true;
-
-            // Step 6.g.
-            throw e;
-        }
-
-        // Step 6.h.
-        _DefineDataProperty(values, valuesCount++, undefined);
-
-        // Steps 6.i-j.
-        let nextPromise = callContentFunction(constructor.resolve, constructor, nextValue);
-
-        // Steps 6.k-p.
-        let resolveElement = CreatePromiseAllResolveElementFunction(index, values,
-                                                                    resultCapability,
-                                                                    remainingElementsCount);
-
-        // Step 6.q.
-        remainingElementsCount.value++;
-
-        // Steps 6.r-s.
-        BlockOnPromise(nextPromise, allPromise, resolveElement, resultCapability.reject);
-
-        // Step 6.t.
-        index++;
-    }
-}
-
-/**
- * Unforgeable version of Promise.all for internal use.
- *
- * Takes a dense array of Promise objects and returns a promise that's
- * resolved with an array of resolution values when all those promises ahve
- * been resolved, or rejected with the rejection value of the first rejected
- * promise.
- *
- * Asserts if the array isn't dense or one of the entries isn't a Promise.
- */
-function GetWaitForAllPromise(promises) {
-    let resultCapability = NewPromiseCapability(GetBuiltinConstructor('Promise'));
-    let allPromise = resultCapability.promise;
-
-    // Step 3.
-    // Immediately create an Array instead of a List, so we can skip step 6.d.iii.1.
-    let values = [];
-    let valuesCount = 0;
-
-    // Step 4.
-    let remainingElementsCount = {value: 0};
-
-    // Step 6.
-    for (let i = 0; i < promises.length; i++) {
-        // Parts of step 6 for deriving next promise, vastly simplified.
-        assert(callFunction(std_Object_hasOwnProperty, promises, i),
-               "GetWaitForAllPromise must be called with a dense array of promises");
-        let nextPromise = promises[i];
-        assert(IsPromise(nextPromise) || IsWrappedPromise(nextPromise),
-               "promises list must only contain possibly wrapped promises");
-
-        // Step 6.h.
-        _DefineDataProperty(values, valuesCount++, undefined);
-
-        // Steps 6.k-p.
-        let resolveElement = CreatePromiseAllResolveElementFunction(i, values,
-                                                                    resultCapability,
-                                                                    remainingElementsCount);
-
-        // Step 6.q.
-        remainingElementsCount.value++;
-
-        // Steps 6.r-s, very roughly.
-        EnqueuePromiseReactions(nextPromise, allPromise, resolveElement, resultCapability.reject);
-    }
-
-    if (remainingElementsCount.value === 0)
-        callFunction(resultCapability.resolve, undefined, values);
-
-    return allPromise;
-}
-
-// ES6, 25.4.4.1.2.
-function CreatePromiseAllResolveElementFunction(index, values, promiseCapability,
-                                                remainingElementsCount)
-{
-    var alreadyCalled = false;
-    return function PromiseAllResolveElementFunction(x) {
-        // Steps 1-2.
-        if (alreadyCalled)
-            return undefined;
-
-        // Step 3.
-        alreadyCalled = true;
-
-        // Steps 4-7 (implicit).
-
-        // Step 8.
-        // Note: this can't throw because the slot was initialized to `undefined` earlier.
-        values[index] = x;
-
-        // Step 9.
-        remainingElementsCount.value--;
-        assert(remainingElementsCount.value >= 0, "remainingElementsCount mustn't be negative.");
-
-        // Step 10.
-        if (remainingElementsCount.value === 0) {
-            // Step 10.a (implicit).
-
-            // Step 10.b.
-            return callContentFunction(promiseCapability.resolve, undefined, values);
-        }
-
-        // Step 11 (implicit).
-    };
-}
-
-// ES2016, 25.4.4.3.
-function Promise_static_race(iterable) {
-    // Step 1.
-    let C = this;
-
-    // Step 2.
-    if (!IsObject(C))
-        ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, "Receiver of Promise.race call");
-
-    // step 3.
-    let promiseCapability = NewPromiseCapability(C);
-
-    // Steps 4-5.
-    let iterator;
-    try {
-        iterator = GetIterator(iterable);
-    } catch (e) {
-        callContentFunction(promiseCapability.reject, undefined, e);
-        return promiseCapability.promise;
-    }
-
-    // Step 6.
-    let iteratorRecord = {__proto__: null, iterator, done: false};
-
-    // Steps 7-9.
-    try {
-        // Steps 7,9.
-        return PerformPromiseRace(iteratorRecord, promiseCapability, C);
-    } catch (e) {
-        // Step 8.a.
-        // TODO: implement iterator closing.
-
-        // Step 8.b.
-        callContentFunction(promiseCapability.reject, undefined, e);
-        return promiseCapability.promise;
-    }
-}
-
-// ES2016, 25.4.4.3.1.
-function PerformPromiseRace(iteratorRecord, resultCapability, C) {
-    assert(IsConstructor(C), "PerformPromiseRace called with non-constructor");
-    assert(IsPromiseCapability(resultCapability), "Invalid promise capability record");
-
-    // Step 1.
-    let iterator = iteratorRecord.iterator;
-    let racePromise = resultCapability.promise;
-    let next;
-    let nextValue;
-    while (true) {
-        try {
-            // Step 1.a.
-            next = callContentFunction(iterator.next, iterator);
-            if (!IsObject(next))
-                ThrowTypeError(JSMSG_NEXT_RETURNED_PRIMITIVE);
-        } catch (e) {
-            // Step 1.b.
-            iteratorRecord.done = true;
-
-            // Step 1.c.
-            throw (e);
-        }
-
-        // Step 1.d.
-        if (next.done) {
-            // Step 1.d.i.
-            iteratorRecord.done = true;
-
-            // Step 1.d.ii.
-            return racePromise;
-        }
-        try {
-            // Step 1.e.
-            nextValue = next.value;
-        } catch (e) {
-            // Step 1.f.
-            iteratorRecord.done = true;
-
-            // Step 1.g.
-            throw e;
-        }
-
-        // Step 1.h.
-        let nextPromise = callContentFunction(C.resolve, C, nextValue);
-
-        // Steps 1.i.
-        BlockOnPromise(nextPromise, racePromise, resultCapability.resolve,
-                       resultCapability.reject);
-    }
-    assert(false, "Shouldn't reach the end of PerformPromiseRace");
-}
-
-/**
- * Calls |promise.then| with the provided hooks and adds |blockedPromise| to
- * its list of dependent promises. Used by |Promise.all| and |Promise.race|.
- *
- * If |promise.then| is the original |Promise.prototype.then| function and
- * the call to |promise.then| would use the original |Promise| constructor to
- * create the resulting promise, this function skips the call to |promise.then|
- * and thus creating a new promise that would not be observable by content.
- */
-function BlockOnPromise(promise, blockedPromise, onResolve, onReject) {
-    let then = promise.then;
-
-    // By default, the blocked promise is added as an extra entry to the
-    // rejected promises list.
-    let addToDependent = true;
-    if (then === Promise_then && IsObject(promise) && IsPromise(promise)) {
-        // |then| is the original |Promise.prototype.then|, inline it here.
-        // 25.4.5.3., steps 3-4.
-        let PromiseCtor = GetBuiltinConstructor('Promise');
-        let C = SpeciesConstructor(promise, PromiseCtor);
-        let resultCapability;
-
-        if (C === PromiseCtor) {
-            resultCapability = {
-                __proto__: PromiseCapabilityRecordProto,
-                promise: blockedPromise,
-                reject: NullFunction,
-                resolve: NullFunction
-            };
-            addToDependent = false;
-        } else {
-            // 25.4.5.3., steps 5-6.
-            resultCapability = NewPromiseCapability(C);
-        }
-
-        // 25.4.5.3., step 7.
-        PerformPromiseThen(promise, onResolve, onReject, resultCapability);
-    } else {
-        // Optimization failed, do the normal call.
-        callContentFunction(then, promise, onResolve, onReject);
-    }
-    if (!addToDependent)
-        return;
-
-    // The object created by the |promise.then| call or the inlined version
-    // of it above is visible to content (either because |promise.then| was
-    // overridden by content and could leak it, or because a constructor
-    // other than the original value of |Promise| was used to create it).
-    // To have both that object and |blockedPromise| show up as dependent
-    // promises in the debugger, add a dummy reaction to the list of reject
-    // reactions that contains |blockedPromise|, but otherwise does nothing.
-    // If the object isn't a maybe-wrapped instance of |Promise|, we ignore
-    // it. All this does is lose some small amount of debug information
-    // in scenarios that are highly unlikely to occur in useful code.
-    if (IsPromise(promise))
-        return callFunction(AddDependentPromise, promise, blockedPromise);
-
-    if (IsWrappedPromise(promise))
-        callFunction(CallPromiseMethodIfWrapped, promise, blockedPromise, "AddDependentPromise");
-}
-
-/**
- * Invoked with a Promise as the receiver, AddDependentPromise adds an entry
- * to the reactions list.
- *
- * This is only used to make dependent promises visible in the devtools, so no
- * callbacks are provided. To make handling that case easier elsewhere,
- * they're all set to NullFunction.
- *
- * The reason for the target Promise to be passed as the receiver is so the
- * same function can be used for wrapped and unwrapped Promise objects.
- */
-function AddDependentPromise(dependentPromise) {
-    assert(IsPromise(this), "AddDependentPromise expects an unwrapped Promise as the receiver");
-
-    if (GetPromiseState(this) !== PROMISE_STATE_PENDING)
-        return;
-
-    let reaction = new PromiseReactionRecord(dependentPromise, NullFunction, NullFunction,
-                                             NullFunction, NullFunction, null);
-
-    let reactions = UnsafeGetReservedSlot(this, PROMISE_REACTIONS_OR_RESULT_SLOT);
-
-    // The reactions list might not have been allocated yet.
-    if (!reactions)
-        UnsafeSetReservedSlot(dependentPromise, PROMISE_REACTIONS_OR_RESULT_SLOT, [reaction]);
-    else
-        _DefineDataProperty(reactions, reactions.length, reaction);
-}
-
-// ES2016, 25.4.4.4 (implemented in C++).
-
-// ES2016, 25.4.4.5 (implemented in C++).
-
 // ES6, 25.4.4.6.
 function Promise_static_get_species() {
     // Step 1.
     return this;
 }
 _SetCanonicalName(Promise_static_get_species, "get [Symbol.species]");
 
 // ES6, 25.4.5.1.
 function Promise_catch(onRejected) {
     // Steps 1-2.
     return callContentFunction(this.then, this, undefined, onRejected);
 }
-
-// ES6, 25.4.5.3.
-function Promise_then(onFulfilled, onRejected) {
-    // Step 1.
-    let promise = this;
-
-    // Step 2.
-    if (!IsObject(promise))
-        ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, "Receiver of Promise.prototype.then call");
-
-    let isPromise = IsPromise(promise);
-    let isWrappedPromise = isPromise ? false : IsWrappedPromise(promise);
-    if (!(isPromise || isWrappedPromise))
-        ThrowTypeError(JSMSG_INCOMPATIBLE_PROTO, "Promise", "then", "value");
-
-    // Steps 3-4.
-    let C = SpeciesConstructor(promise, GetBuiltinConstructor('Promise'));
-
-    // Steps 5-6.
-    let resultCapability = NewPromiseCapability(C);
-
-    // Step 7.
-    if (isWrappedPromise) {
-        // See comment above GetPromiseHandlerForwarders for why this is needed.
-        let handlerForwarders = GetPromiseHandlerForwarders(onFulfilled, onRejected);
-        return callFunction(CallPromiseMethodIfWrapped, promise,
-                            handlerForwarders[0], handlerForwarders[1],
-                            resultCapability.promise, resultCapability.resolve,
-                            resultCapability.reject, "UnwrappedPerformPromiseThen");
-    }
-    return PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability);
-}
-
-/**
- * Enqueues resolve/reject reactions in the given Promise's reactions lists
- * in a content-invisible way.
- *
- * Used internally to implement DOM functionality.
- *
- * Note: the reactions pushed using this function contain a `promise` field
- * that can contain null. That field is only ever used by devtools, which have
- * to treat these reactions specially.
- */
-function EnqueuePromiseReactions(promise, dependentPromise, onFulfilled, onRejected) {
-    let isWrappedPromise = false;
-    if (!IsPromise(promise)) {
-        assert(IsWrappedPromise(promise),
-               "EnqueuePromiseReactions must be provided with a possibly wrapped promise");
-        isWrappedPromise = true;
-    }
-
-    assert(dependentPromise === null || IsPromise(dependentPromise),
-           "EnqueuePromiseReactions's dependentPromise argument must be a Promise or null");
-
-    if (isWrappedPromise) {
-        // See comment above GetPromiseHandlerForwarders for why this is needed.
-        let handlerForwarders = GetPromiseHandlerForwarders(onFulfilled, onRejected);
-        return callFunction(CallPromiseMethodIfWrapped, promise, handlerForwarders[0],
-                            handlerForwarders[1], dependentPromise, NullFunction, NullFunction,
-                            "UnwrappedPerformPromiseThen");
-    }
-    let capability = {
-        __proto__: PromiseCapabilityRecordProto,
-        promise: dependentPromise,
-        resolve: NullFunction,
-        reject: NullFunction
-    };
-    return PerformPromiseThen(promise, onFulfilled, onRejected, capability);
-}
-
-/**
- * Returns a set of functions that are (1) self-hosted, and (2) exact
- * forwarders of the passed-in functions, for use by
- * UnwrappedPerformPromiseThen.
- *
- * When calling `then` on an xray-wrapped promise, the receiver isn't
- * unwrapped. Instead, Promise_then operates on the wrapped Promise. Just
- * calling PerformPromiseThen from Promise_then as we normally would doesn't
- * work in this case: PerformPromiseThen can only deal with unwrapped
- * Promises. Instead, we use the CallPromiseMethodIfWrapped intrinsic to
- * switch compartments before calling PerformPromiseThen, via
- * UnwrappedPerformPromiseThen.
- *
- * This is almost enough, but there's an additional wrinkle: when calling the
- * fulfillment and rejection handlers, we might pass in Object-type arguments
- * from within the xray-ed, lower-privileged compartment. By default, this
- * doesn't work, because they're wrapped into wrappers that disallow passing
- * in Object-typed arguments (so the higher-privileged code doesn't
- * accidentally operate on objects assuming they're higher-privileged, too.)
- * So instead UnwrappedPerformPromiseThen adds another level of indirection:
- * it closes over the, by now cross-compartment-wrapped, handler forwarders
- * created by GetPromiseHandlerForwarders and creates a second set of
- * forwarders around them, which use UnsafeCallWrappedFunction to call the
- * initial forwarders.
-
- * Note that both above-mentioned guarantees are required: while it may seem
- * as though the original handlers would always be wrappers once they reach
- * UnwrappedPerformPromiseThen (because the call to `then` originated in the
- * higher-privileged compartment, and after unwrapping we end up in the
- * lower-privileged one), that's not necessarily the case. One or both of the
- * handlers can originate from the lower-privileged compartment, so they'd
- * actually be unwrapped functions when they reach
- * UnwrappedPerformPromiseThen.
- */
-function GetPromiseHandlerForwarders(fulfilledHandler, rejectedHandler) {
-    // If non-callable values are passed, we have to preserve them so steps
-    // 3 and 4 of PerformPromiseThen work as expected.
-    return [
-        IsCallable(fulfilledHandler) ? function onFulfilled(argument) {
-            return callContentFunction(fulfilledHandler, this, argument);
-        } : fulfilledHandler,
-        IsCallable(rejectedHandler) ? function onRejected(argument) {
-            return callContentFunction(rejectedHandler, this, argument);
-        } : rejectedHandler
-    ];
-}
-
-/**
- * Forwarder used to invoke PerformPromiseThen on an unwrapped Promise, while
- * wrapping the resolve/reject callbacks into functions that invoke them in
- * their original compartment. See the comment for GetPromiseHandlerForwarders
- * for details.
- */
-function UnwrappedPerformPromiseThen(fulfilledHandler, rejectedHandler, promise, resolve, reject) {
-    let resultCapability = {
-        __proto__: PromiseCapabilityRecordProto,
-        promise,
-        resolve(resolution) {
-            // Under some circumstances, we have an unwrapped `resolve`
-            // function here. One way this happens is if the constructor
-            // passed to `NewPromiseCapability` is from the same global as the
-            // Promise object on which `Promise_then` was called, but where
-            // `Promise_then` is from a different global, so we end up here.
-            // In that case, the `resolve` and `reject` functions aren't
-            // wrappers in the current global.
-            if (IsFunctionObject(resolve))
-                return resolve(resolution);
-            return UnsafeCallWrappedFunction(resolve, undefined, resolution);
-        },
-        reject(reason) {
-            // See comment inside `resolve` above.
-            if (IsFunctionObject(reject))
-                return reject(reason);
-            return UnsafeCallWrappedFunction(reject, undefined, reason);
-        }
-    };
-    function onFulfilled(argument) {
-        return UnsafeCallWrappedFunction(fulfilledHandler, undefined, argument);
-    }
-    function onRejected(argument) {
-        return UnsafeCallWrappedFunction(rejectedHandler, undefined, argument);
-    }
-    return PerformPromiseThen(this, IsCallable(fulfilledHandler) ? onFulfilled : fulfilledHandler,
-                              IsCallable(rejectedHandler) ? onRejected : rejectedHandler,
-                              resultCapability);
-}
-
-// ES2016, 25.4.5.3.1.
-function PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability) {
-    // Step 1.
-    assert(IsPromise(promise), "Can't call PerformPromiseThen on non-Promise objects");
-
-    // Step 2.
-    assert(IsPromiseCapability(resultCapability), "Invalid promise capability record");
-
-    // Step 3.
-    if (!IsCallable(onFulfilled))
-        onFulfilled = PROMISE_HANDLER_IDENTITY;
-
-    // Step 4.
-    if (!IsCallable(onRejected))
-        onRejected = PROMISE_HANDLER_THROWER;
-
-    let incumbentGlobal = _GetObjectFromIncumbentGlobal();
-    // Steps 5,6.
-    // Instead of creating separate reaction records for fulfillment and
-    // rejection, we create a combined record. All places we use the record
-    // can handle that.
-    let reaction = new PromiseReactionRecord(resultCapability.promise,
-                                             resultCapability.resolve,
-                                             resultCapability.reject,
-                                             onFulfilled,
-                                             onRejected,
-                                             incumbentGlobal);
-
-    // Step 7.
-    let state = GetPromiseState(promise);
-    let flags = UnsafeGetInt32FromReservedSlot(promise, PROMISE_FLAGS_SLOT);
-    if (state === PROMISE_STATE_PENDING) {
-        // Steps 7.a,b.
-        // We only have a single list for fulfill and reject reactions.
-        let reactions = UnsafeGetReservedSlot(promise, PROMISE_REACTIONS_OR_RESULT_SLOT);
-        if (!reactions)
-            UnsafeSetReservedSlot(promise, PROMISE_REACTIONS_OR_RESULT_SLOT, [reaction]);
-        else
-            _DefineDataProperty(reactions, reactions.length, reaction);
-    }
-
-    // Step 8.
-    else if (state === PROMISE_STATE_FULFILLED) {
-        // Step 8.a.
-        let value = UnsafeGetReservedSlot(promise, PROMISE_REACTIONS_OR_RESULT_SLOT);
-
-        // Step 8.b.
-        EnqueuePromiseReactionJob(reaction, PROMISE_JOB_TYPE_FULFILL, value);
-    }
-
-    // Step 9.
-    else {
-        // Step 9.a.
-        assert(state === PROMISE_STATE_REJECTED, "Invalid Promise state " + state);
-
-        // Step 9.b.
-        let reason = UnsafeGetReservedSlot(promise, PROMISE_REACTIONS_OR_RESULT_SLOT);
-
-        // Step 9.c.
-        if (!(flags & PROMISE_FLAG_HANDLED))
-            HostPromiseRejectionTracker(promise, PROMISE_REJECTION_TRACKER_OPERATION_HANDLE);
-
-        // Step 9.d.
-        EnqueuePromiseReactionJob(reaction, PROMISE_JOB_TYPE_REJECT, reason);
-    }
-
-    // Step 10.
-    UnsafeSetReservedSlot(promise, PROMISE_FLAGS_SLOT, flags | PROMISE_FLAG_HANDLED);
-
-    // Step 11.
-    return resultCapability.promise;
-}
-
-/// Utility functions below.
-function IsPromiseReaction(record) {
-    return std_Reflect_getPrototypeOf(record) === PromiseReactionRecordProto;
-}
-
-function IsPromiseCapability(capability) {
-    return std_Reflect_getPrototypeOf(capability) === PromiseCapabilityRecordProto;
-}
-
-function GetPromiseState(promise) {
-    let flags = UnsafeGetInt32FromReservedSlot(promise, PROMISE_FLAGS_SLOT);
-    if (!(flags & PROMISE_FLAG_RESOLVED)) {
-        assert(!(flags & PROMISE_STATE_FULFILLED), "Fulfilled promises are resolved, too");
-        return PROMISE_STATE_PENDING;
-    }
-    if (flags & PROMISE_FLAG_FULFILLED)
-        return PROMISE_STATE_FULFILLED;
-    return PROMISE_STATE_REJECTED;
-}
--- a/js/src/builtin/SelfHostingDefines.h
+++ b/js/src/builtin/SelfHostingDefines.h
@@ -73,43 +73,16 @@
 #define ITERATOR_SLOT_ITEM_KIND 2
 // Used for ListIterator.
 #define ITERATOR_SLOT_NEXT_METHOD 2
 
 #define ITEM_KIND_KEY 0
 #define ITEM_KIND_VALUE 1
 #define ITEM_KIND_KEY_AND_VALUE 2
 
-#define PROMISE_FLAGS_SLOT               0
-#define PROMISE_REACTIONS_OR_RESULT_SLOT 1
-#define PROMISE_RESOLVE_FUNCTION_SLOT    2
-#define PROMISE_ALLOCATION_SITE_SLOT     3
-#define PROMISE_RESOLUTION_SITE_SLOT     4
-#define PROMISE_ALLOCATION_TIME_SLOT     5
-#define PROMISE_RESOLUTION_TIME_SLOT     6
-#define PROMISE_ID_SLOT                  7
-
-#define PROMISE_STATE_PENDING   0
-#define PROMISE_STATE_FULFILLED 1
-#define PROMISE_STATE_REJECTED  2
-
-#define PROMISE_FLAG_RESOLVED  0x1
-#define PROMISE_FLAG_FULFILLED 0x2
-#define PROMISE_FLAG_HANDLED   0x4
-#define PROMISE_FLAG_REPORTED  0x8
-
-#define PROMISE_HANDLER_IDENTITY 0
-#define PROMISE_HANDLER_THROWER  1
-
-#define PROMISE_REJECTION_TRACKER_OPERATION_REJECT false
-#define PROMISE_REJECTION_TRACKER_OPERATION_HANDLE true
-
-#define PROMISE_JOB_TYPE_FULFILL "fulfillHandler"
-#define PROMISE_JOB_TYPE_REJECT  "rejectHandler"
-
 // NB: keep these in sync with the copy in jsfriendapi.h.
 #define JSITER_OWNONLY    0x8   /* iterate over obj's own properties only */
 #define JSITER_HIDDEN     0x10  /* also enumerate non-enumerable properties */
 #define JSITER_SYMBOLS    0x20  /* also include symbol property keys */
 #define JSITER_SYMBOLSONLY 0x40 /* exclude string property keys */
 
 
 #define REGEXP_FLAGS_SLOT 2
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -1446,25 +1446,52 @@ SettlePromiseNow(JSContext* cx, unsigned
     if (!args.requireAtLeast(cx, "settlePromiseNow", 1))
         return false;
     if (!args[0].isObject() || !args[0].toObject().is<PromiseObject>()) {
         JS_ReportErrorASCII(cx, "first argument must be a Promise object");
         return false;
     }
 
     RootedNativeObject promise(cx, &args[0].toObject().as<NativeObject>());
-    int32_t flags = promise->getFixedSlot(PROMISE_FLAGS_SLOT).toInt32();
-    promise->setFixedSlot(PROMISE_FLAGS_SLOT,
+    int32_t flags = promise->getFixedSlot(PromiseSlot_Flags).toInt32();
+    promise->setFixedSlot(PromiseSlot_Flags,
                           Int32Value(flags | PROMISE_FLAG_RESOLVED | PROMISE_FLAG_FULFILLED));
-    promise->setFixedSlot(PROMISE_REACTIONS_OR_RESULT_SLOT, UndefinedValue());
+    promise->setFixedSlot(PromiseSlot_ReactionsOrResult, UndefinedValue());
 
     JS::dbg::onPromiseSettled(cx, promise);
     return true;
 }
 
+static bool
+GetWaitForAllPromise(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    if (!args.requireAtLeast(cx, "getWaitForAllPromise", 1))
+        return false;
+    if (!args[0].isObject() || !IsPackedArray(&args[0].toObject())) {
+        JS_ReportErrorASCII(cx, "first argument must be a dense Array of Promise objects");
+        return false;
+    }
+    RootedNativeObject list(cx, &args[0].toObject().as<NativeObject>());
+    AutoObjectVector promises(cx);
+    uint32_t count = list->getDenseInitializedLength();
+    if (!promises.resize(count))
+        return false;
+
+    for (uint32_t i = 0; i < count; i++)
+        promises[i].set(&list->getDenseElement(i).toObject());
+
+    RootedObject resultPromise(cx, JS::GetWaitForAllPromise(cx, promises));
+    if (!resultPromise)
+        return false;
+
+    args.rval().set(ObjectValue(*resultPromise));
+    return true;
+}
+
 #else
 
 static const js::Class FakePromiseClass = {
     "Promise", JSCLASS_IS_ANONYMOUS
 };
 
 static bool
 MakeFakePromise(JSContext* cx, unsigned argc, Value* vp)
@@ -4106,16 +4133,20 @@ static const JSFunctionSpecWithHelp Test
 
 #ifdef SPIDERMONKEY_PROMISE
     JS_FN_HELP("settlePromiseNow", SettlePromiseNow, 1, 0,
 "settlePromiseNow(promise)",
 "  'Settle' a 'promise' immediately. This just marks the promise as resolved\n"
 "  with a value of `undefined` and causes the firing of any onPromiseSettled\n"
 "  hooks set on Debugger instances that are observing the given promise's\n"
 "  global as a debuggee."),
+    JS_FN_HELP("getWaitForAllPromise", GetWaitForAllPromise, 1, 0,
+"getWaitForAllPromise(densePromisesArray)",
+"  Calls the 'GetWaitForAllPromise' JSAPI function and returns the result\n"
+"  Promise."),
 #else
     JS_FN_HELP("makeFakePromise", MakeFakePromise, 0, 0,
 "makeFakePromise()",
 "  Create an object whose [[Class]] name is 'Promise' and call\n"
 "  JS::dbg::onNewPromise on it before returning it. It doesn't actually have\n"
 "  any of the other behavior associated with promises."),
 
     JS_FN_HELP("settleFakePromise", SettleFakePromise, 1, 0,
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -4787,107 +4787,66 @@ JS::RejectPromise(JSContext* cx, JS::Han
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, promise, rejectionValue);
 
     MOZ_ASSERT(promise->is<PromiseObject>());
     return promise->as<PromiseObject>().reject(cx, rejectionValue);
 }
 
 JS_PUBLIC_API(JSObject*)
-JS::CallOriginalPromiseThen(JSContext* cx, JS::HandleObject promise,
-                            JS::HandleObject onResolve, JS::HandleObject onReject)
-{
-    AssertHeapIsIdle(cx);
-    CHECK_REQUEST(cx);
-    assertSameCompartment(cx, promise, onResolve, onReject);
-
-    MOZ_ASSERT(promise->is<PromiseObject>());
-    MOZ_ASSERT(onResolve == nullptr || IsCallable(onResolve));
-    MOZ_ASSERT(onReject == nullptr || IsCallable(onReject));
-
-    JSObject* obj;
-    {
-        FixedInvokeArgs<2> args(cx);
-
-        args[0].setObjectOrNull(onResolve);
-        args[1].setObjectOrNull(onReject);
-
-        RootedValue thisvOrRval(cx, ObjectValue(*promise));
-        if (!CallSelfHostedFunction(cx, "Promise_then", thisvOrRval, args, &thisvOrRval))
-            return nullptr;
-
-        MOZ_ASSERT(thisvOrRval.isObject());
-        obj = &thisvOrRval.toObject();
-    }
-
-    MOZ_ASSERT(obj->is<PromiseObject>());
-    return obj;
-}
-
-JS_PUBLIC_API(bool)
-JS::AddPromiseReactions(JSContext* cx, JS::HandleObject promise,
-                        JS::HandleObject onResolve, JS::HandleObject onReject)
-{
-    AssertHeapIsIdle(cx);
-    CHECK_REQUEST(cx);
-    assertSameCompartment(cx, promise, onResolve, onReject);
-
-    MOZ_ASSERT(promise->is<PromiseObject>());
-    MOZ_ASSERT(IsCallable(onResolve));
-    MOZ_ASSERT(IsCallable(onReject));
-
-    FixedInvokeArgs<4> args(cx);
-
-    args[0].setObject(*promise);
-    args[1].setNull();
-    args[2].setObject(*onResolve);
-    args[3].setObject(*onReject);
-
-    RootedValue dummy(cx);
-    return CallSelfHostedFunction(cx, "EnqueuePromiseReactions", UndefinedHandleValue, args,
-                                  &dummy);
-}
-
+JS::CallOriginalPromiseThen(JSContext* cx, JS::HandleObject promiseObj,
+                            JS::HandleObject onResolveObj, JS::HandleObject onRejectObj)
+{
+    AssertHeapIsIdle(cx);
+    CHECK_REQUEST(cx);
+    assertSameCompartment(cx, promiseObj, onResolveObj, onRejectObj);
+
+    MOZ_ASSERT_IF(onResolveObj, IsCallable(onResolveObj));
+    MOZ_ASSERT_IF(onRejectObj, IsCallable(onRejectObj));
+
+    Rooted<PromiseObject*> promise(cx, &promiseObj->as<PromiseObject>());
+    RootedValue onFulfilled(cx, ObjectOrNullValue(onResolveObj));
+    RootedValue onRejected(cx, ObjectOrNullValue(onRejectObj));
+    return OriginalPromiseThen(cx, promise, onFulfilled, onRejected);
+}
+
+JS_PUBLIC_API(bool)
+JS::AddPromiseReactions(JSContext* cx, JS::HandleObject promiseObj,
+                        JS::HandleObject onResolvedObj, JS::HandleObject onRejectedObj)
+{
+    AssertHeapIsIdle(cx);
+    CHECK_REQUEST(cx);
+    assertSameCompartment(cx, promiseObj, onResolvedObj, onRejectedObj);
+
+    MOZ_ASSERT(IsCallable(onResolvedObj));
+    MOZ_ASSERT(IsCallable(onRejectedObj));
+
+    Rooted<PromiseObject*> promise(cx, &promiseObj->as<PromiseObject>());
+    RootedValue onResolved(cx, ObjectValue(*onResolvedObj));
+    RootedValue onRejected(cx, ObjectValue(*onRejectedObj));
+    return EnqueuePromiseReactions(cx, promise, nullptr, onResolved, onRejected);
+}
+
+/**
+ * Unforgeable version of Promise.all for internal use.
+ *
+ * Takes a dense array of Promise objects and returns a promise that's
+ * resolved with an array of resolution values when all those promises ahve
+ * been resolved, or rejected with the rejection value of the first rejected
+ * promise.
+ *
+ * Asserts that the array is dense and all entries are Promise objects.
+ */
 JS_PUBLIC_API(JSObject*)
 JS::GetWaitForAllPromise(JSContext* cx, const JS::AutoObjectVector& promises)
 {
     AssertHeapIsIdle(cx);
     CHECK_REQUEST(cx);
 
-    RootedArrayObject arr(cx, NewDenseFullyAllocatedArray(cx, promises.length()));
-    if (!arr)
-        return nullptr;
-    arr->ensureDenseInitializedLength(cx, 0, promises.length());
-    for (size_t i = 0, len = promises.length(); i < len; i++) {
-#ifdef DEBUG
-        JSObject* obj = promises[i];
-        assertSameCompartment(cx, obj);
-        if (IsWrapper(obj))
-            obj = UncheckedUnwrap(obj);
-        MOZ_ASSERT(obj->is<PromiseObject>());
-#endif
-        arr->setDenseElement(i, ObjectValue(*promises[i]));
-    }
-
-    JSObject* obj;
-    {
-        FixedInvokeArgs<1> args(cx);
-
-        args[0].setObject(*arr);
-
-        RootedValue thisvOrRval(cx, UndefinedValue());
-        if (!CallSelfHostedFunction(cx, "GetWaitForAllPromise", thisvOrRval, args, &thisvOrRval))
-            return nullptr;
-
-        MOZ_ASSERT(thisvOrRval.isObject());
-        obj = &thisvOrRval.toObject();
-    }
-
-    MOZ_ASSERT(obj->is<PromiseObject>());
-    return obj;
+    return js::GetWaitForAllPromise(cx, promises);
 }
 
 JS_PUBLIC_API(void)
 JS::SetAsyncTaskCallbacks(JSContext* cx, JS::StartAsyncTaskCallback start,
                           JS::FinishAsyncTaskCallback finish)
 {
     cx->startAsyncTaskCallback = start;
     cx->finishAsyncTaskCallback = finish;
--- a/js/src/jsobj.cpp
+++ b/js/src/jsobj.cpp
@@ -1969,16 +1969,57 @@ js::GetBuiltinPrototype(ExclusiveContext
 bool
 js::IsStandardPrototype(JSObject* obj, JSProtoKey key)
 {
     GlobalObject& global = obj->global();
     Value v = global.getPrototype(key);
     return v.isObject() && obj == &v.toObject();
 }
 
+
+/**
+ * Returns the original Object.prototype from the embedding-provided incumbent
+ * global.
+ *
+ * Really, we want the incumbent global itself so we can pass it to other
+ * embedding hooks which need it. Specifically, the enqueue promise hook
+ * takes an incumbent global so it can set that on the PromiseCallbackJob
+ * it creates.
+ *
+ * The reason for not just returning the global itself is that we'd need to
+ * wrap it into the current compartment, and later unwrap it. Unwrapping
+ * globals is tricky, though: we might accidentally unwrap through an inner
+ * to its outer window and end up with the wrong global. Plain objects don't
+ * have this problem, so we use the global's Object.prototype. The code using
+ * it - e.g. EnqueuePromiseReactionJob - can then unwrap the object and get
+ * its global without fear of unwrapping too far.
+ */
+bool
+js::GetObjectFromIncumbentGlobal(JSContext* cx, MutableHandleObject obj)
+{
+    RootedObject globalObj(cx, cx->runtime()->getIncumbentGlobal(cx));
+    if (!globalObj) {
+        obj.set(nullptr);
+        return true;
+    }
+
+    {
+        AutoCompartment ac(cx, globalObj);
+        obj.set(globalObj->as<GlobalObject>().getOrCreateObjectPrototype(cx));
+        if (!obj)
+            return false;
+    }
+
+    // The object might be from a different compartment, so wrap it.
+    if (obj && !cx->compartment()->wrap(cx, obj))
+        return false;
+
+    return true;
+}
+
 JSProtoKey
 JS::IdentifyStandardInstance(JSObject* obj)
 {
     // Note: The prototype shares its JSClass with instances.
     MOZ_ASSERT(!obj->is<CrossCompartmentWrapperObject>());
     JSProtoKey key = StandardProtoKeyOrNull(obj);
     if (key != JSProto_Null && !IsStandardPrototype(obj, key))
         return key;
@@ -3898,16 +3939,26 @@ js::SpeciesConstructor(JSContext* cx, Ha
     if (!Call(cx, func, UndefinedHandleValue, args, pctor))
         return false;
 
     pctor.set(args.rval());
     return true;
 }
 
 bool
+js::SpeciesConstructor(JSContext* cx, HandleObject obj, JSProtoKey ctorKey,
+                       MutableHandleValue pctor)
+{
+    if (!GlobalObject::ensureConstructor(cx, cx->global(), ctorKey))
+        return false;
+    RootedValue defaultCtor(cx, cx->global()->getConstructor(ctorKey));
+    return SpeciesConstructor(cx, obj, defaultCtor, pctor);
+}
+
+bool
 js::Unbox(JSContext* cx, HandleObject obj, MutableHandleValue vp)
 {
     if (MOZ_UNLIKELY(obj->is<ProxyObject>()))
         return Proxy::boxedValue_unbox(cx, obj, vp);
 
     if (obj->is<BooleanObject>())
         vp.setBoolean(obj->as<BooleanObject>().unbox());
     else if (obj->is<NumberObject>())
--- a/js/src/jsobj.h
+++ b/js/src/jsobj.h
@@ -1370,11 +1370,17 @@ FreezeObject(JSContext* cx, HandleObject
  * Object.isFrozen.
  */
 extern bool
 TestIntegrityLevel(JSContext* cx, HandleObject obj, IntegrityLevel level, bool* resultp);
 
 extern bool
 SpeciesConstructor(JSContext* cx, HandleObject obj, HandleValue defaultCtor, MutableHandleValue pctor);
 
+extern bool
+SpeciesConstructor(JSContext* cx, HandleObject obj, JSProtoKey ctorKey, MutableHandleValue pctor);
+
+extern bool
+GetObjectFromIncumbentGlobal(JSContext* cx, MutableHandleObject obj);
+
 }  /* namespace js */
 
 #endif /* jsobj_h */
--- a/js/src/jsprototypes.h
+++ b/js/src/jsprototypes.h
@@ -103,17 +103,17 @@
     real(Float64Array,          32,     InitViaClassSpec,       TYPED_ARRAY_CLASP(Float64)) \
     real(Uint8ClampedArray,     33,     InitViaClassSpec,       TYPED_ARRAY_CLASP(Uint8Clamped)) \
     real(Proxy,                 34,     InitProxyClass,         js::ProxyClassPtr) \
     real(WeakMap,               35,     InitWeakMapClass,       OCLASP(WeakMap)) \
     real(Map,                   36,     InitMapClass,           OCLASP(Map)) \
     real(Set,                   37,     InitSetClass,           OCLASP(Set)) \
     real(DataView,              38,     InitDataViewClass,      OCLASP(DataView)) \
     real(Symbol,                39,     InitSymbolClass,        OCLASP(Symbol)) \
-IF_SAB(real,imaginary)(SharedArrayBuffer,       40,     InitSharedArrayBufferClass, &js::SharedArrayBufferObject::protoClass) \
+IF_SAB(real,imaginary)(SharedArrayBuffer,       40,     InitViaClassSpec, OCLASP(SharedArrayBuffer)) \
 IF_INTL(real,imaginary) (Intl,                  41,     InitIntlClass,          CLASP(Intl)) \
 IF_BDATA(real,imaginary)(TypedObject,           42,     InitTypedObjectModuleObject,   OCLASP(TypedObjectModule)) \
     real(Reflect,               43,     InitReflect,            nullptr) \
 IF_SIMD(real,imaginary)(SIMD,                   44,     InitSimdClass, OCLASP(Simd)) \
     real(WeakSet,               45,     InitWeakSetClass,       OCLASP(WeakSet)) \
     real(TypedArray,            46,     InitViaClassSpec,       &js::TypedArrayObject::sharedTypedArrayPrototypeClass) \
 IF_SAB(real,imaginary)(Atomics, 47,     InitAtomicsClass, OCLASP(Atomics)) \
     real(SavedFrame,            48,     InitViaClassSpec,       &js::SavedFrame::class_) \
--- a/js/src/tests/ecma_6/Promise/get-wait-for-all-promise.js
+++ b/js/src/tests/ecma_6/Promise/get-wait-for-all-promise.js
@@ -1,67 +1,65 @@
 // |reftest| skip-if(!xulRuntime.shell) -- needs getSelfHostedValue and drainJobQueue
 
 if (!this.Promise) {
     this.reportCompare && reportCompare(true,true);
     quit(0);
 }
 
-let GetWaitForAllPromise = getSelfHostedValue('GetWaitForAllPromise');
-
 function onResolved(val) {
     result = 'resolved with ' + val;
 }
 
 function onRejected(val) {
     result = 'rejected with ' + val;
 }
 
-// Replacing `Promise#then` shouldn't affect GetWaitForAllPromise.
+// Replacing `Promise#then` shouldn't affect getWaitForAllPromise.
 let originalThen = Promise.prototype.then;
 Promise.prototype.then = 1;
 
-// Replacing Promise[@@species] shouldn't affect GetWaitForAllPromise.
+// Replacing Promise[@@species] shouldn't affect getWaitForAllPromise.
 Promise[Symbol.species] = function(){};
 
-// Replacing `Promise` shouldn't affect GetWaitForAllPromise.
+// Replacing `Promise` shouldn't affect getWaitForAllPromise.
 let PromiseCtor = Promise;
 Promise = {};
 
-// Replacing Array[@@iterator] shouldn't affect GetWaitForAllPromise.
+// Replacing Array[@@iterator] shouldn't affect getWaitForAllPromise.
 Array.prototype[Symbol.iterator] = function(){};
 
 let resolveFunctions = [];
 let rejectFunctions = [];
 let promises = [];
 for (let i = 0; i < 3; i++) {
     let p = new PromiseCtor(function(res_, rej_) {
         resolveFunctions.push(res_);
         rejectFunctions.push(rej_);
     });
     promises.push(p);
 }
 
-let allPromise = GetWaitForAllPromise(promises);
+let allPromise = getWaitForAllPromise(promises);
 let then = originalThen.call(allPromise, onResolved, onRejected);
 
 resolveFunctions.forEach((fun, i)=>fun(i));
 drainJobQueue();
 
 assertEq(result, 'resolved with 0,1,2');
 
 // Empty lists result in a promise resolved with an empty array.
 result = undefined;
-originalThen.call(GetWaitForAllPromise([]), v=>(result = v));
+originalThen.call(getWaitForAllPromise([]), v=>(result = v));
 drainJobQueue();
 assertEq(result instanceof Array, true);
 assertEq(result.length, 0);
 
 //Empty lists result in a promise resolved with an empty array.
 result = undefined;
-originalThen.call(GetWaitForAllPromise([]), v=>(result = v));
+originalThen.call(getWaitForAllPromise([]), v=>(result = v));
 
 drainJobQueue();
 
 assertEq(result instanceof Array, true);
 assertEq(result.length, 0);
 
 this.reportCompare && reportCompare(true,true);
--- a/js/src/vm/CommonPropertyNames.h
+++ b/js/src/vm/CommonPropertyNames.h
@@ -116,17 +116,16 @@
     macro(float64, float64, "float64") \
     macro(Float64x2, Float64x2, "Float64x2") \
     macro(forceInterpreter, forceInterpreter, "forceInterpreter") \
     macro(forEach, forEach, "forEach") \
     macro(format, format, "format") \
     macro(frame, frame, "frame") \
     macro(from, from, "from") \
     macro(fulfilled, fulfilled, "fulfilled") \
-    macro(fulfillHandler, fulfillHandler, "fulfillHandler") \
     macro(futexNotEqual, futexNotEqual, "not-equal") \
     macro(futexOK, futexOK, "ok") \
     macro(futexTimedOut, futexTimedOut, "timed-out") \
     macro(gcCycleNumber, gcCycleNumber, "gcCycleNumber") \
     macro(Generator, Generator, "Generator") \
     macro(GeneratorFunction, GeneratorFunction, "GeneratorFunction") \
     macro(get, get, "get") \
     macro(getInternals, getInternals, "getInternals") \
@@ -249,17 +248,16 @@
     macro(RegExpFlagsGetter, RegExpFlagsGetter, "RegExpFlagsGetter") \
     macro(RegExpMatcher, RegExpMatcher, "RegExpMatcher") \
     macro(RegExpSearcher, RegExpSearcher, "RegExpSearcher") \
     macro(RegExpTester, RegExpTester, "RegExpTester") \
     macro(RegExp_prototype_Exec, RegExp_prototype_Exec, "RegExp_prototype_Exec") \
     macro(Reify, Reify, "Reify") \
     macro(reject, reject, "reject") \
     macro(rejected, rejected, "rejected") \
-    macro(rejectHandler, rejectHandler, "rejectHandler") \
     macro(RequireObjectCoercible, RequireObjectCoercible, "RequireObjectCoercible") \
     macro(resolve, resolve, "resolve") \
     macro(resumeGenerator, resumeGenerator, "resumeGenerator") \
     macro(return, return_, "return") \
     macro(revoke, revoke, "revoke") \
     macro(script, script, "script") \
     macro(scripts, scripts, "scripts") \
     macro(second, second, "second") \
--- a/js/src/vm/GlobalObject.h
+++ b/js/src/vm/GlobalObject.h
@@ -17,19 +17,16 @@
 #include "js/Vector.h"
 #include "vm/ArrayBufferObject.h"
 #include "vm/ErrorObject.h"
 #include "vm/RegExpStatics.h"
 #include "vm/Runtime.h"
 
 namespace js {
 
-extern JSObject*
-InitSharedArrayBufferClass(JSContext* cx, HandleObject obj);
-
 class Debugger;
 class TypedObjectModuleObject;
 class LexicalEnvironmentObject;
 
 class SimdTypeDescr;
 enum class SimdType;
 
 /*
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -728,17 +728,18 @@ JSRuntime::enqueuePromiseJob(JSContext* 
     RootedObject allocationSite(cx);
     if (promise) {
         RootedObject unwrappedPromise(cx, promise);
         // While the job object is guaranteed to be unwrapped, the promise
         // might be wrapped. See the comments in
         // intrinsic_EnqueuePromiseReactionJob for details.
         if (IsWrapper(promise))
             unwrappedPromise = UncheckedUnwrap(promise);
-        allocationSite = JS::GetPromiseAllocationSite(unwrappedPromise);
+        if (unwrappedPromise->is<PromiseObject>())
+            allocationSite = JS::GetPromiseAllocationSite(unwrappedPromise);
     }
     return cx->runtime()->enqueuePromiseJobCallback(cx, job, allocationSite, incumbentGlobal, data);
 }
 
 void
 JSRuntime::addUnhandledRejectedPromise(JSContext* cx, js::HandleObject promise)
 {
     MOZ_ASSERT(promise->is<PromiseObject>());
--- a/js/src/vm/SelfHosting.cpp
+++ b/js/src/vm/SelfHosting.cpp
@@ -22,17 +22,16 @@
 #include "jsweakmap.h"
 #include "jswrapper.h"
 #include "selfhosted.out.h"
 
 #include "builtin/Intl.h"
 #include "builtin/MapObject.h"
 #include "builtin/ModuleObject.h"
 #include "builtin/Object.h"
-#include "builtin/Promise.h"
 #include "builtin/Reflect.h"
 #include "builtin/SelfHostingDefines.h"
 #include "builtin/SIMD.h"
 #include "builtin/TypedObject.h"
 #include "builtin/WeakSetObject.h"
 #include "gc/Marking.h"
 #include "gc/Policy.h"
 #include "jit/AtomicOperations.h"
@@ -173,69 +172,16 @@ intrinsic_IsCallable(JSContext* cx, unsi
 static bool
 intrinsic_IsConstructor(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     args.rval().setBoolean(IsConstructor(args[0]));
     return true;
 }
 
-/**
- * Intrinsic for calling a wrapped self-hosted function without invoking the
- * wrapper's security checks.
- *
- * Takes a wrapped function as the first and the receiver object as the
- * second argument. Any additional arguments are passed on to the unwrapped
- * function.
- *
- * Xray wrappers prevent lower-privileged code from passing objects to wrapped
- * functions from higher-privileged realms. In some cases, this check is too
- * strict, so this intrinsic allows getting around it.
- *
- * Note that it's not possible to replace all usages with dedicated intrinsics
- * as the function in question might be an inner function that closes over
- * state relevant to its execution.
- *
- * Right now, this is used for the Promise implementation to enable creating
- * resolution functions for xrayed Promises in the privileged realm and then
- * creating the Promise instance in the non-privileged one. The callbacks have
- * to be called by non-privileged code in various places, in many cases
- * passing objects as arguments.
- */
-static bool
-intrinsic_UnsafeCallWrappedFunction(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-    MOZ_ASSERT(args.length() >= 2);
-    MOZ_ASSERT(IsCallable(args[0]));
-    MOZ_ASSERT(IsWrapper(&args[0].toObject()));
-    MOZ_ASSERT(args[1].isObject() || args[1].isUndefined());
-
-    MOZ_RELEASE_ASSERT(args[0].isObject());
-    RootedObject wrappedFun(cx, &args[0].toObject());
-    RootedObject fun(cx, UncheckedUnwrap(wrappedFun));
-    MOZ_RELEASE_ASSERT(fun->is<JSFunction>());
-    MOZ_RELEASE_ASSERT(fun->as<JSFunction>().isSelfHostedOrIntrinsic());
-
-    InvokeArgs args2(cx);
-    if (!args2.init(cx, args.length() - 2))
-        return false;
-
-    args2.setThis(args[1]);
-
-    for (size_t i = 0; i < args2.length(); i++)
-        args2[i].set(args[i + 2]);
-
-    AutoWaivePolicy waivePolicy(cx, wrappedFun, JSID_VOIDHANDLE, BaseProxyHandler::CALL);
-    if (!CrossCompartmentWrapper::singleton.call(cx, wrappedFun, args2))
-        return false;
-    args.rval().set(args2.rval());
-    return true;
-}
-
 template<typename T>
 static bool
 intrinsic_IsInstanceOfBuiltin(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     MOZ_ASSERT(args.length() == 1);
     MOZ_ASSERT(args[0].isObject());
 
@@ -1880,64 +1826,16 @@ js::ReportIncompatibleSelfHostedMethod(J
         }
         ++iter;
     }
 
     MOZ_ASSERT_UNREACHABLE("How did we not find a useful self-hosted frame?");
     return false;
 }
 
-static bool
-intrinsic_EnqueuePromiseReactionJob(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-    MOZ_ASSERT(args.length() == 6);
-
-    RootedValue handler(cx, args[0]);
-    MOZ_ASSERT((handler.isNumber() &&
-                (handler.toNumber() == PROMISE_HANDLER_IDENTITY ||
-                 handler.toNumber() == PROMISE_HANDLER_THROWER)) ||
-               handler.toObject().isCallable());
-
-    RootedValue handlerArg(cx, args[1]);
-
-    RootedObject resolve(cx, &args[2].toObject());
-    MOZ_ASSERT(IsCallable(resolve));
-
-    RootedObject reject(cx, &args[3].toObject());
-    MOZ_ASSERT(IsCallable(reject));
-
-    RootedObject promise(cx, args[4].toObjectOrNull());
-    RootedObject objectFromIncumbentGlobal(cx, args[5].toObjectOrNull());
-
-    if (!EnqueuePromiseReactionJob(cx, handler, handlerArg, resolve, reject, promise,
-                                   objectFromIncumbentGlobal))
-    {
-        return false;
-    }
-    args.rval().setUndefined();
-    return true;
-}
-
-// ES2016, February 12 draft, 25.4.1.9.
-static bool
-intrinsic_HostPromiseRejectionTracker(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-    MOZ_ASSERT(args.length() == 2);
-    MOZ_ASSERT(args[0].toObject().is<PromiseObject>());
-
-    Rooted<PromiseObject*> promise(cx, &args[0].toObject().as<PromiseObject>());
-    mozilla::DebugOnly<bool> isHandled = args[1].toBoolean();
-    MOZ_ASSERT(isHandled, "HostPromiseRejectionTracker intrinsic currently only marks as handled");
-    cx->runtime()->removeUnhandledRejectedPromise(cx, promise);
-    args.rval().setUndefined();
-    return true;
-}
-
 /**
  * Returns the default locale as a well-formed, but not necessarily canonicalized,
  * BCP-47 language tag.
  */
 static bool
 intrinsic_RuntimeDefaultLocale(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
@@ -2054,71 +1952,16 @@ intrinsic_NameForTypedArray(JSContext* c
 
     JSProtoKey protoKey = StandardProtoKeyOrNull(object);
     MOZ_ASSERT(protoKey);
 
     args.rval().setString(ClassName(protoKey, cx));
     return true;
 }
 
-/**
- * Returns an object created in the embedding-provided incumbent global.
- *
- * Really, we want the incumbent global itself so we can pass it to other
- * embedding hooks which need it. Specifically, the enqueue promise hook
- * takes an incumbent global so it can set that on the PromiseCallbackJob
- * it creates.
- *
- * The reason for not just returning the global itself is that we'd need to
- * wrap it into the current compartment, and later unwrap it. Unwrapping
- * globals is tricky, though: we might accidentally unwrap through an inner
- * to its outer window and end up with the wrong global. Plain objects don't
- * have this problem, so we create one and return it. The code using it -
- * e.g. EnqueuePromiseReactionJob - can then unwrap the object and get its
- * global without fear of unwrapping too far.
- */
-static bool
-intrinsic_GetObjectFromIncumbentGlobal(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-    MOZ_ASSERT(args.length() == 0);
-
-    RootedObject obj(cx);
-    RootedObject global(cx, cx->runtime()->getIncumbentGlobal(cx));
-    if (global) {
-        MOZ_ASSERT(global->is<GlobalObject>());
-        AutoCompartment ac(cx, global);
-        obj = NewBuiltinClassInstance<PlainObject>(cx);
-        if (!obj)
-            return false;
-    }
-
-    RootedValue objVal(cx, ObjectOrNullValue(obj));
-
-    // The object might be from a different compartment, so wrap it.
-    if (obj && !cx->compartment()->wrap(cx, &objVal))
-        return false;
-
-    args.rval().set(objVal);
-    return true;
-}
-
-static bool
-intrinsic_IsWrappedPromiseObject(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-    MOZ_ASSERT(args.length() == 1);
-
-    RootedObject obj(cx, &args[0].toObject());
-    MOZ_ASSERT(!obj->is<PromiseObject>(),
-               "Unwrapped promises should be filtered out in inlineable code");
-    args.rval().setBoolean(CheckedUnwrap(obj)->is<PromiseObject>());
-    return true;
-}
-
 static bool
 intrinsic_HostResolveImportedModule(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     MOZ_ASSERT(args.length() == 2);
     MOZ_ASSERT(args[0].toObject().is<ModuleObject>());
     MOZ_ASSERT(args[1].isString());
 
@@ -2403,17 +2246,16 @@ static const JSFunctionSpec intrinsic_fu
                     IntrinsicUnsafeGetObjectFromReservedSlot),
     JS_INLINABLE_FN("UnsafeGetInt32FromReservedSlot",   intrinsic_UnsafeGetInt32FromReservedSlot,  2,0,
                     IntrinsicUnsafeGetInt32FromReservedSlot),
     JS_INLINABLE_FN("UnsafeGetStringFromReservedSlot",  intrinsic_UnsafeGetStringFromReservedSlot, 2,0,
                     IntrinsicUnsafeGetStringFromReservedSlot),
     JS_INLINABLE_FN("UnsafeGetBooleanFromReservedSlot", intrinsic_UnsafeGetBooleanFromReservedSlot,2,0,
                     IntrinsicUnsafeGetBooleanFromReservedSlot),
 
-    JS_FN("UnsafeCallWrappedFunction", intrinsic_UnsafeCallWrappedFunction,2,0),
     JS_FN("NewArrayInCompartment",   intrinsic_NewArrayInCompartment,   1,0),
 
     JS_FN("IsPackedArray",           intrinsic_IsPackedArray,           1,0),
 
     JS_FN("GetIteratorPrototype",    intrinsic_GetIteratorPrototype,    0,0),
 
     JS_FN("NewArrayIterator",        intrinsic_NewArrayIterator,        0,0),
     JS_FN("CallArrayIteratorMethodIfWrapped",
@@ -2516,24 +2358,16 @@ static const JSFunctionSpec intrinsic_fu
           CallNonGenericSelfhostedMethod<Is<LegacyGeneratorObject>>, 2, 0),
     JS_FN("CallStarGeneratorMethodIfWrapped",
           CallNonGenericSelfhostedMethod<Is<StarGeneratorObject>>, 2, 0),
 
     JS_FN("IsWeakSet", intrinsic_IsInstanceOfBuiltin<WeakSetObject>, 1,0),
     JS_FN("CallWeakSetMethodIfWrapped",
           CallNonGenericSelfhostedMethod<Is<WeakSetObject>>, 2, 0),
 
-    JS_FN("_GetObjectFromIncumbentGlobal",  intrinsic_GetObjectFromIncumbentGlobal, 0, 0),
-    JS_FN("IsPromise",                      intrinsic_IsInstanceOfBuiltin<PromiseObject>, 1,0),
-    JS_FN("IsWrappedPromise",               intrinsic_IsWrappedPromiseObject,     1, 0),
-    JS_FN("_EnqueuePromiseReactionJob",     intrinsic_EnqueuePromiseReactionJob,  2, 0),
-    JS_FN("HostPromiseRejectionTracker",    intrinsic_HostPromiseRejectionTracker,2, 0),
-    JS_FN("CallPromiseMethodIfWrapped",
-          CallNonGenericSelfhostedMethod<Is<PromiseObject>>,      2,0),
-
     // See builtin/TypedObject.h for descriptors of the typedobj functions.
     JS_FN("NewOpaqueTypedObject",           js::NewOpaqueTypedObject, 1, 0),
     JS_FN("NewDerivedTypedObject",          js::NewDerivedTypedObject, 3, 0),
     JS_FN("TypedObjectBuffer",              TypedObject::GetBuffer, 1, 0),
     JS_FN("TypedObjectByteOffset",          TypedObject::GetByteOffset, 1, 0),
     JS_FN("AttachTypedObject",              js::AttachTypedObject, 3, 0),
     JS_FN("TypedObjectIsAttached",          js::TypedObjectIsAttached, 1, 0),
     JS_FN("TypedObjectTypeDescr",           js::TypedObjectTypeDescr, 1, 0),
--- a/js/src/vm/SharedArrayObject.cpp
+++ b/js/src/vm/SharedArrayObject.cpp
@@ -199,20 +199,16 @@ SharedArrayRawBuffer::dropReference()
         // previously-inaccessible region.
         VALGRIND_ENABLE_ADDR_ERROR_REPORTING_IN_RANGE(address, mappedSize);
 # endif
     } else {
         UnmapMemory(address, allocSize);
     }
 }
 
-const JSFunctionSpec SharedArrayBufferObject::jsfuncs[] = {
-    /* Nothing yet */
-    JS_FS_END
-};
 
 MOZ_ALWAYS_INLINE bool
 SharedArrayBufferObject::byteLengthGetterImpl(JSContext* cx, const CallArgs& args)
 {
     MOZ_ASSERT(IsSharedArrayBuffer(args.thisv()));
     args.rval().setInt32(args.thisv().toObject().as<SharedArrayBufferObject>().byteLength());
     return true;
 }
@@ -326,84 +322,92 @@ SharedArrayBufferObject::addSizeOfExclud
     // some threads might be to high (if the refcount goes up) or too low (if
     // the refcount goes down). But that's unlikely and hard to avoid, so we
     // just live with the risk.
     const SharedArrayBufferObject& buf = obj->as<SharedArrayBufferObject>();
     info->objectsNonHeapElementsShared +=
         buf.byteLength() / buf.rawBufferObject()->refcount();
 }
 
-const Class SharedArrayBufferObject::protoClass = {
+static const ClassSpec SharedArrayBufferObjectProtoClassSpec = {
+    DELEGATED_CLASSSPEC(SharedArrayBufferObject::class_.spec),
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    ClassSpec::IsDelegated
+};
+
+static const Class SharedArrayBufferObjectProtoClass = {
     "SharedArrayBufferPrototype",
-    JSCLASS_HAS_CACHED_PROTO(JSProto_SharedArrayBuffer)
+    JSCLASS_HAS_CACHED_PROTO(JSProto_SharedArrayBuffer),
+    JS_NULL_CLASS_OPS,
+    &SharedArrayBufferObjectProtoClassSpec
 };
 
+static JSObject*
+CreateSharedArrayBufferPrototype(JSContext* cx, JSProtoKey key)
+{
+    return cx->global()->createBlankPrototype(cx, &SharedArrayBufferObjectProtoClass);
+}
+
 static const ClassOps SharedArrayBufferObjectClassOps = {
     nullptr, /* addProperty */
     nullptr, /* delProperty */
     nullptr, /* getProperty */
     nullptr, /* setProperty */
     nullptr, /* enumerate */
     nullptr, /* resolve */
     nullptr, /* mayResolve */
     SharedArrayBufferObject::Finalize,
     nullptr, /* call */
     nullptr, /* hasInstance */
     nullptr, /* construct */
     nullptr, /* trace */
 };
 
+static const JSFunctionSpec static_functions[] = {
+    JS_FS_END
+};
+
+static const JSPropertySpec static_properties[] = {
+    JS_PS_END
+};
+
+static const JSFunctionSpec prototype_functions[] = {
+    JS_FS_END
+};
+
+static const JSPropertySpec prototype_properties[] = {
+    JS_PSG("byteLength", SharedArrayBufferObject::byteLengthGetter, 0),
+    JS_PS_END
+};
+
+static const ClassSpec ArrayBufferObjectClassSpec = {
+    GenericCreateConstructor<SharedArrayBufferObject::class_constructor, 1, gc::AllocKind::FUNCTION>,
+    CreateSharedArrayBufferPrototype,
+    static_functions,
+    static_properties,
+    prototype_functions,
+    prototype_properties
+};
+
 const Class SharedArrayBufferObject::class_ = {
     "SharedArrayBuffer",
     JSCLASS_DELAY_METADATA_BUILDER |
     JSCLASS_HAS_RESERVED_SLOTS(SharedArrayBufferObject::RESERVED_SLOTS) |
     JSCLASS_HAS_CACHED_PROTO(JSProto_SharedArrayBuffer) |
     JSCLASS_BACKGROUND_FINALIZE,
     &SharedArrayBufferObjectClassOps,
-    JS_NULL_CLASS_SPEC,
+    &ArrayBufferObjectClassSpec,
     JS_NULL_CLASS_EXT
 };
 
-JSObject*
-js::InitSharedArrayBufferClass(JSContext* cx, HandleObject obj)
-{
-    MOZ_ASSERT(obj->isNative());
-    Rooted<GlobalObject*> global(cx, &obj->as<GlobalObject>());
-    RootedNativeObject proto(cx, global->createBlankPrototype(cx, &SharedArrayBufferObject::protoClass));
-    if (!proto)
-        return nullptr;
-
-    RootedFunction ctor(cx, global->createConstructor(cx, SharedArrayBufferObject::class_constructor,
-                                                      cx->names().SharedArrayBuffer, 1));
-    if (!ctor)
-        return nullptr;
-
-    if (!LinkConstructorAndPrototype(cx, ctor, proto))
-        return nullptr;
-
-    RootedId byteLengthId(cx, NameToId(cx->names().byteLength));
-    unsigned attrs = JSPROP_SHARED | JSPROP_GETTER | JSPROP_PERMANENT;
-    JSObject* getter =
-        NewNativeFunction(cx, SharedArrayBufferObject::byteLengthGetter, 0, nullptr);
-    if (!getter)
-        return nullptr;
-
-    if (!NativeDefineProperty(cx, proto, byteLengthId, UndefinedHandleValue,
-                              JS_DATA_TO_FUNC_PTR(GetterOp, getter), nullptr, attrs))
-        return nullptr;
-
-    if (!JS_DefineFunctions(cx, proto, SharedArrayBufferObject::jsfuncs))
-        return nullptr;
-
-    if (!GlobalObject::initBuiltinConstructor(cx, global, JSProto_SharedArrayBuffer, ctor, proto))
-        return nullptr;
-
-    return proto;
-}
-
 bool
 js::IsSharedArrayBuffer(HandleValue v)
 {
     return v.isObject() && v.toObject().is<SharedArrayBufferObject>();
 }
 
 bool
 js::IsSharedArrayBuffer(HandleObject o)
--- a/js/src/vm/SharedArrayObject.h
+++ b/js/src/vm/SharedArrayObject.h
@@ -122,18 +122,16 @@ class SharedArrayBufferObject : public A
   public:
     // RAWBUF_SLOT holds a pointer (as "private" data) to the
     // SharedArrayRawBuffer object, which is manually managed storage.
     static const uint8_t RAWBUF_SLOT = 0;
 
     static const uint8_t RESERVED_SLOTS = 1;
 
     static const Class class_;
-    static const Class protoClass;
-    static const JSFunctionSpec jsfuncs[];
 
     static bool byteLengthGetter(JSContext* cx, unsigned argc, Value* vp);
 
     static bool class_constructor(JSContext* cx, unsigned argc, Value* vp);
 
     // Create a SharedArrayBufferObject with a new SharedArrayRawBuffer.
     static SharedArrayBufferObject* New(JSContext* cx,
                                         uint32_t length,
--- a/js/src/vm/TypedArrayObject.cpp
+++ b/js/src/vm/TypedArrayObject.cpp
@@ -789,17 +789,17 @@ class TypedArrayObjectTemplate : public 
              * ArrayBufferObject in that same compartment.
              */
             JSObject* wrapped = CheckedUnwrap(bufobj);
             if (!wrapped) {
                 JS_ReportErrorASCII(cx, "Permission denied to access object");
                 return nullptr;
             }
 
-            if (!IsArrayBuffer(wrapped) && !IsSharedArrayBuffer(wrapped)) {
+            if (!IsAnyArrayBuffer(wrapped)) {
                 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_BAD_ARGS);
                 return nullptr; // must be arrayBuffer
             }
 
             /*
              * And for even more fun, the new view's prototype should be
              * set to the origin compartment's prototype object, not the
              * target's (specifically, the actual view in the target
@@ -832,17 +832,17 @@ class TypedArrayObjectTemplate : public 
             RootedValue thisv(cx, ObjectValue(*bufobj));
             RootedValue rval(cx);
             if (!js::Call(cx, fval, thisv, args, &rval))
                 return nullptr;
 
             return &rval.toObject();
         }
 
-        if (!IsArrayBuffer(bufobj) && !IsSharedArrayBuffer(bufobj)) {
+        if (!IsAnyArrayBuffer(bufobj)) {
             JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_BAD_ARGS);
             return nullptr; // must be arrayBuffer
         }
 
         Rooted<ArrayBufferObjectMaybeShared*> buffer(cx);
         if (IsArrayBuffer(bufobj)) {
             ArrayBufferObject& buf = AsArrayBuffer(bufobj);
             if (buf.isDetached()) {
@@ -1535,17 +1535,17 @@ TypedArrayObject::sharedTypedArrayProtot
     &TypedArrayObjectSharedTypedArrayPrototypeClassSpec
 };
 
 template<typename T>
 bool
 ArrayBufferObject::createTypedArrayFromBufferImpl(JSContext* cx, const CallArgs& args)
 {
     typedef TypedArrayObjectTemplate<T> ArrayType;
-    MOZ_ASSERT(IsArrayBuffer(args.thisv()));
+    MOZ_ASSERT(IsAnyArrayBuffer(args.thisv()));
     MOZ_ASSERT(args.length() == 3);
 
     Rooted<JSObject*> buffer(cx, &args.thisv().toObject());
     Rooted<JSObject*> proto(cx, &args[2].toObject());
 
     Rooted<JSObject*> obj(cx);
     double byteOffset = args[0].toNumber();
     MOZ_ASSERT(0 <= byteOffset);
@@ -1559,17 +1559,17 @@ ArrayBufferObject::createTypedArrayFromB
     return true;
 }
 
 template<typename T>
 bool
 ArrayBufferObject::createTypedArrayFromBuffer(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
-    return CallNonGenericMethod<IsArrayBuffer, createTypedArrayFromBufferImpl<T> >(cx, args);
+    return CallNonGenericMethod<IsAnyArrayBuffer, createTypedArrayFromBufferImpl<T> >(cx, args);
 }
 
 // this default implementation is only valid for integer types
 // less than 32-bits in size.
 template<typename NativeType>
 Value
 TypedArrayObjectTemplate<NativeType>::getIndexValue(JSObject* tarray, uint32_t index)
 {
--- a/js/src/vm/TypedArrayObject.h
+++ b/js/src/vm/TypedArrayObject.h
@@ -556,16 +556,34 @@ ClampIntForUint8Array(int32_t x)
 {
     if (x < 0)
         return 0;
     if (x > 255)
         return 255;
     return x;
 }
 
+static inline bool
+IsAnyArrayBuffer(HandleObject obj)
+{
+    return IsArrayBuffer(obj) || IsSharedArrayBuffer(obj);
+}
+
+static inline bool
+IsAnyArrayBuffer(JSObject* obj)
+{
+    return IsArrayBuffer(obj) || IsSharedArrayBuffer(obj);
+}
+
+static inline bool
+IsAnyArrayBuffer(HandleValue v)
+{
+    return v.isObject() && IsAnyArrayBuffer(&v.toObject());
+}
+
 } // namespace js
 
 template <>
 inline bool
 JSObject::is<js::TypedArrayObject>() const
 {
     return js::IsTypedArrayClass(getClass());
 }
--- a/js/xpconnect/tests/chrome/test_xrayToJS.xul
+++ b/js/xpconnect/tests/chrome/test_xrayToJS.xul
@@ -252,16 +252,23 @@ https://bugzilla.mozilla.org/show_bug.cg
   gConstructorProperties['Promise'] =
     constructorProps(["resolve", "reject", "all", "race", Symbol.species]);
 
   gPrototypeProperties['ArrayBuffer'] =
     ["constructor", "byteLength", "slice", Symbol.toStringTag];
   gConstructorProperties['ArrayBuffer'] =
     constructorProps(["isView", "slice", Symbol.species]);
 
+  if (!isReleaseOrBeta) {
+    gPrototypeProperties['SharedArrayBuffer'] = ["constructor", "byteLength"];
+    gConstructorProperties['SharedArrayBuffer'] = constructorProps([]);
+  } else {
+    is(typeof SharedArrayBuffer, "undefined", "Enable tests!");
+  }
+
   // Sort an array that may contain symbols as well as strings.
   function sortProperties(arr) {
     function sortKey(prop) {
       return typeof prop + ":" + prop.toString();
     }
     arr.sort((a, b) => sortKey(a) < sortKey(b) ? -1 : +1);
   }
 
@@ -888,41 +895,54 @@ for (var prop of props) {
     is(Cu.getGlobalForObject(pr.wrappedJSObject.catch), iwin, "Underlying global is correct");
 
     isnot(pr.then, Cu.unwaiveXrays(pr.wrappedJSObject.then), "Different function identities");
     is(Cu.getGlobalForObject(pr.then), window, "Xray global is correct");
     is(Cu.getGlobalForObject(pr.wrappedJSObject.then), iwin, "Underlying global is correct");
   }
 
   function testArrayBuffer() {
-    testXray('ArrayBuffer', new iwin.ArrayBuffer(0), new iwin.ArrayBuffer(12));
+    let constructors = ['ArrayBuffer'];
 
-    var t = new iwin.ArrayBuffer(12);
-    is(t.byteLength, 12, "ArrayBuffer byteLength is correct");
-    is(t.slice(4).byteLength, 8, "ArrayBuffer byteLength is correct after slicing");
-    is(Cu.getGlobalForObject(t.slice(4)), iwin, "Slice results lives in the target compartment");
-    is(Object.getPrototypeOf(t.slice(4)), iwin.ArrayBuffer.prototype, "Slice results proto lives in target compartment")
-    is(ArrayBuffer.slice(t, 4).byteLength, 8, "ArrayBuffer.slice (deprecated) works");
+    if (!isReleaseOrBeta) {
+      constructors.push('SharedArrayBuffer');
+      // If this fails enable the test a bit below.
+      is(typeof SharedArrayBuffer.prototype.slice, "undefined", "SharedArrayBuffer doesn't have slice");
+    }
+
+    for (const c of constructors) {
+      testXray(c, new iwin[c](0), new iwin[c](12));
+
+      var t = new iwin[c](12);
+      is(t.byteLength, 12, `${c} byteLength is correct`);
 
-    var i32Array = new Int32Array(t);
-    // i32Array is going to be created in the buffer's target compartment,
-    // but usually this is unobservable, because the proto is set to
-    // the current compartment's prototype.
-    // However Xrays ignore the object's proto and claim its proto is
-    // the default proto for that class in the relevant compartment,
-    // so see through this proto hack.
-    todo_is(Object.getPrototypeOf(i32Array), Int32Array.prototype, "Int32Array has correct proto");
-    is(i32Array.length, 3, "Int32Array created from Xray ArrayBuffer has the correct length");
-    is(i32Array.buffer, t, "Int32Array has the correct buffer that we passed in");
+      if (c === 'ArrayBuffer') {
+        is(t.slice(4).byteLength, 8, `${c} byteLength is correct after slicing`);
+        is(Cu.getGlobalForObject(t.slice(4)), iwin, "Slice results lives in the target compartment");
+        is(Object.getPrototypeOf(t.slice(4)), iwin[c].prototype, "Slice results proto lives in target compartment")
+        is(ArrayBuffer.slice(t, 4).byteLength, 8, `${c}.slice (deprecated) works`);
+      }
 
-    i32Array = new iwin.Int32Array(t);
-    is(Object.getPrototypeOf(i32Array), iwin.Int32Array.prototype, "Xray Int32Array has correct proto");
-    is(i32Array.length, 3, "Xray Int32Array created from Xray ArrayBuffer has the correct length");
-    is(i32Array.buffer, t, "Xray Int32Array has the correct buffer that we passed in");
+      var i32Array = new Int32Array(t);
+      // i32Array is going to be created in the buffer's target compartment,
+      // but usually this is unobservable, because the proto is set to
+      // the current compartment's prototype.
+      // However Xrays ignore the object's proto and claim its proto is
+      // the default proto for that class in the relevant compartment,
+      // so see through this proto hack.
+      todo_is(Object.getPrototypeOf(i32Array), Int32Array.prototype, "Int32Array has correct proto");
+      is(i32Array.length, 3, `Int32Array created from Xray ${c} has the correct length`);
+      is(i32Array.buffer, t, "Int32Array has the correct buffer that we passed in");
 
-    t = (new iwin.Int32Array(2)).buffer;
-    is(t.byteLength, 8, "Can access ArrayBuffer returned by buffer property");
+      i32Array = new iwin.Int32Array(t);
+      is(Object.getPrototypeOf(i32Array), iwin.Int32Array.prototype, "Xray Int32Array has correct proto");
+      is(i32Array.length, 3, `Xray Int32Array created from Xray ${c} has the correct length`);
+      is(i32Array.buffer, t, "Xray Int32Array has the correct buffer that we passed in");
+
+      t = (new iwin.Int32Array(2)).buffer;
+      is(t.byteLength, 8, `Can access ${c} returned by buffer property`);
+    }
   }
 
   ]]>
   </script>
   <iframe id="ifr" onload="go();" src="http://example.org/tests/js/xpconnect/tests/mochitest/file_empty.html" />
 </window>
--- a/js/xpconnect/wrappers/XrayWrapper.cpp
+++ b/js/xpconnect/wrappers/XrayWrapper.cpp
@@ -83,16 +83,17 @@ IsJSXraySupported(JSProtoKey key)
       case JSProto_Object:
       case JSProto_Array:
       case JSProto_Function:
       case JSProto_TypedArray:
       case JSProto_SavedFrame:
       case JSProto_RegExp:
       case JSProto_Promise:
       case JSProto_ArrayBuffer:
+      case JSProto_SharedArrayBuffer:
         return true;
       default:
         return false;
     }
 }
 
 XrayType
 GetXrayType(JSObject* obj)
--- a/layout/base/nsPresShell.cpp
+++ b/layout/base/nsPresShell.cpp
@@ -2976,17 +2976,17 @@ PresShell::CreateReferenceRenderingConte
   nsDeviceContext* devCtx = mPresContext->DeviceContext();
   RefPtr<gfxContext> rc;
   if (mPresContext->IsScreen()) {
     rc = gfxContext::CreateOrNull(gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget());
   } else {
     // We assume the devCtx has positive width and height for this call.
     // However, width and height, may be outside of the reasonable range
     // so rc may still be null.
-    rc = devCtx->CreateRenderingContext();
+    rc = devCtx->CreateReferenceRenderingContext();
   }
 
   return rc ? rc.forget() : nullptr;
 }
 
 nsresult
 PresShell::GoToAnchor(const nsAString& aAnchorName, bool aScroll,
                       uint32_t aAdditionalScrollFlags)
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -75,16 +75,17 @@ using namespace mozilla::widget;
 using namespace mozilla::ipc;
 using namespace mozilla::layout;
 
 static mozilla::LazyLogModule sRefreshDriverLog("nsRefreshDriver");
 #define LOG(...) MOZ_LOG(sRefreshDriverLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
 
 #define DEFAULT_THROTTLED_FRAME_RATE 1
 #define DEFAULT_RECOMPUTE_VISIBILITY_INTERVAL_MS 1000
+#define DEFAULT_NOTIFY_INTERSECTION_OBSERVERS_INTERVAL_MS 100
 // after 10 minutes, stop firing off inactive timers
 #define DEFAULT_INACTIVE_TIMER_DISABLE_SECONDS 600
 
 // The number of seconds spent skipping frames because we are waiting for the compositor
 // before logging.
 #if defined(MOZ_ASAN)
 # define REFRESH_WAIT_WARNING 5
 #elif defined(DEBUG) && !defined(MOZ_VALGRIND)
@@ -1036,16 +1037,27 @@ nsRefreshDriver::GetMinRecomputeVisibili
   int32_t interval =
     Preferences::GetInt("layout.visibility.min-recompute-interval-ms", -1);
   if (interval <= 0) {
     interval = DEFAULT_RECOMPUTE_VISIBILITY_INTERVAL_MS;
   }
   return TimeDuration::FromMilliseconds(interval);
 }
 
+/* static */ mozilla::TimeDuration
+nsRefreshDriver::GetMinNotifyIntersectionObserversInterval()
+{
+  int32_t interval =
+    Preferences::GetInt("layout.visibility.min-notify-intersection-observers-interval-ms", -1);
+  if (interval <= 0) {
+    interval = DEFAULT_NOTIFY_INTERSECTION_OBSERVERS_INTERVAL_MS;
+  }
+  return TimeDuration::FromMilliseconds(interval);
+}
+
 double
 nsRefreshDriver::GetRefreshTimerInterval() const
 {
   return mThrottled ? GetThrottledTimerInterval() : GetRegularTimerInterval();
 }
 
 RefreshDriverTimer*
 nsRefreshDriver::ChooseTimer() const
@@ -1078,16 +1090,18 @@ nsRefreshDriver::nsRefreshDriver(nsPresC
     mPresContext(aPresContext),
     mRootRefresh(nullptr),
     mPendingTransaction(0),
     mCompletedTransaction(0),
     mFreezeCount(0),
     mThrottledFrameRequestInterval(TimeDuration::FromMilliseconds(
                                      GetThrottledTimerInterval())),
     mMinRecomputeVisibilityInterval(GetMinRecomputeVisibilityInterval()),
+    mMinNotifyIntersectionObserversInterval(
+      GetMinNotifyIntersectionObserversInterval()),
     mThrottled(false),
     mNeedToRecomputeVisibility(false),
     mTestControllingRefreshes(false),
     mViewManagerFlushIsPending(false),
     mRequestedHighPrecision(false),
     mInRefresh(false),
     mWaitingForTransaction(false),
     mSkippedPaints(false),
@@ -1098,16 +1112,17 @@ nsRefreshDriver::nsRefreshDriver(nsPresC
   MOZ_ASSERT(mPresContext,
              "Need a pres context to tell us to call Disconnect() later "
              "and decrement sRefreshDriverCount.");
   mMostRecentRefreshEpochTime = JS_Now();
   mMostRecentRefresh = TimeStamp::Now();
   mMostRecentTick = mMostRecentRefresh;
   mNextThrottledFrameRequestTick = mMostRecentTick;
   mNextRecomputeVisibilityTick = mMostRecentTick;
+  mNextNotifyIntersectionObserversTick = mMostRecentTick;
 
   ++sRefreshDriverCount;
 }
 
 nsRefreshDriver::~nsRefreshDriver()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(ObserverCount() == 0,
@@ -1888,16 +1903,32 @@ nsRefreshDriver::Tick(int64_t aNowEpoch,
       aNowTime >= mNextRecomputeVisibilityTick &&
       !presShell->IsPaintingSuppressed()) {
     mNextRecomputeVisibilityTick = aNowTime + mMinRecomputeVisibilityInterval;
     mNeedToRecomputeVisibility = false;
 
     presShell->ScheduleAppro