merge mozilla-inbound to mozilla-central. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Wed, 18 Oct 2017 11:48:34 +0200
changeset 437616 a29052590fc6538b89641e690d11731ca8e78120
parent 437551 8b57edba9837c1f18d48470b8e2a940f5d0111ce (current diff)
parent 437615 f605961878cc80936b242290ef1674e82dac97fe (diff)
child 437617 d3af7a8818dae4f8e29d13b1d90e9abd6cd2e0f4
child 437652 0b8e3bba27dfc350fb911e68febd670dc1c2dd93
child 437726 a8a1e8cc1980498ea030bd9285570236c4d95dc2
push id8114
push userjlorenzo@mozilla.com
push dateThu, 02 Nov 2017 16:33:21 +0000
treeherdermozilla-beta@73e0d89a540f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge, merge
milestone58.0a1
first release with
nightly linux32
a29052590fc6 / 58.0a1 / 20171018100140 / files
nightly linux64
a29052590fc6 / 58.0a1 / 20171018100140 / files
nightly mac
a29052590fc6 / 58.0a1 / 20171018100140 / files
nightly win32
a29052590fc6 / 58.0a1 / 20171018100140 / files
nightly win64
a29052590fc6 / 58.0a1 / 20171018100140 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge mozilla-inbound to mozilla-central. r=merge a=merge MozReview-Commit-ID: 790IXj5MZ4f
config/external/ffi/Makefile.in
dom/file/tests/fileutils.js
dom/file/tests/test_fileapi.html
dom/file/tests/test_fileapi_slice.html
dom/quota/test/unit/test_version3_0upgrade.js
dom/quota/test/unit/version3_0upgrade_profile.zip
dom/workers/test/fileapi_chromeScript.js
dom/workers/test/test_fileReader.html
dom/workers/test/worker_fileReader.js
layout/generic/crashtests/crashtests.list
layout/reftests/bugs/reftest.list
testing/mochitest/mochitest_options.py
testing/web-platform/meta/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html.ini
--- a/accessible/html/HTMLTableAccessible.cpp
+++ b/accessible/html/HTMLTableAccessible.cpp
@@ -170,33 +170,27 @@ HTMLTableCellAccessible::Table() const
   }
 
   return nullptr;
 }
 
 uint32_t
 HTMLTableCellAccessible::ColIdx() const
 {
-  nsITableCellLayout* cellLayout = GetCellLayout();
-  NS_ENSURE_TRUE(cellLayout, 0);
-
-  int32_t colIdx = 0;
-  cellLayout->GetColIndex(colIdx);
-  return colIdx > 0 ? static_cast<uint32_t>(colIdx) : 0;
+  nsTableCellFrame* cellFrame = GetCellFrame();
+  NS_ENSURE_TRUE(cellFrame, 0);
+  return cellFrame->ColIndex();
 }
 
 uint32_t
 HTMLTableCellAccessible::RowIdx() const
 {
-  nsITableCellLayout* cellLayout = GetCellLayout();
-  NS_ENSURE_TRUE(cellLayout, 0);
-
-  int32_t rowIdx = 0;
-  cellLayout->GetRowIndex(rowIdx);
-  return rowIdx > 0 ? static_cast<uint32_t>(rowIdx) : 0;
+  nsTableCellFrame* cellFrame = GetCellFrame();
+  NS_ENSURE_TRUE(cellFrame, 0);
+  return cellFrame->RowIndex();
 }
 
 uint32_t
 HTMLTableCellAccessible::ColExtent() const
 {
   int32_t rowIdx = -1, colIdx = -1;
   GetCellIndexes(rowIdx, colIdx);
 
@@ -280,16 +274,22 @@ HTMLTableCellAccessible::Selected()
 // HTMLTableCellAccessible: protected implementation
 
 nsITableCellLayout*
 HTMLTableCellAccessible::GetCellLayout() const
 {
   return do_QueryFrame(mContent->GetPrimaryFrame());
 }
 
+nsTableCellFrame*
+HTMLTableCellAccessible::GetCellFrame() const
+{
+  return do_QueryFrame(mContent->GetPrimaryFrame());
+}
+
 nsresult
 HTMLTableCellAccessible::GetCellIndexes(int32_t& aRowIdx, int32_t& aColIdx) const
 {
   nsITableCellLayout *cellLayout = GetCellLayout();
   NS_ENSURE_STATE(cellLayout);
 
   return cellLayout->GetCellIndexes(aRowIdx, aColIdx);
 }
@@ -515,21 +515,19 @@ HTMLTableAccessible::SelectedCellCount()
 
   uint32_t count = 0, rowCount = RowCount(), colCount = ColCount();
   for (uint32_t rowIdx = 0; rowIdx < rowCount; rowIdx++) {
     for (uint32_t colIdx = 0; colIdx < colCount; colIdx++) {
       nsTableCellFrame* cellFrame = tableFrame->GetCellFrameAt(rowIdx, colIdx);
       if (!cellFrame || !cellFrame->IsSelected())
         continue;
 
-      int32_t startRow = -1, startCol = -1;
-      cellFrame->GetRowIndex(startRow);
-      cellFrame->GetColIndex(startCol);
-      if (startRow >= 0 && (uint32_t)startRow == rowIdx &&
-          startCol >= 0 && (uint32_t)startCol == colIdx)
+      uint32_t startRow = cellFrame->RowIndex();
+      uint32_t startCol = cellFrame->ColIndex();
+      if (startRow == rowIdx && startCol == colIdx)
         count++;
     }
   }
 
   return count;
 }
 
 uint32_t
@@ -565,21 +563,19 @@ HTMLTableAccessible::SelectedCells(nsTAr
 
   uint32_t rowCount = RowCount(), colCount = ColCount();
   for (uint32_t rowIdx = 0; rowIdx < rowCount; rowIdx++) {
     for (uint32_t colIdx = 0; colIdx < colCount; colIdx++) {
       nsTableCellFrame* cellFrame = tableFrame->GetCellFrameAt(rowIdx, colIdx);
       if (!cellFrame || !cellFrame->IsSelected())
         continue;
 
-      int32_t startCol = -1, startRow = -1;
-      cellFrame->GetRowIndex(startRow);
-      cellFrame->GetColIndex(startCol);
-      if ((startRow >= 0 && (uint32_t)startRow != rowIdx) ||
-          (startCol >= 0 && (uint32_t)startCol != colIdx))
+      uint32_t startRow = cellFrame->RowIndex();
+      uint32_t startCol = cellFrame->ColIndex();
+      if (startRow != rowIdx || startCol != colIdx)
         continue;
 
       Accessible* cell = mDoc->GetAccessible(cellFrame->GetContent());
         aCells->AppendElement(cell);
     }
   }
 }
 
@@ -592,21 +588,19 @@ HTMLTableAccessible::SelectedCellIndices
 
   uint32_t rowCount = RowCount(), colCount = ColCount();
   for (uint32_t rowIdx = 0; rowIdx < rowCount; rowIdx++) {
     for (uint32_t colIdx = 0; colIdx < colCount; colIdx++) {
       nsTableCellFrame* cellFrame = tableFrame->GetCellFrameAt(rowIdx, colIdx);
       if (!cellFrame || !cellFrame->IsSelected())
         continue;
 
-      int32_t startRow = -1, startCol = -1;
-      cellFrame->GetColIndex(startCol);
-      cellFrame->GetRowIndex(startRow);
-      if (startRow >= 0 && (uint32_t)startRow == rowIdx &&
-          startCol >= 0 && (uint32_t)startCol == colIdx)
+      uint32_t startCol = cellFrame->ColIndex();
+      uint32_t startRow = cellFrame->RowIndex();
+      if (startRow == rowIdx && startCol == colIdx)
         aCells->AppendElement(CellIndexAt(rowIdx, colIdx));
     }
   }
 }
 
 void
 HTMLTableAccessible::SelectedColIndices(nsTArray<uint32_t>* aCols)
 {
--- a/accessible/html/HTMLTableAccessible.h
+++ b/accessible/html/HTMLTableAccessible.h
@@ -6,16 +6,17 @@
 #ifndef mozilla_a11y_HTMLTableAccessible_h__
 #define mozilla_a11y_HTMLTableAccessible_h__
 
 #include "HyperTextAccessibleWrap.h"
 #include "TableAccessible.h"
 #include "TableCellAccessible.h"
 
 class nsITableCellLayout;
+class nsTableCellFrame;
 
 namespace mozilla {
 namespace a11y {
 
 /**
  * HTML table cell accessible (html:td).
  */
 class HTMLTableCellAccessible : public HyperTextAccessibleWrap,
@@ -49,16 +50,21 @@ protected:
   virtual ~HTMLTableCellAccessible() {}
 
   /**
    * Return nsITableCellLayout of the table cell frame.
    */
   nsITableCellLayout* GetCellLayout() const;
 
   /**
+   * Return the table cell frame.
+   */
+  nsTableCellFrame* GetCellFrame() const;
+
+  /**
    * Return row and column indices of the cell.
    */
   nsresult GetCellIndexes(int32_t& aRowIdx, int32_t& aColIdx) const;
 };
 
 
 /**
  * HTML table row/column header accessible (html:th or html:td@scope).
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -103,16 +103,17 @@ function CustomizeMode(aWindow) {
   this.areas = new Set();
 
   // There are two palettes - there's the palette that can be overlayed with
   // toolbar items in browser.xul. This is invisible, and never seen by the
   // user. Then there's the visible palette, which gets populated and displayed
   // to the user when in customizing mode.
   this.visiblePalette = this.document.getElementById(kPaletteId);
   this.paletteEmptyNotice = this.document.getElementById("customization-empty");
+  this.pongArena = this.document.getElementById("customization-pong-arena");
   if (Services.prefs.getCharPref("general.skins.selectedSkin") != "classic/1.0") {
     let lwthemeButton = this.document.getElementById("customization-lwtheme-button");
     lwthemeButton.setAttribute("hidden", "true");
   }
   if (AppConstants.CAN_DRAW_IN_TITLEBAR) {
     this._updateTitlebarCheckbox();
     this._updateDragSpaceCheckbox();
     Services.prefs.addObserver(kDrawInTitlebarPref, this);
@@ -403,19 +404,17 @@ CustomizeMode.prototype = {
 
     CustomizableUI.removeListener(this);
 
     this.document.removeEventListener("keypress", this);
 
     let window = this.window;
     let document = this.document;
 
-    // Hide the palette before starting the transition for increased perf.
-    this.visiblePalette.hidden = true;
-    this.visiblePalette.removeAttribute("showing");
+    this.togglePong(false);
     this.paletteEmptyNotice.hidden = true;
 
     // Disable the reset and undo reset buttons while transitioning:
     let resetButton = this.document.getElementById("customization-reset-button");
     let undoResetButton = this.document.getElementById("customization-undo-reset-button");
     undoResetButton.hidden = resetButton.disabled = true;
 
     this._transitioning = true;
@@ -1533,16 +1532,25 @@ CustomizeMode.prototype = {
       this._updateEmptyPaletteNotice();
     }
     CustomizableUI.dispatchToolboxEvent("customizationchange");
   },
 
   _updateEmptyPaletteNotice() {
     let paletteItems = this.visiblePalette.getElementsByTagName("toolbarpaletteitem");
     this.paletteEmptyNotice.hidden = !!paletteItems.length;
+    let readyPlayerOne = this.document.getElementById("ready-player-one");
+
+    if (paletteItems.length == 1 &&
+        paletteItems[0].id.includes("wrapper-customizableui-special-spring")) {
+      readyPlayerOne.hidden = false;
+    } else {
+      this.togglePong(false);
+      readyPlayerOne.hidden = true;
+    }
   },
 
   _updateResetButton() {
     let btn = this.document.getElementById("customization-reset-button");
     btn.disabled = CustomizableUI.inDefaultState;
   },
 
   _updateUndoResetButton() {
@@ -2515,16 +2523,225 @@ CustomizeMode.prototype = {
   },
 
   onDownloadsAutoHideChange(event) {
     let checkbox = event.target.ownerDocument.getElementById(kDownloadAutohideCheckboxId);
     Services.prefs.setBoolPref(kDownloadAutoHidePref, checkbox.checked);
     // Ensure we move the button (back) after the user leaves customize mode.
     event.view.gCustomizeMode._moveDownloadsButtonToNavBar = checkbox.checked;
   },
+
+  togglePong(enabled) {
+    // It's possible we're toggling for a reason other than hitting
+    // the button (we might be exiting, for example), so make sure that
+    // the state and checkbox are in sync.
+    let readyPlayerOne = this.document.getElementById("ready-player-one");
+    readyPlayerOne.checked = enabled;
+
+    if (enabled) {
+      this.visiblePalette.setAttribute("whimsypong", "true");
+      this.pongArena.hidden = false;
+      if (!this.uninitWhimsy) {
+        this.uninitWhimsy = this.whimsypong();
+      }
+    } else {
+      this.visiblePalette.removeAttribute("whimsypong");
+      if (this.uninitWhimsy) {
+        this.uninitWhimsy();
+        this.uninitWhimsy = null;
+      }
+      this.pongArena.hidden = true;
+    }
+  },
+
+  whimsypong() {
+    function update() {
+      updateBall();
+      updatePlayers();
+    }
+
+    function updateBall() {
+      if (ball[1] <= 0 || ball[1] >= gameSide) {
+        if ((ball[1] <= 0 && (ball[0] < p1 || ball[0] > p1 + paddleWidth)) ||
+            (ball[1] >= gameSide && (ball[0] < p2 || ball[0] > p2 + paddleWidth))) {
+          updateScore(ball[1] <= 0 ? 0 : 1);
+        } else {
+          if ((ball[1] <= 0 && (ball[0] - p1 < paddleEdge || p1 + paddleWidth - ball[0] < paddleEdge)) ||
+              (ball[1] >= gameSide && (ball[0] - p2 < paddleEdge || p2 + paddleWidth - ball[0] < paddleEdge))) {
+            ballDxDy[0] *= Math.random() + 1.3;
+            ballDxDy[0] = Math.max(Math.min(ballDxDy[0], 6), -6);
+            if (Math.abs(ballDxDy[0]) == 6) {
+              ballDxDy[0] += Math.sign(ballDxDy[0]) * Math.random();
+            }
+          } else {
+            ballDxDy[0] /= 1.1;
+          }
+          ballDxDy[1] *= -1;
+          ball[1] = ball[1] <= 0 ? 0 : gameSide;
+        }
+      }
+      ball = [Math.max(Math.min(ball[0] + ballDxDy[0], gameSide), 0),
+              Math.max(Math.min(ball[1] + ballDxDy[1], gameSide), 0)];
+      if (ball[0] <= 0 || ball[0] >= gameSide) {
+        ballDxDy[0] *= -1;
+      }
+    }
+
+    function updatePlayers() {
+      if (keydown) {
+        p1 += (keydown == 37 ? -1 : 1) * 10 * keydownAdj;
+      }
+
+      let sign = Math.sign(ballDxDy[0]);
+      if ((sign > 0 && ball[0] > p2 + paddleWidth / 2) ||
+          (sign < 0 && ball[0] < p2 + paddleWidth / 2)) {
+        p2 += sign * 3;
+      } else if ((sign > 0 && ball[0] > p2 + paddleWidth / 1.1) ||
+                 (sign < 0 && ball[0] < p2 + paddleWidth / 1.1)) {
+        p2 += sign * 9;
+      }
+
+      if (score >= winScore) {
+        p1 = ball[0];
+        p2 = ball[0];
+      }
+      p1 = Math.max(Math.min(p1, gameSide - paddleWidth), 0);
+      p2 = Math.max(Math.min(p2, gameSide - paddleWidth), 0);
+    }
+
+    function updateScore(adj) {
+      if (adj) {
+        score += adj;
+      } else if (--lives == 0) {
+        quit = true;
+      }
+      ball = ballDef.slice();
+      ballDxDy = ballDxDyDef.slice();
+      ballDxDy[1] *= score / winScore + 1;
+    }
+
+    function draw() {
+      elements.player1.style.transform = "translate(" + p1 + "px, -37px)";
+      elements.player2.style.transform = "translate(" + p2 + "px, 300px)";
+      elements.ball.style.transform = "translate(" + ball[0] + "px, " + ball[1] + "px)";
+      elements.score.textContent = score;
+      elements.lives.setAttribute("lives", lives);
+      if (score >= winScore) {
+        let arena = elements.arena;
+        let image = "url(chrome://browser/skin/customizableui/whimsy.png)";
+        let position = `${ball[0] - 10}px ${ball[1] - 10}px`;
+        let repeat = "no-repeat";
+        let size = "20px";
+        if (arena.style.backgroundImage) {
+          if (arena.style.backgroundImage.split(",").length >= 160) {
+            quit = true;
+          }
+
+          image += ", " + arena.style.backgroundImage;
+          position += ", " + arena.style.backgroundPosition;
+          repeat += ", " + arena.style.backgroundRepeat;
+          size += ", " + arena.style.backgroundSize;
+        }
+        arena.style.backgroundImage = image;
+        arena.style.backgroundPosition = position;
+        arena.style.backgroundRepeat = repeat;
+        arena.style.backgroundSize = size;
+      }
+    }
+
+    function onkeydown(event) {
+      if (event.which == 37 /* left */ ||
+          event.which == 39 /* right */) {
+        keydown = event.which;
+        keydownAdj *= 1.05;
+      }
+    }
+
+    function onkeyup(event) {
+      if (event.which == 37 || event.which == 39) {
+        keydownAdj = 1;
+        keydown = 0;
+      }
+    }
+
+    function uninit() {
+      document.removeEventListener("keydown", onkeydown);
+      document.removeEventListener("keyup", onkeyup);
+      if (rAFHandle) {
+        window.cancelAnimationFrame(rAFHandle);
+      }
+      let arena = elements.arena;
+      while (arena.firstChild) {
+        arena.firstChild.remove();
+      }
+      arena.removeAttribute("score");
+      arena.style.removeProperty("background-image");
+      arena.style.removeProperty("background-position");
+      arena.style.removeProperty("background-repeat");
+      arena.style.removeProperty("background-size");
+      elements = null;
+      document = null;
+      quit = true;
+    }
+
+    if (this.uninitWhimsy) {
+      return this.uninitWhimsy;
+    }
+
+    let ballDef = [10, 10];
+    let ball = [10, 10];
+    let ballDxDyDef = [2, 2];
+    let ballDxDy = [2, 2];
+    let score = 0;
+    let p1 = 0;
+    let p2 = 10;
+    let gameSide = 300;
+    let paddleEdge = 30;
+    let paddleWidth = 84;
+    let keydownAdj = 1;
+    let keydown = 0;
+    let lives = 5;
+    let winScore = 11;
+    let quit = false;
+    let document = this.document;
+    let rAFHandle = 0;
+    let elements = {
+      arena: document.getElementById("customization-pong-arena")
+    };
+
+    document.addEventListener("keydown", onkeydown);
+    document.addEventListener("keyup", onkeyup);
+
+    for (let id of ["player1", "player2", "ball", "score", "lives"]) {
+      let el = document.createElement("box");
+      el.id = id;
+      elements[id] = elements.arena.appendChild(el);
+    }
+
+    let spacer = this.visiblePalette.querySelector("toolbarpaletteitem");
+    for (let player of ["#player1", "#player2"]) {
+      let val = "-moz-element(#" + spacer.id + ") no-repeat";
+      elements.arena.querySelector(player).style.background = val;
+    }
+
+    let window = this.window;
+    rAFHandle = window.requestAnimationFrame(function animate() {
+      update();
+      draw();
+      if (quit) {
+        elements.score.textContent = score;
+        elements.lives && elements.lives.setAttribute("lives", lives);
+        elements.arena.setAttribute("score", score);
+      } else {
+        rAFHandle = window.requestAnimationFrame(animate);
+      }
+    });
+
+    return uninit;
+  },
 };
 
 function __dumpDragData(aEvent, caller) {
   if (!gDebug) {
     return;
   }
   let str = "Dumping drag data (" + (caller ? caller + " in " : "") + "CustomizeMode.jsm) {\n";
   str += "  type: " + aEvent.type + "\n";
--- a/browser/components/customizableui/content/customizeMode.inc.xul
+++ b/browser/components/customizableui/content/customizeMode.inc.xul
@@ -13,16 +13,17 @@
         <label onclick="BrowserOpenAddonsMgr('addons://discover/');"
                onkeypress="BrowserOpenAddonsMgr('addons://discover/');"
                id="customization-more-tools"
                class="text-link">
           &customizeMode.menuAndToolbars.emptyLink;
         </label>
       </hbox>
       <vbox id="customization-palette" class="customization-palette" hidden="true"/>
+      <vbox id="customization-pong-arena" hidden="true"/>
       <spacer id="customization-spacer"/>
     </box>
     <vbox id="customization-panel-container">
       <vbox id="customization-panelWrapper">
         <box class="panel-arrowbox">
           <image class="panel-arrow" side="top"/>
         </box>
         <box class="panel-arrowcontent" side="top" flex="1">
@@ -122,16 +123,22 @@
         <checkbox id="customization-uidensity-autotouchmode-checkbox"
                   hidden="true"
                   label="&customizeMode.uidensity.autoTouchMode.checkbox.label;"
                   oncommand="gCustomizeMode.updateAutoTouchMode(this.checked)"/>
 #endif
       </panel>
     </button>
 
+    <button id="ready-player-one"
+            type="checkbox"
+            class="customizationmode-button"
+            oncommand="gCustomizeMode.togglePong(this.checked);"
+            hidden="true"/>
+
     <spacer id="customization-footer-spacer"/>
     <button id="customization-undo-reset-button"
             class="customizationmode-button"
             hidden="true"
             oncommand="gCustomizeMode.undoReset();"
             label="&undoCmd.label;"/>
     <button id="customization-reset-button"
             oncommand="gCustomizeMode.reset();"
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -40,17 +40,17 @@ const MAX_CONCURRENT_TAB_RESTORES = 3;
 const SCREEN_EDGE_SLOP = 8;
 
 // global notifications observed
 const OBSERVING = [
   "browser-window-before-show", "domwindowclosed",
   "quit-application-granted", "browser-lastwindow-close-granted",
   "quit-application", "browser:purge-session-history",
   "browser:purge-domain-data",
-  "idle-daily",
+  "idle-daily", "clear-origin-attributes-data"
 ];
 
 // XUL Window properties to (re)store
 // Restored in restoreDimensions()
 const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"];
 
 // Hideable window features to (re)store
 // Restored in restoreWindowFeatures()
@@ -794,16 +794,23 @@ var SessionStoreInternal = {
       case "nsPref:changed": // catch pref changes
         this.onPrefChange(aData);
         this._notifyOfClosedObjectsChange();
         break;
       case "idle-daily":
         this.onIdleDaily();
         this._notifyOfClosedObjectsChange();
         break;
+      case "clear-origin-attributes-data":
+        let userContextId = 0;
+        try {
+          userContextId = JSON.parse(aData).userContextId;
+        } catch (e) {}
+        if (userContextId)
+          this._forgetTabsWithUserContextId(userContextId);
     }
   },
 
   /**
    * This method handles incoming messages sent by the session store content
    * script via the Frame Message Manager or Parent Process Message Manager,
    * and thus enables communication with OOP tabs.
    */
@@ -2712,16 +2719,43 @@ var SessionStoreInternal = {
     if ("image" in tabData) {
       // Use the serialized contentPrincipal with the new icon load.
       let loadingPrincipal = Utils.deserializePrincipal(tabData.iconLoadingPrincipal);
       win.gBrowser.setIcon(tab, tabData.image, loadingPrincipal);
       TabStateCache.update(browser, { image: null, iconLoadingPrincipal: null });
     }
   },
 
+  // This method deletes all the closedTabs matching userContextId.
+  _forgetTabsWithUserContextId(userContextId) {
+    let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+    while (windowsEnum.hasMoreElements()) {
+      let window = windowsEnum.getNext();
+      let windowState = this._windows[window.__SSi];
+      if (windowState) {
+        // In order to remove the tabs in the correct order, we store the
+        // indexes, into an array, then we revert the array and remove closed
+        // data from the last one going backward.
+        let indexes = [];
+        windowState._closedTabs.forEach((closedTab, index) => {
+          if (closedTab.state.userContextId == userContextId) {
+            indexes.push(index);
+          }
+        });
+
+        for (let index of indexes.reverse()) {
+          this.removeClosedTabData(windowState._closedTabs, index);
+        }
+      }
+    }
+
+    // Notify of changes to closed objects.
+    this._notifyOfClosedObjectsChange();
+  },
+
   /**
    * Restores the session state stored in LastSession. This will attempt
    * to merge data into the current session. If a window was opened at startup
    * with pinned tab(s), then the remaining data from the previous session for
    * that window will be opened into that window. Otherwise new windows will
    * be opened.
    */
   restoreLastSession: function ssi_restoreLastSession() {
--- a/browser/themes/shared/customizableui/customizeMode.inc.css
+++ b/browser/themes/shared/customizableui/customizeMode.inc.css
@@ -116,17 +116,18 @@
 
 .customizationmode-button > .box-inherit > .button-menu-dropmarker {
   margin-inline-end: 0;
   padding-inline-end: 0;
   list-style-image: url(chrome://global/skin/icons/arrow-dropdown-16.svg);
 }
 
 .customizationmode-button:-moz-any(:focus,:active,:hover):not([disabled]),
-.customizationmode-button[open] {
+.customizationmode-button[open],
+.customizationmode-button[checked] {
   background-color: #e1e1e5;
 }
 
 #customization-done-button:-moz-any(:focus,:active,:hover):not([disabled]) {
   background-color: #0060df;
 }
 
 .customizationmode-button[disabled="true"] {
@@ -552,8 +553,94 @@ toolbarpaletteitem[place=toolbar] > tool
 #downloads-button-autohide-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 5px 12px;
 }
 
 #downloads-button-autohide-checkbox {
   margin: 0;
   padding: 0;
 }
+
+#ready-player-one {
+  /* Don't need HiDPI versions since the size used will be scaled down to 20x20. */
+  list-style-image: url("chrome://browser/skin/customizableui/whimsy.png");
+}
+
+#ready-player-one > .button-box > .button-icon {
+  width: 16px;
+  height: 16px;
+}
+
+#customization-palette[whimsypong] {
+  /* Keep the palette in the render tree but invisible
+     so -moz-element() will work. */
+  padding: 0;
+  min-height: 0;
+  max-height: 0;
+}
+
+#customization-palette[whimsypong] > toolbarpaletteitem > toolbarspring {
+  margin: 0 -7px;
+}
+
+#lives,
+#ball {
+  /* Don't need HiDPI versions since the size used will be scaled down to 20x20. */
+  background-image: url("chrome://browser/skin/customizableui/whimsy.png");
+  background-size: contain;
+  width: 20px;
+}
+
+#customization-pong-arena {
+  width: 300px;
+  height: 300px;
+  border-left: 1px solid currentColor;
+  border-right: 1px solid currentColor;
+  margin: 16px auto 0;
+}
+
+#ball {
+  margin-left: -10px;
+  margin-top: -10px;
+  height: 20px;
+}
+
+#player1,
+#player2 {
+  width: 84px;
+  height: calc(39px + 3em);
+  background-color: rgba(255,255,0,.5);
+}
+
+#player1,
+#player2,
+#ball,
+#score {
+  position: fixed;
+}
+
+#score {
+  transform: translateX(-4ch);
+}
+
+#lives {
+  transform: translate(-4ch, 1ch);
+}
+
+#lives[lives="5"] {
+  height: 100px;
+}
+
+#lives[lives="4"] {
+  height: 80px;
+}
+
+#lives[lives="3"] {
+  height: 60px;
+}
+
+#lives[lives="2"] {
+  height: 40px;
+}
+
+#lives[lives="1"] {
+  height: 20px;
+}
deleted file mode 100644
--- a/config/external/ffi/Makefile.in
+++ /dev/null
@@ -1,12 +0,0 @@
-# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
-# vim: set filetype=python:
-# 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/.
-
-# libffi's assembly files want to be pre-processed, so we still use the libffi
-# wrapper to combine the preprocessor and assembler stages.
-# Bug 1299959 is on file to find a better way to do this in moz.build.
-ifdef _MSC_VER
-AS = $(topsrcdir)/js/src/ctypes/libffi/msvcc.sh
-endif
--- a/config/external/ffi/moz.build
+++ b/config/external/ffi/moz.build
@@ -80,23 +80,42 @@ else:
             ASFLAGS += ['-no-integrated-as']
     elif CONFIG['FFI_TARGET'] == 'AARCH64':
         ffi_srcs = ('sysv.S', 'ffi.c')
     elif CONFIG['FFI_TARGET'] == 'X86':
         ffi_srcs = ('ffi.c', 'sysv.S', 'win32.S')
     elif CONFIG['FFI_TARGET'] == 'X86_64':
         ffi_srcs = ('ffi64.c', 'unix64.S', 'ffi.c', 'sysv.S')
     elif CONFIG['FFI_TARGET'] == 'X86_WIN32':
+        ffi_srcs = ['ffi.c']
         # MinGW Build for 32 bit
         if CONFIG['CC_TYPE'] == 'gcc':
             DEFINES['SYMBOL_UNDERSCORE'] = True
-        ffi_srcs = ('ffi.c', 'win32.S')
+            ffi_srcs += ['win32.S']
+        else:
+            # libffi asm needs to be preprocessed for MSVC
+            GENERATED_FILES += ['win32.asm']
+            asm = GENERATED_FILES['win32.asm']
+            asm.inputs = ['/js/src/ctypes/libffi/src/x86/win32.S']
+            asm.script = 'preprocess_libffi_asm.py'
+            asm.flags = ['$(DEFINES)', '$(LOCAL_INCLUDES)']
+            SOURCES += ['!win32.asm']
+            ASFLAGS += ['-safeseh']
     elif CONFIG['FFI_TARGET'] == 'X86_WIN64':
-        ffi_srcs = ('ffi.c', 'win64.S')
-        ASFLAGS += ['-m64']
+        ffi_srcs = ['ffi.c']
+        if CONFIG['CC_TYPE'] == 'gcc':
+            ffi_srcs += ['win64.S']
+        else:
+            # libffi asm needs to be preprocessed for MSVC
+            GENERATED_FILES += ['win64.asm']
+            asm = GENERATED_FILES['win64.asm']
+            asm.inputs = ['/js/src/ctypes/libffi/src/x86/win64.S']
+            asm.script = 'preprocess_libffi_asm.py'
+            asm.flags = ['$(DEFINES)', '$(LOCAL_INCLUDES)']
+            SOURCES += ['!win64.asm']
     elif CONFIG['FFI_TARGET'] == 'X86_DARWIN':
         DEFINES['FFI_MMAP_EXEC_WRIT'] = True
         if CONFIG['OS_TEST'] != 'x86_64':
             ffi_srcs = ('ffi.c', 'darwin.S', 'ffi64.c', 'darwin64.S',
                         'win32.S')
             DEFINES['SYMBOL_UNDERSCORE'] = True
         else:
             ffi_srcs = ('ffi.c', 'darwin.S', 'ffi64.c', 'darwin64.S')
new file mode 100644
--- /dev/null
+++ b/config/external/ffi/preprocess_libffi_asm.py
@@ -0,0 +1,26 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Souce Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distibuted with this
+# file, You can obtain one at http://mozilla.og/MPL/2.0/.
+
+import buildconfig
+import mozpack.path as mozpath
+import os
+import re
+import shlex
+import subprocess
+
+def main(output, input_asm, defines, includes):
+    defines = shlex.split(defines)
+    includes = shlex.split(includes)
+    # CPP uses -E which generates #line directives. -EP suppresses them.
+    cpp = buildconfig.substs['CPP'] + ['-EP']
+    input_asm = mozpath.relpath(input_asm, os.getcwd())
+    args = cpp + defines + includes + [input_asm]
+    print(' '.join(args))
+    preprocessed = subprocess.check_output(args)
+    r = re.compile('F[dpa][^ ]*')
+    for line in preprocessed.splitlines():
+        output.write(r.sub('', line))
+        output.write('\n')
--- a/dom/cache/DBSchema.cpp
+++ b/dom/cache/DBSchema.cpp
@@ -30,18 +30,54 @@
 #include "nsTArray.h"
 
 namespace mozilla {
 namespace dom {
 namespace cache {
 namespace db {
 const int32_t kFirstShippedSchemaVersion = 15;
 namespace {
+// ## Firefox 57 Cache API v25/v26/v27 Schema Hack Info
+// ### Overview
+// In Firefox 57 we introduced Cache API schema version 26 and Quota Manager
+// schema v3 to support tracking padding for opaque responses.  Unfortunately,
+// Firefox 57 is a big release that may potentially result in users downgrading
+// to Firefox 56 due to 57 retiring add-ons.  These schema changes have the
+// unfortunate side-effect of causing QuotaManager and all its clients to break
+// if the user downgrades to 56.  In order to avoid making a bad situation
+// worse, we're now retrofitting 57 so that Firefox 56 won't freak out.
+//
+// ### Implementation
+// We're introducing a new schema version 27 that uses an on-disk schema version
+// of v25.  We differentiate v25 from v27 by the presence of the column added
+// by v26.  This translates to:
+// - v25: on-disk schema=25, no "response_padding_size" column in table
+//   "entries".
+// - v26: on-disk schema=26, yes "response_padding_size" column in table
+//   "entries".
+// - v27: on-disk schema=25, yes "response_padding_size" column in table
+//   "entries".
+//
+// ### Fallout
+// Firefox 57 is happy because it sees schema 27 and everything is as it
+// expects.
+//
+// Firefox 56 non-DEBUG build is fine/happy, but DEBUG builds will not be.
+// - Our QuotaClient will invoke `NS_WARNING("Unknown Cache file found!");`
+//   at QuotaManager init time.  This is harmless but annoying and potentially
+//   misleading.
+// - The DEBUG-only Validate() call will error out whenever an attempt is made
+//   to open a DOM Cache database because it will notice the schema is broken
+//   and there is no attempt at recovery.
+//
+const int32_t kHackyDowngradeSchemaVersion = 25;
+const int32_t kHackyPaddingSizePresentVersion = 27;
+//
 // Update this whenever the DB schema is changed.
-const int32_t kLatestSchemaVersion = 26;
+const int32_t kLatestSchemaVersion = 27;
 // ---------
 // The following constants define the SQL schema.  These are defined in the
 // same order the SQL should be executed in CreateOrMigrateSchema().  They are
 // broken out as constants for convenient use in validation and migration.
 // ---------
 // The caches table is the single source of truth about what Cache
 // objects exist for the origin.  The contents of the Cache are stored
 // in the entries table that references back to caches.
@@ -351,16 +387,18 @@ static nsresult BindId(mozIStorageStatem
 static nsresult ExtractId(mozIStorageStatement* aState, uint32_t aPos,
                           nsID* aIdOut);
 static nsresult CreateAndBindKeyStatement(mozIStorageConnection* aConn,
                                           const char* aQueryFormat,
                                           const nsAString& aKey,
                                           mozIStorageStatement** aStateOut);
 static nsresult HashCString(nsICryptoHash* aCrypto, const nsACString& aIn,
                             nsACString& aOut);
+nsresult GetEffectiveSchemaVersion(mozIStorageConnection* aConn,
+                                   int32_t& schemaVersion);
 nsresult Validate(mozIStorageConnection* aConn);
 nsresult Migrate(mozIStorageConnection* aConn);
 } // namespace
 
 class MOZ_RAII AutoDisableForeignKeyChecking
 {
 public:
   explicit AutoDisableForeignKeyChecking(mozIStorageConnection* aConn)
@@ -407,17 +445,17 @@ private:
 
 nsresult
 CreateOrMigrateSchema(mozIStorageConnection* aConn)
 {
   MOZ_ASSERT(!NS_IsMainThread());
   MOZ_DIAGNOSTIC_ASSERT(aConn);
 
   int32_t schemaVersion;
-  nsresult rv = aConn->GetSchemaVersion(&schemaVersion);
+  nsresult rv = GetEffectiveSchemaVersion(aConn, schemaVersion);
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
   if (schemaVersion == kLatestSchemaVersion) {
     // We already have the correct schema version.  Validate it matches
     // our expected schema and then proceed.
     rv = Validate(aConn);
     if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
@@ -468,20 +506,20 @@ CreateOrMigrateSchema(mozIStorageConnect
     if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
     rv = aConn->ExecuteSimpleSQL(nsDependentCString(kTableResponseUrlList));
     if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
     rv = aConn->ExecuteSimpleSQL(nsDependentCString(kTableStorage));
     if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
-    rv = aConn->SetSchemaVersion(kLatestSchemaVersion);
+    rv = aConn->SetSchemaVersion(kHackyDowngradeSchemaVersion);
     if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
-    rv = aConn->GetSchemaVersion(&schemaVersion);
+    rv = GetEffectiveSchemaVersion(aConn, schemaVersion);
     if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
   }
 
   rv = Validate(aConn);
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
   rv = trans.Commit();
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
@@ -2458,16 +2496,54 @@ IncrementalVacuum(mozIStorageConnection*
   MOZ_ASSERT(freePages <= kMaxFreePages);
 #endif
 
   return NS_OK;
 }
 
 namespace {
 
+// Wrapper around mozIStorageConnection::GetSchemaVersion() that compensates
+// for hacky downgrade schema version tricks.  See the block comments for
+// kHackyDowngradeSchemaVersion and kHackyPaddingSizePresentVersion.
+nsresult
+GetEffectiveSchemaVersion(mozIStorageConnection* aConn,
+                          int32_t& schemaVersion)
+{
+  nsresult rv = aConn->GetSchemaVersion(&schemaVersion);
+  if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
+
+  if (schemaVersion == kHackyDowngradeSchemaVersion) {
+    // This is the special case.  Check for the existence of the
+    // "response_padding_size" colum in table "entries".
+    //
+    // (pragma_table_info is a table-valued function format variant of
+    // "PRAGMA table_info" supported since SQLite 3.16.0.  Firefox 53 shipped
+    // was the first release with this functionality, shipping 3.16.2.)
+    nsCOMPtr<mozIStorageStatement> stmt;
+    nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING(
+      "SELECT name FROM pragma_table_info('entries') WHERE "
+      "name = 'response_padding_size'"
+    ), getter_AddRefs(stmt));
+    if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
+
+    // If there are any result rows, then the column is present.
+    bool hasColumn = false;
+    rv = stmt->ExecuteStep(&hasColumn);
+    if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
+
+    if (hasColumn) {
+      schemaVersion = kHackyPaddingSizePresentVersion;
+    }
+  }
+
+  return NS_OK;
+}
+
+
 #ifdef DEBUG
 struct Expect
 {
   // Expect exact SQL
   Expect(const char* aName, const char* aType, const char* aSql)
     : mName(aName)
     , mType(aType)
     , mSql(aSql)
@@ -2487,17 +2563,17 @@ struct Expect
   const bool mIgnoreSql;
 };
 #endif
 
 nsresult
 Validate(mozIStorageConnection* aConn)
 {
   int32_t schemaVersion;
-  nsresult rv = aConn->GetSchemaVersion(&schemaVersion);
+  nsresult rv = GetEffectiveSchemaVersion(aConn, schemaVersion);
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
   if (NS_WARN_IF(schemaVersion != kLatestSchemaVersion)) {
     return NS_ERROR_FAILURE;
   }
 
 #ifdef DEBUG
   // This is the schema we expect the database at the latest version to
@@ -2593,29 +2669,31 @@ nsresult MigrateFrom17To18(mozIStorageCo
 nsresult MigrateFrom18To19(mozIStorageConnection* aConn, bool& aRewriteSchema);
 nsresult MigrateFrom19To20(mozIStorageConnection* aConn, bool& aRewriteSchema);
 nsresult MigrateFrom20To21(mozIStorageConnection* aConn, bool& aRewriteSchema);
 nsresult MigrateFrom21To22(mozIStorageConnection* aConn, bool& aRewriteSchema);
 nsresult MigrateFrom22To23(mozIStorageConnection* aConn, bool& aRewriteSchema);
 nsresult MigrateFrom23To24(mozIStorageConnection* aConn, bool& aRewriteSchema);
 nsresult MigrateFrom24To25(mozIStorageConnection* aConn, bool& aRewriteSchema);
 nsresult MigrateFrom25To26(mozIStorageConnection* aConn, bool& aRewriteSchema);
+nsresult MigrateFrom26To27(mozIStorageConnection* aConn, bool& aRewriteSchema);
 // Configure migration functions to run for the given starting version.
 Migration sMigrationList[] = {
   Migration(15, MigrateFrom15To16),
   Migration(16, MigrateFrom16To17),
   Migration(17, MigrateFrom17To18),
   Migration(18, MigrateFrom18To19),
   Migration(19, MigrateFrom19To20),
   Migration(20, MigrateFrom20To21),
   Migration(21, MigrateFrom21To22),
   Migration(22, MigrateFrom22To23),
   Migration(23, MigrateFrom23To24),
   Migration(24, MigrateFrom24To25),
   Migration(25, MigrateFrom25To26),
+  Migration(26, MigrateFrom26To27),
 };
 uint32_t sMigrationListLength = sizeof(sMigrationList) / sizeof(Migration);
 nsresult
 RewriteEntriesSchema(mozIStorageConnection* aConn)
 {
   nsresult rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
     "PRAGMA writable_schema = ON"
   ));
@@ -2644,17 +2722,17 @@ RewriteEntriesSchema(mozIStorageConnecti
 
 nsresult
 Migrate(mozIStorageConnection* aConn)
 {
   MOZ_ASSERT(!NS_IsMainThread());
   MOZ_DIAGNOSTIC_ASSERT(aConn);
 
   int32_t currentVersion = 0;
-  nsresult rv = aConn->GetSchemaVersion(&currentVersion);
+  nsresult rv = GetEffectiveSchemaVersion(aConn, currentVersion);
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
   bool rewriteSchema = false;
 
   while (currentVersion < kLatestSchemaVersion) {
     // Wiping old databases is handled in DBAction because it requires
     // making a whole new mozIStorageConnection.  Make sure we don't
     // accidentally get here for one of those old databases.
@@ -2670,17 +2748,17 @@ Migrate(mozIStorageConnection* aConn)
         }
         break;
       }
     }
 
 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
     int32_t lastVersion = currentVersion;
 #endif
-    rv = aConn->GetSchemaVersion(&currentVersion);
+    rv = GetEffectiveSchemaVersion(aConn, currentVersion);
     if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
     MOZ_DIAGNOSTIC_ASSERT(currentVersion > lastVersion);
   }
 
   // Don't release assert this since people do sometimes share profiles
   // across schema versions.  Our check in Validate() will catch it.
   MOZ_ASSERT(currentVersion == kLatestSchemaVersion);
 
@@ -3169,13 +3247,23 @@ nsresult MigrateFrom25To26(mozIStorageCo
   rv = aConn->SetSchemaVersion(26);
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
   aRewriteSchema = true;
 
   return rv;
 }
 
+nsresult MigrateFrom26To27(mozIStorageConnection* aConn, bool& aRewriteSchema)
+{
+  MOZ_ASSERT(!NS_IsMainThread());
+  MOZ_DIAGNOSTIC_ASSERT(aConn);
+
+  nsresult rv = aConn->SetSchemaVersion(kHackyDowngradeSchemaVersion);
+  if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
+  return rv;
+}
+
 } // anonymous namespace
 } // namespace db
 } // namespace cache
 } // namespace dom
 } // namespace mozilla
--- a/dom/cache/QuotaClient.cpp
+++ b/dom/cache/QuotaClient.cpp
@@ -296,17 +296,17 @@ public:
   {
     AssertIsOnBackgroundThread();
 
     // spins the event loop and synchronously shuts down all Managers
     Manager::ShutdownAll();
   }
 
   nsresult
-  UpgradeStorageFrom2_0To3_0(nsIFile* aDirectory) override
+  UpgradeStorageFrom2_0To2_1(nsIFile* aDirectory) override
   {
     AssertIsOnIOThread();
     MOZ_DIAGNOSTIC_ASSERT(aDirectory);
 
     MutexAutoLock lock(mDirPaddingFileMutex);
 
     nsresult rv = mozilla::dom::cache::LockedDirectoryPaddingInit(aDirectory);
     if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/common_blob.js
@@ -0,0 +1,293 @@
+const RANGE_1 = 1;
+const RANGE_2 = 2;
+
+function testBlob(file, contents, testName) {
+  // Load file using FileReader
+  return new Promise(resolve => {
+    let r = new FileReader();
+    r.onload = event => {
+      is(event.target.readyState, FileReader.DONE,
+         "[FileReader] readyState in test FileReader.readAsBinaryString of " + testName);
+      is(event.target.error, null,
+         "[FileReader] no error in test FileReader.readAsBinaryString of " + testName);
+      // Do not use |is(event.target.result, contents, "...");| that may output raw binary data.
+      is(event.target.result.length, contents.length,
+         "[FileReader] Length of result in test FileReader.readAsBinaryString of " + testName);
+      ok(event.target.result == contents,
+         "[FileReader] Content of result in test FileReader.readAsBinaryString of " + testName);
+      is(event.lengthComputable, true,
+         "[FileReader] lengthComputable in test FileReader.readAsBinaryString of " + testName);
+      is(event.loaded, contents.length,
+         "[FileReader] Loaded length in test FileReader.readAsBinaryString of " + testName);
+      is(event.total, contents.length,
+         "[FileReader] Total length in test FileReader.readAsBinaryString of " + testName);
+      resolve();
+    }
+    r.readAsBinaryString(file);
+  })
+
+  // Load file using URL.createObjectURL and XMLHttpRequest
+  .then(() => {
+    return new Promise(resolve => {
+      let xhr = new XMLHttpRequest();
+      xhr.open("GET", URL.createObjectURL(file));
+      xhr.onload = event => {
+        XHRLoadHandler(event, resolve, contents, "XMLHttpRequest load of " + testName);
+      };
+      xhr.overrideMimeType('text/plain; charset=x-user-defined');
+      xhr.send();
+    });
+  })
+
+  // Send file to server using FormData and XMLHttpRequest
+  .then(() => {
+    return new Promise(resolve => {
+      let xhr = new XMLHttpRequest();
+      xhr.onload = function(event) {
+        checkMPSubmission(JSON.parse(event.target.responseText),
+                          [{ name: "hello", value: "world"},
+                           { name: "myfile",
+                             value: contents,
+                             fileName: file.name || "blob",
+                             contentType: file.type || "application/octet-stream" }]);
+        resolve();
+      }
+      xhr.open("POST", "../../../dom/html/test/form_submit_server.sjs");
+
+      let fd = new FormData;
+      fd.append("hello", "world");
+      fd.append("myfile", file);
+
+      xhr.send(fd);
+    });
+  })
+
+  // Send file to server using plain XMLHttpRequest
+  .then(() => {
+    return new Promise(resolve => {
+      let xhr = new XMLHttpRequest();
+      xhr.open("POST", "../../../dom/xhr/tests/file_XHRSendData.sjs");
+
+      xhr.onload = function (event) {
+        is(event.target.getResponseHeader("Result-Content-Type"),
+           file.type ? file.type : null,
+           "request content-type in XMLHttpRequest send of " + testName);
+        is(event.target.getResponseHeader("Result-Content-Length"),
+           String(file.size),
+           "request content-length in XMLHttpRequest send of " + testName);
+      };
+
+      xhr.addEventListener("load", event => {
+        XHRLoadHandler(event, resolve, contents, "XMLHttpRequest send of " + testName);
+      });
+      xhr.overrideMimeType('text/plain; charset=x-user-defined');
+      xhr.send(file);
+    });
+  });
+}
+
+function testSlice(file, size, type, contents, fileType, range) {
+  is(file.type, type, fileType + " file is correct type");
+  is(file.size, size, fileType + " file is correct size");
+  ok(file instanceof File, fileType + " file is a File");
+  ok(file instanceof Blob, fileType + " file is also a Blob");
+
+  let slice = file.slice(0, size);
+  ok(slice instanceof Blob, fileType + " fullsize slice is a Blob");
+  ok(!(slice instanceof File), fileType + " fullsize slice is not a File");
+
+  slice = file.slice(0, 1234);
+  ok(slice instanceof Blob, fileType + " sized slice is a Blob");
+  ok(!(slice instanceof File), fileType + " sized slice is not a File");
+
+  slice = file.slice(0, size, "foo/bar");
+  is(slice.type, "foo/bar", fileType + " fullsize slice foo/bar type");
+
+  slice = file.slice(0, 5432, "foo/bar");
+  is(slice.type, "foo/bar", fileType + " sized slice foo/bar type");
+
+  is(slice.slice(0, 10).type, "", fileType + " slice-slice type");
+  is(slice.slice(0, 10).size, 10, fileType + " slice-slice size");
+  is(slice.slice(0, 10, "hello/world").type, "hello/world", fileType + " slice-slice hello/world type");
+  is(slice.slice(0, 10, "hello/world").size, 10, fileType + " slice-slice hello/world size");
+
+  // Start, end, expected size
+  var indexes_range_1 = [[0, size, size],
+                         [0, 1234, 1234],
+                         [size-500, size, 500],
+                         [size-500, size+500, 500],
+                         [size+500, size+1500, 0],
+                         [0, 0, 0],
+                         [1000, 1000, 0],
+                         [size, size, 0],
+                         [undefined, undefined, size],
+                         [0, undefined, size],
+                        ];
+
+  var indexes_range_2 = [[100, undefined, size-100],
+                         [-100, undefined, 100],
+                         [100, -100, size-200],
+                         [-size-100, undefined, size],
+                         [-2*size-100, 500, 500],
+                         [0, -size-100, 0],
+                         [100, -size-100, 0],
+                         [50, -size+100, 50],
+                         [0, 33000, 33000],
+                         [1000, 34000, 33000],
+                        ];
+
+  let indexes;
+  if (range == RANGE_1) {
+    indexes = indexes_range_1;
+  } else if (range == RANGE_2) {
+    indexes = indexes_range_2;
+  } else {
+    throw "Invalid range!"
+  }
+
+  function runNextTest() {
+    if (indexes.length == 0) {
+      return Promise.resolve(true);
+    }
+
+    let index = indexes.shift();
+
+    let sliceContents;
+    let testName;
+    if (index[0] == undefined) {
+      slice = file.slice();
+      sliceContents = contents.slice();
+      testName = fileType + " slice()";
+    } else if (index[1] == undefined) {
+      slice = file.slice(index[0]);
+      sliceContents = contents.slice(index[0]);
+      testName = fileType + " slice(" + index[0] + ")";
+    } else {
+      slice = file.slice(index[0], index[1]);
+      sliceContents = contents.slice(index[0], index[1]);
+      testName = fileType + " slice(" + index[0] + ", " + index[1] + ")";
+    }
+
+    is(slice.type, "", testName + " type");
+    is(slice.size, index[2], testName + " size");
+    is(sliceContents.length, index[2], testName + " data size");
+
+    return testBlob(slice, sliceContents, testName).then(runNextTest);
+  }
+
+  return runNextTest()
+  .then(() => {
+    // Slice of slice
+    let slice = file.slice(0, 40000);
+    return testBlob(slice.slice(5000, 42000), contents.slice(5000, 40000), "file slice slice");
+  })
+  .then(() => {
+    // ...of slice of slice
+    let slice = file.slice(0, 40000).slice(5000, 42000).slice(400, 700);
+    SpecialPowers.gc();
+    return testBlob(slice, contents.slice(5400, 5700), "file slice slice slice");
+  });
+}
+
+function convertXHRBinary(s) {
+  let res = "";
+  for (let i = 0; i < s.length; ++i) {
+    res += String.fromCharCode(s.charCodeAt(i) & 255);
+  }
+  return res;
+}
+
+function XHRLoadHandler(event, resolve, contents, testName) {
+  is(event.target.readyState, 4, "[XHR] readyState in test " + testName);
+  is(event.target.status, 200, "[XHR] no error in test " + testName);
+  // Do not use |is(convertXHRBinary(event.target.responseText), contents, "...");| that may output raw binary data.
+  let convertedData = convertXHRBinary(event.target.responseText);
+  is(convertedData.length, contents.length, "[XHR] Length of result in test " + testName);
+  ok(convertedData == contents, "[XHR] Content of result in test " + testName);
+  is(event.lengthComputable, event.total != 0, "[XHR] lengthComputable in test " + testName);
+  is(event.loaded, contents.length, "[XHR] Loaded length in test " + testName);
+  is(event.total, contents.length, "[XHR] Total length in test " + testName);
+  resolve();
+}
+
+function checkMPSubmission(sub, expected) {
+  function getPropCount(o) {
+    let x, l = 0;
+    for (x in o) ++l;
+    return l;
+  }
+
+  is(sub.length, expected.length,
+     "Correct number of items");
+  let i;
+  for (i = 0; i < expected.length; ++i) {
+    if (!("fileName" in expected[i])) {
+      is(sub[i].headers["Content-Disposition"],
+         "form-data; name=\"" + expected[i].name + "\"",
+         "Correct name (A)");
+      is (getPropCount(sub[i].headers), 1,
+          "Wrong number of headers (A)");
+    }
+    else {
+      is(sub[i].headers["Content-Disposition"],
+         "form-data; name=\"" + expected[i].name + "\"; filename=\"" +
+           expected[i].fileName + "\"",
+         "Correct name (B)");
+      is(sub[i].headers["Content-Type"],
+         expected[i].contentType,
+         "Correct content type (B)");
+      is (getPropCount(sub[i].headers), 2,
+          "Wrong number of headers (B)");
+    }
+    // Do not use |is(sub[i].body, expected[i].value, "...");| that may output raw binary data.
+    is(sub[i].body.length, expected[i].value.length,
+       "Length of correct value");
+    ok(sub[i].body == expected[i].value,
+       "Content of correct value");
+  }
+}
+
+function createCanvasURL() {
+  return new Promise(resolve => {
+    // Create a decent-sized image
+    let cx = $("canvas").getContext('2d');
+    let s = cx.canvas.width;
+    let grad = cx.createLinearGradient(0, 0, s-1, s-1);
+    for (i = 0; i < 0.95; i += .1) {
+      grad.addColorStop(i, "white");
+      grad.addColorStop(i + .05, "black");
+    }
+    grad.addColorStop(1, "white");
+    cx.fillStyle = grad;
+    cx.fillRect(0, 0, s-1, s-1);
+    cx.fillStyle = "rgba(200, 0, 0, 0.9)";
+    cx.fillRect(.1 * s, .1 * s, .7 * s, .7 * s);
+    cx.strokeStyle = "rgba(0, 0, 130, 0.5)";
+    cx.lineWidth = .14 * s;
+    cx.beginPath();
+    cx.arc(.6 * s, .6 * s, .3 * s, 0, Math.PI*2, true);
+    cx.stroke();
+    cx.closePath();
+    cx.fillStyle = "rgb(0, 255, 0)";
+    cx.beginPath();
+    cx.arc(.1 * s, .8 * s, .1 * s, 0, Math.PI*2, true);
+    cx.fill();
+    cx.closePath();
+
+    let data = atob(cx.canvas.toDataURL("image/png").substring("data:text/png;base64,".length + 1));
+
+    // This might fail if we dramatically improve the png encoder. If that happens
+    // please increase the complexity or size of the image generated above to ensure
+    // that we're testing with files that are large enough.
+    ok(data.length > 65536, "test data sufficiently large");
+
+    resolve(data);
+  });
+}
+
+function createFile(data, name) {
+  return new Promise(resolve => {
+    SpecialPowers.createFiles([{name, data}],
+                              files => { resolve(files[0]); });
+  });
+}
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/common_fileReader.js
@@ -0,0 +1,580 @@
+function test_setup() {
+  return new Promise(resolve => {
+    const minFileSize = 20000;
+
+    // Create strings containing data we'll test with. We'll want long
+    // strings to ensure they span multiple buffers while loading
+    let testTextData = "asd b\tlah\u1234w\u00a0r";
+    while (testTextData.length < minFileSize) {
+      testTextData = testTextData + testTextData;
+    }
+
+    let testASCIIData = "abcdef 123456\n";
+    while (testASCIIData.length < minFileSize) {
+      testASCIIData = testASCIIData + testASCIIData;
+    }
+
+    let testBinaryData = "";
+    for (let i = 0; i < 256; i++) {
+      testBinaryData += String.fromCharCode(i);
+    }
+    while (testBinaryData.length < minFileSize) {
+      testBinaryData = testBinaryData + testBinaryData;
+    }
+
+    let dataurldata0 = testBinaryData.substr(0, testBinaryData.length -
+                                             testBinaryData.length % 3);
+    let dataurldata1 = testBinaryData.substr(0, testBinaryData.length - 2 -
+                                             testBinaryData.length % 3);
+    let dataurldata2 = testBinaryData.substr(0, testBinaryData.length - 1 -
+                                             testBinaryData.length % 3);
+
+
+    //Set up files for testing
+    let openerURL = SimpleTest.getTestFileURL("fileapi_chromeScript.js");
+    let opener = SpecialPowers.loadChromeScript(openerURL);
+
+    opener.addMessageListener("files.opened", message => {
+      let [
+        asciiFile,
+        binaryFile,
+        nonExistingFile,
+        utf8TextFile,
+        utf16TextFile,
+        emptyFile,
+        dataUrlFile0,
+        dataUrlFile1,
+        dataUrlFile2,
+      ] = message;
+
+      resolve({ blobs:{ asciiFile, binaryFile, nonExistingFile, utf8TextFile,
+                        utf16TextFile, emptyFile, dataUrlFile0, dataUrlFile1,
+                        dataUrlFile2 },
+                data: { text: testTextData,
+                        ascii: testASCIIData,
+                        binary: testBinaryData,
+                        url0: dataurldata0,
+                        url1: dataurldata1,
+                        url2: dataurldata2, }});
+    });
+
+    opener.sendAsyncMessage("files.open", [
+      testASCIIData,
+      testBinaryData,
+      null,
+      convertToUTF8(testTextData),
+      convertToUTF16(testTextData),
+      "",
+      dataurldata0,
+      dataurldata1,
+      dataurldata2,
+    ]);
+  });
+}
+
+function runBasicTests(data) {
+  return test_basic()
+    .then(() => {
+      return test_readAsText(data.blobs.asciiFile, data.data.ascii);
+    })
+    .then(() => {
+      return test_readAsBinaryString(data.blobs.binaryFile, data.data.binary);
+    })
+    .then(() => {
+      return test_readAsArrayBuffer(data.blobs.binaryFile, data.data.binary);
+    });
+}
+
+function runEncodingTests(data) {
+  return test_readAsTextWithEncoding(data.blobs.asciiFile, data.data.ascii,
+                                     data.data.ascii.length, "")
+    .then(() => {
+      return test_readAsTextWithEncoding(data.blobs.asciiFile, data.data.ascii,
+                                         data.data.ascii.length, "iso8859-1");
+    })
+    .then(() => {
+      return test_readAsTextWithEncoding(data.blobs.utf8TextFile, data.data.text,
+                                         convertToUTF8(data.data.text).length,
+                                         "utf8");
+    })
+    .then(() => {
+      return test_readAsTextWithEncoding(data.blobs.utf16TextFile, data.data.text,
+                                         convertToUTF16(data.data.text).length,
+                                         "utf-16");
+    })
+    .then(() => {
+      return test_readAsTextWithEncoding(data.blobs.emptyFile, "", 0, "");
+    })
+    .then(() => {
+      return test_readAsTextWithEncoding(data.blobs.emptyFile, "", 0, "utf8");
+    })
+    .then(() => {
+      return test_readAsTextWithEncoding(data.blobs.emptyFile, "", 0, "utf-16");
+    });
+}
+
+function runEmptyTests(data) {
+  return test_onlyResult()
+    .then(() => {
+      return test_readAsText(data.blobs.emptyFile, "");
+    })
+    .then(() => {
+      return test_readAsBinaryString(data.blobs.emptyFile, "");
+    })
+    .then(() => {
+      return test_readAsArrayBuffer(data.blobs.emptyFile, "");
+    })
+    .then(() => {
+      return test_readAsDataURL(data.blobs.emptyFile, convertToDataURL(""), 0);
+    });
+}
+
+function runTwiceTests(data) {
+  return test_readAsTextTwice(data.blobs.asciiFile, data.data.ascii)
+    .then(() => {
+      return test_readAsBinaryStringTwice(data.blobs.binaryFile,
+                                          data.data.binary);
+    })
+    .then(() => {
+      return test_readAsDataURLTwice(data.blobs.binaryFile,
+                                     convertToDataURL(data.data.binary),
+                                     data.data.binary.length);
+    })
+    .then(() => {
+      return test_readAsArrayBufferTwice(data.blobs.binaryFile,
+                                         data.data.binary);
+    })
+    .then(() => {
+      return test_readAsArrayBufferTwice2(data.blobs.binaryFile,
+                                          data.data.binary);
+    });
+}
+
+function runOtherTests(data) {
+  return test_readAsDataURL_customLength(data.blobs.dataUrlFile0,
+                                         convertToDataURL(data.data.url0),
+                                         data.data.url0.length, 0)
+    .then(() => {
+      return test_readAsDataURL_customLength(data.blobs.dataUrlFile1,
+                                             convertToDataURL(data.data.url1),
+                                             data.data.url1.length, 1);
+    })
+    .then(() => {
+      return test_readAsDataURL_customLength(data.blobs.dataUrlFile2,
+                                             convertToDataURL(data.data.url2),
+                                             data.data.url2.length, 2);
+    })
+    .then(() => {
+      return test_abort(data.blobs.asciiFile);
+    })
+    .then(() => {
+      return test_abort_readAsX(data.blobs.asciiFile, data.data.ascii);
+    })
+    .then(() => {
+      return test_nonExisting(data.blobs.nonExistingFile);
+    });
+}
+
+function convertToUTF16(s) {
+  let res = "";
+  for (let i = 0; i < s.length; ++i) {
+    c = s.charCodeAt(i);
+    res += String.fromCharCode(c & 255, c >>> 8);
+  }
+  return res;
+}
+
+function convertToUTF8(s) {
+  return unescape(encodeURIComponent(s));
+}
+
+function convertToDataURL(s) {
+  return "data:application/octet-stream;base64," + btoa(s);
+}
+
+function loadEventHandler_string(event, resolve, reader, data, dataLength, testName) {
+  is(event.target, reader, "Correct target.");
+  is(event.target.readyState, FileReader.DONE, "readyState in test " + testName);
+  is(event.target.error, null, "no error in test " + testName);
+  is(event.target.result, data, "result in test " + testName);
+  is(event.lengthComputable, true, "lengthComputable in test " + testName);
+  is(event.loaded, dataLength, "loaded in test " + testName);
+  is(event.total, dataLength, "total in test " + testName);
+  resolve();
+}
+
+function loadEventHandler_arrayBuffer(event, resolve, reader, data, testName) {
+  is(event.target.readyState, FileReader.DONE, "readyState in test " + testName);
+  is(event.target.error, null, "no error in test " +  testName);
+  is(event.lengthComputable, true, "lengthComputable in test " + testName);
+  is(event.loaded, data.length, "loaded in test " + testName);
+  is(event.total, data.length, "total in test " + testName);
+  is(event.target.result.byteLength, data.length, "array buffer size in test " + testName);
+
+  let u8v = new Uint8Array(event.target.result);
+  is(String.fromCharCode.apply(String, u8v), data,
+     "array buffer contents in test " + testName);
+  u8v = null;
+
+  if ("SpecialPowers" in self) {
+    SpecialPowers.gc();
+
+    is(event.target.result.byteLength, data.length,
+       "array buffer size after gc in test " + testName);
+    u8v = new Uint8Array(event.target.result);
+    is(String.fromCharCode.apply(String, u8v), data,
+       "array buffer contents after gc in test " + testName);
+  }
+
+  resolve();
+}
+
+function test_basic() {
+  return new Promise(resolve => {
+    is(FileReader.EMPTY, 0, "correct EMPTY value");
+    is(FileReader.LOADING, 1, "correct LOADING value");
+    is(FileReader.DONE, 2, "correct DONE value");
+    resolve();
+  });
+}
+
+function test_readAsText(blob, text) {
+  return new Promise(resolve => {
+    let onloadHasRun = false;
+    let onloadStartHasRun = false;
+
+    let r = new FileReader();
+    is(r.readyState, FileReader.EMPTY, "correct initial text readyState");
+
+    r.onload = event => {
+      loadEventHandler_string(event, resolve, r, text, text.length, "readAsText");
+    }
+
+    r.addEventListener("load", () => { onloadHasRun = true });
+    r.addEventListener("loadstart", () => { onloadStartHasRun = true });
+
+    r.readAsText(blob);
+
+    is(r.readyState, FileReader.LOADING, "correct loading text readyState");
+    is(onloadHasRun, false, "text loading must be async");
+    is(onloadStartHasRun, true, "text loadstart should fire sync");
+  });
+}
+
+function test_readAsBinaryString(blob, text) {
+  return new Promise(resolve => {
+    let onloadHasRun = false;
+    let onloadStartHasRun = false;
+
+    let r = new FileReader();
+    is(r.readyState, FileReader.EMPTY, "correct initial binary readyState");
+
+    r.addEventListener("load", function() { onloadHasRun = true });
+    r.addEventListener("loadstart", function() { onloadStartHasRun = true });
+
+    r.readAsBinaryString(blob);
+
+    r.onload = event => {
+      loadEventHandler_string(event, resolve, r, text, text.length, "readAsBinaryString");
+    }
+
+    is(r.readyState, FileReader.LOADING, "correct loading binary readyState");
+    is(onloadHasRun, false, "binary loading must be async");
+    is(onloadStartHasRun, true, "binary loadstart should fire sync");
+  });
+}
+
+function test_readAsArrayBuffer(blob, text) {
+  return new Promise(resolve => {
+    let onloadHasRun = false;
+    let onloadStartHasRun = false;
+
+    r = new FileReader();
+    is(r.readyState, FileReader.EMPTY, "correct initial arrayBuffer readyState");
+
+    r.addEventListener("load", function() { onloadHasRun = true });
+    r.addEventListener("loadstart", function() { onloadStartHasRun = true });
+
+    r.readAsArrayBuffer(blob);
+
+    r.onload = event => {
+      loadEventHandler_arrayBuffer(event, resolve, r, text, "readAsArrayBuffer");
+    }
+
+    is(r.readyState, FileReader.LOADING, "correct loading arrayBuffer readyState");
+    is(onloadHasRun, false, "arrayBuffer loading must be async");
+    is(onloadStartHasRun, true, "arrayBuffer loadstart should fire sync");
+  });
+}
+
+// Test a variety of encodings, and make sure they work properly
+function test_readAsTextWithEncoding(blob, text, length, charset) {
+  return new Promise(resolve => {
+    let r = new FileReader();
+    r.onload = event => {
+      loadEventHandler_string(event, resolve, r, text, length, "readAsText-" + charset);
+    }
+    r.readAsText(blob, charset);
+  });
+}
+
+// Test get result without reading
+function test_onlyResult() {
+  return new Promise(resolve => {
+    let r = new FileReader();
+    is(r.readyState, FileReader.EMPTY, "readyState in test reader get result without reading");
+    is(r.error, null, "no error in test reader get result without reading");
+    is(r.result, null, "result in test reader get result without reading");
+    resolve();
+  });
+}
+
+function test_readAsDataURL(blob, text, length) {
+  return new Promise(resolve => {
+    let r = new FileReader();
+    r.onload = event => {
+      loadEventHandler_string(event, resolve, r, text, length, "readAsDataURL");
+    }
+    r.readAsDataURL(blob);
+  });
+}
+
+// Test reusing a FileReader to read multiple times
+function test_readAsTextTwice(blob, text) {
+  return new Promise(resolve => {
+    let r = new FileReader();
+    r.onload = event => {
+      loadEventHandler_string(event, () => {}, r, text, text.length, "readAsText-reused-once");
+    }
+
+    let anotherListener = event => {
+      let r = event.target;
+      r.removeEventListener("load", anotherListener);
+      r.onload = event => {
+        loadEventHandler_string(event, resolve, r, text, text.length, "readAsText-reused-twice");
+      }
+      r.readAsText(blob);
+    };
+
+    r.addEventListener("load", anotherListener);
+    r.readAsText(blob);
+  });
+}
+
+// Test reusing a FileReader to read multiple times
+function test_readAsBinaryStringTwice(blob, text) {
+  return new Promise(resolve => {
+    let r = new FileReader();
+    r.onload = event => {
+      loadEventHandler_string(event, () => {}, r, text, text.length, "readAsBinaryString-reused-once");
+    }
+
+    let anotherListener = event => {
+      let r = event.target;
+      r.removeEventListener("load", anotherListener);
+      r.onload = event => {
+        loadEventHandler_string(event, resolve, r, text, text.length, "readAsBinaryString-reused-twice");
+      }
+      r.readAsBinaryString(blob);
+    };
+
+    r.addEventListener("load", anotherListener);
+    r.readAsBinaryString(blob);
+  });
+}
+
+function test_readAsDataURLTwice(blob, text, length) {
+  return new Promise(resolve => {
+    let r = new FileReader();
+    r.onload = event => {
+      loadEventHandler_string(event, () => {}, r, text, length, "readAsDataURL-reused-once");
+    }
+
+    let anotherListener = event => {
+      let r = event.target;
+      r.removeEventListener("load", anotherListener);
+      r.onload = event => {
+        loadEventHandler_string(event, resolve, r, text, length, "readAsDataURL-reused-twice");
+      }
+      r.readAsDataURL(blob);
+    };
+
+    r.addEventListener("load", anotherListener);
+    r.readAsDataURL(blob);
+  });
+}
+
+function test_readAsArrayBufferTwice(blob, text) {
+  return new Promise(resolve => {
+    let r = new FileReader();
+    r.onload = event => {
+      loadEventHandler_arrayBuffer(event, () => {}, r, text, "readAsArrayBuffer-reused-once");
+    }
+
+    let anotherListener = event => {
+      let r = event.target;
+      r.removeEventListener("load", anotherListener);
+      r.onload = event => {
+        loadEventHandler_arrayBuffer(event, resolve, r, text, "readAsArrayBuffer-reused-twice");
+      }
+      r.readAsArrayBuffer(blob);
+    };
+
+    r.addEventListener("load", anotherListener);
+    r.readAsArrayBuffer(blob);
+  });
+}
+
+// Test first reading as ArrayBuffer then read as something else (BinaryString)
+// and doesn't crash
+function test_readAsArrayBufferTwice2(blob, text) {
+  return new Promise(resolve => {
+    let r = new FileReader();
+    r.onload = event => {
+      loadEventHandler_arrayBuffer(event, () => {}, r, text, "readAsArrayBuffer-reused-once2");
+    }
+
+    let anotherListener = event => {
+      let r = event.target;
+      r.removeEventListener("load", anotherListener);
+      r.onload = event => {
+        loadEventHandler_string(event, resolve, r, text, text.length, "readAsArrayBuffer-reused-twice2");
+      }
+      r.readAsBinaryString(blob);
+    };
+
+    r.addEventListener("load", anotherListener);
+    r.readAsArrayBuffer(blob);
+  });
+}
+
+function test_readAsDataURL_customLength(blob, text, length, numb) {
+  return new Promise(resolve => {
+    is(length % 3, numb, "Want to test data with length %3 == " + numb);
+    let r = new FileReader();
+    r.onload = event => {
+      loadEventHandler_string(event, resolve, r, text, length, "dataurl reading, %3 = " + numb);
+    }
+    r.readAsDataURL(blob);
+  });
+}
+
+// Test abort()
+function test_abort(blob) {
+  return new Promise(resolve => {
+    let abortHasRun = false;
+    let loadEndHasRun = false;
+
+    let r = new FileReader();
+
+    r.onabort = function (event) {
+      is(abortHasRun, false, "abort should only fire once");
+      is(loadEndHasRun, false, "loadend shouldn't have fired yet");
+      abortHasRun = true;
+      is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
+      is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
+      is(event.target.result, null, "file data should be null on aborted reads");
+    }
+
+    r.onloadend = function (event) {
+      is(abortHasRun, true, "abort should fire before loadend");
+      is(loadEndHasRun, false, "loadend should only fire once");
+      loadEndHasRun = true;
+      is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
+      is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
+      is(event.target.result, null, "file data should be null on aborted reads");
+    }
+
+    r.onload = function() { ok(false, "load should not fire for aborted reads") };
+    r.onerror = function() { ok(false, "error should not fire for aborted reads") };
+    r.onprogress = function() { ok(false, "progress should not fire for aborted reads") };
+
+    let abortThrew = false;
+    try {
+      r.abort();
+    } catch(e) {
+      abortThrew = true;
+    }
+
+    is(abortThrew, false, "abort() doesn't throw");
+    is(abortHasRun, false, "abort() is a no-op unless loading");
+
+    r.readAsText(blob);
+    r.abort();
+
+    is(abortHasRun, true, "abort should fire sync");
+    is(loadEndHasRun, true, "loadend should fire sync");
+
+    resolve();
+  });
+}
+
+// Test calling readAsX to cause abort()
+function test_abort_readAsX(blob, text) {
+  return new Promise(resolve => {
+    let reuseAbortHasRun = false;
+
+    let r = new FileReader();
+    r.onabort = function (event) {
+      is(reuseAbortHasRun, false, "abort should only fire once");
+      reuseAbortHasRun = true;
+      is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
+      is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
+      is(event.target.result, null, "file data should be null on aborted reads");
+    }
+    r.onload = function() { ok(false, "load should fire for nested reads"); };
+
+    let abortThrew = false;
+    try {
+      r.abort();
+    } catch(e) {
+      abortThrew = true;
+    }
+
+    is(abortThrew, false, "abort() should not throw");
+    is(reuseAbortHasRun, false, "abort() is a no-op unless loading");
+    r.readAsText(blob);
+
+    let readThrew = false;
+    try {
+      r.readAsText(blob);
+    } catch(e) {
+      readThrew = true;
+    }
+
+    is(readThrew, true, "readAsText() must throw if loading");
+    is(reuseAbortHasRun, false, "abort should not fire");
+
+    r.onload = event => {
+      loadEventHandler_string(event, resolve, r, text, text.length, "reuse-as-abort reading");
+    }
+  });
+}
+
+// Test reading from nonexistent files
+function test_nonExisting(blob) {
+  return new Promise(resolve => {
+    let r = new FileReader();
+
+    r.onerror = function (event) {
+      is(event.target.readyState, FileReader.DONE, "should be DONE while firing onerror");
+      is(event.target.error.name, "NotFoundError", "error set to NotFoundError for nonexistent files");
+      is(event.target.result, null, "file data should be null on aborted reads");
+      resolve();
+    };
+    r.onload = function (event) {
+      is(false, "nonexistent file shouldn't load! (FIXME: bug 1122788)");
+    };
+
+    let didThrow = false;
+    try {
+      r.readAsDataURL(blob);
+    } catch(ex) {
+      didThrow = true;
+    }
+
+    // Once this test passes, we should test that onerror gets called and
+    // that the FileReader object is in the right state during that call.
+    is(didThrow, false, "shouldn't throw when opening nonexistent file, should fire error instead");
+  });
+}
--- a/dom/file/tests/fileapi_chromeScript.js
+++ b/dom/file/tests/fileapi_chromeScript.js
@@ -1,19 +1,20 @@
 var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 Cu.importGlobalProperties(["File"]);
 
-var fileNum = 1;
-
 function createFileWithData(fileData) {
   var willDelete = fileData === null;
+
   var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
+
   var testFile = dirSvc.get("ProfD", Ci.nsIFile);
-  testFile.append("fileAPItestfile" + fileNum);
-  fileNum++;
+  testFile.append("fileAPItestfile");
+  testFile.createUnique(Components.interfaces.nsIFile.FILE_TYPE, 0o600);
+
   var outStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
   outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate
                  0o666, 0);
   if (willDelete) {
     fileData = "some irrelevant test data\n";
   }
   outStream.write(fileData, fileData.length);
   outStream.close();
deleted file mode 100644
--- a/dom/file/tests/fileutils.js
+++ /dev/null
@@ -1,244 +0,0 @@
-// Utility functions
-var testRanCounter = 0;
-var expectedTestCount = 0;
-
-function testHasRun() {
- ++testRanCounter;
- if (testRanCounter == expectedTestCount) {
-    SimpleTest.finish();
-  }
-}
-
-
-function testFile(file, contents, test) {
-  SimpleTest.requestLongerTimeout(2);
-
-  // Load file using FileReader
-  var r = new FileReader();
-  r.onload = getFileReaderLoadHandler(contents, contents.length, "FileReader.readAsBinaryString of " + test);
-  r.readAsBinaryString(file);
-  expectedTestCount++;
-
-  // Load file using URL.createObjectURL and XMLHttpRequest
-  var xhr = new XMLHttpRequest;
-  xhr.open("GET", URL.createObjectURL(file));
-  xhr.onload = getXHRLoadHandler(contents, contents.length,
-                                 "XMLHttpRequest load of " + test);
-  xhr.overrideMimeType('text/plain; charset=x-user-defined');
-  xhr.send();
-  expectedTestCount++;
-
-  // Send file to server using FormData and XMLHttpRequest
-  xhr = new XMLHttpRequest();
-  xhr.onload = function(event) {
-    checkMPSubmission(JSON.parse(event.target.responseText),
-                      [{ name: "hello", value: "world"},
-                       { name: "myfile",
-                         value: contents,
-                         fileName: file.name || "blob",
-                         contentType: file.type || "application/octet-stream" }]);
-    testHasRun();
-  }
-  xhr.open("POST", "../../../dom/html/test/form_submit_server.sjs");
-  var fd = new FormData;
-  fd.append("hello", "world");
-  fd.append("myfile", file);
-  xhr.send(fd);
-  expectedTestCount++;
-
-  // Send file to server using plain XMLHttpRequest
-  var xhr = new XMLHttpRequest;
-  xhr.open("POST", "../../../dom/xhr/tests/file_XHRSendData.sjs");
-  xhr.onload = function (event) {
-    is(event.target.getResponseHeader("Result-Content-Type"),
-       file.type ? file.type : null,
-       "request content-type in XMLHttpRequest send of " + test);
-    is(event.target.getResponseHeader("Result-Content-Length"),
-       String(file.size),
-       "request content-length in XMLHttpRequest send of " + test);
-  };
-  xhr.addEventListener("load",
-                       getXHRLoadHandler(contents, contents.length,
-                                         "XMLHttpRequest send of " + test));
-  xhr.overrideMimeType('text/plain; charset=x-user-defined');
-  xhr.send(file);
-  expectedTestCount++;
-}
-
-function getFileReaderLoadHandler(expectedResult, expectedLength, testName) {
-  return function (event) {
-    is(event.target.readyState, FileReader.DONE,
-       "[FileReader] readyState in test " + testName);
-    is(event.target.error, null,
-       "[FileReader] no error in test " + testName);
-    // Do not use |is(event.target.result, expectedResult, "...");| that may output raw binary data.
-    is(event.target.result.length, expectedResult.length,
-       "[FileReader] Length of result in test " + testName);
-    ok(event.target.result == expectedResult,
-       "[FileReader] Content of result in test " + testName);
-    is(event.lengthComputable, true,
-       "[FileReader] lengthComputable in test " + testName);
-    is(event.loaded, expectedLength,
-       "[FileReader] Loaded length in test " + testName);
-    is(event.total, expectedLength,
-       "[FileReader] Total length in test " + testName);
-    testHasRun();
-  }
-}
-
-function getXHRLoadHandler(expectedResult, expectedLength, testName) {
-  return function (event) {
-    is(event.target.readyState, 4,
-       "[XHR] readyState in test " + testName);
-    is(event.target.status, 200,
-       "[XHR] no error in test " + testName);
-    // Do not use |is(convertXHRBinary(event.target.responseText), expectedResult, "...");| that may output raw binary data.
-    var convertedData = convertXHRBinary(event.target.responseText);
-    is(convertedData.length, expectedResult.length,
-       "[XHR] Length of result in test " + testName);
-    ok(convertedData == expectedResult,
-       "[XHR] Content of result in test " + testName);
-    is(event.lengthComputable, event.total != 0,
-       "[XHR] lengthComputable in test " + testName);
-    is(event.loaded, expectedLength,
-       "[XHR] Loaded length in test " + testName);
-    is(event.total, expectedLength,
-       "[XHR] Total length in test " + testName);
-
-    testHasRun();
-  }
-}
-
-function convertXHRBinary(s) {
-  var res = "";
-  for (var i = 0; i < s.length; ++i) {
-    res += String.fromCharCode(s.charCodeAt(i) & 255);
-  }
-  return res;
-}
-
-function gc() {
-  window.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
-        .getInterface(SpecialPowers.Ci.nsIDOMWindowUtils)
-        .garbageCollect();
-}
-
-function checkMPSubmission(sub, expected) {
-  function getPropCount(o) {
-    var x, l = 0;
-    for (x in o) ++l;
-    return l;
-  }
-
-  is(sub.length, expected.length,
-     "Correct number of items");
-  var i;
-  for (i = 0; i < expected.length; ++i) {
-    if (!("fileName" in expected[i])) {
-      is(sub[i].headers["Content-Disposition"],
-         "form-data; name=\"" + expected[i].name + "\"",
-         "Correct name (A)");
-      is (getPropCount(sub[i].headers), 1,
-          "Wrong number of headers (A)");
-    }
-    else {
-      is(sub[i].headers["Content-Disposition"],
-         "form-data; name=\"" + expected[i].name + "\"; filename=\"" +
-           expected[i].fileName + "\"",
-         "Correct name (B)");
-      is(sub[i].headers["Content-Type"],
-         expected[i].contentType,
-         "Correct content type (B)");
-      is (getPropCount(sub[i].headers), 2,
-          "Wrong number of headers (B)");
-    }
-    // Do not use |is(sub[i].body, expected[i].value, "...");| that may output raw binary data.
-    is(sub[i].body.length, expected[i].value.length,
-       "Length of correct value");
-    ok(sub[i].body == expected[i].value,
-       "Content of correct value");
-  }
-}
-
-function testSlice(file, size, type, contents, fileType) {
-  is(file.type, type, fileType + " file is correct type");
-  is(file.size, size, fileType + " file is correct size");
-  ok(file instanceof File, fileType + " file is a File");
-  ok(file instanceof Blob, fileType + " file is also a Blob");
-  
-  var slice = file.slice(0, size);
-  ok(slice instanceof Blob, fileType + " fullsize slice is a Blob");
-  ok(!(slice instanceof File), fileType + " fullsize slice is not a File");
-
-  slice = file.slice(0, 1234);
-  ok(slice instanceof Blob, fileType + " sized slice is a Blob");
-  ok(!(slice instanceof File), fileType + " sized slice is not a File");
-  
-  slice = file.slice(0, size, "foo/bar");
-  is(slice.type, "foo/bar", fileType + " fullsize slice foo/bar type");
-
-  slice = file.slice(0, 5432, "foo/bar");
-  is(slice.type, "foo/bar", fileType + " sized slice foo/bar type");
-  
-  is(slice.slice(0, 10).type, "", fileType + " slice-slice type");
-  is(slice.slice(0, 10).size, 10, fileType + " slice-slice size");
-  is(slice.slice(0, 10, "hello/world").type, "hello/world", fileType + " slice-slice hello/world type");
-  is(slice.slice(0, 10, "hello/world").size, 10, fileType + " slice-slice hello/world size");
-
-  // Start, end, expected size
-  var indexes = [[0, size, size],
-                 [0, 1234, 1234],
-                 [size-500, size, 500],
-                 [size-500, size+500, 500],
-                 [size+500, size+1500, 0],
-                 [0, 0, 0],
-                 [1000, 1000, 0],
-                 [size, size, 0],
-                 [undefined, undefined, size],
-                 [0, undefined, size],
-                 [100, undefined, size-100],
-                 [-100, undefined, 100],
-                 [100, -100, size-200],
-                 [-size-100, undefined, size],
-                 [-2*size-100, 500, 500],
-                 [0, -size-100, 0],
-                 [100, -size-100, 0],
-                 [50, -size+100, 50],
-                 [0, 33000, 33000],
-                 [1000, 34000, 33000],
-                ];
-  
-  for (var i = 0; i < indexes.length; ++i) {
-    var sliceContents;
-    var testName;
-    if (indexes[i][0] == undefined) {
-      slice = file.slice();
-      sliceContents = contents.slice();
-      testName = fileType + " slice()";
-    }
-    else if (indexes[i][1] == undefined) {
-      slice = file.slice(indexes[i][0]);
-      sliceContents = contents.slice(indexes[i][0]);
-      testName = fileType + " slice(" + indexes[i][0] + ")";
-    }
-    else {
-      slice = file.slice(indexes[i][0], indexes[i][1]);
-      sliceContents = contents.slice(indexes[i][0], indexes[i][1]);
-      testName = fileType + " slice(" + indexes[i][0] + ", " + indexes[i][1] + ")";
-    }
-    is(slice.type, "", testName + " type");
-    is(slice.size, indexes[i][2], testName + " size");
-    is(sliceContents.length, indexes[i][2], testName + " data size");
-    testFile(slice, sliceContents, testName);
-  }
-
-  // Slice of slice
-  var slice = file.slice(0, 40000);
-  testFile(slice.slice(5000, 42000), contents.slice(5000, 40000), "file slice slice");
-  
-  // ...of slice of slice
-  slice = slice.slice(5000, 42000).slice(400, 700);
-  SpecialPowers.gc();
-  testFile(slice, contents.slice(5400, 5700), "file slice slice slice");
-}
-
--- a/dom/file/tests/mochitest.ini
+++ b/dom/file/tests/mochitest.ini
@@ -1,28 +1,45 @@
 [DEFAULT]
 support-files =
+  common_blob.js
   create_file_objects.js
+  common_fileReader.js
   file_blobURL_expiring.html
   file_mozfiledataurl_img.jpg
   file_mozfiledataurl_audio.ogg
   file_mozfiledataurl_doc.html
   file_mozfiledataurl_text.txt
   file_mozfiledataurl_inner.html
   file_nonascii_blob_url.html
-  fileutils.js
   fileapi_chromeScript.js
+  worker_fileReader.js
   !/dom/html/test/form_submit_server.sjs
   !/dom/xhr/tests/file_XHRSendData.sjs
 
 [test_blob_fragment_and_query.html]
 [test_blobconstructor.html]
 [test_blobURL_expiring.html]
 [test_file_from_blob.html]
 [test_ipc_messagemanager_blob.html]
 support-files = file_ipc_messagemanager_blob.html
 [test_nonascii_blob_url.html]
 [test_file_negative_date.html]
-[test_fileapi.html]
-[test_fileapi_slice.html]
+[test_fileapi_basic.html]
+[test_fileapi_encoding.html]
+[test_fileapi_twice.html]
+[test_fileapi_other.html]
+[test_fileapi_basic_worker.html]
+[test_fileapi_encoding_worker.html]
+[test_fileapi_twice_worker.html]
+[test_fileapi_other_worker.html]
+[test_fileapi_slice_realFile_1.html]
+skip-if = (toolkit == 'android') # Android: Bug 775227
+[test_fileapi_slice_realFile_2.html]
+skip-if = (toolkit == 'android') # Android: Bug 775227
+[test_fileapi_slice_memFile_1.html]
+skip-if = (toolkit == 'android') # Android: Bug 775227
+[test_fileapi_slice_memFile_2.html]
+skip-if = (toolkit == 'android') # Android: Bug 775227
+[test_fileapi_slice_image.html]
 skip-if = (toolkit == 'android') # Android: Bug 775227
 [test_mozfiledataurl.html]
 skip-if = toolkit == 'android' #TIMED_OUT
--- a/dom/file/tests/test_blobconstructor.html
+++ b/dom/file/tests/test_blobconstructor.html
@@ -2,17 +2,17 @@
 <html>
 <!--
 https://bugzilla.mozilla.org/show_bug.cgi?id=721569
 -->
 <head>
   <title>Test for Blob constructor (Bug 721569)</title>
   <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="text/javascript" src="fileutils.js"></script>
+  <script type="text/javascript" src="common_blob.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=721569">Mozilla Bug 721569</a>
 <p id="display"></p>
 <div id="content" style="display: none">
   
 </div>
@@ -191,56 +191,56 @@ let testData =
     // Test type coercion of a number
     [[3, int8View, "foo"], {},
                             [{start: 0, length: 8, contents:  "3ABCDEFG"},
                              {start: 8, length:10, contents:  "HIJKLMNOPf"},
                              {start: 17, length: 4, contents: "foo"},
                              {start: 4, length: 8, contents:  "DEFGHIJK"}]]
  ];
 
+let currentTest = null;
 let testCounter = 0;
 
-function doTest(data) {
-  testCounter++;
-
-  var [blobs, options, tests] = data;
-
-  function runTest(test) {
+function runTests() {
+  if (!currentTest || currentTest[2].length == 0) {
+    if (testData.length == 0) {
+      SimpleTest.finish();
+      return;
+    }
 
-    let blob;
-    if (options !== undefined) {
-      blob = new Blob(blobs, options);
-    } else {
-      blob = new Blob(blobs);
-    }
-    ok(blob, "Test " + testCounter + " got blob");
-    ok(blob instanceof Blob, "Test " + testCounter + " blob is a Blob");
-    ok(!(blob instanceof File), "Test " + testCounter + " blob is not a File");
+    currentTest = testData.shift();
+    ++testCounter;
+  }
+
+  let [blobs, options] = currentTest;
+  let test = currentTest[2].shift();
 
-    let slice = blob.slice(test.start, test.start + test.length);
-    ok(slice, "Test " + testCounter + " got slice");
-    ok(slice instanceof Blob, "Test " + testCounter + " slice is a Blob");
-    ok(!(slice instanceof File), "Test " + testCounter + " slice is not a File");
-    is(slice.size, test.contents.length,
-       "Test " + testCounter + " slice is correct size");
-
-    testFile(slice, test.contents, "Test " + testCounter);
+  let blob;
+  if (options !== undefined) {
+    blob = new Blob(blobs, options);
+  } else {
+    blob = new Blob(blobs);
   }
-  if (Array.isArray(tests)) {
-    tests.forEach(runTest);
-  } else {
-    try {
-      let blob = new Blob(blobs, options);
-      ok(false, "NOT REACHED");
-    } catch (e) {
-      is(e.name, tests, "Blob constructor should throw " + tests);
-    }
-  }
-  SpecialPowers.gc();
+
+  ok(blob, "Test " + testCounter + " got blob");
+  ok(blob instanceof Blob, "Test " + testCounter + " blob is a Blob");
+  ok(!(blob instanceof File), "Test " + testCounter + " blob is not a File");
+
+  let slice = blob.slice(test.start, test.start + test.length);
+  ok(slice, "Test " + testCounter + " got slice");
+  ok(slice instanceof Blob, "Test " + testCounter + " slice is a Blob");
+  ok(!(slice instanceof File), "Test " + testCounter + " slice is not a File");
+  is(slice.size, test.contents.length, "Test " + testCounter + " slice is correct size");
+
+  testBlob(slice, test.contents, "Test " + testCounter).then(() => {
+    SpecialPowers.gc();
+    runTests();
+  });
 }
 
+SimpleTest.requestLongerTimeout(2);
 SimpleTest.waitForExplicitFinish();
-testData.forEach(doTest);
+runTests();
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/file/tests/test_file_negative_date.html
+++ b/dom/file/tests/test_file_negative_date.html
@@ -2,38 +2,32 @@
 <html>
 <!--
 https://bugzilla.mozilla.org/show_bug.cgi?id=1158437
 -->
 <head>
   <title>Test for negative date in File (Bug 1158437)</title>
   <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="text/javascript" src="fileutils.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1158437">Mozilla Bug 1158437</a>
-<p id="display"></p>
-<div id="content" style="display: none">
 
-</div>
-<pre id="test">
 <script class="testbody" type="text/javascript">
 "use strict";
 
 var blob = new Blob(['hello world']);
 var f1 = new File([blob], 'f1.txt', { lastModified: 0 });
 var f2 = new File([blob], 'f2.txt', { lastModified: -1 });
 var f3 = new File([blob], 'f3.txt', { lastModified: -1000 });
 
 is(f1.lastModified, 0, "lastModified == 0 is supported");
 ok(f1.lastModifiedDate.toString(), (new Date(0)).toString(), "Correct f1.lastModifiedDate value");
 is(f2.lastModified, -1, "lastModified == -1 is supported");
 ok(f2.lastModifiedDate.toString(), (new Date(-1)).toString(), "Correct f2.lastModifiedDate value");
 is(f3.lastModified, -1000, "lastModified == -1000 is supported");
 ok(f3.lastModifiedDate.toString(), (new Date(-1000)).toString(), "Correct f3.lastModifiedDate value");
 
 </script>
-</pre>
 </body>
 </html>
 
deleted file mode 100644
--- a/dom/file/tests/test_fileapi.html
+++ /dev/null
@@ -1,487 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=414796
--->
-  <title>Test for Bug 414796</title>
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
-</head>
-
-<body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=414796">Mozilla Bug 414796</a>
-<p id="display">
-</p>
-<div id="content" style="display: none">
-</div>
-
-<pre id="test">
-<script class="testbody" type="text/javascript">
-
-// File constructors should not work from non-chrome code
-try {
-  var file = File("/etc/passwd");
-  ok(false, "Did not throw on unprivileged attempt to construct a File");
-} catch (e) {
-  ok(true, "Threw on an unprivileged attempt to construct a File");
-}
-
-const minFileSize = 20000;
-var testRanCounter = 0;
-var expectedTestCount = 0;
-var testSetupFinished = false;
-SimpleTest.waitForExplicitFinish();
-
-is(FileReader.EMPTY, 0, "correct EMPTY value");
-is(FileReader.LOADING, 1, "correct LOADING value");
-is(FileReader.DONE, 2, "correct DONE value");
-
-// Create strings containing data we'll test with. We'll want long
-// strings to ensure they span multiple buffers while loading
-var testTextData = "asd b\tlah\u1234w\u00a0r";
-while (testTextData.length < minFileSize) {
-  testTextData = testTextData + testTextData;
-}
-
-var testASCIIData = "abcdef 123456\n";
-while (testASCIIData.length < minFileSize) {
-  testASCIIData = testASCIIData + testASCIIData;
-}
-
-var testBinaryData = "";
-for (var i = 0; i < 256; i++) {
-  testBinaryData += String.fromCharCode(i);
-}
-while (testBinaryData.length < minFileSize) {
-  testBinaryData = testBinaryData + testBinaryData;
-}
-
-var dataurldata0 = testBinaryData.substr(0, testBinaryData.length -
-					 testBinaryData.length % 3);
-var dataurldata1 = testBinaryData.substr(0, testBinaryData.length - 2 -
-					 testBinaryData.length % 3);
-var dataurldata2 = testBinaryData.substr(0, testBinaryData.length - 1 -
-					 testBinaryData.length % 3);
-
-
-//Set up files for testing
-var openerURL = SimpleTest.getTestFileURL("fileapi_chromeScript.js");
-var opener = SpecialPowers.loadChromeScript(openerURL);
-opener.addMessageListener("files.opened", onFilesOpened);
-opener.sendAsyncMessage("files.open", [
-  testASCIIData,
-  testBinaryData,
-  null,
-  convertToUTF8(testTextData),
-  convertToUTF16(testTextData),
-  "",
-  dataurldata0,
-  dataurldata1,
-  dataurldata2,
-]);
-
-function onFilesOpened(message) {
-  let [
-    asciiFile,
-    binaryFile,
-    nonExistingFile,
-    utf8TextFile,
-    utf16TextFile,
-    emptyFile,
-    dataUrlFile0,
-    dataUrlFile1,
-    dataUrlFile2,
-  ] = message;
-
-  // Test that plain reading works and fires events as expected, both
-  // for text and binary reading
-
-  var onloadHasRunText = false;
-  var onloadStartHasRunText = false;
-  r = new FileReader();
-  is(r.readyState, FileReader.EMPTY, "correct initial text readyState");
-  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "plain reading");
-  r.addEventListener("load", function() { onloadHasRunText = true });
-  r.addEventListener("loadstart", function() { onloadStartHasRunText = true });
-  r.readAsText(asciiFile);
-  is(r.readyState, FileReader.LOADING, "correct loading text readyState");
-  is(onloadHasRunText, false, "text loading must be async");
-  is(onloadStartHasRunText, true, "text loadstart should fire sync");
-  expectedTestCount++;
-
-  var onloadHasRunBinary = false;
-  var onloadStartHasRunBinary = false;
-  r = new FileReader();
-  is(r.readyState, FileReader.EMPTY, "correct initial binary readyState");
-  r.addEventListener("load", function() { onloadHasRunBinary = true });
-  r.addEventListener("loadstart", function() { onloadStartHasRunBinary = true });
-  r.readAsBinaryString(binaryFile);
-  r.onload = getLoadHandler(testBinaryData, testBinaryData.length, "binary reading");
-  is(r.readyState, FileReader.LOADING, "correct loading binary readyState");
-  is(onloadHasRunBinary, false, "binary loading must be async");
-  is(onloadStartHasRunBinary, true, "binary loadstart should fire sync");
-  expectedTestCount++;
-
-  var onloadHasRunArrayBuffer = false;
-  var onloadStartHasRunArrayBuffer = false;
-  r = new FileReader();
-  is(r.readyState, FileReader.EMPTY, "correct initial arrayBuffer readyState");
-  r.addEventListener("load", function() { onloadHasRunArrayBuffer = true });
-  r.addEventListener("loadstart", function() { onloadStartHasRunArrayBuffer = true });
-  r.readAsArrayBuffer(binaryFile);
-  r.onload = getLoadHandlerForArrayBuffer(testBinaryData, testBinaryData.length, "array buffer reading");
-  is(r.readyState, FileReader.LOADING, "correct loading arrayBuffer readyState");
-  is(onloadHasRunArrayBuffer, false, "arrayBuffer loading must be async");
-  is(onloadStartHasRunArrayBuffer, true, "arrayBuffer loadstart should fire sync");
-  expectedTestCount++;
-
-  // Test a variety of encodings, and make sure they work properly
-  r = new FileReader();
-  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "no encoding reading");
-  r.readAsText(asciiFile, "");
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "iso8859 reading");
-  r.readAsText(asciiFile, "iso-8859-1");
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.onload = getLoadHandler(testTextData,
-                            convertToUTF8(testTextData).length,
-                            "utf8 reading");
-  r.readAsText(utf8TextFile, "utf8");
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.readAsText(utf16TextFile, "utf-16");
-  r.onload = getLoadHandler(testTextData,
-                            convertToUTF16(testTextData).length,
-                            "utf16 reading");
-  expectedTestCount++;
-
-  // Test get result without reading
-  r = new FileReader();
-  is(r.readyState, FileReader.EMPTY,
-     "readyState in test reader get result without reading");
-  is(r.error, null,
-     "no error in test reader get result without reading");
-  is(r.result, null,
-     "result in test reader get result without reading");
-
-  // Test loading an empty file works (and doesn't crash!)
-  r = new FileReader();
-  r.onload = getLoadHandler("", 0, "empty no encoding reading");
-  r.readAsText(emptyFile, "");
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.onload = getLoadHandler("", 0, "empty utf8 reading");
-  r.readAsText(emptyFile, "utf8");
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.onload = getLoadHandler("", 0, "empty utf16 reading");
-  r.readAsText(emptyFile, "utf-16");
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.onload = getLoadHandler("", 0, "empty binary string reading");
-  r.readAsBinaryString(emptyFile);
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.onload = getLoadHandlerForArrayBuffer("", 0, "empty array buffer reading");
-  r.readAsArrayBuffer(emptyFile);
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.onload = getLoadHandler(convertToDataURL(""), 0, "empt binary string reading");
-  r.readAsDataURL(emptyFile);
-  expectedTestCount++;
-
-  // Test reusing a FileReader to read multiple times
-  r = new FileReader();
-  r.onload = getLoadHandler(testASCIIData,
-                            testASCIIData.length,
-                            "to-be-reused reading text")
-  var makeAnotherReadListener = function(event) {
-    r = event.target;
-    r.removeEventListener("load", makeAnotherReadListener);
-    r.onload = getLoadHandler(testASCIIData,
-                              testASCIIData.length,
-                              "reused reading text");
-    r.readAsText(asciiFile);
-  };
-  r.addEventListener("load", makeAnotherReadListener);
-  r.readAsText(asciiFile);
-  expectedTestCount += 2;
-
-  r = new FileReader();
-  r.onload = getLoadHandler(testBinaryData,
-                            testBinaryData.length,
-                            "to-be-reused reading binary")
-  var makeAnotherReadListener2 = function(event) {
-    r = event.target;
-    r.removeEventListener("load", makeAnotherReadListener2);
-    r.onload = getLoadHandler(testBinaryData,
-                              testBinaryData.length,
-                              "reused reading binary");
-    r.readAsBinaryString(binaryFile);
-  };
-  r.addEventListener("load", makeAnotherReadListener2);
-  r.readAsBinaryString(binaryFile);
-  expectedTestCount += 2;
-
-  r = new FileReader();
-  r.onload = getLoadHandler(convertToDataURL(testBinaryData),
-                            testBinaryData.length,
-                            "to-be-reused reading data url")
-  var makeAnotherReadListener3 = function(event) {
-    r = event.target;
-    r.removeEventListener("load", makeAnotherReadListener3);
-    r.onload = getLoadHandler(convertToDataURL(testBinaryData),
-                              testBinaryData.length,
-                              "reused reading data url");
-    r.readAsDataURL(binaryFile);
-  };
-  r.addEventListener("load", makeAnotherReadListener3);
-  r.readAsDataURL(binaryFile);
-  expectedTestCount += 2;
-
-  r = new FileReader();
-  r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
-                                          testBinaryData.length,
-                                          "to-be-reused reading arrayBuffer")
-  var makeAnotherReadListener4 = function(event) {
-    r = event.target;
-    r.removeEventListener("load", makeAnotherReadListener4);
-    r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
-                                            testBinaryData.length,
-                                            "reused reading arrayBuffer");
-    r.readAsArrayBuffer(binaryFile);
-  };
-  r.addEventListener("load", makeAnotherReadListener4);
-  r.readAsArrayBuffer(binaryFile);
-  expectedTestCount += 2;
-
-  // Test first reading as ArrayBuffer then read as something else
-  // (BinaryString) and doesn't crash
-  r = new FileReader();
-  r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
-                                          testBinaryData.length,
-                                          "to-be-reused reading arrayBuffer")
-  var makeAnotherReadListener5 = function(event) {
-    r = event.target;
-    r.removeEventListener("load", makeAnotherReadListener5);
-    r.onload = getLoadHandler(testBinaryData,
-                              testBinaryData.length,
-                              "reused reading binary string");
-    r.readAsBinaryString(binaryFile);
-  };
-  r.addEventListener("load", makeAnotherReadListener5);
-  r.readAsArrayBuffer(binaryFile);
-  expectedTestCount += 2;
-
-  //Test data-URI encoding on differing file sizes
-  is(dataurldata0.length % 3, 0, "Want to test data with length % 3 == 0");
-  r = new FileReader();
-  r.onload = getLoadHandler(convertToDataURL(dataurldata0),
-                            dataurldata0.length,
-                            "dataurl reading, %3 = 0");
-  r.readAsDataURL(dataUrlFile0);
-  expectedTestCount++;
-
-  is(dataurldata1.length % 3, 1, "Want to test data with length % 3 == 1");
-  r = new FileReader();
-  r.onload = getLoadHandler(convertToDataURL(dataurldata1),
-                            dataurldata1.length,
-                            "dataurl reading, %3 = 1");
-  r.readAsDataURL(dataUrlFile1);
-  expectedTestCount++;
-
-  is(dataurldata2.length % 3, 2, "Want to test data with length % 3 == 2");
-  r = new FileReader();
-  r.onload = getLoadHandler(convertToDataURL(dataurldata2),
-                            dataurldata2.length,
-                            "dataurl reading, %3 = 2");
-  r.readAsDataURL(dataUrlFile2),
-  expectedTestCount++;
-
-
-  // Test abort()
-  var abortHasRun = false;
-  var loadEndHasRun = false;
-  r = new FileReader();
-  r.onabort = function (event) {
-    is(abortHasRun, false, "abort should only fire once");
-    is(loadEndHasRun, false, "loadend shouldn't have fired yet");
-    abortHasRun = true;
-    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
-    is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
-    is(event.target.result, null, "file data should be null on aborted reads");
-  }
-  r.onloadend = function (event) {
-    is(abortHasRun, true, "abort should fire before loadend");
-    is(loadEndHasRun, false, "loadend should only fire once");
-    loadEndHasRun = true;
-    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
-    is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
-    is(event.target.result, null, "file data should be null on aborted reads");
-  }
-  r.onload = function() { ok(false, "load should not fire for aborted reads") };
-  r.onerror = function() { ok(false, "error should not fire for aborted reads") };
-  r.onprogress = function() { ok(false, "progress should not fire for aborted reads") };
-  var abortThrew = false;
-  try {
-    r.abort();
-  } catch(e) {
-    abortThrew = true;
-  }
-  is(abortThrew, false, "abort() doesn't throw");
-  is(abortHasRun, false, "abort() is a no-op unless loading");
-  r.readAsText(asciiFile);
-  r.abort();
-  is(abortHasRun, true, "abort should fire sync");
-  is(loadEndHasRun, true, "loadend should fire sync");
-
-  // Test calling readAsX to cause abort()
-  var reuseAbortHasRun = false;
-  r = new FileReader();
-  r.onabort = function (event) {
-    is(reuseAbortHasRun, false, "abort should only fire once");
-    reuseAbortHasRun = true;
-    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
-    is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
-    is(event.target.result, null, "file data should be null on aborted reads");
-  }
-  r.onload = function() { ok(false, "load should fire for nested reads"); };
-
-  var abortThrew = false;
-  try {
-    r.abort();
-  } catch(e) {
-    abortThrew = true;
-  }
-  is(abortThrew, false, "abort() should not throw");
-  is(reuseAbortHasRun, false, "abort() is a no-op unless loading");
-  r.readAsText(asciiFile);
-
-  var readThrew = false;
-  try {
-    r.readAsText(asciiFile);
-  } catch(e) {
-    readThrew = true;
-  }
-  is(readThrew, true, "readAsText() must throw if loading");
-  is(reuseAbortHasRun, false, "abort should not fire");
-  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "reuse-as-abort reading");
-  expectedTestCount++;
-
-
-  // Test reading from nonexistent files
-  r = new FileReader();
-  var didThrow = false;
-  r.onerror = function (event) {
-    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onerror");
-    is(event.target.error.name, "NotFoundError", "error set to NotFoundError for nonexistent files");
-    is(event.target.result, null, "file data should be null on aborted reads");
-    testHasRun();
-  };
-  r.onload = function (event) {
-    is(false, "nonexistent file shouldn't load! (FIXME: bug 1122788)");
-    testHasRun();
-  };
-  try {
-    r.readAsDataURL(nonExistingFile);
-    expectedTestCount++;
-  } catch(ex) {
-    didThrow = true;
-  }
-  // Once this test passes, we should test that onerror gets called and
-  // that the FileReader object is in the right state during that call.
-  is(didThrow, false, "shouldn't throw when opening nonexistent file, should fire error instead");
-
-
-  function getLoadHandler(expectedResult, expectedLength, testName) {
-    return function (event) {
-      is(event.target.readyState, FileReader.DONE,
-         "readyState in test " + testName);
-      is(event.target.error, null,
-         "no error in test " + testName);
-      is(event.target.result, expectedResult,
-         "result in test " + testName);
-      is(event.lengthComputable, true,
-         "lengthComputable in test " + testName);
-      is(event.loaded, expectedLength,
-         "loaded in test " + testName);
-      is(event.total, expectedLength,
-         "total in test " + testName);
-      testHasRun();
-    }
-  }
-
-  function getLoadHandlerForArrayBuffer(expectedResult, expectedLength, testName) {
-    return function (event) {
-      is(event.target.readyState, FileReader.DONE,
-         "readyState in test " + testName);
-      is(event.target.error, null,
-         "no error in test " +  testName);
-      is(event.lengthComputable, true,
-         "lengthComputable in test " + testName);
-      is(event.loaded, expectedLength,
-         "loaded in test " + testName);
-      is(event.total, expectedLength,
-         "total in test " + testName);
-      is(event.target.result.byteLength, expectedLength,
-         "array buffer size in test " + testName);
-      var u8v = new Uint8Array(event.target.result);
-      is(String.fromCharCode.apply(String, u8v), expectedResult,
-         "array buffer contents in test " + testName);
-      u8v = null;
-      SpecialPowers.gc();
-      is(event.target.result.byteLength, expectedLength,
-         "array buffer size after gc in test " + testName);
-      u8v = new Uint8Array(event.target.result);
-      is(String.fromCharCode.apply(String, u8v), expectedResult,
-         "array buffer contents after gc in test " + testName);
-      testHasRun();
-    }
-  }
-
-  function testHasRun() {
-    //alert(testRanCounter);
-    ++testRanCounter;
-    if (testRanCounter == expectedTestCount) {
-      is(testSetupFinished, true, "test setup should have finished; check for exceptions");
-      is(onloadHasRunText, true, "onload text should have fired by now");
-      is(onloadHasRunBinary, true, "onload binary should have fired by now");
-      opener.destroy();
-      SimpleTest.finish();
-    }
-  }
-
-  testSetupFinished = true;
-}
-
-function convertToUTF16(s) {
-  res = "";
-  for (var i = 0; i < s.length; ++i) {
-    c = s.charCodeAt(i);
-    res += String.fromCharCode(c & 255, c >>> 8);
-  }
-  return res;
-}
-
-function convertToUTF8(s) {
-  return unescape(encodeURIComponent(s));
-}
-
-function convertToDataURL(s) {
-  return "data:application/octet-stream;base64," + btoa(s);
-}
-
-</script>
-</pre>
-</body> </html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/test_fileapi_basic.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for FileReader API</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+  return runBasicTests(data);
+})
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/test_fileapi_basic_worker.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for FileReader API in workers</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+  let worker = new Worker('worker_fileReader.js');
+  worker.postMessage({ tests: 'basic', data });
+
+  worker.onmessage = event => {
+    if (event.data.type == 'finish') {
+      SimpleTest.finish();
+      return;
+    }
+
+    if (event.data.type == 'check') {
+      ok(event.data.status, event.data.msg);
+      return;
+    }
+
+    ok(false, "Unknown message.");
+  }
+});
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/test_fileapi_encoding.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for FileReader API</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+  return runEncodingTests(data);
+})
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/test_fileapi_encoding_worker.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for FileReader API in workers</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+  let worker = new Worker('worker_fileReader.js');
+  worker.postMessage({ tests: 'encoding', data });
+
+  worker.onmessage = event => {
+    if (event.data.type == 'finish') {
+      SimpleTest.finish();
+      return;
+    }
+
+    if (event.data.type == 'check') {
+      ok(event.data.status, event.data.msg);
+      return;
+    }
+
+    ok(false, "Unknown message.");
+  }
+});
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/test_fileapi_other.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for FileReader API</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+  return runOtherTests(data);
+})
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/test_fileapi_other_worker.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for FileReader API in workers</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+  let worker = new Worker('worker_fileReader.js');
+  worker.postMessage({ tests: 'other', data });
+
+  worker.onmessage = event => {
+    if (event.data.type == 'finish') {
+      SimpleTest.finish();
+      return;
+    }
+
+    if (event.data.type == 'check') {
+      ok(event.data.status, event.data.msg);
+      return;
+    }
+
+    ok(false, "Unknown message.");
+  }
+});
+
+</script>
+</body>
+</html>
deleted file mode 100644
--- a/dom/file/tests/test_fileapi_slice.html
+++ /dev/null
@@ -1,167 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=575946
--->
-  <title>Test for Bug 575946</title>
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="text/javascript" src="fileutils.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
-</head>
-
-<body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=575946">Mozilla Bug 575946</a>
-<p id="display">
-  <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas>
-  <canvas id=testcanvas hidden moz-opaque></canvas>
-  <input id="fileList" type="file"></input>
-</p>
-<div id="content" style="display: none">
-</div>
-
-<pre id="test">
-<script class="testbody" type="text/javascript">
-var fileNum = 1;
-SimpleTest.waitForExplicitFinish();
-
-// Create files containing data we'll test with. We'll want long
-// strings to ensure they span multiple buffers while loading
-
-// Create a decent-sized image
-cx = $("canvas").getContext('2d');
-var s = cx.canvas.width;
-var grad = cx.createLinearGradient(0, 0, s-1, s-1);
-for (i = 0; i < 0.95; i += .1) {
-  grad.addColorStop(i, "white");
-  grad.addColorStop(i + .05, "black");
-}
-grad.addColorStop(1, "white");
-cx.fillStyle = grad;
-cx.fillRect(0, 0, s-1, s-1);
-cx.fillStyle = "rgba(200, 0, 0, 0.9)";
-cx.fillRect(.1 * s, .1 * s, .7 * s, .7 * s);
-cx.strokeStyle = "rgba(0, 0, 130, 0.5)";
-cx.lineWidth = .14 * s;
-cx.beginPath();
-cx.arc(.6 * s, .6 * s, .3 * s, 0, Math.PI*2, true);
-cx.stroke();
-cx.closePath();
-cx.fillStyle = "rgb(0, 255, 0)";
-cx.beginPath();
-cx.arc(.1 * s, .8 * s, .1 * s, 0, Math.PI*2, true);
-cx.fill();
-cx.closePath();
-
-
-function imageLoadHandler(event) {
-  var origcanvas = $("canvas");
-  var testcanvas = $("testcanvas");
-  var image = event.target;
-  is(image.naturalWidth, origcanvas.width, "width correct");
-  is(image.naturalHeight, origcanvas.height, "height correct");
-
-  testcanvas.width = origcanvas.width;
-  testcanvas.height = origcanvas.height;
-  testcanvas.getContext("2d").drawImage(image, 0, 0);
-  // Do not use |is(testcanvas.toDataURL("image/png"), origcanvas.toDataURL("image/png"), "...");| that results in a _very_ long line.
-  var origDataURL = origcanvas.toDataURL("image/png");
-  var testDataURL = testcanvas.toDataURL("image/png");
-  is(testDataURL.length, origDataURL.length,
-     "Length of correct image data");
-  ok(testDataURL == origDataURL,
-     "Content of correct image data");
-
-  testHasRun();
-}
-
-var fileData =
-  atob(cx.canvas.toDataURL("image/png").substring("data:text/png;base64,".length + 1));
-var size = fileData.length;
-var testBinaryData = "";
-
-// This might fail if we dramatically improve the png encoder. If that happens
-// please increase the complexity or size of the image generated above to ensure
-// that we're testing with files that are large enough.
-ok(size > 65536, "test data sufficiently large");
-
-SpecialPowers.createFiles([{name: "basicTestFile", data: fileData}],
-                          basicTest);
-
-function basicTest(files) {
-  var fileFile = files[0];
-
-  // Test that basic properties work
-  var memFile = cx.canvas.mozGetAsFile("image/png");
-  testSlice(memFile, size, "image/png", fileData, "memFile");
-  testSlice(fileFile, size, "", fileData, "fileFile");
-
-  // Try loading directly from slice into an image
-  for (var i = 0; i < 256; i++) {
-    testBinaryData += String.fromCharCode(i);
-  }
-  while (testBinaryData.length < 20000) {
-    testBinaryData += testBinaryData;
-  }
-
-  // image in the middle
-  SpecialPowers.createFiles([{name: "middleTestFile",
-                             data: testBinaryData + fileData + testBinaryData}],
-                            imageMiddleTest);
-}
-
-function imageMiddleTest(files) {
-  var imgfile = files[0];
-  is(imgfile.size, size + testBinaryData.length * 2, "correct file size (middle)");
-  var img = new Image;
-  img.src = URL.createObjectURL(imgfile.slice(testBinaryData.length, testBinaryData.length + size));
-  img.onload = imageLoadHandler;
-  expectedTestCount++;
-
-  // image at start
-  SpecialPowers.createFiles([{name: "startTestFile",
-                             data: fileData + testBinaryData}],
-                            imageStartTest);
-}
-
-function imageStartTest(files) {
-  var imgfile = files[0];
-  is(imgfile.size, size + testBinaryData.length, "correct file size (start)");
-  var img = new Image;
-  img.src = URL.createObjectURL(imgfile.slice(0, size));
-  img.onload = imageLoadHandler;
-  expectedTestCount++;
-
-  // image at end
-  SpecialPowers.createFiles([{name: "endTestFile",
-                             data: testBinaryData + fileData}],
-                            imageEndTest);
-}
-
-function imageEndTest(files) {
-  var imgfile = files[0];
-  is(imgfile.size, size + testBinaryData.length, "correct file size (end)");
-  var img = new Image;
-  img.src = URL.createObjectURL(imgfile.slice(testBinaryData.length, testBinaryData.length + size));
-  img.onload = imageLoadHandler;
-  expectedTestCount++;
-
-  // image past end
-  SpecialPowers.createFiles([{name: "pastEndTestFile",
-                             data: testBinaryData + fileData}],
-                            imagePastEndTest);
-}
-
-function imagePastEndTest(files) {
-  var imgfile = files[0];
-  is(imgfile.size, size + testBinaryData.length, "correct file size (past end)");
-  var img = new Image;
-  img.src = URL.createObjectURL(imgfile.slice(testBinaryData.length, testBinaryData.length + size + 1000));
-  img.onload = imageLoadHandler;
-  expectedTestCount++;
-}
-
-</script>
-</pre>
-</body>
-</html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/test_fileapi_slice_image.html
@@ -0,0 +1,139 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for File API + Slice</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="common_blob.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<p id="display">
+  <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas>
+  <canvas id=testcanvas hidden moz-opaque></canvas>
+</p>
+
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(4);
+
+// Create files containing data we'll test with. We'll want long
+// strings to ensure they span multiple buffers while loading
+
+let canvasData;
+let testBinaryData;
+
+function imageLoadHandler(event, resolve) {
+  let origcanvas = $("canvas");
+  let testcanvas = $("testcanvas");
+  let image = event.target;
+  is(image.naturalWidth, origcanvas.width, "width correct");
+  is(image.naturalHeight, origcanvas.height, "height correct");
+
+  testcanvas.width = origcanvas.width;
+  testcanvas.height = origcanvas.height;
+  testcanvas.getContext("2d").drawImage(image, 0, 0);
+  // Do not use |is(testcanvas.toDataURL("image/png"), origcanvas.toDataURL("image/png"), "...");| that results in a _very_ long line.
+  let origDataURL = origcanvas.toDataURL("image/png");
+  let testDataURL = testcanvas.toDataURL("image/png");
+  is(testDataURL.length, origDataURL.length,
+     "Length of correct image data");
+  ok(testDataURL == origDataURL,
+     "Content of correct image data");
+  resolve();
+}
+
+createCanvasURL()
+.then(data => {
+  for (var i = 0; i < 256; i++) {
+    testBinaryData += String.fromCharCode(i);
+  }
+  while (testBinaryData.length < 20000) {
+    testBinaryData += testBinaryData;
+  }
+
+  canvasData = data;
+})
+
+// image in the middle
+.then(() => {
+  return createFile(testBinaryData + canvasData + testBinaryData, "middleTestFile");
+})
+
+// image in the middle - loader
+.then(file => {
+  return new Promise(resolve => {
+    is(file.size, canvasData.length + testBinaryData.length * 2, "correct file size (middle)");
+
+    var img = new Image();
+    img.src = URL.createObjectURL(file.slice(testBinaryData.length,
+                                             testBinaryData.length + canvasData.length));
+    img.onload = event => {
+      imageLoadHandler(event, resolve);
+    }
+  });
+})
+
+// image at start
+.then(() => {
+  return createFile(canvasData + testBinaryData, "startTestFile");
+})
+
+// image at start - loader
+.then(file => {
+  return new Promise(resolve => {
+    is(file.size, canvasData.length + testBinaryData.length, "correct file size (start)");
+
+    var img = new Image();
+    img.src = URL.createObjectURL(file.slice(0, canvasData.length));
+    img.onload = event => {
+      imageLoadHandler(event, resolve);
+    }
+  });
+})
+
+// image at end
+.then(() => {
+  return createFile(testBinaryData + canvasData, "endTestFile");
+})
+
+// image at end - loader
+.then(file => {
+  return new Promise(resolve => {
+    is(file.size, canvasData.length + testBinaryData.length, "correct file size (end)");
+
+    var img = new Image();
+    img.src = URL.createObjectURL(file.slice(testBinaryData.length,
+                                             testBinaryData.length + canvasData.length));
+    img.onload = event => {
+      imageLoadHandler(event, resolve);
+    }
+  });
+})
+
+// image past end
+.then(() => {
+  return createFile(testBinaryData + canvasData, "pastEndTestFile");
+})
+
+// image past end - loader
+.then(file => {
+  return new Promise(resolve => {
+    is(file.size, canvasData.length + testBinaryData.length, "correct file size (end)");
+
+    var img = new Image();
+    img.src = URL.createObjectURL(file.slice(testBinaryData.length,
+                                             testBinaryData.length + canvasData.length + 1000));
+    img.onload = event => {
+      imageLoadHandler(event, resolve);
+    }
+  });
+})
+
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/test_fileapi_slice_memFile_1.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for File API + Slice (in memory)</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="common_blob.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<p id="display">
+  <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas>
+</p>
+
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(4);
+
+createCanvasURL()
+.then(data => {
+  let cx = $("canvas").getContext('2d');
+  let memFile = cx.canvas.mozGetAsFile("image/png");
+  return testSlice(memFile, data.length, "image/png", data, "memFile", RANGE_1);
+})
+
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/test_fileapi_slice_memFile_2.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for File API + Slice (in memory)</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="common_blob.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<p id="display">
+  <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas>
+</p>
+
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(4);
+
+createCanvasURL()
+.then(data => {
+  let cx = $("canvas").getContext('2d');
+  let memFile = cx.canvas.mozGetAsFile("image/png");
+  return testSlice(memFile, data.length, "image/png", data, "memFile", RANGE_2);
+})
+
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/test_fileapi_slice_realFile_1.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for File API + Slice (in file)</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="common_blob.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<p id="display">
+  <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas>
+</p>
+
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(4);
+
+let canvasData;
+
+createCanvasURL()
+.then(data => {
+  canvasData = data;
+  return createFile(data, "basicTestFile1");
+})
+
+.then(file => {
+  return testSlice(file, canvasData.length, "", canvasData, "fileFile", RANGE_1);
+})
+
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/test_fileapi_slice_realFile_2.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for File API + Slice (in file)</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="common_blob.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<p id="display">
+  <canvas id=canvas width=1100 height=1100 hidden moz-opaque></canvas>
+</p>
+
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(4);
+
+let canvasData;
+
+createCanvasURL()
+.then(data => {
+  canvasData = data;
+  return createFile(data, "basicTestFile2");
+})
+
+.then(file => {
+  return testSlice(file, canvasData.length, "", canvasData, "fileFile", RANGE_2);
+})
+
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/test_fileapi_twice.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for FileReader API</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+  return runTwiceTests(data);
+})
+.then(SimpleTest.finish);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/test_fileapi_twice_worker.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for FileReader API in workers</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="common_fileReader.js"></script>
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(3);
+
+test_setup()
+.then(data => {
+  let worker = new Worker('worker_fileReader.js');
+  worker.postMessage({ tests: 'twice', data });
+
+  worker.onmessage = event => {
+    if (event.data.type == 'finish') {
+      SimpleTest.finish();
+      return;
+    }
+
+    if (event.data.type == 'check') {
+      ok(event.data.status, event.data.msg);
+      return;
+    }
+
+    ok(false, "Unknown message.");
+  }
+});
+
+</script>
+</body>
+</html>
--- a/dom/file/tests/test_nonascii_blob_url.html
+++ b/dom/file/tests/test_nonascii_blob_url.html
@@ -1,15 +1,14 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>Test blob URL for non-ascii domain</title>
   <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="text/javascript" src="fileutils.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <div id="content"></div>
 <script class="testbody" type="text/javascript">
 
 var iframe = document.createElement('iframe');
 iframe.src = 'http://xn--exmple-cua.test/tests/dom/file/tests/file_nonascii_blob_url.html';
@@ -20,11 +19,10 @@ iframe.onload = function() {
     SimpleTest.finish();
   }
 }
 
 document.getElementById('content').appendChild(iframe);
 SimpleTest.waitForExplicitFinish();
 
 </script>
-</pre>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/worker_fileReader.js
@@ -0,0 +1,30 @@
+importScripts('common_fileReader.js');
+
+function ok(a, msg) {
+  postMessage({type:'check', msg, status: !!a});
+}
+
+function is(a, b, msg) {
+  ok(a === b, msg);
+}
+
+onmessage = event => {
+  let p;
+
+  if (event.data.tests == 'basic') {
+    p = runBasicTests(event.data.data);
+  } else if (event.data.tests == 'encoding') {
+    p = runEncodingTests(event.data.data);
+  } else if (event.data.tests == 'twice') {
+    p = runTwiceTests(event.data.data);
+  } else if (event.data.tests == 'other') {
+    p = runOtherTests(event.data.data);
+  } else {
+    postMessage({type: 'error'});
+    return;
+  }
+
+  p.then(() => {
+    postMessage({ type: 'finish' });
+  });
+};
--- a/dom/media/TrackUnionStream.cpp
+++ b/dom/media/TrackUnionStream.cpp
@@ -169,34 +169,34 @@ TrackUnionStream::TrackUnionStream()
                                    "input stream %p track %d, desired id %d",
                                    this, aTrack->GetID(), aPort->GetSource(),
                                    aTrack->GetID(),
                                    aPort->GetDestinationTrackId()));
 
     TrackID id;
     if (IsTrackIDExplicit(id = aPort->GetDestinationTrackId())) {
       MOZ_ASSERT(id >= mNextAvailableTrackID &&
-                 mUsedTracks.BinaryIndexOf(id) == mUsedTracks.NoIndex,
+                 !mUsedTracks.ContainsSorted(id),
                  "Desired destination id taken. Only provide a destination ID "
                  "if you can assure its availability, or we may not be able "
                  "to bind to the correct DOM-side track.");
 #ifdef DEBUG
       AutoTArray<MediaInputPort*, 32> inputs(mInputs);
       inputs.AppendElements(mSuspendedInputs);
       for (size_t i = 0; inputs[i] != aPort; ++i) {
         MOZ_ASSERT(inputs[i]->GetSourceTrackId() != TRACK_ANY,
                    "You are adding a MediaInputPort with a track mapping "
                    "while there already exist generic MediaInputPorts for this "
                    "destination stream. This can lead to TrackID collisions!");
       }
 #endif
       mUsedTracks.InsertElementSorted(id);
     } else if ((id = aTrack->GetID()) &&
                id > mNextAvailableTrackID &&
-               mUsedTracks.BinaryIndexOf(id) == mUsedTracks.NoIndex) {
+               !mUsedTracks.ContainsSorted(id)) {
       // Input id available. Mark it used in mUsedTracks.
       mUsedTracks.InsertElementSorted(id);
     } else {
       // No desired destination id and Input id taken, allocate a new one.
       id = mNextAvailableTrackID;
 
       // Update mNextAvailableTrackID and prune any mUsedTracks members it now
       // covers.
--- a/dom/media/webrtc/MediaEngineWebRTC.cpp
+++ b/dom/media/webrtc/MediaEngineWebRTC.cpp
@@ -106,16 +106,18 @@ void AudioInputCubeb::UpdateDeviceList()
   mDevices = devices;
 }
 
 MediaEngineWebRTC::MediaEngineWebRTC(MediaEnginePrefs &aPrefs)
   : mMutex("mozilla::MediaEngineWebRTC"),
     mVoiceEngine(nullptr),
     mAudioInput(nullptr),
     mFullDuplex(aPrefs.mFullDuplex),
+    mDelayAgnostic(aPrefs.mDelayAgnostic),
+    mExtendedFilter(aPrefs.mExtendedFilter),
     mHasTabVideoSource(false)
 {
   nsCOMPtr<nsIComponentRegistrar> compMgr;
   NS_GetComponentRegistrar(getter_AddRefs(compMgr));
   if (compMgr) {
     compMgr->IsContractIDRegistered(NS_TABSOURCESERVICE_CONTRACTID, &mHasTabVideoSource);
   }
 
@@ -289,17 +291,17 @@ MediaEngineWebRTC::EnumerateAudioDevices
 
   if (webrtc::VoiceEngine::SetAndroidObjects(jvm, (void*)context) != 0) {
     LOG(("VoiceEngine:SetAndroidObjects Failed"));
     return;
   }
 #endif
 
   if (!mVoiceEngine) {
-    mVoiceEngine = webrtc::VoiceEngine::Create(/*mConfig*/);
+    mVoiceEngine = webrtc::VoiceEngine::Create();
     if (!mVoiceEngine) {
       return;
     }
   }
 
   ptrVoEBase = webrtc::VoEBase::GetInterface(mVoiceEngine);
   if (!ptrVoEBase) {
     return;
@@ -362,17 +364,18 @@ MediaEngineWebRTC::EnumerateAudioDevices
         // The platform_supports_full_duplex.
 
         // For cubeb, it has state (the selected ID)
         // XXX just use the uniqueID for cubeb and support it everywhere, and get rid of this
         // XXX Small window where the device list/index could change!
         audioinput = new mozilla::AudioInputCubeb(mVoiceEngine, i);
       }
       aSource = new MediaEngineWebRTCMicrophoneSource(mVoiceEngine, audioinput,
-                                                      i, deviceName, uniqueId);
+                                                      i, deviceName, uniqueId,
+                                                      mDelayAgnostic, mExtendedFilter);
       mAudioSources.Put(uuid, aSource); // Hashtable takes ownership.
       aASources->AppendElement(aSource);
     }
   }
 }
 
 void
 MediaEngineWebRTC::Shutdown()
--- a/dom/media/webrtc/MediaEngineWebRTC.h
+++ b/dom/media/webrtc/MediaEngineWebRTC.h
@@ -494,17 +494,19 @@ class MediaEngineWebRTCMicrophoneSource 
                                           public webrtc::VoEMediaProcess
 {
   typedef MediaEngineAudioSource Super;
 public:
   MediaEngineWebRTCMicrophoneSource(webrtc::VoiceEngine* aVoiceEnginePtr,
                                     mozilla::AudioInput* aAudioInput,
                                     int aIndex,
                                     const char* name,
-                                    const char* uuid);
+                                    const char* uuid,
+                                    bool aDelayAgnostic,
+                                    bool aExtendedFilter);
 
   void GetName(nsAString& aName) const override;
   void GetUUID(nsACString& aUUID) const override;
 
   nsresult Deallocate(AllocationHandle* aHandle) override;
   nsresult Start(SourceMediaStream* aStream,
                  TrackID aID,
                  const PrincipalHandle& aPrincipalHandle) override;
@@ -616,16 +618,18 @@ private:
   // EndTrack()). mSources[] and mPrincipalHandles[] are accessed from webrtc
   // threads.
   Monitor mMonitor;
   nsTArray<RefPtr<SourceMediaStream>> mSources;
   nsTArray<PrincipalHandle> mPrincipalHandles; // Maps to mSources.
 
   int mCapIndex;
   int mChannel;
+  bool mDelayAgnostic;
+  bool mExtendedFilter;
   MOZ_INIT_OUTSIDE_CTOR TrackID mTrackID;
   bool mStarted;
 
   nsString mDeviceName;
   nsCString mDeviceUUID;
 
   int32_t mSampleFrequency;
   uint64_t mTotalFrames;
@@ -670,16 +674,18 @@ private:
 
   nsCOMPtr<nsIThread> mThread;
 
   // gUM runnables can e.g. Enumerate from multiple threads
   Mutex mMutex;
   webrtc::VoiceEngine* mVoiceEngine;
   RefPtr<mozilla::AudioInput> mAudioInput;
   bool mFullDuplex;
+  bool mDelayAgnostic;
+  bool mExtendedFilter;
   bool mHasTabVideoSource;
 
   // Store devices we've already seen in a hashtable for quick return.
   // Maps UUID to MediaEngineSource (one set for audio, one for video).
   nsRefPtrHashtable<nsStringHashKey, MediaEngineVideoSource> mVideoSources;
   nsRefPtrHashtable<nsStringHashKey, MediaEngineAudioSource> mAudioSources;
 };
 
--- a/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
+++ b/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
@@ -186,23 +186,27 @@ AudioOutputObserver::InsertFarEnd(const 
   }
 }
 
 MediaEngineWebRTCMicrophoneSource::MediaEngineWebRTCMicrophoneSource(
     webrtc::VoiceEngine* aVoiceEnginePtr,
     mozilla::AudioInput* aAudioInput,
     int aIndex,
     const char* name,
-    const char* uuid)
+    const char* uuid,
+    bool aDelayAgnostic,
+    bool aExtendedFilter)
   : MediaEngineAudioSource(kReleased)
   , mVoiceEngine(aVoiceEnginePtr)
   , mAudioInput(aAudioInput)
   , mMonitor("WebRTCMic.Monitor")
   , mCapIndex(aIndex)
   , mChannel(-1)
+  , mDelayAgnostic(aDelayAgnostic)
+  , mExtendedFilter(aExtendedFilter)
   , mTrackID(TRACK_NONE)
   , mStarted(false)
   , mSampleFrequency(MediaEngine::DEFAULT_SAMPLE_RATE)
   , mTotalFrames(0)
   , mLastLogFrames(0)
   , mPlayoutDelay(0)
   , mNullTransport(nullptr)
   , mSkipProcessing(false)
@@ -776,16 +780,20 @@ MediaEngineWebRTCMicrophoneSource::Devic
 
 bool
 MediaEngineWebRTCMicrophoneSource::InitEngine()
 {
   MOZ_ASSERT(!mVoEBase);
   mVoEBase = webrtc::VoEBase::GetInterface(mVoiceEngine);
 
   mVoEBase->Init();
+  webrtc::Config config;
+  config.Set<webrtc::ExtendedFilter>(new webrtc::ExtendedFilter(mExtendedFilter));
+  config.Set<webrtc::DelayAgnostic>(new webrtc::DelayAgnostic(mDelayAgnostic));
+  mVoEBase->audio_processing()->SetExtraOptions(config);
 
   mVoERender = webrtc::VoEExternalMedia::GetInterface(mVoiceEngine);
   if (mVoERender) {
     mVoENetwork = webrtc::VoENetwork::GetInterface(mVoiceEngine);
     if (mVoENetwork) {
       mVoEProcessing = webrtc::VoEAudioProcessing::GetInterface(mVoiceEngine);
       if (mVoEProcessing) {
         mNullTransport = new NullTransport();
--- a/dom/performance/PerformanceTiming.cpp
+++ b/dom/performance/PerformanceTiming.cpp
@@ -66,32 +66,37 @@ PerformanceTiming::PerformanceTiming(Per
 
 // Copy the timing info from the channel so we don't need to keep the channel
 // alive just to get the timestamps.
 void
 PerformanceTiming::InitializeTimingInfo(nsITimedChannel* aChannel)
 {
   if (aChannel) {
     aChannel->GetAsyncOpen(&mAsyncOpen);
-    aChannel->GetDispatchFetchEventStart(&mWorkerStart);
     aChannel->GetAllRedirectsSameOrigin(&mAllRedirectsSameOrigin);
     aChannel->GetRedirectCount(&mRedirectCount);
     aChannel->GetRedirectStart(&mRedirectStart);
     aChannel->GetRedirectEnd(&mRedirectEnd);
     aChannel->GetDomainLookupStart(&mDomainLookupStart);
     aChannel->GetDomainLookupEnd(&mDomainLookupEnd);
     aChannel->GetConnectStart(&mConnectStart);
     aChannel->GetSecureConnectionStart(&mSecureConnectionStart);
     aChannel->GetConnectEnd(&mConnectEnd);
     aChannel->GetRequestStart(&mRequestStart);
     aChannel->GetResponseStart(&mResponseStart);
     aChannel->GetCacheReadStart(&mCacheReadStart);
     aChannel->GetResponseEnd(&mResponseEnd);
     aChannel->GetCacheReadEnd(&mCacheReadEnd);
 
+    aChannel->GetDispatchFetchEventStart(&mWorkerStart);
+    aChannel->GetHandleFetchEventStart(&mWorkerRequestStart);
+    // TODO: Track when FetchEvent.respondWith() promise resolves as
+    //       ServiceWorker interception responseStart?
+    aChannel->GetHandleFetchEventEnd(&mWorkerResponseEnd);
+
     // The performance timing api essentially requires that the event timestamps
     // have a strict relation with each other. The truth, however, is the browser
     // engages in a number of speculative activities that sometimes mean connections
     // and lookups begin at different times. Workaround that here by clamping
     // these values to what we expect FetchStart to be.  This means the later of
     // AsyncOpen or WorkerStart times.
     if (!mAsyncOpen.IsNull()) {
       // We want to clamp to the expected FetchStart value.  This is later of
@@ -394,16 +399,21 @@ PerformanceTiming::ConnectEnd()
 
 DOMHighResTimeStamp
 PerformanceTiming::RequestStartHighRes()
 {
   if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() ||
       nsContentUtils::ShouldResistFingerprinting()) {
     return mZeroTime;
   }
+
+  if (mRequestStart.IsNull()) {
+    mRequestStart = mWorkerRequestStart;
+  }
+
   return TimeStampToDOMHighResOrFetchStart(mRequestStart);
 }
 
 DOMTimeMilliSec
 PerformanceTiming::RequestStart()
 {
   return static_cast<int64_t>(RequestStartHighRes());
 }
@@ -439,16 +449,19 @@ PerformanceTiming::ResponseEndHighRes()
   if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() ||
       nsContentUtils::ShouldResistFingerprinting()) {
     return mZeroTime;
   }
   if (mResponseEnd.IsNull() ||
      (!mCacheReadEnd.IsNull() && mCacheReadEnd < mResponseEnd)) {
     mResponseEnd = mCacheReadEnd;
   }
+  if (mResponseEnd.IsNull()) {
+    mResponseEnd = mWorkerResponseEnd;
+  }
   // Bug 1155008 - nsHttpTransaction is racy. Return ResponseStart when null
   return mResponseEnd.IsNull() ? ResponseStartHighRes()
                                : TimeStampToDOMHighRes(mResponseEnd);
 }
 
 DOMTimeMilliSec
 PerformanceTiming::ResponseEnd()
 {
--- a/dom/performance/PerformanceTiming.h
+++ b/dom/performance/PerformanceTiming.h
@@ -273,29 +273,34 @@ private:
 
   // This is an offset that will be added to each timing ([ms] resolution).
   // There are only 2 possible values: (1) logicaly equal to navigationStart
   // TimeStamp (results are absolute timstamps - wallclock); (2) "0" (results
   // are relative to the navigation start).
   DOMHighResTimeStamp mZeroTime;
 
   TimeStamp mAsyncOpen;
-  TimeStamp mWorkerStart;
   TimeStamp mRedirectStart;
   TimeStamp mRedirectEnd;
   TimeStamp mDomainLookupStart;
   TimeStamp mDomainLookupEnd;
   TimeStamp mConnectStart;
   TimeStamp mSecureConnectionStart;
   TimeStamp mConnectEnd;
   TimeStamp mRequestStart;
   TimeStamp mResponseStart;
   TimeStamp mCacheReadStart;
   TimeStamp mResponseEnd;
   TimeStamp mCacheReadEnd;
+
+  // ServiceWorker interception timing information
+  TimeStamp mWorkerStart;
+  TimeStamp mWorkerRequestStart;
+  TimeStamp mWorkerResponseEnd;
+
   uint8_t mRedirectCount;
   bool mTimingAllowed;
   bool mAllRedirectsSameOrigin;
   bool mInitialized;
 
   // If the resourceTiming object should have non-zero redirectStart and
   // redirectEnd attributes. It is false if there were no redirects, or if
   // any of the responses didn't pass the timing-allow-check
--- a/dom/quota/ActorsParent.cpp
+++ b/dom/quota/ActorsParent.cpp
@@ -119,33 +119,56 @@ const char QuotaManager::kReplaceChars[]
 namespace {
 
 /*******************************************************************************
  * Constants
  ******************************************************************************/
 
 const uint32_t kSQLitePageSizeOverride = 512;
 
+
+const uint32_t kHackyDowngradeMajorStorageVersion = 2;
+const uint32_t kHackyDowngradeMinorStorageVersion = 1;
+
+// Important version history:
+// - Bug 1290481 bumped our schema from major.minor 2.0 to 3.0 in Firefox 57
+//   which caused Firefox 57 release concerns because the major schema upgrade
+//   means anyone downgrading to Firefox 56 will experience a non-operational
+//   QuotaManager and all of its clients.
+// - Bug 1404344 got very concerned about that and so we decided to effectively
+//   rename 3.0 to 2.1, effective in Firefox 57.  This works because post
+//   storage.sqlite v1.0, QuotaManager doesn't care about minor storage version
+//   increases.  It also works because all the upgrade did was give the DOM
+//   Cache API QuotaClient an opportunity to create its newly added .padding
+//   files during initialization/upgrade, which isn't functionally necessary as
+//   that can be done on demand.
+
 // Major storage version. Bump for backwards-incompatible changes.
-const uint32_t kMajorStorageVersion = 3;
+// (The next major version should be 4 to distinguish from the Bug 1290481
+// downgrade snafu.)
+const uint32_t kMajorStorageVersion = 2;
 
 // Minor storage version. Bump for backwards-compatible changes.
-const uint32_t kMinorStorageVersion = 0;
+const uint32_t kMinorStorageVersion = 1;
 
 // The storage version we store in the SQLite database is a (signed) 32-bit
 // integer. The major version is left-shifted 16 bits so the max value is
 // 0xFFFF. The minor version occupies the lower 16 bits and its max is 0xFFFF.
 static_assert(kMajorStorageVersion <= 0xFFFF,
               "Major version needs to fit in 16 bits.");
 static_assert(kMinorStorageVersion <= 0xFFFF,
               "Minor version needs to fit in 16 bits.");
 
 const int32_t kStorageVersion =
   int32_t((kMajorStorageVersion << 16) + kMinorStorageVersion);
 
+// See comments above about why these are a thing.
+const int32_t kHackyPreDowngradeStorageVersion = int32_t((3 << 16) + 0);
+const int32_t kHackyPostDowngradeStorageVersion = int32_t((2 << 16) + 1);
+
 static_assert(
   static_cast<uint32_t>(StorageType::Persistent) ==
   static_cast<uint32_t>(PERSISTENCE_TYPE_PERSISTENT),
   "Enum values should match.");
 
 static_assert(
   static_cast<uint32_t>(StorageType::Temporary) ==
   static_cast<uint32_t>(PERSISTENCE_TYPE_TEMPORARY),
@@ -1812,21 +1835,21 @@ private:
 
   nsresult
   ProcessOriginDirectory(const OriginProps& aOriginProps) override;
 };
 
 // XXXtt: The following class is duplicated from
 // UpgradeStorageFrom1_0To2_0Helper and it should be extracted out in
 // bug 1395102.
-class UpgradeStorageFrom2_0To3_0Helper final
+class UpgradeStorageFrom2_0To2_1Helper final
   : public StorageDirectoryHelper
 {
 public:
-  UpgradeStorageFrom2_0To3_0Helper(nsIFile* aDirectory,
+  UpgradeStorageFrom2_0To2_1Helper(nsIFile* aDirectory,
                                    bool aPersistent)
     : StorageDirectoryHelper(aDirectory, aPersistent)
   { }
 
   nsresult
   DoUpgrade();
 
 private:
@@ -4808,17 +4831,17 @@ QuotaManager::UpgradeStorageFrom1_0To2_0
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
 nsresult
-QuotaManager::UpgradeStorageFrom2_0To3_0(mozIStorageConnection* aConnection)
+QuotaManager::UpgradeStorageFrom2_0To2_1(mozIStorageConnection* aConnection)
 {
   AssertIsOnIOThread();
   MOZ_ASSERT(aConnection);
 
   // The upgrade is mainly to create a directory padding file in DOM Cache
   // directory to record the overall padding size of an origin.
 
   nsresult rv;
@@ -4841,18 +4864,18 @@ QuotaManager::UpgradeStorageFrom2_0To3_0
       return rv;
     }
 
     if (!exists) {
       continue;
     }
 
     bool persistent = persistenceType == PERSISTENCE_TYPE_PERSISTENT;
-    RefPtr<UpgradeStorageFrom2_0To3_0Helper> helper =
-      new UpgradeStorageFrom2_0To3_0Helper(directory, persistent);
+    RefPtr<UpgradeStorageFrom2_0To2_1Helper> helper =
+      new UpgradeStorageFrom2_0To2_1Helper(directory, persistent);
 
     rv = helper->DoUpgrade();
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
   }
 
 #ifdef DEBUG
@@ -4862,17 +4885,17 @@ QuotaManager::UpgradeStorageFrom2_0To3_0
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
     MOZ_ASSERT(storageVersion == MakeStorageVersion(2, 0));
   }
 #endif
 
-  rv = aConnection->SetSchemaVersion(MakeStorageVersion(3, 0));
+  rv = aConnection->SetSchemaVersion(MakeStorageVersion(2, 1));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
 #ifdef DEBUG
@@ -4945,16 +4968,27 @@ QuotaManager::EnsureStorageIsInitialized
 
   // Check to make sure that the storage version is correct.
   int32_t storageVersion;
   rv = connection->GetSchemaVersion(&storageVersion);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
+  // Hacky downgrade logic!
+  // If we see major.minor of 3.0, downgrade it to be 2.1.
+  if (storageVersion == kHackyPreDowngradeStorageVersion) {
+    storageVersion = kHackyPostDowngradeStorageVersion;
+    rv = connection->SetSchemaVersion(storageVersion);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      MOZ_ASSERT(false, "Downgrade didn't take.");
+      return rv;
+    }
+  }
+
   if (GetMajorStorageVersion(storageVersion) > kMajorStorageVersion) {
     NS_WARNING("Unable to initialize storage, version is too high!");
     return NS_ERROR_FAILURE;
   }
 
   if (storageVersion < kStorageVersion) {
     const bool newDatabase = !storageVersion;
 
@@ -5018,26 +5052,26 @@ QuotaManager::EnsureStorageIsInitialized
       if (NS_WARN_IF(NS_FAILED(rv))) {
         return rv;
       }
 
       MOZ_ASSERT(NS_SUCCEEDED(connection->GetSchemaVersion(&storageVersion)));
       MOZ_ASSERT(storageVersion == kStorageVersion);
     } else {
       // This logic needs to change next time we change the storage!
-      static_assert(kStorageVersion == int32_t((3 << 16) + 0),
+      static_assert(kStorageVersion == int32_t((2 << 16) + 1),
                     "Upgrade function needed due to storage version increase.");
 
       while (storageVersion != kStorageVersion) {
         if (storageVersion == 0) {
           rv = UpgradeStorageFrom0_0To1_0(connection);
         } else if (storageVersion == MakeStorageVersion(1, 0)) {
           rv = UpgradeStorageFrom1_0To2_0(connection);
         } else if (storageVersion == MakeStorageVersion(2, 0)) {
-          rv = UpgradeStorageFrom2_0To3_0(connection);
+          rv = UpgradeStorageFrom2_0To2_1(connection);
         } else {
           NS_WARNING("Unable to initialize storage, no upgrade path is "
                      "available!");
           return NS_ERROR_FAILURE;
         }
 
         if (NS_WARN_IF(NS_FAILED(rv))) {
           return rv;
@@ -9391,17 +9425,17 @@ UpgradeStorageFrom1_0To2_0Helper::Proces
       return rv;
     }
   }
 
   return NS_OK;
 }
 
 nsresult
-UpgradeStorageFrom2_0To3_0Helper::DoUpgrade()
+UpgradeStorageFrom2_0To2_1Helper::DoUpgrade()
 {
   AssertIsOnIOThread();
 
   DebugOnly<bool> exists;
   MOZ_ASSERT(NS_SUCCEEDED(mDirectory->Exists(&exists)));
   MOZ_ASSERT(exists);
 
   nsCOMPtr<nsISimpleEnumerator> entries;
@@ -9494,17 +9528,17 @@ UpgradeStorageFrom2_0To3_0Helper::DoUpgr
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
 nsresult
-UpgradeStorageFrom2_0To3_0Helper::MaybeUpgradeClients(
+UpgradeStorageFrom2_0To2_1Helper::MaybeUpgradeClients(
                                                 const OriginProps& aOriginProps)
 {
   AssertIsOnIOThread();
   MOZ_ASSERT(aOriginProps.mDirectory);
 
   QuotaManager* quotaManager = QuotaManager::Get();
   MOZ_ASSERT(quotaManager);
 
@@ -9554,30 +9588,30 @@ UpgradeStorageFrom2_0To3_0Helper::MaybeU
     if (NS_FAILED(rv)) {
       UNKNOWN_FILE_WARNING(leafName);
       continue;
     }
 
     Client* client = quotaManager->GetClient(clientType);
     MOZ_ASSERT(client);
 
-    rv = client->UpgradeStorageFrom2_0To3_0(file);
+    rv = client->UpgradeStorageFrom2_0To2_1(file);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
   }
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
 nsresult
-UpgradeStorageFrom2_0To3_0Helper::ProcessOriginDirectory(
+UpgradeStorageFrom2_0To2_1Helper::ProcessOriginDirectory(
                                                 const OriginProps& aOriginProps)
 {
   AssertIsOnIOThread();
 
   nsresult rv;
 
   if (aOriginProps.mNeedsRestore) {
     rv = CreateDirectoryMetadata(aOriginProps.mDirectory,
--- a/dom/quota/Client.h
+++ b/dom/quota/Client.h
@@ -94,17 +94,17 @@ public:
   // Methods which are called on the IO thread.
   virtual nsresult
   UpgradeStorageFrom1_0To2_0(nsIFile* aDirectory)
   {
     return NS_OK;
   }
 
   virtual nsresult
-  UpgradeStorageFrom2_0To3_0(nsIFile* aDirectory)
+  UpgradeStorageFrom2_0To2_1(nsIFile* aDirectory)
   {
     return NS_OK;
   }
 
   virtual nsresult
   InitOrigin(PersistenceType aPersistenceType,
              const nsACString& aGroup,
              const nsACString& aOrigin,
--- a/dom/quota/QuotaManager.h
+++ b/dom/quota/QuotaManager.h
@@ -472,17 +472,17 @@ private:
 
   nsresult
   UpgradeStorageFrom0_0To1_0(mozIStorageConnection* aConnection);
 
   nsresult
   UpgradeStorageFrom1_0To2_0(mozIStorageConnection* aConnection);
 
   nsresult
-  UpgradeStorageFrom2_0To3_0(mozIStorageConnection* aConnection);
+  UpgradeStorageFrom2_0To2_1(mozIStorageConnection* aConnection);
 
   nsresult
   InitializeRepository(PersistenceType aPersistenceType);
 
   nsresult
   InitializeOrigin(PersistenceType aPersistenceType,
                    const nsACString& aGroup,
                    const nsACString& aOrigin,
rename from dom/quota/test/unit/test_version3_0upgrade.js
rename to dom/quota/test/unit/test_version2_1upgrade.js
--- a/dom/quota/test/unit/test_version3_0upgrade.js
+++ b/dom/quota/test/unit/test_version2_1upgrade.js
@@ -25,36 +25,36 @@ function* testSteps()
   // was temporarily added to xpcshell.ini and then executed:
   //   mach xpcshell-test --interactive dom/quota/test/unit/create_cache.js
   // Note: it only creates the directory "storage/default/chrome/cache".
   // To make it become the profile in the test, two more manual steps are
   // needed.
   // 1. Remove the folder "storage/temporary".
   // 2. Copy the content under the "storage/default/chrome" to
   //    "storage/default/http+++www.mozilla.org".
-  installPackage("version3_0upgrade_profile");
+  installPackage("version2_1upgrade_profile");
 
   info("Checking padding file before upgrade (QM version 2.0)");
 
   for (let origin of origins) {
     let paddingFile = getRelativeFile(origin + paddingFilePath);
 
     let exists = paddingFile.exists();
     ok(!exists, "Padding file doesn't exist");
   }
 
   info("Initializing");
 
-  // Initialize QuotaManager to trigger upgrade the QM to version 3.0
+  // Initialize QuotaManager to trigger upgrade the QM to version 2.1
   let request = init(continueToNextStepSync);
   yield undefined;
 
   ok(request.resultCode == NS_OK, "Initialization succeeded");
 
-  info("Checking padding files after upgrade (QM version 3.0)");
+  info("Checking padding files after upgrade (QM version 2.1)");
 
   for (let origin of origins) {
     let paddingFile = getRelativeFile(origin + paddingFilePath);
 
     let exists = paddingFile.exists();
     ok(exists, "Padding file does exist");
 
     info("Reading out contents of padding file");
rename from dom/quota/test/unit/version3_0upgrade_profile.zip
rename to dom/quota/test/unit/version2_1upgrade_profile.zip
--- a/dom/quota/test/unit/xpcshell.ini
+++ b/dom/quota/test/unit/xpcshell.ini
@@ -11,25 +11,25 @@ support-files =
   idbSubdirUpgrade1_profile.zip
   idbSubdirUpgrade2_profile.zip
   morgueCleanup_profile.zip
   obsoleteOriginAttributes_profile.zip
   originAttributesUpgrade_profile.zip
   removeAppsUpgrade_profile.zip
   storagePersistentUpgrade_profile.zip
   tempMetadataCleanup_profile.zip
-  version3_0upgrade_profile.zip
+  version2_1upgrade_profile.zip
 
 [test_basics.js]
 [test_bad_origin_directory.js]
 skip-if = release_or_beta
 [test_defaultStorageUpgrade.js]
 [test_getUsage.js]
 [test_idbSubdirUpgrade.js]
 [test_morgueCleanup.js]
 [test_obsoleteOriginAttributesUpgrade.js]
 [test_originAttributesUpgrade.js]
 [test_persist.js]
 [test_removeAppsUpgrade.js]
 [test_storagePersistentUpgrade.js]
 [test_tempMetadataCleanup.js]
 [test_unknownFiles.js]
-[test_version3_0upgrade.js]
+[test_version2_1upgrade.js]
new file mode 100644
--- /dev/null
+++ b/dom/smil/crashtests/1322849-1.svg
@@ -0,0 +1,2 @@
+<svg>
+<set fill='freeze' dur='8' repeatCount='1844674737095516'>
--- a/dom/smil/crashtests/crashtests.list
+++ b/dom/smil/crashtests/crashtests.list
@@ -47,10 +47,11 @@ load 678938-1.svg
 load 690994-1.svg
 load 691337-1.svg
 load 691337-2.svg
 load 697640-1.svg
 load 699325-1.svg
 load 709907-1.svg
 load 720103-1.svg
 load 1010681-1.svg
+load 1322849-1.svg
 load 1375596-1.svg
 load 1402547-1.html
--- a/dom/smil/nsSMILTimeValue.cpp
+++ b/dom/smil/nsSMILTimeValue.cpp
@@ -1,17 +1,18 @@
 /* -*- 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 "nsSMILTimeValue.h"
 
-const nsSMILTime nsSMILTimeValue::kUnresolvedMillis = INT64_MAX;
+const nsSMILTime nsSMILTimeValue::kUnresolvedMillis =
+  std::numeric_limits<nsSMILTime>::max();
 
 //----------------------------------------------------------------------
 // nsSMILTimeValue methods:
 
 static inline int8_t
 Cmp(int64_t aA, int64_t aB)
 {
   return aA == aB ? 0 : (aA > aB ? 1 : -1);
--- a/dom/smil/nsSMILTimedElement.cpp
+++ b/dom/smil/nsSMILTimedElement.cpp
@@ -225,17 +225,18 @@ const nsAttrValue::EnumTable nsSMILTimed
 
 const nsAttrValue::EnumTable nsSMILTimedElement::sRestartModeTable[] = {
       {"always", RESTART_ALWAYS},
       {"whenNotActive", RESTART_WHENNOTACTIVE},
       {"never", RESTART_NEVER},
       {nullptr, 0}
 };
 
-const nsSMILMilestone nsSMILTimedElement::sMaxMilestone(INT64_MAX, false);
+const nsSMILMilestone nsSMILTimedElement::sMaxMilestone(
+  std::numeric_limits<nsSMILTime>::max(), false);
 
 // The thresholds at which point we start filtering intervals and instance times
 // indiscriminately.
 // See FilterIntervals and FilterInstanceTimes.
 const uint8_t nsSMILTimedElement::sMaxNumIntervals = 20;
 const uint8_t nsSMILTimedElement::sMaxNumInstanceTimes = 100;
 
 // Detect if we arrive in some sort of undetected recursive syncbase dependency
@@ -1922,18 +1923,21 @@ nsSMILTimedElement::CalcActiveEnd(const 
   return result;
 }
 
 nsSMILTimeValue
 nsSMILTimedElement::GetRepeatDuration() const
 {
   nsSMILTimeValue multipliedDuration;
   if (mRepeatCount.IsDefinite() && mSimpleDur.IsDefinite()) {
-    multipliedDuration.SetMillis(
-      nsSMILTime(mRepeatCount * double(mSimpleDur.GetMillis())));
+    if (mRepeatCount * double(mSimpleDur.GetMillis()) <=
+        std::numeric_limits<nsSMILTime>::max()) {
+      multipliedDuration.SetMillis(
+        nsSMILTime(mRepeatCount * mSimpleDur.GetMillis()));
+    }
   } else {
     multipliedDuration.SetIndefinite();
   }
 
   nsSMILTimeValue repeatDuration;
 
   if (mRepeatDur.IsResolved()) {
     repeatDuration = std::min(multipliedDuration, mRepeatDur);
@@ -2202,23 +2206,23 @@ nsSMILTimedElement::SampleFillValue()
     mClient->SampleAt(simpleTime, mSimpleDur, repeatIteration);
   }
 }
 
 nsresult
 nsSMILTimedElement::AddInstanceTimeFromCurrentTime(nsSMILTime aCurrentTime,
     double aOffsetSeconds, bool aIsBegin)
 {
-  double offset = aOffsetSeconds * PR_MSEC_PER_SEC;
+  double offset = NS_round(aOffsetSeconds * PR_MSEC_PER_SEC);
 
   // Check we won't overflow the range of nsSMILTime
-  if (aCurrentTime + NS_round(offset) > INT64_MAX)
+  if (aCurrentTime + offset > std::numeric_limits<nsSMILTime>::max())
     return NS_ERROR_ILLEGAL_VALUE;
 
-  nsSMILTimeValue timeVal(aCurrentTime + int64_t(NS_round(offset)));
+  nsSMILTimeValue timeVal(aCurrentTime + int64_t(offset));
 
   RefPtr<nsSMILInstanceTime> instanceTime =
     new nsSMILInstanceTime(timeVal, nsSMILInstanceTime::SOURCE_DOM);
 
   AddInstanceTime(instanceTime, aIsBegin);
 
   return NS_OK;
 }
--- a/dom/workers/ServiceWorkerEvents.cpp
+++ b/dom/workers/ServiceWorkerEvents.cpp
@@ -159,36 +159,140 @@ FetchEvent::Constructor(const GlobalObje
   e->mRequest = aOptions.mRequest;
   e->mClientId = aOptions.mClientId;
   e->mIsReload = aOptions.mIsReload;
   return e.forget();
 }
 
 namespace {
 
+struct RespondWithClosure
+{
+  nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel;
+  nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration;
+  const nsString mRequestURL;
+  const nsCString mRespondWithScriptSpec;
+  const uint32_t mRespondWithLineNumber;
+  const uint32_t mRespondWithColumnNumber;
+
+  RespondWithClosure(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
+                     nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration,
+                     const nsAString& aRequestURL,
+                     const nsACString& aRespondWithScriptSpec,
+                     uint32_t aRespondWithLineNumber,
+                     uint32_t aRespondWithColumnNumber)
+    : mInterceptedChannel(aChannel)
+    , mRegistration(aRegistration)
+    , mRequestURL(aRequestURL)
+    , mRespondWithScriptSpec(aRespondWithScriptSpec)
+    , mRespondWithLineNumber(aRespondWithLineNumber)
+    , mRespondWithColumnNumber(aRespondWithColumnNumber)
+  {
+  }
+};
+
 class FinishResponse final : public Runnable
 {
   nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel;
+
+public:
+  explicit FinishResponse(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel)
+    : Runnable("dom::workers::FinishResponse")
+    , mChannel(aChannel)
+  {
+  }
+
+  NS_IMETHOD
+  Run() override
+  {
+    AssertIsOnMainThread();
+
+    nsresult rv = mChannel->FinishSynthesizedResponse();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      mChannel->CancelInterception(NS_ERROR_INTERCEPTION_FAILED);
+      return NS_OK;
+    }
+
+    TimeStamp timeStamp = TimeStamp::Now();
+    mChannel->SetHandleFetchEventEnd(timeStamp);
+    mChannel->SetFinishSynthesizedResponseEnd(timeStamp);
+    mChannel->SaveTimeStamps();
+
+    return rv;
+  }
+};
+
+class BodyCopyHandle final : public nsIInterceptedBodyCallback
+{
+  UniquePtr<RespondWithClosure> mClosure;
+
+  ~BodyCopyHandle()
+  {
+  }
+
+public:
+  NS_DECL_THREADSAFE_ISUPPORTS
+
+  explicit BodyCopyHandle(UniquePtr<RespondWithClosure>&& aClosure)
+    : mClosure(Move(aClosure))
+  {
+  }
+
+  NS_IMETHOD
+  BodyComplete(nsresult aRv) override
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    nsCOMPtr<nsIRunnable> event;
+    if (NS_WARN_IF(NS_FAILED(aRv))) {
+      AsyncLog(mClosure->mInterceptedChannel, mClosure->mRespondWithScriptSpec,
+               mClosure->mRespondWithLineNumber,
+               mClosure->mRespondWithColumnNumber,
+               NS_LITERAL_CSTRING("InterceptionFailedWithURL"),
+               mClosure->mRequestURL);
+      event = new CancelChannelRunnable(mClosure->mInterceptedChannel,
+                                        mClosure->mRegistration,
+                                        NS_ERROR_INTERCEPTION_FAILED);
+    } else {
+      event = new FinishResponse(mClosure->mInterceptedChannel);
+    }
+
+    mClosure.reset();
+
+    event->Run();
+
+    return NS_OK;
+  }
+};
+
+NS_IMPL_ISUPPORTS(BodyCopyHandle, nsIInterceptedBodyCallback)
+
+class StartResponse final : public Runnable
+{
+  nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel;
   RefPtr<InternalResponse> mInternalResponse;
   ChannelInfo mWorkerChannelInfo;
   const nsCString mScriptSpec;
   const nsCString mResponseURLSpec;
+  UniquePtr<RespondWithClosure> mClosure;
 
 public:
-  FinishResponse(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
-                 InternalResponse* aInternalResponse,
-                 const ChannelInfo& aWorkerChannelInfo,
-                 const nsACString& aScriptSpec,
-                 const nsACString& aResponseURLSpec)
-    : Runnable("dom::workers::FinishResponse")
+  StartResponse(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
+                InternalResponse* aInternalResponse,
+                const ChannelInfo& aWorkerChannelInfo,
+                const nsACString& aScriptSpec,
+                const nsACString& aResponseURLSpec,
+                UniquePtr<RespondWithClosure>&& aClosure)
+    : Runnable("dom::workers::StartResponse")
     , mChannel(aChannel)
     , mInternalResponse(aInternalResponse)
     , mWorkerChannelInfo(aWorkerChannelInfo)
     , mScriptSpec(aScriptSpec)
     , mResponseURLSpec(aResponseURLSpec)
+    , mClosure(Move(aClosure))
   {
   }
 
   NS_IMETHOD
   Run() override
   {
     AssertIsOnMainThread();
 
@@ -228,34 +332,36 @@ public:
     mInternalResponse->UnfilteredHeaders()->GetEntries(entries);
     for (uint32_t i = 0; i < entries.Length(); ++i) {
        mChannel->SynthesizeHeader(entries[i].mName, entries[i].mValue);
     }
 
     auto castLoadInfo = static_cast<LoadInfo*>(loadInfo.get());
     castLoadInfo->SynthesizeServiceWorkerTainting(mInternalResponse->GetTainting());
 
-    rv = mChannel->FinishSynthesizedResponse(mResponseURLSpec);
+    nsCOMPtr<nsIInputStream> body;
+    mInternalResponse->GetUnfilteredBody(getter_AddRefs(body));
+    RefPtr<BodyCopyHandle> copyHandle;
+    copyHandle = new BodyCopyHandle(Move(mClosure));
+
+    rv = mChannel->StartSynthesizedResponse(body, copyHandle,
+                                            mResponseURLSpec);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       mChannel->CancelInterception(NS_ERROR_INTERCEPTION_FAILED);
       return NS_OK;
     }
 
-    TimeStamp timeStamp = TimeStamp::Now();
-    mChannel->SetHandleFetchEventEnd(timeStamp);
-    mChannel->SetFinishSynthesizedResponseEnd(timeStamp);
-    mChannel->SaveTimeStamps();
-
     nsCOMPtr<nsIObserverService> obsService = services::GetObserverService();
     if (obsService) {
       obsService->NotifyObservers(underlyingChannel, "service-worker-synthesized-response", nullptr);
     }
 
     return rv;
   }
+
   bool CSPPermitsResponse(nsILoadInfo* aLoadInfo)
   {
     AssertIsOnMainThread();
     MOZ_ASSERT(aLoadInfo);
     nsresult rv;
     nsCOMPtr<nsIURI> uri;
     nsCString url = mInternalResponse->GetUnfilteredURL();
     if (url.IsEmpty()) {
@@ -344,81 +450,16 @@ private:
       ::AsyncLog(mInterceptedChannel, mRespondWithScriptSpec,
                  mRespondWithLineNumber, mRespondWithColumnNumber,
                  NS_LITERAL_CSTRING("InterceptionFailedWithURL"), mRequestURL);
       CancelRequest(NS_ERROR_INTERCEPTION_FAILED);
     }
   }
 };
 
-struct RespondWithClosure
-{
-  nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel;
-  nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration;
-  RefPtr<InternalResponse> mInternalResponse;
-  ChannelInfo mWorkerChannelInfo;
-  const nsCString mScriptSpec;
-  const nsCString mResponseURLSpec;
-  const nsString mRequestURL;
-  const nsCString mRespondWithScriptSpec;
-  const uint32_t mRespondWithLineNumber;
-  const uint32_t mRespondWithColumnNumber;
-
-  RespondWithClosure(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
-                     nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration,
-                     InternalResponse* aInternalResponse,
-                     const ChannelInfo& aWorkerChannelInfo,
-                     const nsCString& aScriptSpec,
-                     const nsACString& aResponseURLSpec,
-                     const nsAString& aRequestURL,
-                     const nsACString& aRespondWithScriptSpec,
-                     uint32_t aRespondWithLineNumber,
-                     uint32_t aRespondWithColumnNumber)
-    : mInterceptedChannel(aChannel)
-    , mRegistration(aRegistration)
-    , mInternalResponse(aInternalResponse)
-    , mWorkerChannelInfo(aWorkerChannelInfo)
-    , mScriptSpec(aScriptSpec)
-    , mResponseURLSpec(aResponseURLSpec)
-    , mRequestURL(aRequestURL)
-    , mRespondWithScriptSpec(aRespondWithScriptSpec)
-    , mRespondWithLineNumber(aRespondWithLineNumber)
-    , mRespondWithColumnNumber(aRespondWithColumnNumber)
-  {
-  }
-};
-
-void RespondWithCopyComplete(void* aClosure, nsresult aStatus)
-{
-  nsAutoPtr<RespondWithClosure> data(static_cast<RespondWithClosure*>(aClosure));
-  nsCOMPtr<nsIRunnable> event;
-  if (NS_WARN_IF(NS_FAILED(aStatus))) {
-    AsyncLog(data->mInterceptedChannel, data->mRespondWithScriptSpec,
-             data->mRespondWithLineNumber, data->mRespondWithColumnNumber,
-             NS_LITERAL_CSTRING("InterceptionFailedWithURL"),
-             data->mRequestURL);
-    event = new CancelChannelRunnable(data->mInterceptedChannel,
-                                      data->mRegistration,
-                                      NS_ERROR_INTERCEPTION_FAILED);
-  } else {
-    event = new FinishResponse(data->mInterceptedChannel,
-                               data->mInternalResponse,
-                               data->mWorkerChannelInfo,
-                               data->mScriptSpec,
-                               data->mResponseURLSpec);
-  }
-  // In theory this can happen after the worker thread is terminated.
-  WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
-  if (worker) {
-    MOZ_ALWAYS_SUCCEEDS(worker->DispatchToMainThread(event.forget()));
-  } else {
-    MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(event.forget()));
-  }
-}
-
 class MOZ_STACK_CLASS AutoCancel
 {
   RefPtr<RespondWithHandler> mOwner;
   nsCString mSourceSpec;
   uint32_t mLine;
   uint32_t mColumn;
   nsCString mMessageName;
   nsTArray<nsString> mParams;
@@ -622,76 +663,44 @@ RespondWithHandler::ResolvedCallback(JSC
   // cross-origin responses, which are treated as same-origin by consumers.
   nsCString responseURL;
   if (response->Type() == ResponseType::Opaque) {
     responseURL = ir->GetUnfilteredURL();
     if (NS_WARN_IF(responseURL.IsEmpty())) {
       return;
     }
   }
-  nsAutoPtr<RespondWithClosure> closure(new RespondWithClosure(mInterceptedChannel,
-                                                               mRegistration, ir,
-                                                               worker->GetChannelInfo(),
-                                                               mScriptSpec,
-                                                               responseURL,
+
+  UniquePtr<RespondWithClosure> closure(new RespondWithClosure(mInterceptedChannel,
+                                                               mRegistration,
                                                                mRequestURL,
                                                                mRespondWithScriptSpec,
                                                                mRespondWithLineNumber,
                                                                mRespondWithColumnNumber));
+
+  nsCOMPtr<nsIRunnable> startRunnable = new StartResponse(mInterceptedChannel,
+                                                          ir,
+                                                          worker->GetChannelInfo(),
+                                                          mScriptSpec,
+                                                          responseURL,
+                                                          Move(closure));
+
   nsCOMPtr<nsIInputStream> body;
   ir->GetUnfilteredBody(getter_AddRefs(body));
   // Errors and redirects may not have a body.
   if (body) {
     IgnoredErrorResult error;
     response->SetBodyUsed(aCx, error);
     if (NS_WARN_IF(error.Failed())) {
       autoCancel.SetCancelErrorResult(aCx, error);
       return;
     }
-
-    nsCOMPtr<nsIOutputStream> responseBody;
-    rv = mInterceptedChannel->GetResponseBody(getter_AddRefs(responseBody));
-    if (NS_WARN_IF(NS_FAILED(rv)) || !responseBody) {
-      return;
-    }
-
-    const uint32_t kCopySegmentSize = 4096;
+  }
 
-    // Depending on how the Response passed to .respondWith() was created, we may
-    // get a non-buffered input stream.  In addition, in some configurations the
-    // destination channel's output stream can be unbuffered.  We wrap the output
-    // stream side here so that NS_AsyncCopy() works.  Wrapping the output side
-    // provides the most consistent operation since there are fewer stream types
-    // we are writing to.  The input stream can be a wide variety of concrete
-    // objects which may or many not play well with NS_InputStreamIsBuffered().
-    if (!NS_OutputStreamIsBuffered(responseBody)) {
-      nsCOMPtr<nsIOutputStream> buffered;
-      rv = NS_NewBufferedOutputStream(getter_AddRefs(buffered), responseBody,
-           kCopySegmentSize);
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return;
-      }
-      responseBody = buffered;
-    }
-
-    nsCOMPtr<nsIEventTarget> stsThread = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv);
-    if (NS_WARN_IF(!stsThread)) {
-      return;
-    }
-
-    // XXXnsm, Fix for Bug 1141332 means that if we decide to make this
-    // streaming at some point, we'll need a different solution to that bug.
-    rv = NS_AsyncCopy(body, responseBody, stsThread, NS_ASYNCCOPY_VIA_WRITESEGMENTS,
-                      kCopySegmentSize, RespondWithCopyComplete, closure.forget());
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return;
-    }
-  } else {
-    RespondWithCopyComplete(closure.forget(), NS_OK);
-  }
+  MOZ_ALWAYS_SUCCEEDS(worker->DispatchToMainThread(startRunnable.forget()));
 
   MOZ_ASSERT(!closure);
   autoCancel.Reset();
   mRequestWasHandled = true;
 }
 
 void
 RespondWithHandler::RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue)
deleted file mode 100644
--- a/dom/workers/test/fileapi_chromeScript.js
+++ /dev/null
@@ -1,42 +0,0 @@
-var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-Cu.importGlobalProperties(["File"]);
-
-var fileNum = 1;
-
-function createFileWithData(fileData) {
-  var willDelete = fileData === null;
-  var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
-  var testFile = dirSvc.get("ProfD", Ci.nsIFile);
-  testFile.append("fileAPItestfile" + fileNum);
-  fileNum++;
-  var outStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
-  outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate
-                 0o666, 0);
-  if (willDelete) {
-    fileData = "some irrelevant test data\n";
-  }
-  outStream.write(fileData, fileData.length);
-  outStream.close();
-
-  return File.createFromNsIFile(testFile).then(function(domFile) {
-    if (willDelete) {
-      testFile.remove(/* recursive: */ false);
-    }
-    return domFile;
-  });
-}
-
-addMessageListener("files.open", function (message) {
-  let promises = [];
-  let list = [];
-
-  for (let fileData of message) {
-    promises.push(createFileWithData(fileData).then(domFile => {
-      list.push(domFile);
-    }));
-  }
-
-  Promise.all(promises).then(() => {
-    sendAsyncMessage("files.opened", list);
-  });
-});
--- a/dom/workers/test/mochitest.ini
+++ b/dom/workers/test/mochitest.ini
@@ -93,18 +93,16 @@ support-files =
   empty.html
   referrer.sjs
   referrer_test_server.sjs
   sharedWorker_ports.js
   sharedWorker_lifetime.js
   worker_referrer.js
   websocket_https.html
   websocket_https_worker.js
-  worker_fileReader.js
-  fileapi_chromeScript.js
   importScripts_3rdParty_worker.js
   worker_bug1278777.js
   worker_setTimeoutWith0.js
   worker_bug1301094.js
   script_createFile.js
   worker_suspended.js
   window_suspended.html
   !/dom/base/test/file_bug945152.jar
@@ -224,17 +222,15 @@ skip-if = toolkit == 'android' #bug 9828
 [test_worker_interfaces_secureContext.html]
 scheme=https
 [test_workersDisabled.html]
 [test_referrer.html]
 [test_referrer_header_worker.html]
 [test_importScripts_3rdparty.html]
 [test_sharedWorker_ports.html]
 [test_sharedWorker_lifetime.html]
-[test_fileReader.html]
-skip-if = !debug # bug 1400098
 [test_navigator_workers_hardwareConcurrency.html]
 [test_bug1278777.html]
 [test_setTimeoutWith0.html]
 [test_bug1301094.html]
 [test_subworkers_suspended.html]
 skip-if = toolkit == 'android' #bug 1366501
 [test_bug1317725.html]
--- a/dom/workers/test/serviceworkers/chrome.ini
+++ b/dom/workers/test/serviceworkers/chrome.ini
@@ -4,16 +4,17 @@ support-files =
   chrome_helpers.js
   empty.js
   fetch.js
   hello.html
   serviceworker.html
   serviceworkerinfo_iframe.html
   serviceworkermanager_iframe.html
   serviceworkerregistrationinfo_iframe.html
+  utils.js
   worker.js
   worker2.js
 
 [test_devtools_serviceworker_interception.html]
 [test_devtools_track_serviceworker_time.html]
 [test_privateBrowsing.html]
 [test_serviceworkerinfo.xul]
 [test_serviceworkermanager.xul]
--- a/dom/workers/test/serviceworkers/fetch.js
+++ b/dom/workers/test/serviceworkers/fetch.js
@@ -1,11 +1,13 @@
 addEventListener('fetch', function(event) {
   if (event.request.url.indexOf("fail.html") !== -1) {
     event.respondWith(fetch("hello.html", {"integrity": "abc"}));
   } else if (event.request.url.indexOf("fake.html") !== -1) {
     event.respondWith(fetch("hello.html"));
   }
 });
 
-addEventListener("activate", function(event) {
-  event.waitUntil(clients.claim());
+addEventListener('message', function(event) {
+  if (event.data === 'claim') {
+    event.waitUntil(clients.claim());
+  }
 });
--- a/dom/workers/test/serviceworkers/test_devtools_serviceworker_interception.html
+++ b/dom/workers/test/serviceworkers/test_devtools_serviceworker_interception.html
@@ -11,16 +11,17 @@
   <link rel="stylesheet"
         type="text/css"
         href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
 </head>
 <body>
 <p id="display"></p>
 <div id="content" style="display: none"></div>
 <pre id="test"></pre>
+<script src="utils.js"></script>
 <script class="testbody" type="text/javascript">
 
 // Constants
 const Ci = Components.interfaces;
 const workerScope = "http://mochi.test:8888/chrome/dom/workers/test/serviceworkers/";
 const workerURL = workerScope + "fetch.js";
 const contentPage = workerScope + "hello.html";
 
@@ -52,22 +53,35 @@ function executeTest(aWindow) {
   return Promise.resolve()
     // Should not be intercepted.
     .then(_ => fetchAndCheckTimedChannel(aWindow, false, true, "hello.html"))
 
     // Regist a service worker.
     .then(_ => register(aWindow, workerURL, workerScope))
     .then(r => registration = r)
 
-    // Should be intercpeted and synthesized.
+    // If this test is re-run then we may end up resurrecting the previous
+    // registration and worker.  In those cases we will have an active instead
+    // of installing.  This happens because because the test window itself
+    // is controlled.  If we were using iframes we could ensure the registration
+    // was removed before ending the test.
+    .then(_ => waitForState(registration.installing || registration.active, 'activated'))
+
+    // When run consecutively we sometime end up resurrecting a previous
+    // service worker.  In that case our active event does not run and claim
+    // the window.  So do the claim for a message event instead.
+    .then(_ => registration.active.postMessage('claim'))
+    .then(_ => waitForControlled(aWindow))
+
+    // Should be intercepted and synthesized.
     .then(_ => fetchAndCheckTimedChannel(aWindow, true, false, "fake.html"))
 
     // Should be intercepted but still fetch from network.
     .then(_ => fetchAndCheckTimedChannel(aWindow, true, true,
-                                         "hello.html?ForBypassingHttpCache"))
+                                         "hello.html?ForBypassingHttpCache=" + Date.now()))
 
     // Tear down
     .then(_ => registration.unregister())
     .then(_ => aWindow.close());
 }
 
 function register(aWindow, aURL, aScope) {
   return aWindow.navigator.serviceWorker.register(aURL, {scope: aScope})
@@ -82,18 +96,17 @@ function register(aWindow, aURL, aScope)
       });
     });
 }
 
 function fetchAndCheckTimedChannel(aWindow, aIntercepted, aFetch, aURL) {
   var resolveFunction;
   var promise = new Promise(aResolve => resolveFunction = aResolve);
 
-  var topic = aFetch ? "http-on-examine-response"
-                     : "service-worker-synthesized-response";
+  var topic = "http-on-stop-request";
 
   function observer(aSubject) {
     var channel = aSubject.QueryInterface(Ci.nsIChannel);
 
     // Since we cannot make sure that the network event triggered by the fetch()
     // in this testcase is the very next event processed by ObserverService, we
     // have to wait until we catch the one we want.
     if (!channel.URI.spec.endsWith(aURL)) {
--- a/dom/workers/test/serviceworkers/test_fetch_integrity.html
+++ b/dom/workers/test/serviceworkers/test_fetch_integrity.html
@@ -51,16 +51,18 @@ add_task(async function test_integrity_s
 
   // The SW will claim us once it activates; this is async, start listening now.
   let waitForControlled = new Promise((resolve) => {
     navigator.serviceWorker.oncontrollerchange = resolve;
   });
 
   let registration = await navigator.serviceWorker.register("fetch.js",
                                                             { scope: "./" });
+  let worker = registration.installing || registration.active;
+  worker.postMessage('claim');
   await waitForControlled;
 
   info("Test for mNavigationInterceptions.")
   // The client_win will reload to another URL after opening filename2.
   let client_win = window.open(filename2);
 
   // XXX windowID should be innerWindowID
   let mainWindowID = SpecialPowers.getDOMWindowUtils(window).outerWindowID;
--- a/dom/workers/test/serviceworkers/utils.js
+++ b/dom/workers/test/serviceworkers/utils.js
@@ -7,8 +7,19 @@ function waitForState(worker, state, con
     worker.addEventListener('statechange', function onStateChange() {
       if (worker.state === state) {
         worker.removeEventListener('statechange', onStateChange);
         resolve(context);
       }
     });
   });
 }
+
+function waitForControlled(win) {
+  return new Promise(resolve => {
+    if (win.navigator.serviceWorker.controller) {
+      return resolve();
+    }
+
+    win.navigator.serviceWorker.addEventListener('controllerchange', resolve,
+                                                 { once: true });
+  });
+}
deleted file mode 100644
--- a/dom/workers/test/test_fileReader.html
+++ /dev/null
@@ -1,100 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>Test for FileReader in workers</title>
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
-</head>
-
-<body>
-<script type="text/javascript">
-
-const minFileSize = 20000;
-SimpleTest.waitForExplicitFinish();
-
-// Create strings containing data we'll test with. We'll want long
-// strings to ensure they span multiple buffers while loading
-var testTextData = "asd b\tlah\u1234w\u00a0r";
-while (testTextData.length < minFileSize) {
-  testTextData = testTextData + testTextData;
-}
-
-var testASCIIData = "abcdef 123456\n";
-while (testASCIIData.length < minFileSize) {
-  testASCIIData = testASCIIData + testASCIIData;
-}
-
-var testBinaryData = "";
-for (var i = 0; i < 256; i++) {
-  testBinaryData += String.fromCharCode(i);
-}
-while (testBinaryData.length < minFileSize) {
-  testBinaryData = testBinaryData + testBinaryData;
-}
-
-var dataurldata0 = testBinaryData.substr(0, testBinaryData.length -
-					 testBinaryData.length % 3);
-var dataurldata1 = testBinaryData.substr(0, testBinaryData.length - 2 -
-					 testBinaryData.length % 3);
-var dataurldata2 = testBinaryData.substr(0, testBinaryData.length - 1 -
-					 testBinaryData.length % 3);
-
-
-//Set up files for testing
-var openerURL = SimpleTest.getTestFileURL("fileapi_chromeScript.js");
-var opener = SpecialPowers.loadChromeScript(openerURL);
-opener.addMessageListener("files.opened", onFilesOpened);
-opener.sendAsyncMessage("files.open", [
-  testASCIIData,
-  testBinaryData,
-  null,
-  convertToUTF8(testTextData),
-  convertToUTF16(testTextData),
-  "",
-  dataurldata0,
-  dataurldata1,
-  dataurldata2,
-]);
-
-function onFilesOpened(message) {
-  var worker = new Worker('worker_fileReader.js');
-  worker.postMessage({ blobs: message,
-                       testTextData: testTextData,
-                       testASCIIData: testASCIIData,
-                       testBinaryData: testBinaryData,
-                       dataurldata0: dataurldata0,
-                       dataurldata1: dataurldata1,
-                       dataurldata2: dataurldata2 });
-
-  worker.onmessage = function(e) {
-    var msg = e.data;
-    if (msg.type == 'finish') {
-      SimpleTest.finish();
-      return;
-    }
-
-    if (msg.type == 'check') {
-      ok(msg.status, msg.msg);
-      return;
-    }
-
-    ok(false, "Unknown message.");
-  }
-}
-
-function convertToUTF16(s) {
-  res = "";
-  for (var i = 0; i < s.length; ++i) {
-    c = s.charCodeAt(i);
-    res += String.fromCharCode(c & 255, c >>> 8);
-  }
-  return res;
-}
-
-function convertToUTF8(s) {
-  return unescape(encodeURIComponent(s));
-}
-
-</script>
-</body>
-</html>
deleted file mode 100644
--- a/dom/workers/test/worker_fileReader.js
+++ /dev/null
@@ -1,419 +0,0 @@
-var testRanCounter = 0;
-var expectedTestCount = 0;
-var testSetupFinished = false;
-
-function ok(a, msg) {
-  postMessage({type: 'check', status: !!a, msg: msg });
-}
-
-function is(a, b, msg) {
-  ok(a === b, msg);
-}
-
-function finish() {
-  postMessage({type: 'finish'});
-}
-
-function convertToUTF16(s) {
-  res = "";
-  for (var i = 0; i < s.length; ++i) {
-    c = s.charCodeAt(i);
-    res += String.fromCharCode(c & 255, c >>> 8);
-  }
-  return res;
-}
-
-function convertToUTF8(s) {
-  return unescape(encodeURIComponent(s));
-}
-
-function convertToDataURL(s) {
-  return "data:application/octet-stream;base64," + btoa(s);
-}
-
-onmessage = function(message) {
-  is(FileReader.EMPTY, 0, "correct EMPTY value");
-  is(FileReader.LOADING, 1, "correct LOADING value");
-  is(FileReader.DONE, 2, "correct DONE value");
-
-  // List of blobs.
-  var asciiFile = message.data.blobs.shift();
-  var binaryFile = message.data.blobs.shift();
-  var nonExistingFile = message.data.blobs.shift();
-  var utf8TextFile = message.data.blobs.shift();
-  var utf16TextFile = message.data.blobs.shift();
-  var emptyFile = message.data.blobs.shift();
-  var dataUrlFile0 = message.data.blobs.shift();
-  var dataUrlFile1 = message.data.blobs.shift();
-  var dataUrlFile2 = message.data.blobs.shift();
-
-  // List of buffers for testing.
-  var testTextData = message.data.testTextData;
-  var testASCIIData = message.data.testASCIIData;
-  var testBinaryData = message.data.testBinaryData;
-  var dataurldata0 = message.data.dataurldata0;
-  var dataurldata1 = message.data.dataurldata1;
-  var dataurldata2 = message.data.dataurldata2;
-
-  // Test that plain reading works and fires events as expected, both
-  // for text and binary reading
-
-  var onloadHasRunText = false;
-  var onloadStartHasRunText = false;
-  r = new FileReader();
-  is(r.readyState, FileReader.EMPTY, "correct initial text readyState");
-  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "plain reading");
-  r.addEventListener("load", function() { onloadHasRunText = true });
-  r.addEventListener("loadstart", function() { onloadStartHasRunText = true });
-  r.readAsText(asciiFile);
-  is(r.readyState, FileReader.LOADING, "correct loading text readyState");
-  is(onloadHasRunText, false, "text loading must be async");
-  is(onloadStartHasRunText, true, "text loadstart should fire sync");
-  expectedTestCount++;
-
-  var onloadHasRunBinary = false;
-  var onloadStartHasRunBinary = false;
-  r = new FileReader();
-  is(r.readyState, FileReader.EMPTY, "correct initial binary readyState");
-  r.addEventListener("load", function() { onloadHasRunBinary = true });
-  r.addEventListener("loadstart", function() { onloadStartHasRunBinary = true });
-  r.readAsBinaryString(binaryFile);
-  r.onload = getLoadHandler(testBinaryData, testBinaryData.length, "binary reading");
-  is(r.readyState, FileReader.LOADING, "correct loading binary readyState");
-  is(onloadHasRunBinary, false, "binary loading must be async");
-  is(onloadStartHasRunBinary, true, "binary loadstart should fire sync");
-  expectedTestCount++;
-
-  var onloadHasRunArrayBuffer = false;
-  var onloadStartHasRunArrayBuffer = false;
-  r = new FileReader();
-  is(r.readyState, FileReader.EMPTY, "correct initial arrayBuffer readyState");
-  r.addEventListener("load", function() { onloadHasRunArrayBuffer = true });
-  r.addEventListener("loadstart", function() { onloadStartHasRunArrayBuffer = true });
-  r.readAsArrayBuffer(binaryFile);
-  r.onload = getLoadHandlerForArrayBuffer(testBinaryData, testBinaryData.length, "array buffer reading");
-  is(r.readyState, FileReader.LOADING, "correct loading arrayBuffer readyState");
-  is(onloadHasRunArrayBuffer, false, "arrayBuffer loading must be async");
-  is(onloadStartHasRunArrayBuffer, true, "arrayBuffer loadstart should fire sync");
-  expectedTestCount++;
-
-  // Test a variety of encodings, and make sure they work properly
-  r = new FileReader();
-  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "no encoding reading");
-  r.readAsText(asciiFile, "");
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "iso8859 reading");
-  r.readAsText(asciiFile, "iso-8859-1");
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.onload = getLoadHandler(testTextData,
-                            convertToUTF8(testTextData).length,
-                            "utf8 reading");
-  r.readAsText(utf8TextFile, "utf8");
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.readAsText(utf16TextFile, "utf-16");
-  r.onload = getLoadHandler(testTextData,
-                            convertToUTF16(testTextData).length,
-                            "utf16 reading");
-  expectedTestCount++;
-
-  // Test get result without reading
-  r = new FileReader();
-  is(r.readyState, FileReader.EMPTY,
-     "readyState in test reader get result without reading");
-  is(r.error, null,
-     "no error in test reader get result without reading");
-  is(r.result, null,
-     "result in test reader get result without reading");
-
-  // Test loading an empty file works (and doesn't crash!)
-  r = new FileReader();
-  r.onload = getLoadHandler("", 0, "empty no encoding reading");
-  r.readAsText(emptyFile, "");
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.onload = getLoadHandler("", 0, "empty utf8 reading");
-  r.readAsText(emptyFile, "utf8");
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.onload = getLoadHandler("", 0, "empty utf16 reading");
-  r.readAsText(emptyFile, "utf-16");
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.onload = getLoadHandler("", 0, "empty binary string reading");
-  r.readAsBinaryString(emptyFile);
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.onload = getLoadHandlerForArrayBuffer("", 0, "empty array buffer reading");
-  r.readAsArrayBuffer(emptyFile);
-  expectedTestCount++;
-
-  r = new FileReader();
-  r.onload = getLoadHandler(convertToDataURL(""), 0, "empty binary string reading");
-  r.readAsDataURL(emptyFile);
-  expectedTestCount++;
-
-  // Test reusing a FileReader to read multiple times
-  r = new FileReader();
-  r.onload = getLoadHandler(testASCIIData,
-                            testASCIIData.length,
-                            "to-be-reused reading text")
-  var makeAnotherReadListener = function(event) {
-    r = event.target;
-    r.removeEventListener("load", makeAnotherReadListener);
-    r.onload = getLoadHandler(testASCIIData,
-                              testASCIIData.length,
-                              "reused reading text");
-    r.readAsText(asciiFile);
-  };
-  r.addEventListener("load", makeAnotherReadListener);
-  r.readAsText(asciiFile);
-  expectedTestCount += 2;
-
-  r = new FileReader();
-  r.onload = getLoadHandler(testBinaryData,
-                            testBinaryData.length,
-                            "to-be-reused reading binary")
-  var makeAnotherReadListener2 = function(event) {
-    r = event.target;
-    r.removeEventListener("load", makeAnotherReadListener2);
-    r.onload = getLoadHandler(testBinaryData,
-                              testBinaryData.length,
-                              "reused reading binary");
-    r.readAsBinaryString(binaryFile);
-  };
-  r.addEventListener("load", makeAnotherReadListener2);
-  r.readAsBinaryString(binaryFile);
-  expectedTestCount += 2;
-
-  r = new FileReader();
-  r.onload = getLoadHandler(convertToDataURL(testBinaryData),
-                            testBinaryData.length,
-                            "to-be-reused reading data url")
-  var makeAnotherReadListener3 = function(event) {
-    r = event.target;
-    r.removeEventListener("load", makeAnotherReadListener3);
-    r.onload = getLoadHandler(convertToDataURL(testBinaryData),
-                              testBinaryData.length,
-                              "reused reading data url");
-    r.readAsDataURL(binaryFile);
-  };
-  r.addEventListener("load", makeAnotherReadListener3);
-  r.readAsDataURL(binaryFile);
-  expectedTestCount += 2;
-
-  r = new FileReader();
-  r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
-                                          testBinaryData.length,
-                                          "to-be-reused reading arrayBuffer")
-  var makeAnotherReadListener4 = function(event) {
-    r = event.target;
-    r.removeEventListener("load", makeAnotherReadListener4);
-    r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
-                                            testBinaryData.length,
-                                            "reused reading arrayBuffer");
-    r.readAsArrayBuffer(binaryFile);
-  };
-  r.addEventListener("load", makeAnotherReadListener4);
-  r.readAsArrayBuffer(binaryFile);
-  expectedTestCount += 2;
-
-  // Test first reading as ArrayBuffer then read as something else
-  // (BinaryString) and doesn't crash
-  r = new FileReader();
-  r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
-                                          testBinaryData.length,
-                                          "to-be-reused reading arrayBuffer")
-  var makeAnotherReadListener5 = function(event) {
-    r = event.target;
-    r.removeEventListener("load", makeAnotherReadListener5);
-    r.onload = getLoadHandler(testBinaryData,
-                              testBinaryData.length,
-                              "reused reading binary string");
-    r.readAsBinaryString(binaryFile);
-  };
-  r.addEventListener("load", makeAnotherReadListener5);
-  r.readAsArrayBuffer(binaryFile);
-  expectedTestCount += 2;
-
-  //Test data-URI encoding on differing file sizes
-  is(dataurldata0.length % 3, 0, "Want to test data with length % 3 == 0");
-  r = new FileReader();
-  r.onload = getLoadHandler(convertToDataURL(dataurldata0),
-                            dataurldata0.length,
-                            "dataurl reading, %3 = 0");
-  r.readAsDataURL(dataUrlFile0);
-  expectedTestCount++;
-
-  is(dataurldata1.length % 3, 1, "Want to test data with length % 3 == 1");
-  r = new FileReader();
-  r.onload = getLoadHandler(convertToDataURL(dataurldata1),
-                            dataurldata1.length,
-                            "dataurl reading, %3 = 1");
-  r.readAsDataURL(dataUrlFile1);
-  expectedTestCount++;
-
-  is(dataurldata2.length % 3, 2, "Want to test data with length % 3 == 2");
-  r = new FileReader();
-  r.onload = getLoadHandler(convertToDataURL(dataurldata2),
-                            dataurldata2.length,
-                            "dataurl reading, %3 = 2");
-  r.readAsDataURL(dataUrlFile2),
-  expectedTestCount++;
-
-
-  // Test abort()
-  var abortHasRun = false;
-  var loadEndHasRun = false;
-  r = new FileReader();
-  r.onabort = function (event) {
-    is(abortHasRun, false, "abort should only fire once");
-    is(loadEndHasRun, false, "loadend shouldn't have fired yet");
-    abortHasRun = true;
-    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
-    is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
-    is(event.target.result, null, "file data should be null on aborted reads");
-  }
-  r.onloadend = function (event) {
-    is(abortHasRun, true, "abort should fire before loadend");
-    is(loadEndHasRun, false, "loadend should only fire once");
-    loadEndHasRun = true;
-    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
-    is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
-    is(event.target.result, null, "file data should be null on aborted reads");
-  }
-  r.onload = function() { ok(false, "load should not fire for aborted reads") };
-  r.onerror = function() { ok(false, "error should not fire for aborted reads") };
-  r.onprogress = function() { ok(false, "progress should not fire for aborted reads") };
-  var abortThrew = false;
-  try {
-    r.abort();
-  } catch(e) {
-    abortThrew = true;
-  }
-  is(abortThrew, false, "abort() never throws");
-  is(abortHasRun, false, "abort() is a no-op unless loading");
-  r.readAsText(asciiFile);
-  r.abort();
-  is(abortHasRun, true, "1 abort should fire sync");
-  is(loadEndHasRun, true, "loadend should fire sync");
-
-  // Test calling readAsX to cause abort()
-  var reuseAbortHasRun = false;
-  r = new FileReader();
-  r.onabort = function (event) { reuseAbortHasRun = true; }
-  r.onload = function() { ok(true, "load should fire for aborted reads") };
-  var abortThrew = false;
-  try {
-    r.abort();
-  } catch(e) {
-    abortThrew = true;
-  }
-  is(abortThrew, false, "abort() never throws");
-  is(reuseAbortHasRun, false, "abort() is a no-op unless loading");
-  r.readAsText(asciiFile);
-
-  var readThrew = false;
-  try {
-  r.readAsText(asciiFile);
-  } catch(e) {
-    readThrew = true;
-  }
-
-  is(readThrew, true, "readAsText() must throw if loading");
-  is(reuseAbortHasRun, false, "2 abort should fire sync");
-  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "reuse-as-abort reading");
-  expectedTestCount++;
-
-
-  // Test reading from nonexistent files
-  r = new FileReader();
-  var didThrow = false;
-  r.onerror = function (event) {
-    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onerror");
-    is(event.target.error.name, "NotFoundError", "error set to NotFoundError for nonexistent files");
-    is(event.target.result, null, "file data should be null on aborted reads");
-    testHasRun();
-  };
-  r.onload = function (event) {
-    is(false, "nonexistent file shouldn't load! (FIXME: bug 1122788)");
-    testHasRun();
-  };
-  try {
-    r.readAsDataURL(nonExistingFile);
-    expectedTestCount++;
-  } catch(ex) {
-    didThrow = true;
-  }
-  // Once this test passes, we should test that onerror gets called and
-  // that the FileReader object is in the right state during that call.
-  is(didThrow, false, "shouldn't throw when opening nonexistent file, should fire error instead");
-
-
-  function getLoadHandler(expectedResult, expectedLength, testName) {
-    return function (event) {
-      is(event.target.readyState, FileReader.DONE,
-         "readyState in test " + testName);
-      is(event.target.error, null,
-         "no error in test " + testName);
-      is(event.target.result, expectedResult,
-         "result in test " + testName);
-      is(event.lengthComputable, true,
-         "lengthComputable in test " + testName);
-      is(event.loaded, expectedLength,
-         "loaded in test " + testName);
-      is(event.total, expectedLength,
-         "total in test " + testName);
-      testHasRun();
-    }
-  }
-
-  function getLoadHandlerForArrayBuffer(expectedResult, expectedLength, testName) {
-    return function (event) {
-      is(event.target.readyState, FileReader.DONE,
-         "readyState in test " + testName);
-      is(event.target.error, null,
-         "no error in test " +  testName);
-      is(event.lengthComputable, true,
-         "lengthComputable in test " + testName);
-      is(event.loaded, expectedLength,
-         "loaded in test " + testName);
-      is(event.total, expectedLength,
-         "total in test " + testName);
-      is(event.target.result.byteLength, expectedLength,
-         "array buffer size in test " + testName);
-      var u8v = new Uint8Array(event.target.result);
-      is(String.fromCharCode.apply(String, u8v), expectedResult,
-         "array buffer contents in test " + testName);
-      u8v = null;
-      is(event.target.result.byteLength, expectedLength,
-         "array buffer size after gc in test " + testName);
-      u8v = new Uint8Array(event.target.result);
-      is(String.fromCharCode.apply(String, u8v), expectedResult,
-         "array buffer contents after gc in test " + testName);
-      testHasRun();
-    }
-  }
-
-  function testHasRun() {
-    //alert(testRanCounter);
-    ++testRanCounter;
-    if (testRanCounter == expectedTestCount) {
-      is(testSetupFinished, true, "test setup should have finished; check for exceptions");
-      is(onloadHasRunText, true, "onload text should have fired by now");
-      is(onloadHasRunBinary, true, "onload binary should have fired by now");
-      finish();
-    }
-  }
-
-  testSetupFinished = true;
-}
--- a/editor/libeditor/HTMLTableEditor.cpp
+++ b/editor/libeditor/HTMLTableEditor.cpp
@@ -2711,18 +2711,18 @@ HTMLEditor::GetCellDataAt(nsIDOMElement*
 
   nsTableCellFrame* cellFrame =
     tableFrame->GetCellFrameAt(aRowIndex, aColIndex);
   if (!cellFrame) {
     return NS_ERROR_FAILURE;
   }
 
   *aIsSelected = cellFrame->IsSelected();
-  cellFrame->GetRowIndex(*aStartRowIndex);
-  cellFrame->GetColIndex(*aStartColIndex);
+  *aStartRowIndex = cellFrame->RowIndex();
+  *aStartColIndex = cellFrame->ColIndex();
   *aRowSpan = cellFrame->GetRowSpan();
   *aColSpan = cellFrame->GetColSpan();
   *aActualRowSpan = tableFrame->GetEffectiveRowSpanAt(aRowIndex, aColIndex);
   *aActualColSpan = tableFrame->GetEffectiveColSpanAt(aRowIndex, aColIndex);
   nsCOMPtr<nsIDOMElement> domCell = do_QueryInterface(cellFrame->GetContent());
   domCell.forget(aCell);
 
   return NS_OK;
--- a/gfx/layers/d3d11/MLGDeviceD3D11.cpp
+++ b/gfx/layers/d3d11/MLGDeviceD3D11.cpp
@@ -1957,16 +1957,37 @@ void
 MLGDeviceD3D11::CopyTexture(MLGTexture* aDest,
                             const gfx::IntPoint& aTarget,
                             MLGTexture* aSource,
                             const gfx::IntRect& aRect)
 {
   MLGTextureD3D11* dest = aDest->AsD3D11();
   MLGTextureD3D11* source = aSource->AsD3D11();
 
+  // We check both the source and destination copy regions, because
+  // CopySubresourceRegion is documented as causing a device reset if
+  // the operation is out-of-bounds. And it's not lying.
+  IntRect sourceBounds(IntPoint(0, 0), aSource->GetSize());
+  if (!sourceBounds.Contains(aRect)) {
+    gfxWarning() << "Attempt to read out-of-bounds in CopySubresourceRegion: " <<
+      Stringify(sourceBounds) <<
+      ", " <<
+      Stringify(aRect);
+    return;
+  }
+
+  IntRect destBounds(IntPoint(0, 0), aDest->GetSize());
+  if (!destBounds.Contains(IntRect(aTarget, aRect.Size()))) {
+    gfxWarning() << "Attempt to write out-of-bounds in CopySubresourceRegion: " <<
+      Stringify(destBounds) <<
+      ", " <<
+      Stringify(aTarget) << ", " << Stringify(aRect.Size());
+    return;
+  }
+
   D3D11_BOX box = RectToBox(aRect);
   mCtx->CopySubresourceRegion(
     dest->GetTexture(), 0,
     aTarget.x, aTarget.y, 0,
     source->GetTexture(), 0,
     &box);
 }
 
--- a/gfx/layers/mlgpu/RenderPassMLGPU.cpp
+++ b/gfx/layers/mlgpu/RenderPassMLGPU.cpp
@@ -1015,32 +1015,35 @@ RenderViewPass::RenderWithBackdropCopy()
 {
   MOZ_ASSERT(mAssignedLayer->NeedsSurfaceCopy());
 
   DebugOnly<Matrix> transform2d;
   const Matrix4x4& transform = mAssignedLayer->GetEffectiveTransform();
   MOZ_ASSERT(transform.Is2D(&transform2d) &&
              !gfx::ThebesMatrix(transform2d).HasNonIntegerTranslation());
 
+  IntPoint translation = IntPoint::Truncate(transform._41, transform._42);
+
+  RenderViewMLGPU* childView = mAssignedLayer->GetRenderView();
+
   IntRect visible = mAssignedLayer->GetShadowVisibleRegion().GetBounds().ToUnknownRect();
-  visible += IntPoint::Truncate(transform._41, transform._42);
-  visible -= mParentView->GetTargetOffset();
+  IntRect sourceRect = visible + translation - mParentView->GetTargetOffset();
+  IntPoint destPoint = visible.TopLeft() - childView->GetTargetOffset();
 
   RefPtr<MLGTexture> dest = mAssignedLayer->GetRenderTarget()->GetTexture();
   RefPtr<MLGTexture> source = mParentView->GetRenderTarget()->GetTexture();
 
-  // Clamp the rect so that we don't read pixels outside the source texture, or
-  // write pixels outside the destination texture.
-  visible = visible.Intersect(IntRect(IntPoint(0, 0), source->GetSize()));
-  visible = visible.Intersect(IntRect(visible.TopLeft(), dest->GetSize()));
+  // Clamp the source rect to the source texture size.
+  sourceRect = sourceRect.Intersect(IntRect(IntPoint(0, 0), source->GetSize()));
 
-  mDevice->CopyTexture(dest, IntPoint(0, 0), source, visible);
+  // Clamp the source rect to the destination texture size.
+  IntRect destRect(destPoint, sourceRect.Size());
+  destRect = destRect.Intersect(IntRect(IntPoint(0, 0), dest->GetSize()));
+  sourceRect = sourceRect.Intersect(IntRect(sourceRect.TopLeft(), destRect.Size()));
 
-  RenderViewMLGPU* childView = mAssignedLayer->GetRenderView();
+  mDevice->CopyTexture(dest, destPoint, source, sourceRect);
   childView->RenderAfterBackdropCopy();
-
   mParentView->RestoreDeviceState();
-
   TexturedRenderPass::ExecuteRendering();
 }
 
 } // namespace layers
 } // namespace mozilla
--- a/gfx/layers/mlgpu/RenderViewMLGPU.cpp
+++ b/gfx/layers/mlgpu/RenderViewMLGPU.cpp
@@ -126,16 +126,23 @@ RenderViewMLGPU::Render()
   }
   ExecuteRendering();
 }
 
 void
 RenderViewMLGPU::RenderAfterBackdropCopy()
 {
   MOZ_ASSERT(mContainer && mContainer->NeedsSurfaceCopy());
+
+  // Update the invalid bounds based on the container's visible region. This
+  // of course won't affect the prepared pipeline, but it will change the
+  // scissor rect in SetDeviceState.
+  mInvalidBounds = mContainer->GetShadowVisibleRegion().GetBounds().ToUnknownRect() -
+                   GetTargetOffset();
+
   ExecuteRendering();
 }
 
 void
 RenderViewMLGPU::FinishBuilding()
 {
   MOZ_ASSERT(!mFinishedBuilding);
   mFinishedBuilding = true;
--- a/gfx/layers/wr/WebRenderCommandBuilder.cpp
+++ b/gfx/layers/wr/WebRenderCommandBuilder.cpp
@@ -109,17 +109,18 @@ WebRenderCommandBuilder::CreateWebRender
                                                                 nsDisplayListBuilder* aDisplayListBuilder,
                                                                 const StackingContextHelper& aSc,
                                                                 wr::DisplayListBuilder& aBuilder,
                                                                 wr::IpcResourceUpdateQueue& aResources)
 {
   bool apzEnabled = mManager->AsyncPanZoomEnabled();
   EventRegions eventRegions;
 
-  for (nsDisplayItem* i = aDisplayList->GetBottom(); i; i = i->GetAbove()) {
+  FlattenedDisplayItemIterator iter(aDisplayListBuilder, aDisplayList);
+  while (nsDisplayItem* i = iter.GetNext()) {
     nsDisplayItem* item = i;
     DisplayItemType itemType = item->GetType();
 
     // If the item is a event regions item, but is empty (has no regions in it)
     // then we should just throw it out
     if (itemType == DisplayItemType::TYPE_LAYER_EVENT_REGIONS) {
       nsDisplayLayerEventRegions* eventRegions =
         static_cast<nsDisplayLayerEventRegions*>(item);
@@ -127,25 +128,25 @@ WebRenderCommandBuilder::CreateWebRender
         continue;
       }
     }
 
     // Peek ahead to the next item and try merging with it or swapping with it
     // if necessary.
     AutoTArray<nsDisplayItem*, 1> mergedItems;
     mergedItems.AppendElement(item);
-    for (nsDisplayItem* peek = item->GetAbove(); peek; peek = peek->GetAbove()) {
+    while (nsDisplayItem* peek = iter.PeekNext()) {
       if (!item->CanMerge(peek)) {
         break;
       }
 
       mergedItems.AppendElement(peek);
 
       // Move the iterator forward since we will merge this item.
-      i = peek;
+      i = iter.GetNext();
     }
 
     if (mergedItems.Length() > 1) {
       item = aDisplayListBuilder->MergeItems(mergedItems);
       MOZ_ASSERT(item && itemType == item->GetType());
     }
 
     bool forceNewLayerData = false;
@@ -207,31 +208,24 @@ WebRenderCommandBuilder::CreateWebRender
       // If we're going to create a new layer data for this item, stash the
       // ASR so that if we recurse into a sublist they will know where to stop
       // walking up their ASR chain when building scroll metadata.
       if (forceNewLayerData) {
         mAsrStack.push_back(asr);
       }
     }
 
-    nsDisplayList* childItems = item->GetSameCoordinateSystemChildren();
-    if (item->ShouldFlattenAway(aDisplayListBuilder)) {
-      MOZ_ASSERT(childItems);
-      CreateWebRenderCommandsFromDisplayList(childItems, aDisplayListBuilder, aSc,
-                                             aBuilder, aResources);
-    } else {
-      // ensure the scope of ScrollingLayersHelper is maintained
-      ScrollingLayersHelper clip(item, aBuilder, aSc, mClipIdCache, apzEnabled);
+    // ensure the scope of ScrollingLayersHelper is maintained
+    ScrollingLayersHelper clip(item, aBuilder, aSc, mClipIdCache, apzEnabled);
 
-      // Note: this call to CreateWebRenderCommands can recurse back into
-      // this function if the |item| is a wrapper for a sublist.
-      if (!item->CreateWebRenderCommands(aBuilder, aResources, aSc, mManager,
-                                         aDisplayListBuilder)) {
-        PushItemAsImage(item, aBuilder, aResources, aSc, aDisplayListBuilder);
-      }
+    // Note: this call to CreateWebRenderCommands can recurse back into
+    // this function if the |item| is a wrapper for a sublist.
+    if (!item->CreateWebRenderCommands(aBuilder, aResources, aSc, mManager,
+                                       aDisplayListBuilder)) {
+      PushItemAsImage(item, aBuilder, aResources, aSc, aDisplayListBuilder);
     }
 
     if (apzEnabled) {
       if (forceNewLayerData) {
         // Pop the thing we pushed before the recursion, so the topmost item on
         // the stack is enclosing display item's ASR (or the stack is empty)
         mAsrStack.pop_back();
         const ActiveScrolledRoot* stopAtAsr =
--- a/js/src/builtin/Intl.cpp
+++ b/js/src/builtin/Intl.cpp
@@ -812,19 +812,18 @@ IntlInitialize(JSContext* cx, HandleObje
                HandleValue locales, HandleValue options)
 {
     FixedInvokeArgs<3> args(cx);
 
     args[0].setObject(*obj);
     args[1].set(locales);
     args[2].set(options);
 
-    RootedValue thisv(cx, NullValue());
     RootedValue ignored(cx);
-    if (!js::CallSelfHostedFunction(cx, initializer, thisv, args, &ignored))
+    if (!js::CallSelfHostedFunction(cx, initializer, NullHandleValue, args, &ignored))
         return false;
 
     MOZ_ASSERT(ignored.isUndefined(),
                "Unexpected return value from non-legacy Intl object initializer");
     return true;
 }
 
 enum class DateTimeFormatOptions
@@ -841,18 +840,17 @@ LegacyIntlInitialize(JSContext* cx, Hand
     FixedInvokeArgs<5> args(cx);
 
     args[0].setObject(*obj);
     args[1].set(thisValue);
     args[2].set(locales);
     args[3].set(options);
     args[4].setBoolean(dtfOptions == DateTimeFormatOptions::EnableMozExtensions);
 
-    RootedValue thisv(cx, NullValue());
-    if (!js::CallSelfHostedFunction(cx, initializer, thisv, args, result))
+    if (!js::CallSelfHostedFunction(cx, initializer, NullHandleValue, args, result))
         return false;
 
     MOZ_ASSERT(result.isObject(), "Legacy Intl object initializer must return an object");
     return true;
 }
 
 // CountAvailable and GetAvailable describe the signatures used for ICU API
 // to determine available locales for various functionality.
@@ -894,18 +892,18 @@ intl_availableLocales(JSContext* cx, Cou
  */
 static JSObject*
 GetInternals(JSContext* cx, HandleObject obj)
 {
     FixedInvokeArgs<1> args(cx);
 
     args[0].setObject(*obj);
 
-    RootedValue v(cx, NullValue());
-    if (!js::CallSelfHostedFunction(cx, cx->names().getInternals, v, args, &v))
+    RootedValue v(cx);
+    if (!js::CallSelfHostedFunction(cx, cx->names().getInternals, NullHandleValue, args, &v))
         return nullptr;
 
     return &v.toObject();
 }
 
 static bool
 equal(const char* s1, const char* s2)
 {
@@ -1051,18 +1049,18 @@ Collator(JSContext* cx, const CallArgs& 
 
     Rooted<CollatorObject*> collator(cx, NewObjectWithGivenProto<CollatorObject>(cx, proto));
     if (!collator)
         return false;
 
     collator->setReservedSlot(CollatorObject::INTERNALS_SLOT, NullValue());
     collator->setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(nullptr));
 
-    RootedValue locales(cx, args.get(0));
-    RootedValue options(cx, args.get(1));
+    HandleValue locales = args.get(0);
+    HandleValue options = args.get(1);
 
     // Step 6.
     if (!IntlInitialize(cx, collator, cx->names().InitializeCollator, locales, options))
         return false;
 
     args.rval().setObject(*collator);
     return true;
 }
@@ -1230,70 +1228,76 @@ NewUCollator(JSContext* cx, Handle<Colla
     UColAttributeValue uAlternate = UCOL_DEFAULT;
     UColAttributeValue uNumeric = UCOL_OFF;
     // Normalization is always on to meet the canonical equivalence requirement.
     UColAttributeValue uNormalization = UCOL_ON;
     UColAttributeValue uCaseFirst = UCOL_DEFAULT;
 
     if (!GetProperty(cx, internals, internals, cx->names().usage, &value))
         return nullptr;
-    JSLinearString* usage = value.toString()->ensureLinear(cx);
-    if (!usage)
-        return nullptr;
-    if (StringEqualsAscii(usage, "search")) {
-        // ICU expects search as a Unicode locale extension on locale.
-        // Unicode locale extensions must occur before private use extensions.
-        const char* oldLocale = locale.ptr();
-        const char* p;
-        size_t index;
-        size_t localeLen = strlen(oldLocale);
-        if ((p = strstr(oldLocale, "-x-")))
-            index = p - oldLocale;
-        else
-            index = localeLen;
-
-        const char* insert;
-        if ((p = strstr(oldLocale, "-u-")) && static_cast<size_t>(p - oldLocale) < index) {
-            index = p - oldLocale + 2;
-            insert = "-co-search";
+
+    {
+        JSLinearString* usage = value.toString()->ensureLinear(cx);
+        if (!usage)
+            return nullptr;
+        if (StringEqualsAscii(usage, "search")) {
+            // ICU expects search as a Unicode locale extension on locale.
+            // Unicode locale extensions must occur before private use extensions.
+            const char* oldLocale = locale.ptr();
+            const char* p;
+            size_t index;
+            size_t localeLen = strlen(oldLocale);
+            if ((p = strstr(oldLocale, "-x-")))
+                index = p - oldLocale;
+            else
+                index = localeLen;
+
+            const char* insert;
+            if ((p = strstr(oldLocale, "-u-")) && static_cast<size_t>(p - oldLocale) < index) {
+                index = p - oldLocale + 2;
+                insert = "-co-search";
+            } else {
+                insert = "-u-co-search";
+            }
+            size_t insertLen = strlen(insert);
+            char* newLocale = cx->pod_malloc<char>(localeLen + insertLen + 1);
+            if (!newLocale)
+                return nullptr;
+            memcpy(newLocale, oldLocale, index);
+            memcpy(newLocale + index, insert, insertLen);
+            memcpy(newLocale + index + insertLen, oldLocale + index, localeLen - index + 1); // '\0'
+            locale.clear();
+            locale.initBytes(JS::UniqueChars(newLocale));
         } else {
-            insert = "-u-co-search";
+            MOZ_ASSERT(StringEqualsAscii(usage, "sort"));
         }
-        size_t insertLen = strlen(insert);
-        char* newLocale = cx->pod_malloc<char>(localeLen + insertLen + 1);
-        if (!newLocale)
-            return nullptr;
-        memcpy(newLocale, oldLocale, index);
-        memcpy(newLocale + index, insert, insertLen);
-        memcpy(newLocale + index + insertLen, oldLocale + index, localeLen - index + 1); // '\0'
-        locale.clear();
-        locale.initBytes(JS::UniqueChars(newLocale));
-    } else {
-        MOZ_ASSERT(StringEqualsAscii(usage, "sort"));
     }
 
     // We don't need to look at the collation property - it can only be set
     // via the Unicode locale extension and is therefore already set on
     // locale.
 
     if (!GetProperty(cx, internals, internals, cx->names().sensitivity, &value))
         return nullptr;
-    JSLinearString* sensitivity = value.toString()->ensureLinear(cx);
-    if (!sensitivity)
-        return nullptr;
-    if (StringEqualsAscii(sensitivity, "base")) {
-        uStrength = UCOL_PRIMARY;
-    } else if (StringEqualsAscii(sensitivity, "accent")) {
-        uStrength = UCOL_SECONDARY;
-    } else if (StringEqualsAscii(sensitivity, "case")) {
-        uStrength = UCOL_PRIMARY;
-        uCaseLevel = UCOL_ON;
-    } else {
-        MOZ_ASSERT(StringEqualsAscii(sensitivity, "variant"));
-        uStrength = UCOL_TERTIARY;
+
+    {
+        JSLinearString* sensitivity = value.toString()->ensureLinear(cx);
+        if (!sensitivity)
+            return nullptr;
+        if (StringEqualsAscii(sensitivity, "base")) {
+            uStrength = UCOL_PRIMARY;
+        } else if (StringEqualsAscii(sensitivity, "accent")) {
+            uStrength = UCOL_SECONDARY;
+        } else if (StringEqualsAscii(sensitivity, "case")) {
+            uStrength = UCOL_PRIMARY;
+            uCaseLevel = UCOL_ON;
+        } else {
+            MOZ_ASSERT(StringEqualsAscii(sensitivity, "variant"));
+            uStrength = UCOL_TERTIARY;
+        }
     }
 
     if (!GetProperty(cx, internals, internals, cx->names().ignorePunctuation, &value))
         return nullptr;
     // According to the ICU team, UCOL_SHIFTED causes punctuation to be
     // ignored. Looking at Unicode Technical Report 35, Unicode Locale Data
     // Markup Language, "shifted" causes whitespace and punctuation to be
     // ignored - that's a bit more than asked for, but there's no way to get
@@ -1620,18 +1624,18 @@ NumberFormat(JSContext* cx, const CallAr
     numberFormat = NewObjectWithGivenProto<NumberFormatObject>(cx, proto);
     if (!numberFormat)
         return false;
 
     numberFormat->setReservedSlot(NumberFormatObject::INTERNALS_SLOT, NullValue());
     numberFormat->setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nullptr));
 
     RootedValue thisValue(cx, construct ? ObjectValue(*numberFormat) : args.thisv());
-    RootedValue locales(cx, args.get(0));
-    RootedValue options(cx, args.get(1));
+    HandleValue locales = args.get(0);
+    HandleValue options = args.get(1);
 
     // Step 3.
     return LegacyIntlInitialize(cx, numberFormat, cx->names().InitializeNumberFormat, thisValue,
                                 locales, options, DateTimeFormatOptions::Standard, args.rval());
 }
 
 static bool
 NumberFormat(JSContext* cx, unsigned argc, Value* vp)
@@ -1729,17 +1733,17 @@ js::intl_numberingSystem(JSContext* cx, 
     if (U_FAILURE(status)) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
         return false;
     }
 
     ScopedICUObject<UNumberingSystem, unumsys_close> toClose(numbers);
 
     const char* name = unumsys_getName(numbers);
-    RootedString jsname(cx, JS_NewStringCopyZ(cx, name));
+    JSString* jsname = JS_NewStringCopyZ(cx, name);
     if (!jsname)
         return false;
 
     args.rval().setString(jsname);
     return true;
 }
 
 
@@ -1853,49 +1857,52 @@ NewUNumberFormat(JSContext* cx, Handle<N
     RootedString currency(cx);
     AutoStableStringChars stableChars(cx);
 
     // We don't need to look at numberingSystem - it can only be set via
     // the Unicode locale extension and is therefore already set on locale.
 
     if (!GetProperty(cx, internals, internals, cx->names().style, &value))
         return nullptr;
-    JSLinearString* style = value.toString()->ensureLinear(cx);
-    if (!style)
-        return nullptr;
-
-    if (StringEqualsAscii(style, "currency")) {
-        if (!GetProperty(cx, internals, internals, cx->names().currency, &value))
-            return nullptr;
-        currency = value.toString();
-        MOZ_ASSERT(currency->length() == 3,
-                   "IsWellFormedCurrencyCode permits only length-3 strings");
-        if (!stableChars.initTwoByte(cx, currency))
-            return nullptr;
-        // uCurrency remains owned by stableChars.
-        uCurrency = stableChars.twoByteRange().begin().get();
-
-        if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay, &value))
+
+    {
+        JSLinearString* style = value.toString()->ensureLinear(cx);
+        if (!style)
             return nullptr;
-        JSLinearString* currencyDisplay = value.toString()->ensureLinear(cx);
-        if (!currencyDisplay)
-            return nullptr;
-        if (StringEqualsAscii(currencyDisplay, "code")) {
-            uStyle = UNUM_CURRENCY_ISO;
-        } else if (StringEqualsAscii(currencyDisplay, "symbol")) {
-            uStyle = UNUM_CURRENCY;
+
+        if (StringEqualsAscii(style, "currency")) {
+            if (!GetProperty(cx, internals, internals, cx->names().currency, &value))
+                return nullptr;
+            currency = value.toString();
+            MOZ_ASSERT(currency->length() == 3,
+                       "IsWellFormedCurrencyCode permits only length-3 strings");
+            if (!stableChars.initTwoByte(cx, currency))
+                return nullptr;
+            // uCurrency remains owned by stableChars.
+            uCurrency = stableChars.twoByteRange().begin().get();
+
+            if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay, &value))
+                return nullptr;
+            JSLinearString* currencyDisplay = value.toString()->ensureLinear(cx);
+            if (!currencyDisplay)
+                return nullptr;
+            if (StringEqualsAscii(currencyDisplay, "code")) {
+                uStyle = UNUM_CURRENCY_ISO;
+            } else if (StringEqualsAscii(currencyDisplay, "symbol")) {
+                uStyle = UNUM_CURRENCY;
+            } else {
+                MOZ_ASSERT(StringEqualsAscii(currencyDisplay, "name"));
+                uStyle = UNUM_CURRENCY_PLURAL;
+            }
+        } else if (StringEqualsAscii(style, "percent")) {
+            uStyle = UNUM_PERCENT;
         } else {
-            MOZ_ASSERT(StringEqualsAscii(currencyDisplay, "name"));
-            uStyle = UNUM_CURRENCY_PLURAL;
+            MOZ_ASSERT(StringEqualsAscii(style, "decimal"));
+            uStyle = UNUM_DECIMAL;
         }
-    } else if (StringEqualsAscii(style, "percent")) {
-        uStyle = UNUM_PERCENT;
-    } else {
-        MOZ_ASSERT(StringEqualsAscii(style, "decimal"));
-        uStyle = UNUM_DECIMAL;
     }
 
     bool hasP;
     if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits, &hasP))
         return nullptr;
 
     if (hasP) {
         if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, &value))
@@ -2478,18 +2485,18 @@ DateTimeFormat(JSContext* cx, const Call
     if (!dateTimeFormat)
         return false;
 
     dateTimeFormat->setReservedSlot(DateTimeFormatObject::INTERNALS_SLOT, NullValue());
     dateTimeFormat->setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT,
                                     PrivateValue(nullptr));
 
     RootedValue thisValue(cx, construct ? ObjectValue(*dateTimeFormat) : args.thisv());
-    RootedValue locales(cx, args.get(0));
-    RootedValue options(cx, args.get(1));
+    HandleValue locales = args.get(0);
+    HandleValue options = args.get(1);
 
     // Step 3.
     return LegacyIntlInitialize(cx, dateTimeFormat, cx->names().InitializeDateTimeFormat,
                                 thisValue, locales, options, dtfOptions, args.rval());
 }
 
 static bool
 DateTimeFormat(JSContext* cx, unsigned argc, Value* vp)
@@ -3582,18 +3589,18 @@ PluralRules(JSContext* cx, unsigned argc
     Rooted<PluralRulesObject*> pluralRules(cx);
     pluralRules = NewObjectWithGivenProto<PluralRulesObject>(cx, proto);
     if (!pluralRules)
         return false;
 
     pluralRules->setReservedSlot(PluralRulesObject::INTERNALS_SLOT, NullValue());
     pluralRules->setReservedSlot(PluralRulesObject::UPLURAL_RULES_SLOT, PrivateValue(nullptr));
 
-    RootedValue locales(cx, args.get(0));
-    RootedValue options(cx, args.get(1));
+    HandleValue locales = args.get(0);
+    HandleValue options = args.get(1);
 
     // Step 3.
     if (!IntlInitialize(cx, pluralRules, cx->names().InitializePluralRules, locales, options))
         return false;
 
     args.rval().setObject(*pluralRules);
     return true;
 }
@@ -3655,16 +3662,18 @@ js::intl_PluralRules_availableLocales(JS
 bool
 js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     MOZ_ASSERT(args.length() == 2);
 
     Rooted<PluralRulesObject*> pluralRules(cx, &args[0].toObject().as<PluralRulesObject>());
 
+    double x = args[1].toNumber();
+
     UNumberFormat* nf = NewUNumberFormatForPluralRules(cx, pluralRules);
     if (!nf)
         return false;
 
     ScopedICUObject<UNumberFormat, unum_close> closeNumberFormat(nf);
 
     RootedObject internals(cx, GetInternals(cx, pluralRules));
     if (!internals)
@@ -3675,28 +3684,29 @@ js::intl_SelectPluralRule(JSContext* cx,
     if (!GetProperty(cx, internals, internals, cx->names().locale, &value))
         return false;
     JSAutoByteString locale(cx, value.toString());
     if (!locale)
         return false;
 
     if (!GetProperty(cx, internals, internals, cx->names().type, &value))
         return false;
-    RootedLinearString type(cx, value.toString()->ensureLinear(cx));
-    if (!type)
-        return false;
-
-    double x = args[1].toNumber();
 
     UPluralType category;
-    if (StringEqualsAscii(type, "cardinal")) {
-        category = UPLURAL_TYPE_CARDINAL;
-    } else {
-        MOZ_ASSERT(StringEqualsAscii(type, "ordinal"));
-        category = UPLURAL_TYPE_ORDINAL;
+    {
+        JSLinearString* type = value.toString()->ensureLinear(cx);
+        if (!type)
+            return false;
+
+        if (StringEqualsAscii(type, "cardinal")) {
+            category = UPLURAL_TYPE_CARDINAL;
+        } else {
+            MOZ_ASSERT(StringEqualsAscii(type, "ordinal"));
+            category = UPLURAL_TYPE_ORDINAL;
+        }
     }
 
     // TODO: Cache UPluralRules in PluralRulesObject::UPluralRulesSlot.
     UErrorCode status = U_ZERO_ERROR;
     UPluralRules* pr = uplrules_openForType(icuLocale(locale.ptr()), category, &status);
     if (U_FAILURE(status)) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
         return false;
@@ -3830,17 +3840,17 @@ static const JSFunctionSpec relativeTime
  * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1
  */
 static bool
 RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     // Step 1.
-    if (!ThrowIfNotConstructing(cx, args, "Intl.PluralRules"))
+    if (!ThrowIfNotConstructing(cx, args, "Intl.RelativeTimeFormat"))
         return false;
 
     // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
     RootedObject proto(cx);
     if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto))
         return false;
 
     if (!proto) {
@@ -3956,70 +3966,73 @@ js::intl_RelativeTimeFormat_availableLoc
 bool
 js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     MOZ_ASSERT(args.length() == 3);
 
     RootedObject relativeTimeFormat(cx, &args[0].toObject());
 
+    double t = args[1].toNumber();
+
     RootedObject internals(cx, GetInternals(cx, relativeTimeFormat));
     if (!internals)
         return false;
 
     RootedValue value(cx);
 
     if (!GetProperty(cx, internals, internals, cx->names().locale, &value))
         return false;
     JSAutoByteString locale(cx, value.toString());
     if (!locale)
         return false;
 
     if (!GetProperty(cx, internals, internals, cx->names().style, &value))
         return false;
-    RootedLinearString style(cx, value.toString()->ensureLinear(cx));
-    if (!style)
-        return false;
-
-    double t = args[1].toNumber();
 
     UDateRelativeDateTimeFormatterStyle relDateTimeStyle;
-
-    if (StringEqualsAscii(style, "short")) {
-        relDateTimeStyle = UDAT_STYLE_SHORT;
-    } else if (StringEqualsAscii(style, "narrow")) {
-        relDateTimeStyle = UDAT_STYLE_NARROW;
-    } else {
-        MOZ_ASSERT(StringEqualsAscii(style, "long"));
-        relDateTimeStyle = UDAT_STYLE_LONG;
+    {
+        JSLinearString* style = value.toString()->ensureLinear(cx);
+        if (!style)
+            return false;
+
+        if (StringEqualsAscii(style, "short")) {
+            relDateTimeStyle = UDAT_STYLE_SHORT;
+        } else if (StringEqualsAscii(style, "narrow")) {
+            relDateTimeStyle = UDAT_STYLE_NARROW;
+        } else {
+            MOZ_ASSERT(StringEqualsAscii(style, "long"));
+            relDateTimeStyle = UDAT_STYLE_LONG;
+        }
     }
 
-    JSLinearString* unit = args[2].toString()->ensureLinear(cx);
-    if (!unit)
-        return false;
-
     URelativeDateTimeUnit relDateTimeUnit;
-
-    if (StringEqualsAscii(unit, "second")) {
-        relDateTimeUnit = UDAT_REL_UNIT_SECOND;
-    } else if (StringEqualsAscii(unit, "minute")) {
-        relDateTimeUnit = UDAT_REL_UNIT_MINUTE;
-    } else if (StringEqualsAscii(unit, "hour")) {
-        relDateTimeUnit = UDAT_REL_UNIT_HOUR;
-    } else if (StringEqualsAscii(unit, "day")) {
-        relDateTimeUnit = UDAT_REL_UNIT_DAY;
-    } else if (StringEqualsAscii(unit, "week")) {
-        relDateTimeUnit = UDAT_REL_UNIT_WEEK;
-    } else if (StringEqualsAscii(unit, "month")) {
-        relDateTimeUnit = UDAT_REL_UNIT_MONTH;
-    } else if (StringEqualsAscii(unit, "quarter")) {
-        relDateTimeUnit = UDAT_REL_UNIT_QUARTER;
-    } else {
-        MOZ_ASSERT(StringEqualsAscii(unit, "year"));
-        relDateTimeUnit = UDAT_REL_UNIT_YEAR;
+    {
+        JSLinearString* unit = args[2].toString()->ensureLinear(cx);
+        if (!unit)
+            return false;
+
+        if (StringEqualsAscii(unit, "second")) {
+            relDateTimeUnit = UDAT_REL_UNIT_SECOND;
+        } else if (StringEqualsAscii(unit, "minute")) {
+            relDateTimeUnit = UDAT_REL_UNIT_MINUTE;
+        } else if (StringEqualsAscii(unit, "hour")) {
+            relDateTimeUnit = UDAT_REL_UNIT_HOUR;
+        } else if (StringEqualsAscii(unit, "day")) {
+            relDateTimeUnit = UDAT_REL_UNIT_DAY;
+        } else if (StringEqualsAscii(unit, "week")) {
+            relDateTimeUnit = UDAT_REL_UNIT_WEEK;
+        } else if (StringEqualsAscii(unit, "month")) {
+            relDateTimeUnit = UDAT_REL_UNIT_MONTH;
+        } else if (StringEqualsAscii(unit, "quarter")) {
+            relDateTimeUnit = UDAT_REL_UNIT_QUARTER;
+        } else {
+            MOZ_ASSERT(StringEqualsAscii(unit, "year"));
+            relDateTimeUnit = UDAT_REL_UNIT_YEAR;
+        }
     }
 
     // ICU doesn't handle -0 well: work around this by converting it to +0.
     // See: http://bugs.icu-project.org/trac/ticket/12936
     if (IsNegativeZero(t))
         t = +0.0;
 
     UErrorCode status = U_ZERO_ERROR;
@@ -4029,31 +4042,35 @@ js::intl_FormatRelativeTime(JSContext* c
     if (U_FAILURE(status)) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
         return false;
     }
 
     ScopedICUObject<URelativeDateTimeFormatter, ureldatefmt_close> closeRelativeTimeFormat(rtf);
 
     JSString* str = Call(cx, [rtf, t, relDateTimeUnit](UChar* chars, int32_t size, UErrorCode* status) {
-            return ureldatefmt_format(rtf, t, relDateTimeUnit, chars, size, status);
-        });
+        return ureldatefmt_format(rtf, t, relDateTimeUnit, chars, size, status);
+    });
     if (!str)
         return false;
 
     args.rval().setString(str);
     return true;
 }
 
 
 /******************** String ********************/
 
 static const char*
-CaseMappingLocale(JSLinearString* locale)
+CaseMappingLocale(JSContext* cx, JSString* str)
 {
+    JSLinearString* locale = str->ensureLinear(cx);
+    if (!locale)
+        return nullptr;
+
     MOZ_ASSERT(locale->length() >= 2, "locale is a valid language tag");
 
     // Lithuanian, Turkish, and Azeri have language dependent case mappings.
     static const char languagesWithSpecialCasing[][3] = { "lt", "tr", "az" };
 
     // All strings in |languagesWithSpecialCasing| are of length two, so we
     // only need to compare the first two characters to find a matching locale.
     // ES2017 Intl, ยง9.2.2 BestAvailableLocale
@@ -4065,38 +4082,32 @@ CaseMappingLocale(JSLinearString* locale
                 return language;
             }
         }
     }
 
     return ""; // ICU root locale
 }
 
-static bool
-HasLanguageDependentCasing(JSLinearString* locale)
-{
-    return !equal(CaseMappingLocale(locale), "");
-}
-
 bool
 js::intl_toLocaleLowerCase(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     MOZ_ASSERT(args.length() == 2);
     MOZ_ASSERT(args[0].isString());
     MOZ_ASSERT(args[1].isString());
 
     RootedString string(cx, args[0].toString());
 
-    RootedLinearString locale(cx, args[1].toString()->ensureLinear(cx));
+    const char* locale = CaseMappingLocale(cx, args[1].toString());
     if (!locale)
         return false;
 
     // Call String.prototype.toLowerCase() for language independent casing.
-    if (!HasLanguageDependentCasing(locale)) {
+    if (equal(locale, "")) {
         JSString* str = js::StringToLowerCase(cx, string);
         if (!str)
             return false;
 
         args.rval().setString(str);
         return true;
     }
 
@@ -4104,19 +4115,18 @@ js::intl_toLocaleLowerCase(JSContext* cx
     if (!inputChars.initTwoByte(cx, string))
         return false;
     mozilla::Range<const char16_t> input = inputChars.twoByteRange();
 
     // Maximum case mapping length is three characters.
     static_assert(JSString::MAX_LENGTH < INT32_MAX / 3,
                   "Case conversion doesn't overflow int32_t indices");
 
-    JSString* str = Call(cx, [&input, &locale](UChar* chars, int32_t size, UErrorCode* status) {
-        return u_strToLower(chars, size, input.begin().get(), input.length(),
-                            CaseMappingLocale(locale), status);
+    JSString* str = Call(cx, [&input, locale](UChar* chars, int32_t size, UErrorCode* status) {
+        return u_strToLower(chars, size, input.begin().get(), input.length(), locale, status);
     });
     if (!str)
         return false;
 
     args.rval().setString(str);
     return true;
 }
 
@@ -4125,22 +4135,22 @@ js::intl_toLocaleUpperCase(JSContext* cx
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     MOZ_ASSERT(args.length() == 2);
     MOZ_ASSERT(args[0].isString());
     MOZ_ASSERT(args[1].isString());
 
     RootedString string(cx, args[0].toString());
 
-    RootedLinearString locale(cx, args[1].toString()->ensureLinear(cx));
+    const char* locale = CaseMappingLocale(cx, args[1].toString());
     if (!locale)
         return false;
 
     // Call String.prototype.toUpperCase() for language independent casing.
-    if (!HasLanguageDependentCasing(locale)) {
+    if (equal(locale, "")) {
         JSString* str = js::StringToUpperCase(cx, string);
         if (!str)
             return false;
 
         args.rval().setString(str);
         return true;
     }
 
@@ -4148,19 +4158,18 @@ js::intl_toLocaleUpperCase(JSContext* cx
     if (!inputChars.initTwoByte(cx, string))
         return false;
     mozilla::Range<const char16_t> input = inputChars.twoByteRange();
 
     // Maximum case mapping length is three characters.
     static_assert(JSString::MAX_LENGTH < INT32_MAX / 3,
                   "Case conversion doesn't overflow int32_t indices");
 
-    JSString* str = Call(cx, [&input, &locale](UChar* chars, int32_t size, UErrorCode* status) {
-        return u_strToUpper(chars, size, input.begin().get(), input.length(),
-                            CaseMappingLocale(locale), status);
+    JSString* str = Call(cx, [&input, locale](UChar* chars, int32_t size, UErrorCode* status) {
+        return u_strToUpper(chars, size, input.begin().get(), input.length(), locale, status);
     });
     if (!str)
         return false;
 
     args.rval().setString(str);
     return true;
 }
 
@@ -4487,37 +4496,37 @@ ComputeSingleDisplayName(JSContext* cx, 
 }
 
 bool
 js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     MOZ_ASSERT(args.length() == 3);
 
-    RootedString str(cx);
-
     // 1. Assert: locale is a string.
-    str = args[0].toString();
+    RootedString str(cx, args[0].toString());
     JSAutoByteString locale;
     if (!locale.encodeUtf8(cx, str))
         return false;
 
     // 2. Assert: style is a string.
-    JSLinearString* style = args[1].toString()->ensureLinear(cx);
-    if (!style)
-        return false;
-
     DisplayNameStyle dnStyle;
-    if (StringEqualsAscii(style, "narrow")) {
-        dnStyle = DisplayNameStyle::Narrow;
-    } else if (StringEqualsAscii(style, "short")) {
-        dnStyle = DisplayNameStyle::Short;
-    } else {
-        MOZ_ASSERT(StringEqualsAscii(style, "long"));
-        dnStyle = DisplayNameStyle::Long;
+    {
+        JSLinearString* style = args[1].toString()->ensureLinear(cx);
+        if (!style)
+            return false;
+
+        if (StringEqualsAscii(style, "narrow")) {
+            dnStyle = DisplayNameStyle::Narrow;
+        } else if (StringEqualsAscii(style, "short")) {
+            dnStyle = DisplayNameStyle::Short;
+        } else {
+            MOZ_ASSERT(StringEqualsAscii(style, "long"));
+            dnStyle = DisplayNameStyle::Long;
+        }
     }
 
     // 3. Assert: keys is an Array.
     RootedArrayObject keys(cx, &args[2].toObject().as<ArrayObject>());
     if (!keys)
         return false;
 
     // 4. Let result be ArrayCreate(0).
--- a/js/src/builtin/Intl.js
+++ b/js/src/builtin/Intl.js
@@ -1675,22 +1675,20 @@ function InitializeCollator(collator, lo
 
     // Step 3.
     var requestedLocales = CanonicalizeLocaleList(locales);
     lazyCollatorData.requestedLocales = requestedLocales;
 
     // Steps 4-5.
     //
     // If we ever need more speed here at startup, we should try to detect the
-    // case where |options === undefined| and Object.prototype hasn't been
-    // mucked with.  (|options| is fully consumed in this method, so it's not a
-    // concern that Object.prototype might be touched between now and when
-    // |resolveCollatorInternals| is called.)  For now, just keep it simple.
+    // case where |options === undefined| and then directly use the default
+    // value for each option.  For now, just keep it simple.
     if (options === undefined)
-        options = {};
+        options = std_Object_create(null);
     else
         options = ToObject(options);
 
     // Compute options that impact interpretation of locale.
     // Step 6.
     var u = GetOption(options, "usage", "string", ["sort", "search"], "sort");
     lazyCollatorData.usage = u;
 
@@ -2170,22 +2168,20 @@ function InitializeNumberFormat(numberFo
 
     // Step 3.
     var requestedLocales = CanonicalizeLocaleList(locales);
     lazyNumberFormatData.requestedLocales = requestedLocales;
 
     // Steps 4-5.
     //
     // If we ever need more speed here at startup, we should try to detect the
-    // case where |options === undefined| and Object.prototype hasn't been
-    // mucked with.  (|options| is fully consumed in this method, so it's not a
-    // concern that Object.prototype might be touched between now and when
-    // |resolveNumberFormatInternals| is called.)  For now just keep it simple.
+    // case where |options === undefined| and then directly use the default
+    // value for each option.  For now, just keep it simple.
     if (options === undefined)
-        options = {};
+        options = std_Object_create(null);
     else
         options = ToObject(options);
 
     // Compute options that impact interpretation of locale.
     // Step 6.
     var opt = new Record();
     lazyNumberFormatData.opt = opt;
 
@@ -3367,17 +3363,17 @@ function InitializePluralRules(pluralRul
     const lazyPluralRulesData = std_Object_create(null);
 
     // Step 3.
     let requestedLocales = CanonicalizeLocaleList(locales);
     lazyPluralRulesData.requestedLocales = requestedLocales;
 
     // Steps 4-5.
     if (options === undefined)
-        options = {};
+        options = std_Object_create(null);
     else
         options = ToObject(options);
 
     // Step 6.
     const type = GetOption(options, "type", "string", ["cardinal", "ordinal"], "cardinal");
     lazyPluralRulesData.type = type;
 
     // Step 8.
@@ -3588,17 +3584,17 @@ function InitializeRelativeTimeFormat(re
     const lazyRelativeTimeFormatData = std_Object_create(null);
 
     // Step 3.
     let requestedLocales = CanonicalizeLocaleList(locales);
     lazyRelativeTimeFormatData.requestedLocales = requestedLocales;
 
     // Steps 4-5.
     if (options === undefined)
-        options = {};
+        options = std_Object_create(null);
     else
         options = ToObject(options);
 
     // Step 6.
     let opt = new Record();
 
     // Steps 7-8.
     let matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit");
@@ -3800,18 +3796,18 @@ function Intl_getCalendarInfo(locales) {
  *
  */
 function Intl_getDisplayNames(locales, options) {
     // 1. Let requestLocales be ? CanonicalizeLocaleList(locales).
     const requestedLocales = CanonicalizeLocaleList(locales);
 
     // 2. If options is undefined, then
     if (options === undefined)
-        // a. Let options be ObjectCreate(%ObjectPrototype%).
-        options = {};
+        // a. Let options be ObjectCreate(null).
+        options = std_Object_create(null);
     // 3. Else,
     else
         // a. Let options be ? ToObject(options).
         options = ToObject(options);
 
     const DateTimeFormat = dateTimeFormatInternalProperties;
 
     // 4. Let localeData be %DateTimeFormat%.[[localeData]].
--- a/js/src/gc/GCRuntime.h
+++ b/js/src/gc/GCRuntime.h
@@ -697,21 +697,16 @@ class MemoryCounter
         bytes_ += ptrdiff_t(bytes);
         if (MOZ_UNLIKELY(isTooMuchMalloc())) {
             if (!triggered_)
                 triggered_ = owner->triggerGCForTooMuchMalloc();
         }
         return triggered_;
     }
 
-    void decrement(size_t bytes) {
-        MOZ_ASSERT(bytes <= bytes_);
-        bytes_ -= bytes;
-    }
-
     void adopt(MemoryCounter<T>& other) {
         bytes_ += other.bytes();
         other.reset();
     }
 
     ptrdiff_t bytes() const { return bytes_; }
     size_t maxBytes() const { return maxBytes_; }
     size_t initialMaxBytes(const AutoLockGC& lock) const { return initialMaxBytes_; }
--- a/js/src/gc/Zone.h
+++ b/js/src/gc/Zone.h
@@ -444,18 +444,17 @@ struct Zone : public JS::shadow::Zone,
 
     void updateGCMallocBytesOnGC(const js::AutoLockGC& lock) {
         gcMallocCounter.updateOnGC(lock);
     }
     void setGCMaxMallocBytes(size_t value, const js::AutoLockGC& lock) {
         gcMallocCounter.setMax(value, lock);
     }
     void updateMallocCounter(size_t nbytes) {
-        if (!runtime_->gc.updateMallocCounter(nbytes))
-            gcMallocCounter.update(this, nbytes);
+        gcMallocCounter.update(this, nbytes);
     }
     void adoptMallocBytes(Zone* other) {
         gcMallocCounter.adopt(other->gcMallocCounter);
     }
     size_t GCMaxMallocBytes() const { return gcMallocCounter.maxBytes(); }
     size_t GCMallocBytes() const { return gcMallocCounter.bytes(); }
 
     void updateJitCodeMallocBytes(size_t size) { jitCodeCounter.update(this, size); }
--- a/js/src/jit-test/tests/basic/bug920484.js
+++ b/js/src/jit-test/tests/basic/bug920484.js
@@ -1,26 +1,27 @@
 load(libdir + "asserts.js");
 
 // Add an invalid "localeMatcher" option to Object.prototype, the exact value does not matter,
 // any value except "best fit" or "lookup" is okay.
 Object.prototype.localeMatcher = "invalid matcher option";
 
 // The Intl API may not be available in the testing environment.
 if (this.hasOwnProperty("Intl")) {
-    // Intl constructors no longer work properly, because "localeMatcher" defaults to the invalid
-    // value from Object.prototype. Except for Intl.DateTimeFormat, cf. ECMA-402 ToDateTimeOptions.
-    assertThrowsInstanceOf(() => new Intl.Collator(), RangeError);
-    assertThrowsInstanceOf(() => new Intl.NumberFormat(), RangeError);
+    // Intl constructors still work perfectly fine. The default options object doesn't inherit
+    // from Object.prototype and the invalid "localeMatcher" value from Object.prototype isn't
+    // consulted.
+    new Intl.Collator().compare("a", "b");
+    new Intl.NumberFormat().format(10);
     new Intl.DateTimeFormat().format(new Date);
 
     // If an explicit "localeMatcher" option is given, the default from Object.prototype is ignored.
     new Intl.Collator(void 0, {localeMatcher: "lookup"}).compare("a", "b");
     new Intl.NumberFormat(void 0, {localeMatcher: "lookup"}).format(10);
     new Intl.DateTimeFormat(void 0, {localeMatcher: "lookup"}).format(new Date);
 
     delete Object.prototype.localeMatcher;
 
-    // After removing the default option from Object.prototype, everything works again as expected.
+    // After removing the default option from Object.prototype, everything still works as expected.
     new Intl.Collator().compare("a", "b");
     new Intl.NumberFormat().format(10);
     new Intl.DateTimeFormat().format(new Date);
 }
--- a/js/src/jsgc.cpp
+++ b/js/src/jsgc.cpp
@@ -4332,31 +4332,25 @@ GCRuntime::markCompartments()
     }
 }
 
 void
 GCRuntime::updateMallocCountersOnGC()
 {
     AutoLockGC lock(rt);
 
-    size_t totalBytesInCollectedZones = 0;
+    // Update the malloc counters for any zones we are collecting.
     for (ZonesIter zone(rt, WithAtoms); !zone.done(); zone.next()) {
-        if (zone->isCollecting()) {
-            totalBytesInCollectedZones += zone->GCMallocBytes();
+        if (zone->isCollecting())
             zone->updateGCMallocBytesOnGC(lock);
-        }
-    }
-
-    // Update the runtime malloc counter. If we are doing a full GC then clear
-    // it, otherwise decrement it by the previous malloc bytes count for the
-    // zones we did collect.
+    }
+
+    // Update the runtime malloc counter only if we are doing a full GC.
     if (isFull)
         mallocCounter.updateOnGC(lock);
-    else
-        mallocCounter.decrement(totalBytesInCollectedZones);
 }
 
 template <class ZoneIterT>
 void
 GCRuntime::markWeakReferences(gcstats::PhaseKind phase)
 {
     MOZ_ASSERT(marker.isDrained());
 
--- a/js/src/tests/jstests.list
+++ b/js/src/tests/jstests.list
@@ -463,22 +463,16 @@ skip include test262/built-ins/RegExp/pr
 # RegExp lookBehind
 # https://bugzilla.mozilla.org/show_bug.cgi?id=1225665
 skip include test262/built-ins/RegExp/lookBehind/jstests.list
 
 # RegExp named groups
 # https://bugzilla.mozilla.org/show_bug.cgi?id=1362154
 skip include test262/built-ins/RegExp/named-groups/jstests.list
 
-# https://bugzilla.mozilla.org/show_bug.cgi?id=1398185
-skip script test262/intl402/Collator/default-options-object-prototype.js
-skip script test262/intl402/Number/prototype/toLocaleString/default-options-object-prototype.js
-skip script test262/intl402/String/prototype/localeCompare/default-options-object-prototype.js
-skip script test262/intl402/NumberFormat/default-options-object-prototype.js
-
 # https://bugzilla.mozilla.org/show_bug.cgi?id=1386146
 skip script test262/intl402/DateTimeFormat/prototype/resolvedOptions/hourCycle.js
 
 # https://bugzilla.mozilla.org/show_bug.cgi?id=1407587
 skip script test262/language/expressions/assignment/destructuring/keyed-destructuring-property-reference-target-evaluation-order.js
 skip script test262/language/expressions/assignment/destructuring/iterator-destructuring-property-reference-target-evaluation-order.js
 
 # https://bugzilla.mozilla.org/show_bug.cgi?id=1407588
new file mode 100644
--- /dev/null
+++ b/layout/generic/crashtests/1375858.html
@@ -0,0 +1,12 @@
+<html>
+<head>
+<style>
+  *::-moz-list-bullet, * {
+    transform-style:preserve-3d;
+  }
+</style>
+</head>
+<body>
+  <li></li>
+</body>
+</html>
--- a/layout/generic/crashtests/crashtests.list
+++ b/layout/generic/crashtests/crashtests.list
@@ -657,14 +657,15 @@ load 1308876-1.html
 load 1316649.html
 load 1349650.html
 asserts-if(browserIsRemote,0-5) load 1349816-1.html # bug 1350352
 load 1350372.html
 load 1364361-1.html
 load 1367413-1.html
 load 1368617-1.html
 load 1373586.html
+load 1375858.html
 load 1381134.html
 load 1381134-2.html
 load 1401420-1.html
 load 1401709.html
 load 1401807.html
 load 1405443.html
--- a/layout/generic/nsBulletFrame.h
+++ b/layout/generic/nsBulletFrame.h
@@ -82,16 +82,24 @@ public:
                       nsReflowStatus& aStatus) override;
   virtual nscoord GetMinISize(gfxContext *aRenderingContext) override;
   virtual nscoord GetPrefISize(gfxContext *aRenderingContext) override;
   void AddInlineMinISize(gfxContext* aRenderingContext,
                          nsIFrame::InlineMinISizeData* aData) override;
   void AddInlinePrefISize(gfxContext* aRenderingContext,
                           nsIFrame::InlinePrefISizeData* aData) override;
 
+  virtual bool IsFrameOfType(uint32_t aFlags) const override
+  {
+    if (aFlags & eSupportsCSSTransforms) {
+      return false;
+    }
+    return nsFrame::IsFrameOfType(aFlags);
+  }
+
   // nsBulletFrame
   int32_t SetListItemOrdinal(int32_t aNextOrdinal, bool* aChanged,
                              int32_t aIncrement);
 
   /* get list item text, with prefix & suffix */
   void GetListItemText(nsAString& aResult);
 
   void GetSpokenText(nsAString& aText);
--- a/layout/generic/nsFrameSelection.cpp
+++ b/layout/generic/nsFrameSelection.cpp
@@ -2506,26 +2506,25 @@ nsFrameSelection::UnselectCells(nsIConte
         }
 
       } else {
         // Remove cell from selection if it belongs to the given cells range or
         // it is spanned onto the cells range.
         nsTableCellFrame* cellFrame =
           tableFrame->GetCellFrameAt(curRowIndex, curColIndex);
 
-        int32_t origRowIndex, origColIndex;
-        cellFrame->GetRowIndex(origRowIndex);
-        cellFrame->GetColIndex(origColIndex);
+        uint32_t origRowIndex = cellFrame->RowIndex();
+        uint32_t origColIndex = cellFrame->ColIndex();
         uint32_t actualRowSpan =
           tableFrame->GetEffectiveRowSpanAt(origRowIndex, origColIndex);
         uint32_t actualColSpan =
           tableFrame->GetEffectiveColSpanAt(curRowIndex, curColIndex);
-        if (origRowIndex <= maxRowIndex && maxRowIndex >= 0 &&
+        if (origRowIndex <= static_cast<uint32_t>(maxRowIndex) && maxRowIndex >= 0 &&
             origRowIndex + actualRowSpan - 1 >= static_cast<uint32_t>(minRowIndex) &&
-            origColIndex <= maxColIndex && maxColIndex >= 0 &&
+            origColIndex <= static_cast<uint32_t>(maxColIndex) && maxColIndex >= 0 &&
             origColIndex + actualColSpan - 1 >= static_cast<uint32_t>(minColIndex)) {
 
           mDomSelections[index]->RemoveRange(range);
           // Since we've removed the range, decrement pointer to next range
           mSelectedCellIndex--;
         }
       }
     }
@@ -2549,43 +2548,42 @@ nsFrameSelection::AddCellsToSelection(ns
   if (!mDomSelections[index])
     return NS_ERROR_NULL_POINTER;
 
   nsTableWrapperFrame* tableFrame = do_QueryFrame(aTableContent->GetPrimaryFrame());
   if (!tableFrame) // Check that |table| is a table.
     return NS_ERROR_FAILURE;
 
   nsresult result = NS_OK;
-  int32_t row = aStartRowIndex;
+  uint32_t row = aStartRowIndex;
   while(true)
   {
-    int32_t col = aStartColumnIndex;
+    uint32_t col = aStartColumnIndex;
     while(true)
     {
       nsTableCellFrame* cellFrame = tableFrame->GetCellFrameAt(row, col);
 
       // Skip cells that are spanned from previous locations or are already selected
       if (cellFrame) {
-        int32_t origRow, origCol;
-        cellFrame->GetRowIndex(origRow);
-        cellFrame->GetColIndex(origCol);
+        uint32_t origRow = cellFrame->RowIndex();
+        uint32_t origCol = cellFrame->ColIndex();
         if (origRow == row && origCol == col && !cellFrame->IsSelected()) {
           result = SelectCellElement(cellFrame->GetContent());
           if (NS_FAILED(result)) return result;
         }
       }
       // Done when we reach end column
-      if (col == aEndColumnIndex) break;
+      if (col == static_cast<uint32_t>(aEndColumnIndex)) break;
 
       if (aStartColumnIndex < aEndColumnIndex)
         col ++;
       else
         col--;
     }
-    if (row == aEndRowIndex) break;
+    if (row == static_cast<uint32_t>(aEndRowIndex)) break;
 
     if (aStartRowIndex < aEndRowIndex)
       row++;
     else
       row--;
   }
   return result;
 }
--- a/layout/mathml/nsMathMLmtableFrame.cpp
+++ b/layout/mathml/nsMathMLmtableFrame.cpp
@@ -178,50 +178,48 @@ FindCellProperty(const nsIFrame* aCellFr
 
   return propertyData;
 }
 
 static void
 ApplyBorderToStyle(const nsMathMLmtdFrame* aFrame,
                    nsStyleBorder& aStyleBorder)
 {
-  int32_t rowIndex;
-  int32_t columnIndex;
-  aFrame->GetRowIndex(rowIndex);
-  aFrame->GetColIndex(columnIndex);
+  uint32_t rowIndex = aFrame->RowIndex();
+  uint32_t columnIndex = aFrame->ColIndex();
 
   nscoord borderWidth =
     nsPresContext::GetBorderWidthForKeyword(NS_STYLE_BORDER_WIDTH_THIN);
 
   nsTArray<int8_t>* rowLinesList =
     FindCellProperty(aFrame, RowLinesProperty());
 
   nsTArray<int8_t>* columnLinesList =
     FindCellProperty(aFrame, ColumnLinesProperty());
 
   // We don't place a row line on top of the first row
   if (rowIndex > 0 && rowLinesList) {
     // If the row number is greater than the number of provided rowline
     // values, we simply repeat the last value.
-    int32_t listLength = rowLinesList->Length();
+    uint32_t listLength = rowLinesList->Length();
     if (rowIndex < listLength) {
       aStyleBorder.SetBorderStyle(eSideTop,
                     rowLinesList->ElementAt(rowIndex - 1));
     } else {
       aStyleBorder.SetBorderStyle(eSideTop,
                     rowLinesList->ElementAt(listLength - 1));
     }
     aStyleBorder.SetBorderWidth(eSideTop, borderWidth);
   }
 
   // We don't place a column line on the left of the first column.
   if (columnIndex > 0 && columnLinesList) {
     // If the column number is greater than the number of provided columline
     // values, we simply repeat the last value.
-    int32_t listLength = columnLinesList->Length();
+    uint32_t listLength = columnLinesList->Length();
     if (columnIndex < listLength) {
       aStyleBorder.SetBorderStyle(eSideLeft,
                     columnLinesList->ElementAt(columnIndex - 1));
     } else {
       aStyleBorder.SetBorderStyle(eSideLeft,
                     columnLinesList->ElementAt(listLength - 1));
     }
     aStyleBorder.SetBorderWidth(eSideLeft, borderWidth);
@@ -1201,22 +1199,21 @@ uint8_t
 nsMathMLmtdFrame::GetVerticalAlign() const
 {
   // Set the default alignment in case no alignment was specified
   uint8_t alignment = nsTableCellFrame::GetVerticalAlign();
 
   nsTArray<int8_t>* alignmentList = FindCellProperty(this, RowAlignProperty());
 
   if (alignmentList) {
-    int32_t rowIndex;
-    GetRowIndex(rowIndex);
+    uint32_t rowIndex = RowIndex();
 
     // If the row number is greater than the number of provided rowalign values,
     // we simply repeat the last value.
-    if (rowIndex < (int32_t)alignmentList->Length())
+    if (rowIndex < alignmentList->Length())
       alignment = alignmentList->ElementAt(rowIndex);
     else
       alignment = alignmentList->ElementAt(alignmentList->Length() - 1);
   }
 
   return alignment;
 }
 
@@ -1293,22 +1290,21 @@ nsStyleText* nsMathMLmtdInnerFrame::Styl
   // Set the default alignment in case nothing was specified
   uint8_t alignment = StyleText()->mTextAlign;
 
   nsTArray<int8_t>* alignmentList =
     FindCellProperty(this, ColumnAlignProperty());
 
   if (alignmentList) {
     nsMathMLmtdFrame* cellFrame = (nsMathMLmtdFrame*)GetParent();
-    int32_t columnIndex;
-    cellFrame->GetColIndex(columnIndex);
+    uint32_t columnIndex = cellFrame->ColIndex();
 
     // If the column number is greater than the number of provided columalign
     // values, we simply repeat the last value.
-    if (columnIndex < (int32_t)alignmentList->Length())
+    if (columnIndex < alignmentList->Length())
       alignment = alignmentList->ElementAt(columnIndex);
     else
       alignment = alignmentList->ElementAt(alignmentList->Length() - 1);
   }
 
   mUniqueStyleText->mTextAlign = alignment;
   return mUniqueStyleText;
 }
--- a/layout/painting/FrameLayerBuilder.cpp
+++ b/layout/painting/FrameLayerBuilder.cpp
@@ -1096,23 +1096,16 @@ public:
     CollectOldLayers();
   }
 
   /**
    * This is the method that actually walks a display list and builds
    * the child layers.
    */
   void ProcessDisplayItems(nsDisplayList* aList);
-  void ProcessDisplayItems(nsDisplayList* aList,
-                           AnimatedGeometryRoot* aLastAnimatedGeometryRoot,
-                           const ActiveScrolledRoot* aLastASR,
-                           const nsPoint& aLastAGRTopLeft,
-                           nsPoint& aTopLeft,
-                           int32_t aMaxLayers,
-                           int& aLayerCount);
   /**
    * This finalizes all the open PaintedLayers by popping every element off
    * mPaintedLayerDataStack, then sets the children of the container layer
    * to be all the layers in mNewChildLayers in that order and removes any
    * layers as children of the container that aren't in mNewChildLayers.
    * @param aTextContentFlags if any child layer has CONTENT_COMPONENT_ALPHA,
    * set *aTextContentFlags to CONTENT_COMPONENT_ALPHA
    */
@@ -3978,30 +3971,18 @@ ContainerState::ProcessDisplayItems(nsDi
     if (ChooseAnimatedGeometryRoot(*aList, &lastAnimatedGeometryRoot, &lastASR)) {
       lastAGRTopLeft = (*lastAnimatedGeometryRoot)->GetOffsetToCrossDoc(mContainerReferenceFrame);
     }
   }
 
   int32_t maxLayers = gfxPrefs::MaxActiveLayers();
   int layerCount = 0;
 
-  ProcessDisplayItems(aList, lastAnimatedGeometryRoot, lastASR,
-                      lastAGRTopLeft, topLeft, maxLayers, layerCount);
-}
-
-void
-ContainerState::ProcessDisplayItems(nsDisplayList* aList,
-                                    AnimatedGeometryRoot* aLastAnimatedGeometryRoot,
-                                    const ActiveScrolledRoot* aLastASR,
-                                    const nsPoint& aLastAGRTopLeft,
-                                    nsPoint& aTopLeft,
-                                    int32_t aMaxLayers,
-                                    int& aLayerCount)
-{
-  for (nsDisplayItem* i = aList->GetBottom(); i; i = i->GetAbove()) {
+  FlattenedDisplayItemIterator iter(mBuilder, aList);
+  while (nsDisplayItem* i = iter.GetNext()) {
     nsDisplayItem* item = i;
     MOZ_ASSERT(item);
 
     DisplayItemType itemType = item->GetType();
 
     // If the item is a event regions item, but is empty (has no regions in it)
     // then we should just throw it out
     if (itemType == DisplayItemType::TYPE_LAYER_EVENT_REGIONS) {
@@ -4012,46 +3993,34 @@ ContainerState::ProcessDisplayItems(nsDi
         continue;
       }
     }
 
     // Peek ahead to the next item and see if it can be merged with the current
     // item. We create a list of consecutive items that can be merged together.
     AutoTArray<nsDisplayItem*, 1> mergedItems;
     mergedItems.AppendElement(item);
-    for (nsDisplayItem* peek = item->GetAbove(); peek; peek = peek->GetAbove()) {
+    while (nsDisplayItem* peek = iter.PeekNext()) {
       if (!item->CanMerge(peek)) {
         break;
       }
 
       mergedItems.AppendElement(peek);
 
       // Move the iterator forward since we will merge this item.
-      i = peek;
+      i = iter.GetNext();
     }
 
     if (mergedItems.Length() > 1) {
       // We have items that can be merged together. Merge them into a temporary
       // item and process that item immediately.
       item = mBuilder->MergeItems(mergedItems);
       MOZ_ASSERT(item && itemType == item->GetType());
     }
 
-    nsDisplayList* childItems = item->GetSameCoordinateSystemChildren();
-
-    if (item->ShouldFlattenAway(mBuilder)) {
-      MOZ_ASSERT(childItems);
-      ProcessDisplayItems(childItems, aLastAnimatedGeometryRoot, aLastASR,
-                          aLastAGRTopLeft, aTopLeft, aMaxLayers, aLayerCount);
-      if (childItems->NeedsTransparentSurface()) {
-        aList->SetNeedsTransparentSurface();
-      }
-      continue;
-    }
-
     MOZ_ASSERT(item->GetType() != DisplayItemType::TYPE_WRAP_LIST);
 
     NS_ASSERTION(mAppUnitsPerDevPixel == AppUnitsPerDevPixel(item),
       "items in a container layer should all have the same app units per dev pixel");
 
     if (mBuilder->NeedToForceTransparentSurfaceForItem(item)) {
       aList->SetNeedsTransparentSurface();
     }
@@ -4069,19 +4038,19 @@ ContainerState::ProcessDisplayItems(nsDi
     }
 
     bool forceInactive;
     AnimatedGeometryRoot* animatedGeometryRoot;
     const ActiveScrolledRoot* itemASR = nullptr;
     const DisplayItemClipChain* layerClipChain = nullptr;
     if (mFlattenToSingleLayer && layerState != LAYER_ACTIVE_FORCE) {
       forceInactive = true;
-      animatedGeometryRoot = aLastAnimatedGeometryRoot;
-      itemASR = aLastASR;
-      aTopLeft = aLastAGRTopLeft;
+      animatedGeometryRoot = lastAnimatedGeometryRoot;
+      itemASR = lastASR;
+      topLeft = lastAGRTopLeft;
       item->FuseClipChainUpTo(mBuilder, mContainerASR);
     } else {
       forceInactive = false;
       if (mManager->IsWidgetLayerManager()) {
         animatedGeometryRoot = item->GetAnimatedGeometryRoot();
         itemASR = item->GetActiveScrolledRoot();
         const DisplayItemClipChain* itemClipChain = item->GetClipChain();
         if (itemClipChain && itemClipChain->mASR == itemASR &&
@@ -4093,17 +4062,17 @@ ContainerState::ProcessDisplayItems(nsDi
       } else {
         // For inactive layer subtrees, splitting content into PaintedLayers
         // based on animated geometry roots is pointless. It's more efficient
         // to build the minimum number of layers.
         animatedGeometryRoot = mContainerAnimatedGeometryRoot;
         itemASR = mContainerASR;
         item->FuseClipChainUpTo(mBuilder, mContainerASR);
       }
-      aTopLeft = (*animatedGeometryRoot)->GetOffsetToCrossDoc(mContainerReferenceFrame);
+      topLeft = (*animatedGeometryRoot)->GetOffsetToCrossDoc(mContainerReferenceFrame);
     }
 
     const ActiveScrolledRoot* scrollMetadataASR =
         layerClipChain ? ActiveScrolledRoot::PickDescendant(itemASR, layerClipChain->mASR) : itemASR;
 
     bool snap;
     nsRect itemContent = item->GetBounds(mBuilder, &snap);
     if (itemType == DisplayItemType::TYPE_LAYER_EVENT_REGIONS) {
@@ -4150,28 +4119,28 @@ ContainerState::ProcessDisplayItems(nsDi
     // We haven't computed visibility at this point, so item->GetVisibleRect()
     // is just the dirty rect that item was initialized with. We intersect it
     // with the clipped item bounds to get a tighter visible rect.
     if (!prerenderedTransform) {
       itemVisibleRect = itemVisibleRect.Intersect(
         ScaleToOutsidePixels(item->GetVisibleRect(), false));
     }
 
-    if (aMaxLayers != -1 && aLayerCount >= aMaxLayers) {
+    if (maxLayers != -1 && layerCount >= maxLayers) {
       forceInactive = true;
     }
 
     // Assign the item to a layer
     if (layerState == LAYER_ACTIVE_FORCE ||
         (layerState == LAYER_INACTIVE && !mManager->IsWidgetLayerManager()) ||
         (!forceInactive &&
          (layerState == LAYER_ACTIVE_EMPTY ||
           layerState == LAYER_ACTIVE))) {
 
-      aLayerCount++;
+      layerCount++;
 
       // LAYER_ACTIVE_EMPTY means the layer is created just for its metadata.
       // We should never see an empty layer with any visible content!
       NS_ASSERTION(layerState != LAYER_ACTIVE_EMPTY ||
                    itemVisibleRect.IsEmpty(),
                    "State is LAYER_ACTIVE_EMPTY but visible rect is not.");
 
       // As long as the new layer isn't going to be a PaintedLayer,
@@ -4366,21 +4335,21 @@ ContainerState::ProcessDisplayItems(nsDi
       }
 
       if (item->GetType() == DisplayItemType::TYPE_MASK) {
         MOZ_ASSERT(itemClip.GetRoundedRectCount() == 0);
 
         nsDisplayMask* maskItem = static_cast<nsDisplayMask*>(item);
         SetupMaskLayerForCSSMask(ownLayer, maskItem);
 
-        if (i->GetAbove() &&
-            i->GetAbove()->GetType() == DisplayItemType::TYPE_SCROLL_INFO_LAYER) {
+        if (iter.PeekNext() &&
+            iter.PeekNext()->GetType() == DisplayItemType::TYPE_SCROLL_INFO_LAYER) {
           // Since we do build a layer for mask, there is no need for this
           // scroll info layer anymore.
-          i = i->GetAbove();
+          i = iter.GetNext();
         }
       }
 
       // Convert the visible rect to a region and give the item
       // a chance to try restrict it further.
       nsIntRegion itemVisibleRegion = itemVisibleRect;
       nsRegion tightBounds = item->GetTightBounds(mBuilder, &snap);
       if (!tightBounds.IsEmpty()) {
@@ -4479,17 +4448,17 @@ ContainerState::ProcessDisplayItems(nsDi
       mLayerBuilder->AddLayerDisplayItem(ownLayer, item, layerState, nullptr);
     } else {
       PaintedLayerData* paintedLayerData =
         mPaintedLayerDataTree.FindPaintedLayerFor(animatedGeometryRoot, itemASR, layerClipChain,
                                                   itemVisibleRect,
                                                   item->Frame()->In3DContextAndBackfaceIsHidden(),
                                                   [&]() {
           return NewPaintedLayerData(item, animatedGeometryRoot, itemASR, layerClipChain, scrollMetadataASR,
-                                     aTopLeft);
+                                     topLeft);
         });
 
       if (itemType == DisplayItemType::TYPE_LAYER_EVENT_REGIONS) {
         nsDisplayLayerEventRegions* eventRegions =
             static_cast<nsDisplayLayerEventRegions*>(item);
         paintedLayerData->AccumulateEventRegions(this, eventRegions);
       } else {
         // check to see if the new item has rounded rect clips in common with
@@ -4497,28 +4466,29 @@ ContainerState::ProcessDisplayItems(nsDi
         if (mManager->IsWidgetLayerManager()) {
           paintedLayerData->UpdateCommonClipCount(itemClip);
         }
         paintedLayerData->Accumulate(this, item, itemVisibleRect, itemClip, layerState, aList);
 
         if (!paintedLayerData->mLayer) {
           // Try to recycle the old layer of this display item.
           RefPtr<PaintedLayer> layer =
-            AttemptToRecyclePaintedLayer(animatedGeometryRoot, item, aTopLeft);
+            AttemptToRecyclePaintedLayer(animatedGeometryRoot, item, topLeft);
           if (layer) {
             paintedLayerData->mLayer = layer;
 
             NS_ASSERTION(FindIndexOfLayerIn(mNewChildLayers, layer) < 0,
                          "Layer already in list???");
             mNewChildLayers[paintedLayerData->mNewChildLayersIndex].mLayer = layer.forget();
           }
         }
       }
     }
 
+    nsDisplayList* childItems = item->GetSameCoordinateSystemChildren();
     if (childItems && childItems->NeedsTransparentSurface()) {
       aList->SetNeedsTransparentSurface();
     }
   }
 }
 
 void
 ContainerState::InvalidateForLayerChange(nsDisplayItem* aItem, PaintedLayer* aNewLayer)
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -1095,21 +1095,24 @@ void nsDisplayListBuilder::MarkOutOfFlow
   nsRect dirtyRectRelativeToDirtyFrame = GetDirtyRect();
   if (nsLayoutUtils::IsFixedPosFrameInDisplayPort(aFrame) &&
       IsPaintingToWindow()) {
     NS_ASSERTION(aDirtyFrame == aFrame->GetParent(), "Dirty frame should be viewport frame");
     // position: fixed items are reflowed into and only drawn inside the
     // viewport, or the scroll position clamping scrollport size, if one is
     // set.
     nsIPresShell* ps = aFrame->PresContext()->PresShell();
-    dirtyRectRelativeToDirtyFrame.MoveTo(0, 0);
     if (ps->IsScrollPositionClampingScrollPortSizeSet()) {
-      dirtyRectRelativeToDirtyFrame.SizeTo(ps->GetScrollPositionClampingScrollPortSize());
+      dirtyRectRelativeToDirtyFrame =
+        nsRect(nsPoint(0, 0), ps->GetScrollPositionClampingScrollPortSize());
+#ifdef MOZ_WIDGET_ANDROID
     } else {
-      dirtyRectRelativeToDirtyFrame.SizeTo(aDirtyFrame->GetSize());
+      dirtyRectRelativeToDirtyFrame =
+        nsRect(nsPoint(0, 0), aDirtyFrame->GetSize());
+#endif
     }
   }
   nsPoint offset = aFrame->GetOffsetTo(aDirtyFrame);
   nsRect dirty = dirtyRectRelativeToDirtyFrame - offset;
   nsRect overflowRect = aFrame->GetVisualOverflowRect();
 
   if (aFrame->IsTransformed() &&
       EffectCompositor::HasAnimationsForCompositor(aFrame,
@@ -6136,16 +6139,23 @@ CollectItemsWithOpacity(nsDisplayList* a
   }
 
   return true;
 }
 
 bool
 nsDisplayOpacity::ShouldFlattenAway(nsDisplayListBuilder* aBuilder)
 {
+  if (mFrame->GetPrevContinuation() ||
+      mFrame->GetNextContinuation()) {
+    // If we've been split, then we might need to merge, so
+    // don't flatten us away.
+    return false;
+  }
+
   if (NeedsActiveLayer(aBuilder, mFrame) || mOpacity == 0.0) {
     // If our opacity is zero then we'll discard all descendant display items
     // except for layer event regions, so there's no point in doing this
     // optimization (and if we do do it, then invalidations of those descendants
     // might trigger repainting).
     return false;
   }
 
--- a/layout/painting/nsDisplayList.h
+++ b/layout/painting/nsDisplayList.h
@@ -5583,16 +5583,83 @@ public:
   // (i.e. left and right respectively in horizontal writing modes,
   // regardless of bidi directionality; top and bottom in vertical modes).
   nscoord mVisIStartEdge;
   nscoord mVisIEndEdge;
   // Cached result of mFrame->IsSelected().  Only initialized when needed.
   mutable mozilla::Maybe<bool> mIsFrameSelected;
 };
 
+class FlattenedDisplayItemIterator
+{
+public:
+  FlattenedDisplayItemIterator(nsDisplayListBuilder* aBuilder,
+                               nsDisplayList* aList)
+    : mBuilder(aBuilder)
+    , mNext(aList->GetBottom())
+  {
+    ResolveFlattening();
+  }
+
+  nsDisplayItem* GetNext()
+  {
+    nsDisplayItem* next = mNext;
+
+    // Advance mNext to the following item
+    if (next) {
+      mNext = mNext->GetAbove();
+      ResolveFlattening();
+    }
+    return next;
+  }
+
+  nsDisplayItem* PeekNext()
+  {
+    return mNext;
+  }
+
+private:
+  bool AtEndOfNestedList()
+  {
+    return !mNext && mStack.Length() > 0;
+  }
+
+  bool ShouldFlattenNextItem()
+  {
+    return mNext && mNext->ShouldFlattenAway(mBuilder);
+  }
+
+  void ResolveFlattening()
+  {
+    // Handle the case where we reach the end of a nested list, or the current
+    // item should start a new nested list. Repeat this until we find an actual
+    // item, or the very end of the outer list.
+    while (AtEndOfNestedList() || ShouldFlattenNextItem()) {
+      if (AtEndOfNestedList()) {
+        // Pop the last item off the stack.
+        mNext = mStack.LastElement();
+        mStack.RemoveElementAt(mStack.Length() - 1);
+        // We stored the item that was flattened, so advance to the next.
+        mNext = mNext->GetAbove();
+      } else {
+        // This item wants to be flattened. Store the current item on the stack,
+        // and use the first item in the child list instead.
+        mStack.AppendElement(mNext);
+        nsDisplayList* childItems = mNext->GetSameCoordinateSystemChildren();
+        mNext = childItems->GetBottom();
+      }
+    }
+  }
+
+
+  nsDisplayListBuilder* mBuilder;
+  nsDisplayItem* mNext;
+  AutoTArray<nsDisplayItem*, 10> mStack;
+};
+
 namespace mozilla {
 
 class PaintTelemetry
 {
  public:
   enum class Metric {
     DisplayList,
     Layerization,
--- a/layout/reftests/bugs/reftest.list
+++ b/layout/reftests/bugs/reftest.list
@@ -1682,17 +1682,17 @@ HTTP == 652991-3.html 652991-3-ref.html
 HTTP == 652991-4.html 652991-4-ref.html
 fuzzy-if(skiaContent,1,5) == 653930-1.html 653930-1-ref.html
 HTTP(..) == 654057-1.html 654057-1-ref.html
 fuzzy-if(skiaContent,1,4500) == 654950-1.html 654950-1-ref.html # Quartz alpha blending doesn't match GL alpha blending
 == 655549-1.html 655549-1-ref.html
 == 655836-1.html 655836-1-ref.html
 != 656875.html about:blank
 == 658952.html 658952-ref.html
-fuzzy-if(skiaContent,1,3500) == 660682-1.html 660682-1-ref.html
+fuzzy-if(skiaContent,7,3500) fails-if(webrender) == 660682-1.html 660682-1-ref.html
 fuzzy-if(d2d,1,256) skip-if(Android) fuzzy-if(skiaContent,1,68000) fails-if(styloVsGecko) asserts-if(stylo,16-18) == 664127-1.xul 664127-1-ref.xul # Android: Intermittent failures - bug 1019131, stylo: bug 1397644
 == 665597-1.html 665597-1-ref.html
 == 665597-2.html 665597-2-ref.html
 == 667079-1.html 667079-1-ref.html
 fails-if(stylo||styloVsGecko) asserts-if(stylo,8-9) == 668319-1.xul about:blank # bug 1397644
 fails-if(stylo||styloVsGecko) != 669015-1.xul 669015-1-notref.xul # bug 1408235
 skip-if(azureSkiaGL) == 670442-1.html 670442-1-ref.html
 == 670467-1.html 670467-1-ref.html
--- a/layout/style/nsMediaList.cpp
+++ b/layout/style/nsMediaList.cpp
@@ -269,17 +269,17 @@ nsDocumentRuleResultCacheKey::Matches(
                        const nsTArray<css::DocumentRule*>& aRules) const
 {
   MOZ_ASSERT(mFinalized);
   MOZ_ASSERT(ArrayIsSorted(mMatchingRules));
   MOZ_ASSERT(ArrayIsSorted(aRules));
 
 #ifdef DEBUG
   for (css::DocumentRule* rule : mMatchingRules) {
-    MOZ_ASSERT(aRules.BinaryIndexOf(rule) != aRules.NoIndex,
+    MOZ_ASSERT(aRules.ContainsSorted(rule),
                "aRules must contain all rules in mMatchingRules");
   }
 #endif
 
   // First check that aPresContext matches all the rules listed in
   // mMatchingRules.
   for (css::DocumentRule* rule : mMatchingRules) {
     if (!rule->UseForPresentation(aPresContext)) {
--- a/layout/tables/nsCellMap.cpp
+++ b/layout/tables/nsCellMap.cpp
@@ -2426,19 +2426,18 @@ void nsCellMap::Dump(bool aIsBorderColla
     const CellDataArray& row = mRows[rIndex];
     uint32_t colCount = row.Length();
     printf("  ");
     for (colIndex = 0; colIndex < colCount; colIndex++) {
       CellData* cd = row[colIndex];
       if (cd) {
         if (cd->IsOrig()) {
           nsTableCellFrame* cellFrame = cd->GetCellFrame();
-          int32_t cellFrameColIndex;
-          cellFrame->GetColIndex(cellFrameColIndex);
-          printf("C%d,%d=%p(%d)  ", rIndex, colIndex, (void*)cellFrame,
+          uint32_t cellFrameColIndex = cellFrame->ColIndex();
+          printf("C%d,%d=%p(%u)  ", rIndex, colIndex, (void*)cellFrame,
                  cellFrameColIndex);
           cellCount++;
         }
       }
     }
     printf("\n");
   }
 
@@ -2515,18 +2514,17 @@ nsCellMap::GetCellInfoAt(const nsTableCe
       cellFrame = data->GetCellFrame();
       if (aOriginates)
         *aOriginates = true;
     }
     else {
       cellFrame = GetCellFrame(aRowX, aColX, *data, true);
     }
     if (cellFrame && aColSpan) {
-      int32_t initialColIndex;
-      cellFrame->GetColIndex(initialColIndex);
+      uint32_t initialColIndex = cellFrame->ColIndex();
       *aColSpan = GetEffectiveColSpan(aMap, aRowX, initialColIndex);
     }
   }
   return cellFrame;
 }
 
 
 bool nsCellMap::RowIsSpannedInto(int32_t         aRowIndex,
--- a/layout/tables/nsITableCellLayout.h
+++ b/layout/tables/nsITableCellLayout.h
@@ -9,31 +9,26 @@
 
 #define MAX_ROWSPAN 65534 // the cellmap can not handle more.
 #define MAX_COLSPAN 1000 // limit as IE and opera do.  If this ever changes,
                          // change COL_SPAN_OFFSET/COL_SPAN_SHIFT accordingly.
 
 /**
  * nsITableCellLayout
  * interface for layout objects that act like table cells.
+ * XXXbz This interface should really go away...
  *
  * @author  sclark
  */
 class nsITableCellLayout
 {
 public:
 
   NS_DECL_QUERYFRAME_TARGET(nsITableCellLayout)
 
   /** return the mapped cell's row and column indexes (starting at 0 for each) */
   NS_IMETHOD GetCellIndexes(int32_t &aRowIndex, int32_t &aColIndex)=0;
-
-  /** return the mapped cell's row index (starting at 0 for the first row) */
-  virtual nsresult GetRowIndex(int32_t &aRowIndex) const = 0;
-
-  /** return the mapped cell's column index (starting at 0 for the first column) */
-  virtual nsresult GetColIndex(int32_t &aColIndex) const = 0;
 };
 
 #endif
 
 
 
--- a/layout/tables/nsTableCellFrame.cpp
+++ b/layout/tables/nsTableCellFrame.cpp
@@ -55,47 +55,32 @@ nsTableCellFrame::nsTableCellFrame(nsSty
 }
 
 nsTableCellFrame::~nsTableCellFrame()
 {
 }
 
 NS_IMPL_FRAMEARENA_HELPERS(nsTableCellFrame)
 
-nsTableCellFrame*
-nsTableCellFrame::GetNextCell() const
-{
-  nsIFrame* childFrame = GetNextSibling();
-  while (childFrame) {
-    nsTableCellFrame *cellFrame = do_QueryFrame(childFrame);
-    if (cellFrame) {
-      return cellFrame;
-    }
-    childFrame = childFrame->GetNextSibling();
-  }
-  return nullptr;
-}
-
 void
 nsTableCellFrame::Init(nsIContent*       aContent,
                        nsContainerFrame* aParent,
                        nsIFrame*         aPrevInFlow)
 {
   // Let the base class do its initialization
   nsContainerFrame::Init(aContent, aParent, aPrevInFlow);
 
   if (HasAnyStateBits(NS_FRAME_FONT_INFLATION_CONTAINER)) {
     AddStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT);
   }
 
   if (aPrevInFlow) {
     // Set the column index
     nsTableCellFrame* cellFrame = (nsTableCellFrame*)aPrevInFlow;
-    int32_t           colIndex;
-    cellFrame->GetColIndex(colIndex);
+    uint32_t colIndex = cellFrame->ColIndex();
     SetColIndex(colIndex);
   } else {
     // Although the spec doesn't say that writing-mode is not applied to
     // table-cells, we still override style value here because we want to
     // make effective writing mode of table structure frames consistent
     // within a table. The content inside table cells is reflowed by an
     // anonymous block, hence their writing mode is not affected.
     mWritingMode = GetTableFrame()->GetWritingMode();
@@ -186,44 +171,16 @@ nsTableCellFrame::NeedsToObserve(const R
   // the cell (bug 1174711 comment 8); we may need to observe isizes
   // instead of bsizes for orthogonal children.
   return rs->mFrame == this &&
          (PresContext()->CompatibilityMode() == eCompatibility_NavQuirks ||
           fType == LayoutFrameType::TableWrapper);
 }
 
 nsresult
-nsTableCellFrame::GetRowIndex(int32_t &aRowIndex) const
-{
-  nsresult result;
-  nsTableRowFrame* row = static_cast<nsTableRowFrame*>(GetParent());
-  if (row) {
-    aRowIndex = row->GetRowIndex();
-    result = NS_OK;
-  }
-  else {
-    aRowIndex = 0;
-    result = NS_ERROR_NOT_INITIALIZED;
-  }
-  return result;
-}
-
-nsresult
-nsTableCellFrame::GetColIndex(int32_t &aColIndex) const
-{
-  if (GetPrevInFlow()) {
-    return static_cast<nsTableCellFrame*>(FirstInFlow())->GetColIndex(aColIndex);
-  }
-  else {
-    aColIndex = mColIndex;
-    return  NS_OK;
-  }
-}
-
-nsresult
 nsTableCellFrame::AttributeChanged(int32_t         aNameSpaceID,
                                    nsAtom*        aAttribute,
                                    int32_t         aModType)
 {
   // We need to recalculate in this case because of the nowrap quirk in
   // BasicTableLayoutStrategy
   if (aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::nowrap &&
       PresContext()->CompatibilityMode() == eCompatibility_NavQuirks) {
@@ -245,23 +202,23 @@ nsTableCellFrame::DidSetStyleContext(nsS
   nsContainerFrame::DidSetStyleContext(aOldStyleContext);
 
   if (!aOldStyleContext) //avoid this on init
     return;
 
   nsTableFrame* tableFrame = GetTableFrame();
   if (tableFrame->IsBorderCollapse() &&
       tableFrame->BCRecalcNeeded(aOldStyleContext, StyleContext())) {
-    int32_t colIndex, rowIndex;
-    GetColIndex(colIndex);
-    GetRowIndex(rowIndex);
+    uint32_t colIndex = ColIndex();
+    uint32_t rowIndex = RowIndex();
     // row span needs to be clamped as we do not create rows in the cellmap
     // which do not have cells originating in them
     TableArea damageArea(colIndex, rowIndex, GetColSpan(),
-      std::min(GetRowSpan(), tableFrame->GetRowCount() - rowIndex));
+      std::min(static_cast<uint32_t>(GetRowSpan()), 
+               tableFrame->GetRowCount() - rowIndex));
     tableFrame->AddBCDamageArea(damageArea);
   }
 }
 
 #ifdef DEBUG
 void
 nsTableCellFrame::AppendFrames(ChildListID     aListID,
                                nsFrameList&    aFrameList)
@@ -837,24 +794,23 @@ CalcUnpaginatedBSize(nsTableCellFrame& a
     static_cast<nsTableCellFrame*>(aCellFrame.FirstInFlow());
   nsTableFrame* firstTableInFlow  =
     static_cast<nsTableFrame*>(aTableFrame.FirstInFlow());
   nsTableRowFrame* row =
     static_cast<nsTableRowFrame*>(firstCellInFlow->GetParent());
   nsTableRowGroupFrame* firstRGInFlow =
     static_cast<nsTableRowGroupFrame*>(row->GetParent());
 
-  int32_t rowIndex;
-  firstCellInFlow->GetRowIndex(rowIndex);
+  uint32_t rowIndex = firstCellInFlow->RowIndex();
   int32_t rowSpan = aTableFrame.GetEffectiveRowSpan(*firstCellInFlow);
 
   nscoord computedBSize = firstTableInFlow->GetRowSpacing(rowIndex,
                                                           rowIndex + rowSpan - 1);
   computedBSize -= aBlockDirBorderPadding;
-  int32_t rowX;
+  uint32_t rowX;
   for (row = firstRGInFlow->GetFirstRow(), rowX = 0; row; row = row->GetNextRow(), rowX++) {
     if (rowX > rowIndex + rowSpan - 1) {
       break;
     }
     else if (rowX >= rowIndex) {
       computedBSize += row->GetUnpaginatedBSize();
     }
   }
@@ -1060,22 +1016,17 @@ nsTableCellFrame::AccessibleType()
   return a11y::eHTMLTableCellType;
 }
 #endif
 
 /* This is primarily for editor access via nsITableLayout */
 NS_IMETHODIMP
 nsTableCellFrame::GetCellIndexes(int32_t &aRowIndex, int32_t &aColIndex)
 {
-  nsresult res = GetRowIndex(aRowIndex);
-  if (NS_FAILED(res))
-  {
-    aColIndex = 0;
-    return res;
-  }
+  aRowIndex = RowIndex();
   aColIndex = mColIndex;
   return  NS_OK;
 }
 
 nsTableCellFrame*
 NS_NewTableCellFrame(nsIPresShell*   aPresShell,
                      nsStyleContext* aContext,
                      nsTableFrame* aTableFrame)
--- a/layout/tables/nsTableCellFrame.h
+++ b/layout/tables/nsTableCellFrame.h
@@ -165,28 +165,41 @@ public:
   /**
    * return the cell's starting row index (starting at 0 for the first row).
    * for continued cell frames the row index is that of the cell's first-in-flow
    * and the column index (starting at 0 for the first column
    */
   NS_IMETHOD GetCellIndexes(int32_t &aRowIndex, int32_t &aColIndex) override;
 
   /** return the mapped cell's row index (starting at 0 for the first row) */
-  virtual nsresult GetRowIndex(int32_t &aRowIndex) const override;
+  uint32_t RowIndex() const
+  {
+    return static_cast<nsTableRowFrame*>(GetParent())->GetRowIndex();
+  }
 
   /**
    * return the cell's specified col span. this is what was specified in the
    * content model or in the style info, and is always >= 1.
    * to get the effective col span (the actual value that applies), use GetEffectiveColSpan()
    * @see nsTableFrame::GetEffectiveColSpan()
    */
   int32_t GetColSpan();
 
   /** return the cell's column index (starting at 0 for the first column) */
-  virtual nsresult GetColIndex(int32_t &aColIndex) const override;
+  uint32_t ColIndex() const
+  {
+    // NOTE: We copy this from previous continuations, and we don't ever have
+    // dynamic updates when tables split, so our mColIndex always matches our
+    // first continuation's.
+    MOZ_ASSERT(static_cast<nsTableCellFrame*>(FirstContinuation())->mColIndex ==
+               mColIndex,
+               "mColIndex out of sync with first continuation");
+    return mColIndex;
+  }
+    
   void SetColIndex(int32_t aColIndex);
 
   /** return the available isize given to this frame during its last reflow */
   inline nscoord GetPriorAvailISize();
 
   /** set the available isize given to this frame during its last reflow */
   inline void SetPriorAvailISize(nscoord aPriorAvailISize);
 
@@ -197,17 +210,27 @@ public:
   inline void SetDesiredSize(const ReflowOutput & aDesiredSize);
 
   bool GetContentEmpty() const;
   void SetContentEmpty(bool aContentEmpty);
 
   bool HasPctOverBSize();
   void SetHasPctOverBSize(bool aValue);
 
-  nsTableCellFrame* GetNextCell() const;
+  nsTableCellFrame* GetNextCell() const
+  {
+    nsIFrame* sibling = GetNextSibling();
+#ifdef DEBUG
+    if (sibling) {
+      nsTableCellFrame* cellFrame = do_QueryFrame(sibling);
+      MOZ_ASSERT(cellFrame, "How do we have a non-cell sibling?");
+    }
+#endif // DEBUG
+    return static_cast<nsTableCellFrame*>(sibling);
+  }
 
   virtual LogicalMargin GetBorderWidth(WritingMode aWM) const;
 
   virtual DrawResult PaintBackground(gfxContext&          aRenderingContext,
                                      const nsRect&        aDirtyRect,
                                      nsPoint              aPt,
                                      uint32_t             aFlags);
 
@@ -334,9 +357,22 @@ private:
   // These are the entire width of the border (the cell edge contains only
   // the inner half, per the macros in nsTablePainter.h).
   BCPixelSize mBStartBorder;
   BCPixelSize mIEndBorder;
   BCPixelSize mBEndBorder;
   BCPixelSize mIStartBorder;
 };
 
+// Implemented here because that's a sane-ish way to make the includes work out.
+inline nsTableCellFrame* nsTableRowFrame::GetFirstCell() const
+{
+  nsIFrame* firstChild = mFrames.FirstChild();
+#ifdef DEBUG
+    if (firstChild) {
+      nsTableCellFrame* cellFrame = do_QueryFrame(firstChild);
+      MOZ_ASSERT(cellFrame, "How do we have a non-cell sibling?");
+    }
+#endif // DEBUG
+  return static_cast<nsTableCellFrame*>(firstChild);
+}
+
 #endif
--- a/layout/tables/nsTableFrame.cpp
+++ b/layout/tables/nsTableFrame.cpp
@@ -359,19 +359,18 @@ nsTableFrame::SetInitialChildList(ChildL
 
 void
 nsTableFrame::RowOrColSpanChanged(nsTableCellFrame* aCellFrame)
 {
   if (aCellFrame) {
     nsTableCellMap* cellMap = GetCellMap();
     if (cellMap) {
       // for now just remove the cell from the map and reinsert it
-      int32_t rowIndex, colIndex;
-      aCellFrame->GetRowIndex(rowIndex);
-      aCellFrame->GetColIndex(colIndex);
+      uint32_t rowIndex = aCellFrame->RowIndex();
+      uint32_t colIndex = aCellFrame->ColIndex();
       RemoveCell(aCellFrame, rowIndex);
       AutoTArray<nsTableCellFrame*, 1> cells;
       cells.AppendElement(aCellFrame);
       InsertCells(cells, rowIndex, colIndex - 1);
 
       // XXX Should this use eStyleChange?  It currently doesn't need
       // to, but it might given more optimization.
       PresContext()->PresShell()->
@@ -437,46 +436,42 @@ nsTableFrame::GetColFrame(int32_t aColIn
 
 int32_t
 nsTableFrame::GetEffectiveRowSpan(int32_t                 aRowIndex,
                                   const nsTableCellFrame& aCell) const
 {
   nsTableCellMap* cellMap = GetCellMap();
   NS_PRECONDITION (nullptr != cellMap, "bad call, cellMap not yet allocated.");
 
-  int32_t colIndex;
-  aCell.GetColIndex(colIndex);
-  return cellMap->GetEffectiveRowSpan(aRowIndex, colIndex);
+  return cellMap->GetEffectiveRowSpan(aRowIndex, aCell.ColIndex());
 }
 
 int32_t
 nsTableFrame::GetEffectiveRowSpan(const nsTableCellFrame& aCell,
                                   nsCellMap*              aCellMap)
 {
   nsTableCellMap* tableCellMap = GetCellMap(); if (!tableCellMap) ABORT1(1);
 
-  int32_t colIndex, rowIndex;
-  aCell.GetColIndex(colIndex);
-  aCell.GetRowIndex(rowIndex);
+  uint32_t colIndex = aCell.ColIndex();
+  uint32_t rowIndex = aCell.RowIndex();
 
   if (aCellMap)
     return aCellMap->GetRowSpan(rowIndex, colIndex, true);
   else
     return tableCellMap->GetEffectiveRowSpan(rowIndex, colIndex);
 }
 
 int32_t
 nsTableFrame::GetEffectiveColSpan(const nsTableCellFrame& aCell,
                                   nsCellMap*              aCellMap) const
 {
   nsTableCellMap* tableCellMap = GetCellMap(); if (!tableCellMap) ABORT1(1);
 
-  int32_t colIndex, rowIndex;
-  aCell.GetColIndex(colIndex);
-  aCell.GetRowIndex(rowIndex);
+  uint32_t colIndex = aCell.ColIndex();
+  uint32_t rowIndex = aCell.RowIndex();
 
   if (aCellMap)
     return aCellMap->GetEffectiveColSpan(*tableCellMap, rowIndex, colIndex);
   else
     return tableCellMap->GetEffectiveColSpan(rowIndex, colIndex);
 }
 
 bool
@@ -1398,43 +1393,51 @@ PaintRowGroupBackground(nsTableRowGroupF
   }
 }
 
 static void
 PaintRowGroupBackgroundByColIdx(nsTableRowGroupFrame* aRowGroup,
                                 nsIFrame* aFrame,
                                 nsDisplayListBuilder* aBuilder,
                                 const nsDisplayListSet& aLists,
-                                const nsTArray<int32_t>& aColIdx,
+                                const nsTArray<uint32_t>& aColIdx,
                                 const nsPoint& aOffset)
 {
+  MOZ_DIAGNOSTIC_ASSERT(!aColIdx.IsEmpty(),
+                        "Must be painting backgrounds for something");
+
   for (nsTableRowFrame* row = aRowGroup->GetFirstRow(); row; row = row->GetNextRow()) {
     auto rowPos = row->GetNormalPosition() + aOffset;
     if (!aBuilder->GetDirtyRect().Intersects(nsRect(rowPos, row->GetSize()))) {
       continue;
     }
     for (nsTableCellFrame* cell = row->GetFirstCell(); cell; cell = cell->GetNextCell()) {
+      uint32_t curColIdx = cell->ColIndex();
+      if (!aColIdx.ContainsSorted(curColIdx)) {
+        if (curColIdx > aColIdx.LastElement()) {
+          // We can just stop looking at this row.
+          break;
+        }
+        continue;
+      }
+
       if (!cell->ShouldPaintBackground(aBuilder)) {
         continue;
       }
 
-      int32_t curColIdx;
-      cell->GetColIndex(curColIdx);
-      if (aColIdx.Contains(curColIdx)) {
-        auto cellPos = cell->GetNormalPosition() + rowPos;
-        auto cellRect = nsRect(cellPos, cell->GetSize());
-        if (!aBuilder->GetDirtyRect().Intersects(cellRect)) {
-          continue;
-        }
-        nsDisplayBackgroundImage::AppendBackgroundItemsToTop(aBuilder, aFrame, cellRect,
-                                                             aLists.BorderBackground(),
-                                                             false, nullptr,
-                                                             aFrame->GetRectRelativeToSelf(),
-                                                             cell);
-      }
+      auto cellPos = cell->GetNormalPosition() + rowPos;
+      auto cellRect = nsRect(cellPos, cell->GetSize());
+      if (!aBuilder->GetDirtyRect().Intersects(cellRect)) {
+        continue;
+      }
+      nsDisplayBackgroundImage::AppendBackgroundItemsToTop(aBuilder, aFrame, cellRect,
+                                                           aLists.BorderBackground(),
+                                                           false, nullptr,
+                                                           aFrame->GetRectRelativeToSelf(),
+                                                           cell);
     }
   }
 }
 
 static inline bool FrameHasBorder(nsIFrame* f)
 {
   if (!f->StyleVisibility()->IsVisible()) {
     return false;
@@ -1542,35 +1545,37 @@ nsTableFrame::DisplayGenericTablePart(ns
     PaintRowGroupBackground(rowGroup, aFrame, aBuilder, aLists);
   } else if (aFrame->IsTableRowFrame()) {
     nsTableRowFrame* row = static_cast<nsTableRowFrame*>(aFrame);
     PaintRowBackground(row, aFrame, aBuilder, aLists);
   } else if (aFrame->IsTableColGroupFrame()) {
     // Compute background rect by iterating all cell frame.
     nsTableColGroupFrame* colGroup = static_cast<nsTableColGroupFrame*>(aFrame);
     // Collecting column index.
-    AutoTArray<int32_t, 1> colIdx;
+    AutoTArray<uint32_t, 1> colIdx;
     for (nsTableColFrame* col = colGroup->GetFirstColumn(); col; col = col->GetNextCol()) {
+      MOZ_ASSERT(colIdx.IsEmpty() ||
+                 static_cast<uint32_t>(col->GetColIndex()) > colIdx.LastElement());
       colIdx.AppendElement(col->GetColIndex());
     }
 
     nsTableFrame* table = colGroup->GetTableFrame();
     RowGroupArray rowGroups;
     table->OrderRowGroups(rowGroups);
     for (nsTableRowGroupFrame* rowGroup : rowGroups) {
       auto offset = rowGroup->GetNormalPosition() - colGroup->GetNormalPosition();
       if (!aBuilder->GetDirtyRect().Intersects(nsRect(offset, rowGroup->GetSize()))) {
         continue;
       }
       PaintRowGroupBackgroundByColIdx(rowGroup, aFrame, aBuilder, aLists, colIdx, offset);
     }
   } else if (aFrame->IsTableColFrame()) {
     // Compute background rect by iterating all cell frame.
     nsTableColFrame* col = static_cast<nsTableColFrame*>(aFrame);
-    AutoTArray<int32_t, 1> colIdx;
+    AutoTArray<uint32_t, 1> colIdx;
     colIdx.AppendElement(col->GetColIndex());
 
     nsTableFrame* table = col->GetTableFrame();
     RowGroupArray rowGroups;
     table->OrderRowGroups(rowGroups);
     for (nsTableRowGroupFrame* rowGroup : rowGroups) {
       auto offset = rowGroup->GetNormalPosition() -
                     col->GetNormalPosition() -
@@ -4220,19 +4225,18 @@ nsTableFrame::DumpRowGroup(nsIFrame* aKi
   for (nsIFrame* cFrame : aKidFrame->PrincipalChildList()) {
     nsTableRowFrame *rowFrame = do_QueryFrame(cFrame);
     if (rowFrame) {
       printf("row(%d)=%p ", rowFrame->GetRowIndex(),
              static_cast<void*>(rowFrame));
       for (nsIFrame* childFrame : cFrame->PrincipalChildList()) {
         nsTableCellFrame *cellFrame = do_QueryFrame(childFrame);
         if (cellFrame) {
-          int32_t colIndex;
-          cellFrame->GetColIndex(colIndex);
-          printf("cell(%d)=%p ", colIndex, static_cast<void*>(childFrame));
+          uint32_t colIndex = cellFrame->ColIndex();
+          printf("cell(%u)=%p ", colIndex, static_cast<void*>(childFrame));
         }
       }
       printf("\n");
     }
     else {
       DumpRowGroup(rowFrame);
     }
   }
--- a/layout/tables/nsTableRowFrame.cpp
+++ b/layout/tables/nsTableRowFrame.cpp
@@ -255,17 +255,17 @@ nsTableRowFrame::InsertFrames(ChildListI
     nsIFrame *childFrame = e.get();
     NS_ASSERTION(IS_TABLE_CELL(childFrame->Type()),
                  "Not a table cell frame/pseudo frame construction failure");
     cellChildren.AppendElement(static_cast<nsTableCellFrame*>(childFrame));
   }
   // insert the cells into the cell map
   int32_t colIndex = -1;
   if (prevCellFrame) {
-    prevCellFrame->GetColIndex(colIndex);
+    colIndex = prevCellFrame->ColIndex();
   }
   tableFrame->InsertCells(cellChildren, GetRowIndex(), colIndex);
 
   PresContext()->PresShell()->FrameNeedsReflow(this, nsIPresShell::eTreeChange,
                                                NS_FRAME_HAS_DIRTY_CHILDREN);
   tableFrame->SetGeometryDirty();
 }
 
@@ -324,28 +324,16 @@ GetBSizeOfRowsSpannedBelowFirst(nsTableC
       rowX++;
     }
     bsize += aTableFrame.GetRowSpacing(rowX);
     nextRow = nextRow->GetNextSibling();
   }
   return bsize;
 }
 
-nsTableCellFrame*
-nsTableRowFrame::GetFirstCell()
-{
-  for (nsIFrame* childFrame : mFrames) {
-    nsTableCellFrame *cellFrame = do_QueryFrame(childFrame);
-    if (cellFrame) {
-      return cellFrame;
-    }
-  }
-  return nullptr;
-}
-
 /**
  * Post-reflow hook. This is where the table row does its post-processing
  */
 void
 nsTableRowFrame::DidResize()
 {
   // Resize and re-align the cell frames based on our row bsize
   nsTableFrame* tableFrame = GetTableFrame();
@@ -682,18 +670,17 @@ nsTableRowFrame::CalculateCellActualBSiz
 
 // Calculates the available isize for the table cell based on the known
 // column isizes taking into account column spans and column spacing
 static nscoord
 CalcAvailISize(nsTableFrame&     aTableFrame,
                nsTableCellFrame& aCellFrame)
 {
   nscoord cellAvailISize = 0;
-  int32_t colIndex;
-  aCellFrame.GetColIndex(colIndex);
+  uint32_t colIndex = aCellFrame.ColIndex();
   int32_t colspan = aTableFrame.GetEffectiveColSpan(aCellFrame);
   NS_ASSERTION(colspan > 0, "effective colspan should be positive");
   nsTableFrame* fifTable =
     static_cast<nsTableFrame*>(aTableFrame.FirstInFlow());
 
   for (int32_t spanX = 0; spanX < colspan; spanX++) {
     cellAvailISize +=
       fifTable->GetColumnISizeFromFirstInFlow(colIndex + spanX);
@@ -822,22 +809,22 @@ nsTableRowFrame::ReflowChildren(nsPresCo
     }
     if (aReflowInput.mFlags.mSpecialBSizeReflow) {
       if (!isPaginated &&
           !cellFrame->HasAnyStateBits(NS_FRAME_CONTAINS_RELATIVE_BSIZE)) {
         continue;
       }
     }
 
-    int32_t cellColIndex;
-    cellFrame->GetColIndex(cellColIndex);
+    uint32_t cellColIndex = cellFrame->ColIndex();
     cellColSpan = aTableFrame.GetEffectiveColSpan(*cellFrame);
 
     // If the adjacent cell is in a prior row (because of a rowspan) add in the space
-    if (prevColIndex != (cellColIndex - 1)) {
+    // NOTE: prevColIndex can be -1 here.
+    if (prevColIndex != (static_cast<int32_t>(cellColIndex) - 1)) {
       iCoord += GetSpaceBetween(prevColIndex, cellColIndex, cellColSpan, aTableFrame,
                                 false);
     }
 
     // remember the rightmost (ltr) or leftmost (rtl) column this cell spans into
     prevColIndex = cellColIndex + (cellColSpan - 1);
 
     // Reflow the child frame
@@ -1202,18 +1189,17 @@ nsTableRowFrame::CollapseRowIfNecessary(
   nscoord shift = 0;
   nsSize containerSize = mRect.Size();
 
   if (aCollapseGroup || collapseRow) {
     aDidCollapse = true;
     shift = rowRect.BSize(wm);
     nsTableCellFrame* cellFrame = GetFirstCell();
     if (cellFrame) {
-      int32_t rowIndex;
-      cellFrame->GetRowIndex(rowIndex);
+      uint32_t rowIndex = cellFrame->RowIndex();
       shift += tableFrame->GetRowSpacing(rowIndex);
       while (cellFrame) {
         LogicalRect cRect = cellFrame->GetLogicalRect(wm, containerSize);
         // If aRowOffset != 0, there's no point in invalidating the cells, since
         // we've already invalidated our overflow area.  Note that we _do_ still
         // need to invalidate if our row is not moving, because the cell might
         // span out of this row, so invalidating our row rect won't do enough.
         if (aRowOffset == 0) {
@@ -1234,23 +1220,23 @@ nsTableRowFrame::CollapseRowIfNecessary(
     int32_t prevColIndex = -1;
     nscoord iPos = 0; // running total of children inline-axis offset
     nsTableFrame* fifTable =
       static_cast<nsTableFrame*>(tableFrame->FirstInFlow());
 
     for (nsIFrame* kidFrame : mFrames) {
       nsTableCellFrame *cellFrame = do_QueryFrame(kidFrame);
       if (cellFrame) {
-        int32_t cellColIndex;
-        cellFrame->GetColIndex(cellColIndex);
+        uint32_t cellColIndex = cellFrame->ColIndex();
         int32_t cellColSpan = tableFrame->GetEffectiveColSpan(*cellFrame);
 
         // If the adjacent cell is in a prior row (because of a rowspan) add in
         // the space
-        if (prevColIndex != (cellColIndex - 1)) {
+        // NOTE: prevColIndex can be -1 here.
+        if (prevColIndex != (static_cast<int32_t>(cellColIndex) - 1)) {
           iPos += GetSpaceBetween(prevColIndex, cellColIndex, cellColSpan,
                                   *tableFrame, true);
         }
         LogicalRect cRect(wm, iPos, 0, 0, rowRect.BSize(wm));
 
         // remember the last (iend-wards-most) column this cell spans into
         prevColIndex = cellColIndex + cellColSpan - 1;
         int32_t actualColSpan = cellColSpan;
@@ -1353,19 +1339,19 @@ void
 nsTableRowFrame::InsertCellFrame(nsTableCellFrame* aFrame,
                                  int32_t           aColIndex)
 {
   // Find the cell frame where col index < aColIndex
   nsTableCellFrame* priorCell = nullptr;
   for (nsIFrame* child : mFrames) {
     nsTableCellFrame *cellFrame = do_QueryFrame(child);
     if (cellFrame) {
-      int32_t colIndex;
-      cellFrame->GetColIndex(colIndex);
-      if (colIndex < aColIndex) {
+      uint32_t colIndex = cellFrame->ColIndex();
+      // Can aColIndex be -1 here?  Let's assume it can for now.
+      if (static_cast<int32_t>(colIndex) < aColIndex) {
         priorCell = cellFrame;
       }
       else break;
     }
   }
   mFrames.InsertFrame(this, priorCell, aFrame);
 }
 
--- a/layout/tables/nsTableRowFrame.h
+++ b/layout/tables/nsTableRowFrame.h
@@ -75,17 +75,19 @@ public:
 
   virtual nsMargin GetUsedMargin() const override;
   virtual nsMargin GetUsedBorder() const override;
   virtual nsMargin GetUsedPadding() const override;
 
   virtual void BuildDisplayList(nsDisplayListBuilder*   aBuilder,
                                 const nsDisplayListSet& aLists) override;
 
-  nsTableCellFrame* GetFirstCell() ;
+  // Implemented in nsTableCellFrame.h, because it needs to know about the
+  // nsTableCellFrame class, but we can't include nsTableCellFrame.h here.
+  inline nsTableCellFrame* GetFirstCell() const;
 
   /** calls Reflow for all of its child cells.
     * Cells with rowspan=1 are all set to the same height and stacked horizontally.
     * <P> Cells are not split unless absolutely necessary.
     * <P> Cells are resized in nsTableFrame::BalanceColumnWidths
     * and nsTableFrame::ShrinkWrapChildren
     *
     * @param aDesiredSize width set to width of the sum of the cells, height set to
--- a/layout/tables/nsTableRowGroupFrame.cpp
+++ b/layout/tables/nsTableRowGroupFrame.cpp
@@ -176,18 +176,17 @@ nsTableRowGroupFrame::InitRepeatedFrame(
     copyRowFrame->SetRowIndex(rowIndex);
 
     // For each table cell frame set its column index
     nsTableCellFrame* originalCellFrame = originalRowFrame->GetFirstCell();
     nsTableCellFrame* copyCellFrame     = copyRowFrame->GetFirstCell();
     while (copyCellFrame && originalCellFrame) {
       NS_ASSERTION(originalCellFrame->GetContent() == copyCellFrame->GetContent(),
                    "cell frames have different content");
-      int32_t colIndex;
-      originalCellFrame->GetColIndex(colIndex);
+      uint32_t colIndex = originalCellFrame->ColIndex();
       copyCellFrame->SetColIndex(colIndex);
 
       // Move to the next cell frame
       copyCellFrame     = copyCellFrame->GetNextCell();
       originalCellFrame = originalCellFrame->GetNextCell();
     }
 
     // Move to the next row frame
@@ -1038,18 +1037,17 @@ nsTableRowGroupFrame::SplitSpanningCells
           }
           if (aContRow) {
             if (row != &aLastRow) {
               // aContRow needs a continuation for cell, since cell spanned into aLastRow
               // but does not originate there
               nsTableCellFrame* contCell = static_cast<nsTableCellFrame*>(
                 aPresContext.PresShell()->FrameConstructor()->
                   CreateContinuingFrame(&aPresContext, cell, &aLastRow));
-              int32_t colIndex;
-              cell->GetColIndex(colIndex);
+              uint32_t colIndex = cell->ColIndex();
               aContRow->InsertCellFrame(contCell, colIndex);
             }
           }
         }
       }
     }
   }
   if (!haveRowSpan) {
--- a/layout/tools/reftest/reftestcommandline.py
+++ b/layout/tools/reftest/reftestcommandline.py
@@ -325,19 +325,27 @@ class ReftestArgumentsParser(argparse.Ar
                 specialPowersExtensionPath = os.path.join(self.build_obj.topobjdir, "_tests",
                                                           "reftest", "specialpowers")
             else:
                 specialPowersExtensionPath = os.path.join(here, "specialpowers")
             options.specialPowersExtensionPath = os.path.normpath(specialPowersExtensionPath)
 
         options.leakThresholds = {
             "default": options.defaultLeakThreshold,
-            "tab": 5000,  # See dependencies of bug 1051230.
+            "tab": options.defaultLeakThreshold,
         }
 
+        if mozinfo.isWin:
+            if mozinfo.info['bits'] == 32:
+                # See bug 1408554.
+                options.leakThresholds["tab"] = 3000
+            else:
+                # See bug 1404482.
+                options.leakThresholds["tab"] = 100
+
 
 class DesktopArgumentsParser(ReftestArgumentsParser):
     def __init__(self, **kwargs):
         super(DesktopArgumentsParser, self).__init__(**kwargs)
 
         self.add_argument("--run-tests-in-parallel",
                           action="store_true",
                           default=False,
--- a/media/webrtc/signaling/src/media-conduit/VideoConduit.cpp
+++ b/media/webrtc/signaling/src/media-conduit/VideoConduit.cpp
@@ -23,20 +23,25 @@
 
 #include "webrtc/common_types.h"
 #include "webrtc/common_video/libyuv/include/webrtc_libyuv.h"
 #include "webrtc/modules/rtp_rtcp/include/rtp_rtcp_defines.h"
 #include "webrtc/modules/video_coding/codecs/vp8/include/vp8.h"
 #include "webrtc/modules/video_coding/codecs/vp9/include/vp9.h"
 #include "webrtc/common_video/include/video_frame_buffer.h"
 #include "webrtc/api/video/i420_buffer.h"
+
+#ifdef WEBRTC_MAC
+#include <AvailabilityMacros.h>
+#endif
+
 #if defined(MAC_OS_X_VERSION_10_8) && \
   (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_8)
 // XXX not available in Mac 10.7 SDK
-#include "webrtc/sdk/objc/Framework/Classes/corevideo_frame_buffer.h"
+#include "webrtc/common_video/include/corevideo_frame_buffer.h"
 #endif
 
 #include "mozilla/Unused.h"
 
 #if defined(MOZ_WIDGET_ANDROID)
 #include "AndroidJNIWrapper.h"
 #include "VideoEngine.h"
 #endif
@@ -1994,20 +1999,20 @@ WebrtcVideoConduit::SendVideoFrame(webrt
       // No adaption - optimized path.
       buffer = frame.video_frame_buffer();
       // XXX Bug 1367651 - Use nativehandles where possible instead of software scaling
 #ifdef WEBRTC_MAC
 #if defined(MAC_OS_X_VERSION_10_8) && \
   (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_8)
       // XXX not available in Mac 10.7 SDK
       // code adapted from objvideotracksource.mm
-    } else if (frame.nativeHandle) {
+    } else if (frame.video_frame_buffer()->native_handle()) {
       // Adapted CVPixelBuffer frame.
-      buffer = new rtc::RefCountedObject<CoreVideoFrameBuffer>(
-        static_cast<CVPixelBufferRef>(frame.nativeHandle), adapted_width, adapted_height,
+      buffer = new rtc::RefCountedObject<webrtc::CoreVideoFrameBuffer>(
+        static_cast<CVPixelBufferRef>(frame.video_frame_buffer()->native_handle()), adapted_width, adapted_height,
         crop_width, crop_height, crop_x, crop_y);
 #endif
 #elif WEBRTC_WIN
       // XX FIX
 #elif WEBRTC_LINUX
       // XX FIX
 #elif WEBRTC_ANDROID
       // XX FIX
--- a/netwerk/base/nsINetworkInterceptController.idl
+++ b/netwerk/base/nsINetworkInterceptController.idl
@@ -3,16 +3,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 #include "nsIContentPolicy.idl"
 
 interface nsIChannel;
 interface nsIConsoleReportCollector;
+interface nsIInputStream;
 interface nsIOutputStream;
 interface nsIURI;
 
 %{C++
 #include "nsIConsoleReportCollector.h"
 namespace mozilla {
 class TimeStamp;
 
@@ -22,16 +23,26 @@ class ChannelInfo;
 }
 %}
 
 native TimeStamp(mozilla::TimeStamp);
 
 [ptr] native ChannelInfo(mozilla::dom::ChannelInfo);
 
 /**
+ * Interface allowing the nsIInterceptedChannel to callback when it is
+ * done reading from the body stream.
+ */
+[scriptable, uuid(51039eb6-bea0-40c7-b523-ccab56cc4fde)]
+interface nsIInterceptedBodyCallback : nsISupports
+{
+  void bodyComplete(in nsresult aRv);
+};
+
+/**
  * Interface to allow implementors of nsINetworkInterceptController to control the behaviour
  * of intercepted channels without tying implementation details of the interception to
  * the actual channel. nsIInterceptedChannel is expected to be implemented by objects
  * which do not implement nsIChannel.
  */
 
 [scriptable, uuid(f4b82975-6a86-4cc4-87fe-9a1fd430c86d)]
 interface nsIInterceptedChannel : nsISupports
@@ -50,37 +61,47 @@ interface nsIInterceptedChannel : nsISup
 
     /**
      * Attach a header name/value pair to the forthcoming synthesized response.
      * Overwrites any existing header value.
      */
     void synthesizeHeader(in ACString name, in ACString value);
 
     /**
-     * Instruct a channel that has been intercepted that a response has been
-     * synthesized and can now be read. No further header modification is allowed
-     * after this point. The caller may optionally pass a spec for a URL that
-     * this response originates from; an empty string will cause the original
-     * intercepted request's URL to be used instead.
+     * Instruct a channel that has been intercepted that a response is
+     * starting to be synthesized.  No further header modification is allowed
+     * after this point.  There are a few parameters:
+     * - A body stream may be optionally passed.  If nullptr, then an
+     *   empty body is assumed.
+     * - A callback may be optionally passed.  It will be invoked
+     *   when the body is complete.  For a nullptr body this may be
+     *   synchronously on the current thread.  Otherwise it will be invoked
+     *   asynchronously on the current thread.
+     * - The caller may optionally pass a spec for a URL that this response
+     *   originates from; an empty string will cause the original
+     *   intercepted request's URL to be used instead.
      */
-    void finishSynthesizedResponse(in ACString finalURLSpec);
+    void startSynthesizedResponse(in nsIInputStream body,
+                                  in nsIInterceptedBodyCallback callback,
+                                  in ACString finalURLSpec);
+
+    /**
+     * Instruct a channel that has been intercepted that response synthesis
+     * has completed and all outstanding resources can be closed.
+     */
+    void finishSynthesizedResponse();
 
     /**
      * Cancel the pending intercepted request.
      * @return NS_ERROR_FAILURE if the response has already been synthesized or
      *         the original request has been instructed to continue.
      */
     void cancelInterception(in nsresult status);
 
     /**
-     * The synthesized response body to be produced.
-     */
-    readonly attribute nsIOutputStream responseBody;
-
-    /**
      * The underlying channel object that was intercepted.
      */
     readonly attribute nsIChannel channel;
 
     /**
      * The URL of the underlying channel object, corrected for a potential
      * secure upgrade.
      */
--- a/netwerk/protocol/http/HttpChannelChild.cpp
+++ b/netwerk/protocol/http/HttpChannelChild.cpp
@@ -76,17 +76,16 @@ NS_IMPL_ISUPPORTS(InterceptStreamListene
                   nsIStreamListener,
                   nsIRequestObserver,
                   nsIProgressEventSink)
 
 NS_IMETHODIMP
 InterceptStreamListener::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext)
 {
   if (mOwner) {
-    mOwner->SynthesizeResponseStartTime(TimeStamp::Now());
     mOwner->DoOnStartRequest(mOwner, mContext);
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 InterceptStreamListener::OnStatus(nsIRequest* aRequest, nsISupports* aContext,
                                   nsresult status, const char16_t* aStatusArg)
@@ -135,17 +134,16 @@ InterceptStreamListener::OnDataAvailable
   mOwner->DoOnDataAvailable(mOwner, mContext, aInputStream, aOffset, aCount);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 InterceptStreamListener::OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, nsresult aStatusCode)
 {
   if (mOwner) {
-    mOwner->SynthesizeResponseEndTime(TimeStamp::Now());
     mOwner->DoPreOnStopRequest(aStatusCode);
     mOwner->DoOnStopRequest(mOwner, aStatusCode, mContext);
   }
   Cleanup();
   return NS_OK;
 }
 
 void
@@ -1121,16 +1119,18 @@ HttpChannelChild::OnStopRequest(const ns
 
 void
 HttpChannelChild::DoPreOnStopRequest(nsresult aStatus)
 {
   LOG(("HttpChannelChild::DoPreOnStopRequest [this=%p status=%" PRIx32 "]\n",
        this, static_cast<uint32_t>(aStatus)));
   mIsPending = false;
 
+  MaybeCallSynthesizedCallback();
+
   Performance* documentPerformance = GetPerformance();
   if (documentPerformance) {
       documentPerformance->AddEntry(this, this);
   }
 
   if (!mCanceled && NS_SUCCEEDED(mStatus)) {
     mStatus = aStatus;
   }
@@ -1405,16 +1405,18 @@ void
 HttpChannelChild::DoNotifyListenerCleanup()
 {
   LOG(("HttpChannelChild::DoNotifyListenerCleanup [this=%p]\n", this));
 
   if (mInterceptListener) {
     mInterceptListener->Cleanup();
     mInterceptListener = nullptr;
   }
+
+  MaybeCallSynthesizedCallback();
 }
 
 class DeleteSelfEvent : public NeckoTargetChannelEvent<HttpChannelChild>
 {
  public:
   explicit DeleteSelfEvent(HttpChannelChild* child)
   : NeckoTargetChannelEvent<HttpChannelChild>(child) {}
   void Run() { mChild->DeleteSelf(); }
@@ -1428,37 +1430,52 @@ HttpChannelChild::RecvDeleteSelf()
   return IPC_OK();
 }
 
 HttpChannelChild::OverrideRunnable::OverrideRunnable(
   HttpChannelChild* aChannel,
   HttpChannelChild* aNewChannel,
   InterceptStreamListener* aListener,
   nsIInputStream* aInput,
+  nsIInterceptedBodyCallback* aCallback,
   nsAutoPtr<nsHttpResponseHead>& aHead)
   : Runnable("net::HttpChannelChild::OverrideRunnable")
 {
   mChannel = aChannel;
   mNewChannel = aNewChannel;
   mListener = aListener;
   mInput = aInput;
+  mCallback = aCallback;
   mHead = aHead;
 }
 
 void
 HttpChannelChild::OverrideRunnable::OverrideWithSynthesizedResponse()
 {
   if (mNewChannel) {
-    mNewChannel->OverrideWithSynthesizedResponse(mHead, mInput, mListener);
+    mNewChannel->OverrideWithSynthesizedResponse(mHead, mInput, mCallback, mListener);
   }
 }
 
 NS_IMETHODIMP
 HttpChannelChild::OverrideRunnable::Run()
 {
+  // Check to see if the channel was canceled in the middle of the redirect.
+  nsresult rv = NS_OK;
+  Unused << mChannel->GetStatus(&rv);
+  if (NS_FAILED(rv)) {
+    if (mCallback) {
+      mCallback->BodyComplete(rv);
+      mCallback = nullptr;
+    }
+    mChannel->CleanupRedirectingChannel(rv);
+    mNewChannel->Cancel(rv);
+    return NS_OK;
+  }
+
   bool ret = mChannel->Redirect3Complete(this);
 
   // If the method returns false, it means the IPDL connection is being
   // asyncly torn down and reopened, and OverrideWithSynthesizedResponse
   // will be called later from FinishInterceptedRedirect. This object will
   // be assigned to HttpChannelChild::mOverrideRunnable in order to do so.
   // If it is true, we can call the method right now.
   if (ret) {
@@ -2133,19 +2150,22 @@ HttpChannelChild::OnRedirectVerifyCallba
     // nsIHttpChannelChild (it could be a DataChannelChild).
 
     RefPtr<InterceptStreamListener> streamListener =
         new InterceptStreamListener(redirectedChannel, mListenerContext);
 
     nsCOMPtr<nsIEventTarget> neckoTarget = GetNeckoTarget();
     MOZ_ASSERT(neckoTarget);
 
+    nsCOMPtr<nsIInterceptedBodyCallback> callback =
+      mSynthesizedCallback.forget();
+
     Unused << neckoTarget->Dispatch(
       new OverrideRunnable(this, redirectedChannel, streamListener,
-                           mSynthesizedInput, mResponseHead),
+                           mSynthesizedInput, callback, mResponseHead),
       NS_DISPATCH_NORMAL);
 
     return NS_OK;
   }
 
   RequestHeaderTuples emptyHeaders;
   RequestHeaderTuples* headerTuples = &emptyHeaders;
   nsLoadFlags loadFlags = 0;
@@ -2187,16 +2207,18 @@ HttpChannelChild::OnRedirectVerifyCallba
     }
 
     nsCOMPtr<nsIRequest> request = do_QueryInterface(mRedirectChannelChild);
     if (request) {
       request->GetLoadFlags(&loadFlags);
     }
   }
 
+  MaybeCallSynthesizedCallback();
+
   bool chooseAppcache = false;
   nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel =
     do_QueryInterface(newHttpChannel);
   if (appCacheChannel) {
     appCacheChannel->GetChooseApplicationCache(&chooseAppcache);
   }
 
   if (mIPCOpen)
@@ -2217,22 +2239,33 @@ HttpChannelChild::Cancel(nsresult status
   LOG(("HttpChannelChild::Cancel [this=%p]\n", this));
   MOZ_ASSERT(NS_IsMainThread());
 
   if (!mCanceled) {
     // If this cancel occurs before nsHttpChannel has been set up, AsyncOpen
     // is responsible for cleaning up.
     mCanceled = true;
     mStatus = status;
-    if (RemoteChannelExists())
+    if (RemoteChannelExists()) {
       SendCancel(status);
+    }
+
+    // If the channel is intercepted and already pumping, then just
+    // cancel the pump.  This will call OnStopRequest().
     if (mSynthesizedResponsePump) {
       mSynthesizedResponsePump->Cancel(status);
     }
-    mInterceptListener = nullptr;
+
+    // If we are canceled while intercepting, but not yet pumping, then
+    // we must call AsyncAbort() to trigger OnStopRequest().
+    else if (mInterceptListener) {
+      mInterceptListener->Cleanup();
+      mInterceptListener = nullptr;
+      Unused << AsyncAbort(status);
+    }
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 HttpChannelChild::Suspend()
 {
   LOG(("HttpChannelChild::Suspend [this=%p, mSuspendCount=%" PRIu32 ", "
@@ -2386,17 +2419,16 @@ HttpChannelChild::AsyncOpen(nsIStreamLis
   // add ourselves to the load group.
   if (mLoadGroup)
     mLoadGroup->AddRequest(this, nullptr);
 
   if (mCanceled) {
     // We may have been canceled already, either by on-modify-request
     // listeners or by load group observers; in that case, don't create IPDL
     // connection. See nsHttpChannel::AsyncOpen().
-    Unused << AsyncAbort(mStatus);
     return NS_OK;
   }
 
   // Set user agent override from docshell
   HttpBaseChannel::SetDocshellUserAgentOverride();
 
   MOZ_ASSERT_IF(mPostRedirectChannelShouldUpgrade,
                 mPostRedirectChannelShouldIntercept);
@@ -3411,20 +3443,25 @@ HttpChannelChild::ResetInterception()
   mInterceptListener = nullptr;
 
   // The chance to intercept any further requests associated with this channel
   // (such as redirects) has passed.
   if (mRedirectMode != nsIHttpChannelInternal::REDIRECT_MODE_MANUAL) {
     mLoadFlags |= LOAD_BYPASS_SERVICE_WORKER;
   }
 
+  // If the channel has already been aborted or canceled, just stop.
+  if (NS_FAILED(mStatus)) {
+    return;
+  }
+
   // Continue with the original cross-process request
   nsresult rv = ContinueAsyncOpen();
   if (NS_WARN_IF(NS_FAILED(rv))) {
-    Unused << AsyncAbort(rv);
+    Unused << Cancel(rv);
   }
 }
 
 NS_IMETHODIMP
 HttpChannelChild::GetResponseSynthesized(bool* aSynthesized)
 {
   NS_ENSURE_ARG_POINTER(aSynthesized);
   *aSynthesized = mSynthesizedResponse;
@@ -3531,90 +3568,118 @@ HttpChannelChild::CancelOnMainThread(nsr
   UniquePtr<ChannelEvent> cancelEvent = MakeUnique<CancelEvent>(this, aRv);
   mEventQ->PrependEvent(cancelEvent);
   mEventQ->Resume();
 }
 
 void
 HttpChannelChild::OverrideWithSynthesizedResponse(nsAutoPtr<nsHttpResponseHead>& aResponseHead,
                                                   nsIInputStream* aSynthesizedInput,
+                                                  nsIInterceptedBodyCallback* aSynthesizedCallback,
                                                   InterceptStreamListener* aStreamListener)
 {
+  nsresult rv = NS_OK;
+  auto autoCleanup = MakeScopeExit([&] {
+    // Auto-cancel on failure.  Do this first to get mStatus set, if necessary.
+    if (NS_FAILED(rv)) {
+      Cancel(rv);
+    }
+
+    // If we early exit before taking ownership of the body, then automatically
+    // invoke the callback.  This could be due to an error or because we're not
+    // going to consume it due to a redirect, etc.
+    if (aSynthesizedCallback) {
+      aSynthesizedCallback->BodyComplete(mStatus);
+    }
+  });
+
+  if (NS_FAILED(mStatus)) {
+    return;
+  }
+
   mInterceptListener = aStreamListener;
 
   // Intercepted responses should already be decoded.  If its a redirect,
   // however, we want to respect the encoding of the final result instead.
   if (!nsHttpChannel::WillRedirect(aResponseHead)) {
     SetApplyConversion(false);
   }
 
   mResponseHead = aResponseHead;
   mSynthesizedResponse = true;
 
+  mSynthesizedInput = aSynthesizedInput;
+
+  if (!mSynthesizedInput) {
+    rv = NS_NewCStringInputStream(getter_AddRefs(mSynthesizedInput),
+                                  EmptyCString());
+    NS_ENSURE_SUCCESS_VOID(rv);
+  }
+
   if (nsHttpChannel::WillRedirect(mResponseHead)) {
     mShouldInterceptSubsequentRedirect = true;
     // Continue with the original cross-process request
-    nsresult rv = ContinueAsyncOpen();
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      rv = AsyncAbort(rv);
-      MOZ_ASSERT(NS_SUCCEEDED(rv));
-    }
+    rv = ContinueAsyncOpen();
     return;
   }
 
-  // In our current implementation, the FetchEvent handler will copy the
-  // response stream completely into the pipe backing the input stream so we
-  // can treat the available as the length of the stream.
-  uint64_t available;
-  nsresult rv = aSynthesizedInput->Available(&available);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
+  // For progress we trust the content-length for the "maximum" size.
+  // We can't determine the full size from the stream itself since we
+  // only receive the data incrementally.  We can't trust Available()
+  // here.
+  // TODO: We could implement an nsIFixedLengthInputStream interface and
+  //       QI to it here.  This would let us determine the total length
+  //       for streams that support it.  See bug 1388774.
+  rv = GetContentLength(&mSynthesizedStreamLength);
+  if (NS_FAILED(rv)) {
     mSynthesizedStreamLength = -1;
-  } else {
-    mSynthesizedStreamLength = int64_t(available);
   }
 
   nsCOMPtr<nsIEventTarget> neckoTarget = GetNeckoTarget();
   MOZ_ASSERT(neckoTarget);
 
   rv = nsInputStreamPump::Create(getter_AddRefs(mSynthesizedResponsePump),
-                                 aSynthesizedInput, 0, 0, true, neckoTarget);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    aSynthesizedInput->Close();
-    return;
-  }
+                                 mSynthesizedInput, 0, 0, true, neckoTarget);
+  NS_ENSURE_SUCCESS_VOID(rv);
 
   rv = mSynthesizedResponsePump->AsyncRead(aStreamListener, nullptr);
   NS_ENSURE_SUCCESS_VOID(rv);
 
+  // The pump is started, so take ownership of the body callback.  We
+  // clear the argument to avoid auto-completing it via the ScopeExit
+  // lambda.
+  mSynthesizedCallback = aSynthesizedCallback;
+  aSynthesizedCallback = nullptr;
+
   // if this channel has been suspended previously, the pump needs to be
   // correspondingly suspended now that it exists.
   for (uint32_t i = 0; i < mSuspendCount; i++) {
     rv = mSynthesizedResponsePump->Suspend();
     NS_ENSURE_SUCCESS_VOID(rv);
   }
 
-  if (mCanceled) {
-    mSynthesizedResponsePump->Cancel(mStatus);
-  }
+  MOZ_DIAGNOSTIC_ASSERT(!mCanceled);
 }
 
 NS_IMETHODIMP
 HttpChannelChild::ForceIntercepted(bool aPostRedirectChannelShouldIntercept,
                                    bool aPostRedirectChannelShouldUpgrade)
 {
   mShouldParentIntercept = true;
   mPostRedirectChannelShouldIntercept = aPostRedirectChannelShouldIntercept;
   mPostRedirectChannelShouldUpgrade = aPostRedirectChannelShouldUpgrade;
   return NS_OK;
 }
 
 void
-HttpChannelChild::ForceIntercepted(nsIInputStream* aSynthesizedInput)
+HttpChannelChild::ForceIntercepted(nsIInputStream* aSynthesizedInput,
+                                   nsIInterceptedBodyCallback* aSynthesizedCallback)
 {
   mSynthesizedInput = aSynthesizedInput;
+  mSynthesizedCallback = aSynthesizedCallback;
   mSynthesizedResponse = true;
   mRedirectingForSubsequentSynthesizedResponse = true;
 }
 
 mozilla::ipc::IPCResult
 HttpChannelChild::RecvIssueDeprecationWarning(const uint32_t& warning,
                                               const bool& asError)
 {
@@ -3698,21 +3763,20 @@ HttpChannelChild::LogBlockedCORSRequest(
   if (mLoadInfo) {
     uint64_t innerWindowID = mLoadInfo->GetInnerWindowID();
     nsCORSListenerProxy::LogBlockedCORSRequest(innerWindowID, aMessage);
   }
   return NS_OK;
 }
 
 void
-HttpChannelChild::SynthesizeResponseStartTime(const TimeStamp& aTime)
+HttpChannelChild::MaybeCallSynthesizedCallback()
 {
-  mTransactionTimings.responseStart = aTime;
-}
-
-void
-HttpChannelChild::SynthesizeResponseEndTime(const TimeStamp& aTime)
-{
-  mTransactionTimings.responseEnd = aTime;
+  if (!mSynthesizedCallback) {
+    return;
+  }
+
+  mSynthesizedCallback->BodyComplete(mStatus);
+  mSynthesizedCallback = nullptr;
 }
 
 } // namespace net
 } // namespace mozilla
--- a/netwerk/protocol/http/HttpChannelChild.h
+++ b/netwerk/protocol/http/HttpChannelChild.h
@@ -34,16 +34,17 @@
 #include "nsIDivertableChannel.h"
 #include "nsIThreadRetargetableRequest.h"
 #include "mozilla/net/DNS.h"
 
 using mozilla::Telemetry::LABELS_HTTP_CHILD_OMT_STATS;
 
 class nsIEventTarget;
 class nsInputStreamPump;
+class nsIInterceptedBodyCallback;
 
 namespace mozilla {
 namespace net {
 
 class HttpBackgroundChannelChild;
 class InterceptedChannelContent;
 class InterceptStreamListener;
 
@@ -200,25 +201,27 @@ private:
 private:
 
   class OverrideRunnable : public Runnable {
   public:
     OverrideRunnable(HttpChannelChild* aChannel,
                      HttpChannelChild* aNewChannel,
                      InterceptStreamListener* aListener,
                      nsIInputStream* aInput,
+                     nsIInterceptedBodyCallback* aCallback,
                      nsAutoPtr<nsHttpResponseHead>& aHead);
 
     NS_IMETHOD Run() override;
     void OverrideWithSynthesizedResponse();
   private:
     RefPtr<HttpChannelChild> mChannel;
     RefPtr<HttpChannelChild> mNewChannel;
     RefPtr<InterceptStreamListener> mListener;
     nsCOMPtr<nsIInputStream> mInput;
+    nsCOMPtr<nsIInterceptedBodyCallback> mCallback;
     nsAutoPtr<nsHttpResponseHead> mHead;
   };
 
   // Sets the event target for future IPC messages. Messages will either be
   // directed to the TabGroup or DocGroup, depending on the LoadInfo associated
   // with the channel. Should be called when a new channel is being set up,
   // before the constructor message is sent to the parent.
   void SetEventTarget();
@@ -260,37 +263,37 @@ private:
 
   // Discard the prior interception and continue with the original network request.
   void ResetInterception();
 
   // Override this channel's pending response with a synthesized one. The content will be
   // asynchronously read from the pump.
   void OverrideWithSynthesizedResponse(nsAutoPtr<nsHttpResponseHead>& aResponseHead,
                                        nsIInputStream* aSynthesizedInput,
+                                       nsIInterceptedBodyCallback* aSynthesizedCallback,
                                        InterceptStreamListener* aStreamListener);
 
-  void ForceIntercepted(nsIInputStream* aSynthesizedInput);
+  void ForceIntercepted(nsIInputStream* aSynthesizedInput,
+                        nsIInterceptedBodyCallback* aSynthesizedCallback);
 
   // Try send DeletingChannel message to parent side. Dispatch an async task to
   // main thread if invoking on non-main thread.
   void TrySendDeletingChannel();
 
   // Try invoke Cancel if on main thread, or prepend a CancelEvent in mEventQ to
   // ensure Cacnel is processed before any other channel events.
   void CancelOnMainThread(nsresult aRv);
 
   void
-  SynthesizeResponseStartTime(const TimeStamp& aTime);
-
-  void
-  SynthesizeResponseEndTime(const TimeStamp& aTime);
+  MaybeCallSynthesizedCallback();
 
   RequestHeaderTuples mClientSetRequestHeaders;
   RefPtr<nsInputStreamPump> mSynthesizedResponsePump;
   nsCOMPtr<nsIInputStream> mSynthesizedInput;
+  nsCOMPtr<nsIInterceptedBodyCallback> mSynthesizedCallback;
   int64_t mSynthesizedStreamLength;
 
   bool mIsFromCache;
   bool mCacheEntryAvailable;
   uint64_t mCacheEntryId;
   bool mAltDataCacheEntryAvailable;
   int32_t      mCacheFetchCount;
   uint32_t     mCacheExpirationTime;
--- a/netwerk/protocol/http/HttpChannelParentListener.cpp
+++ b/netwerk/protocol/http/HttpChannelParentListener.cpp
@@ -313,17 +313,18 @@ public:
     , mChannel(aChannel)
   {
   }
 
   NS_IMETHOD Run() override
   {
     // The URL passed as an argument here doesn't matter, since the child will
     // receive a redirection notification as a result of this synthesized response.
-    mChannel->FinishSynthesizedResponse(EmptyCString());
+    mChannel->StartSynthesizedResponse(nullptr, nullptr, EmptyCString());
+    mChannel->FinishSynthesizedResponse();
     return NS_OK;
   }
 };
 
 NS_IMETHODIMP
 HttpChannelParentListener::ChannelIntercepted(nsIInterceptedChannel* aChannel)
 {
   // Its possible for the child-side interception to complete and tear down
--- a/netwerk/protocol/http/InterceptedChannel.cpp
+++ b/netwerk/protocol/http/InterceptedChannel.cpp
@@ -2,27 +2,29 @@
 /* vim:set expandtab ts=2 sw=2 sts=2 cin: */
 /* 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 "HttpLog.h"
 
 #include "InterceptedChannel.h"
+#include "nsICancelable.h"
 #include "nsInputStreamPump.h"
 #include "nsIPipe.h"
 #include "nsIStreamListener.h"
 #include "nsITimedChannel.h"
 #include "nsHttpChannel.h"
 #include "HttpChannelChild.h"
 #include "nsHttpResponseHead.h"
 #include "nsNetUtil.h"
 #include "mozilla/ConsoleReportCollector.h"
 #include "mozilla/dom/ChannelInfo.h"
 #include "nsIChannelEventSink.h"
+#include "nsThreadUtils.h"
 
 namespace mozilla {
 namespace net {
 
 extern nsresult
 DoUpdateExpirationTime(nsHttpChannel* aSelf,
                        nsICacheEntry* aCacheEntry,
                        nsHttpResponseHead* aResponseHead,
@@ -43,23 +45,16 @@ InterceptedChannelBase::InterceptedChann
   , mSynthesizedOrReset(Invalid)
 {
 }
 
 InterceptedChannelBase::~InterceptedChannelBase()
 {
 }
 
-NS_IMETHODIMP
-InterceptedChannelBase::GetResponseBody(nsIOutputStream** aStream)
-{
-  NS_IF_ADDREF(*aStream = mResponseBody);
-  return NS_OK;
-}
-
 void
 InterceptedChannelBase::EnsureSynthesizedResponse()
 {
   if (mSynthesizedResponseHead.isNothing()) {
     mSynthesizedResponseHead.emplace(new nsHttpResponseHead());
   }
 }
 
@@ -222,21 +217,16 @@ InterceptedChannelContent::InterceptedCh
 , mStreamListener(aListener)
 , mSecureUpgrade(aSecureUpgrade)
 {
 }
 
 void
 InterceptedChannelContent::NotifyController()
 {
-  nsresult rv = NS_NewPipe(getter_AddRefs(mSynthesizedInput),
-                           getter_AddRefs(mResponseBody),
-                           0, UINT32_MAX, true, true);
-  NS_ENSURE_SUCCESS_VOID(rv);
-
   DoNotifyController();
 }
 
 NS_IMETHODIMP
 InterceptedChannelContent::GetChannel(nsIChannel** aChannel)
 {
   NS_IF_ADDREF(*aChannel = mChannel);
   return NS_OK;
@@ -246,61 +236,52 @@ NS_IMETHODIMP
 InterceptedChannelContent::ResetInterception()
 {
   if (mClosed) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   mReportCollector->FlushConsoleReports(mChannel);
 
-  mResponseBody->Close();
-  mResponseBody = nullptr;
-  mSynthesizedInput = nullptr;
-
   mChannel->ResetInterception();
 
   mClosed = true;
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 InterceptedChannelContent::SynthesizeStatus(uint16_t aStatus, const nsACString& aReason)
 {
-  if (!mResponseBody) {
+  if (mClosed) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   return DoSynthesizeStatus(aStatus, aReason);
 }
 
 NS_IMETHODIMP
 InterceptedChannelContent::SynthesizeHeader(const nsACString& aName, const nsACString& aValue)
 {
-  if (!mResponseBody) {
+  if (mClosed) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   return DoSynthesizeHeader(aName, aValue);
 }
 
 NS_IMETHODIMP
-InterceptedChannelContent::FinishSynthesizedResponse(const nsACString& aFinalURLSpec)
+InterceptedChannelContent::StartSynthesizedResponse(nsIInputStream* aBody,
+                                                    nsIInterceptedBodyCallback* aBodyCallback,
+                                                    const nsACString& aFinalURLSpec)
 {
   if (NS_WARN_IF(mClosed)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
-  // Make sure the body output stream is always closed.  If the channel was
-  // intercepted with a null-body response then its possible the synthesis
-  // completed without a stream copy operation.
-  mResponseBody->Close();
-
-  mReportCollector->FlushConsoleReports(mChannel);
-
   EnsureSynthesizedResponse();
 
   nsCOMPtr<nsIURI> originalURI;
   mChannel->GetURI(getter_AddRefs(originalURI));
 
   nsCOMPtr<nsIURI> responseURI;
   if (!aFinalURLSpec.IsEmpty()) {
     nsresult rv = NS_NewURI(getter_AddRefs(responseURI), aFinalURLSpec);
@@ -311,48 +292,56 @@ InterceptedChannelContent::FinishSynthes
     NS_ENSURE_SUCCESS(rv, rv);
   } else {
     responseURI = originalURI;
   }
 
   bool equal = false;
   originalURI->Equals(responseURI, &equal);
   if (!equal) {
-    mChannel->ForceIntercepted(mSynthesizedInput);
+    mChannel->ForceIntercepted(aBody, aBodyCallback);
     mChannel->BeginNonIPCRedirect(responseURI, *mSynthesizedResponseHead.ptr());
   } else {
     mChannel->OverrideWithSynthesizedResponse(mSynthesizedResponseHead.ref(),
-                                              mSynthesizedInput,
+                                              aBody, aBodyCallback,
                                               mStreamListener);
   }
 
-  mResponseBody = nullptr;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+InterceptedChannelContent::FinishSynthesizedResponse()
+{
+  if (NS_WARN_IF(mClosed)) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  mReportCollector->FlushConsoleReports(mChannel);
+
   mStreamListener = nullptr;
   mClosed = true;
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 InterceptedChannelContent::CancelInterception(nsresult aStatus)
 {
   MOZ_ASSERT(NS_FAILED(aStatus));
 
   if (mClosed) {
     return NS_ERROR_FAILURE;
   }
+  mClosed = true;
 
   mReportCollector->FlushConsoleReports(mChannel);
 
-  // we need to use AsyncAbort instead of Cancel since there's no active pump
-  // to cancel which will provide OnStart/OnStopRequest to the channel.
-  nsresult rv = mChannel->AsyncAbort(aStatus);
-  NS_ENSURE_SUCCESS(rv, rv);
+  Unused << mChannel->Cancel(aStatus);
   mStreamListener = nullptr;
-  mClosed = true;
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 InterceptedChannelContent::SetChannelInfo(dom::ChannelInfo* aChannelInfo)
 {
   if (mClosed) {
--- a/netwerk/protocol/http/InterceptedChannel.h
+++ b/netwerk/protocol/http/InterceptedChannel.h
@@ -25,19 +25,16 @@ class InterceptStreamListener;
 
 // An object representing a channel that has been intercepted. This avoids complicating
 // the actual channel implementation with the details of synthesizing responses.
 class InterceptedChannelBase : public nsIInterceptedChannel {
 protected:
   // The interception controller to notify about the successful channel interception
   nsCOMPtr<nsINetworkInterceptController> mController;
 
-  // The stream to write the body of the synthesized response
-  nsCOMPtr<nsIOutputStream> mResponseBody;
-
   // Response head for use when synthesizing
   Maybe<nsAutoPtr<nsHttpResponseHead>> mSynthesizedResponseHead;
 
   nsCOMPtr<nsIConsoleReportCollector> mReportCollector;
   nsCOMPtr<nsISupports> mReleaseHandle;
 
   bool mClosed;
 
@@ -68,17 +65,16 @@ public:
   explicit InterceptedChannelBase(nsINetworkInterceptController* aController);
 
   // Notify the interception controller that the channel has been intercepted
   // and prepare the response body output stream.
   virtual void NotifyController() = 0;
 
   NS_DECL_ISUPPORTS
 
-  NS_IMETHOD GetResponseBody(nsIOutputStream** aOutput) override;
   NS_IMETHOD GetConsoleReportCollector(nsIConsoleReportCollector** aCollectorOut) override;
   NS_IMETHOD SetReleaseHandle(nsISupports* aHandle) override;
 
   NS_IMETHODIMP
   SetLaunchServiceWorkerStart(TimeStamp aTimeStamp) override
   {
     mLaunchServiceWorkerStart = aTimeStamp;
     return NS_OK;
@@ -166,33 +162,33 @@ public:
   SecureUpgradeChannelURI(nsIChannel* aChannel);
 };
 
 class InterceptedChannelContent : public InterceptedChannelBase
 {
   // The actual channel being intercepted.
   RefPtr<HttpChannelChild> mChannel;
 
-  // Reader-side of the response body when synthesizing in a child proces
-  nsCOMPtr<nsIInputStream> mSynthesizedInput;
-
   // Listener for the synthesized response to fix up the notifications before they reach
   // the actual channel.
   RefPtr<InterceptStreamListener> mStreamListener;
 
   // Set for intercepted channels that have gone through a secure upgrade.
   bool mSecureUpgrade;
 public:
   InterceptedChannelContent(HttpChannelChild* aChannel,
                             nsINetworkInterceptController* aController,
                             InterceptStreamListener* aListener,
                             bool aSecureUpgrade);
 
   NS_IMETHOD ResetInterception() override;
-  NS_IMETHOD FinishSynthesizedResponse(const nsACString& aFinalURLSpec) override;
+  NS_IMETHOD StartSynthesizedResponse(nsIInputStream* aBody,
+                                      nsIInterceptedBodyCallback* aBodyCallback,
+                                      const nsACString& aFinalURLSpec) override;
+  NS_IMETHOD FinishSynthesizedResponse() override;
   NS_IMETHOD GetChannel(nsIChannel** aChannel) override;
   NS_IMETHOD GetSecureUpgradedChannelURI(nsIURI** aURI) override;
   NS_IMETHOD SynthesizeStatus(uint16_t aStatus, const nsACString& aReason) override;
   NS_IMETHOD SynthesizeHeader(const nsACString& aName, const nsACString& aValue) override;
   NS_IMETHOD CancelInterception(nsresult aStatus) override;
   NS_IMETHOD SetChannelInfo(mozilla::dom::ChannelInfo* aChannelInfo) override;
   NS_IMETHOD GetInternalContentPolicyType(nsContentPolicyType *aInternalContentPolicyType) override;
 
--- a/netwerk/protocol/http/InterceptedHttpChannel.cpp
+++ b/netwerk/protocol/http/InterceptedHttpChannel.cpp
@@ -45,19 +45,19 @@ InterceptedHttpChannel::ReleaseListeners
 {
   if (mLoadGroup) {
     mLoadGroup->RemoveRequest(this, nullptr, mStatus);
   }
   HttpBaseChannel::ReleaseListeners();
   mSynthesizedResponseHead.reset();
   mRedirectChannel = nullptr;
   mBodyReader = nullptr;
-  mBodyWriter = nullptr;
   mReleaseHandle = nullptr;
   mProgressSink = nullptr;
+  mBodyCallback = nullptr;
   mPump = nullptr;
   mParentChannel = nullptr;
 
   MOZ_DIAGNOSTIC_ASSERT(!mIsPending);
 }
 
 nsresult
 InterceptedHttpChannel::SetupReplacementChannel(nsIURI *aURI,
@@ -235,18 +235,23 @@ InterceptedHttpChannel::RedirectForOpaqu
   // Perform an internal redirect to another InterceptedHttpChannel using
   // the given cross-origin response URL.  The resulting channel will then
   // process the synthetic response as normal.  This extra redirect is
   // performed so that listeners treat the result as unsafe cross-origin
   // data.
 
   nsresult rv = NS_OK;
 
+  // We want to pass ownership of the body callback to the new synthesized
+  // channel.  We need to hold a reference to the callbacks on the stack
+  // as well, though, so we can call them if a failure occurs.
+  nsCOMPtr<nsIInterceptedBodyCallback> bodyCallback = mBodyCallback.forget();
+
   RefPtr<InterceptedHttpChannel> newChannel =
-    CreateForSynthesis(mResponseHead, mBodyReader,
+    CreateForSynthesis(mResponseHead, mBodyReader, bodyCallback,
                        mChannelCreationTime, mChannelCreationTimestamp,
                        mAsyncOpenTime);
 
   rv = newChannel->Init(aResponseURI, mCaps,
                         static_cast<nsProxyInfo*>(mProxyInfo.get()),
                         mProxyResolveFlags, mProxyURI, mChannelId);
 
   uint32_t flags = nsIChannelEventSink::REDIRECT_INTERNAL;
@@ -259,16 +264,21 @@ InterceptedHttpChannel::RedirectForOpaqu
   rv = SetupReplacementChannel(aResponseURI, newChannel, true, flags);
   NS_ENSURE_SUCCESS(rv, rv);
 
   mRedirectChannel = newChannel;
 
   rv = gHttpHandler->AsyncOnChannelRedirect(this, mRedirectChannel, flags);
 
   if (NS_FAILED(rv)) {
+    // Make sure to call the body callback since we took ownership
+    // above.  Neither the new channel or our standard
+    // OnRedirectVerifyCallback() code will invoke the callback.  Do it here.
+    bodyCallback->BodyComplete(rv);
+
     OnRedirectVerifyCallback(rv);
   }
 
   return rv;
 }
 
 nsresult
 InterceptedHttpChannel::StartPump()
@@ -307,16 +317,18 @@ InterceptedHttpChannel::StartPump()
   rv = mPump->AsyncRead(this, mListenerContext);
   NS_ENSURE_SUCCESS(rv, rv);
 
   uint32_t suspendCount = mSuspendCount;
   while (suspendCount--) {
     mPump->Suspend();
   }
 
+  MOZ_DIAGNOSTIC_ASSERT(!mCanceled);
+
   return rv;
 }
 
 nsresult
 InterceptedHttpChannel::OpenRedirectChannel()
 {
   nsresult rv = NS_OK;
 
@@ -408,16 +420,25 @@ InterceptedHttpChannel::MaybeCallStatusA
                           mStatusHost.get());
 
   mProgressSink->OnProgress(this, mListenerContext, progress,
                             mSynthesizedStreamLength);
 
   mProgressReported = progress;
 }
 
+void
+InterceptedHttpChannel::MaybeCallBodyCallback()
+{
+  nsCOMPtr<nsIInterceptedBodyCallback> callback = mBodyCallback.forget();
+  if (callback) {
+    callback->BodyComplete(mStatus);
+  }
+}
+
 // static
 already_AddRefed<InterceptedHttpChannel>
 InterceptedHttpChannel::CreateForInterception(PRTime aCreationTime,
                                               const TimeStamp& aCreationTimestamp,
                                               const TimeStamp& aAsyncOpenTimestamp)
 {
   // Create an InterceptedHttpChannel that will trigger a FetchEvent
   // in a ServiceWorker when opened.
@@ -427,32 +448,34 @@ InterceptedHttpChannel::CreateForInterce
 
   return ref.forget();
 }
 
 // static
 already_AddRefed<InterceptedHttpChannel>
 InterceptedHttpChannel::CreateForSynthesis(const nsHttpResponseHead* aHead,
                                            nsIInputStream* aBody,
+                                           nsIInterceptedBodyCallback* aBodyCallback,
                                            PRTime aCreationTime,
                                            const TimeStamp& aCreationTimestamp,
                                            const TimeStamp& aAsyncOpenTimestamp)
 {
   MOZ_DIAGNOSTIC_ASSERT(aHead);
   MOZ_DIAGNOSTIC_ASSERT(aBody);
 
   // Create an InterceptedHttpChannel that already has a synthesized response.
   // The synthetic response will be processed when opened.  A FetchEvent
   // will not be triggered.
   RefPtr<InterceptedHttpChannel> ref =
     new InterceptedHttpChannel(aCreationTime, aCreationTimestamp,
                                aAsyncOpenTimestamp);
 
+  ref->mResponseHead = new nsHttpResponseHead(*aHead);
   ref->mBodyReader = aBody;
-  ref->mResponseHead = new nsHttpResponseHead(*aHead);
+  ref->mBodyCallback = aBodyCallback;
 
   return ref.forget();
 }
 
 NS_IMETHODIMP
 InterceptedHttpChannel::Cancel(nsresult aStatus)
 {
   // Note: This class has been designed to send all error results through
@@ -688,88 +711,118 @@ InterceptedHttpChannel::SynthesizeHeader
   nsAutoCString header = aName + NS_LITERAL_CSTRING(": ") + aValue;
   // Overwrite any existing header.
   nsresult rv = mSynthesizedResponseHead->ParseHeaderLine(header);
   NS_ENSURE_SUCCESS(rv, rv);
   return NS_OK;
 }
 
 NS_IMETHODIMP
-InterceptedHttpChannel::FinishSynthesizedResponse(const nsACString& aFinalURLSpec)
+InterceptedHttpChannel::StartSynthesizedResponse(nsIInputStream* aBody,
+                                                 nsIInterceptedBodyCallback* aBodyCallback,
+                                                 const nsACString& aFinalURLSpec)
 {
-  if (mCanceled) {
-    return mStatus;
+  nsresult rv = NS_OK;
+
+  auto autoCleanup = MakeScopeExit([&] {
+    // Auto-cancel on failure.  Do this first to get mStatus set, if necessary.
+    if (NS_FAILED(rv)) {
+      Cancel(rv);
+    }
+
+    // If we early exit before taking ownership of the body, then automatically
+    // invoke the callback.  This could be due to an error or because we're not
+    // going to consume it due to a redirect, etc.
+    if (aBodyCallback) {
+      aBodyCallback->BodyComplete(mStatus);
+    }
+  });
+
+  if (NS_FAILED(mStatus)) {
+    // Return NS_OK.  The channel should fire callbacks with an error code
+    // if it was cancelled before this point.
+    return NS_OK;
   }
 
-  if (mBodyWriter) {
-    mBodyWriter->Close();
-  }
+  // Take ownership of the body callbacks  If a failure occurs we will
+  // automatically Cancel() the channel.  This will then invoke OnStopRequest()
+  // which will invoke the correct callback.  In the case of an opaque response
+  // redirect we pass ownership of the callback to the new channel.
+  mBodyCallback = aBodyCallback;
+  aBodyCallback = nullptr;
 
   if (!mSynthesizedResponseHead) {
     mSynthesizedResponseHead.reset(new nsHttpResponseHead());
   }
 
   mResponseHead = mSynthesizedResponseHead.release();
 
   if (ShouldRedirect()) {
-    return FollowSyntheticRedirect();
+    rv = FollowSyntheticRedirect();
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
   }
 
   // Intercepted responses should already be decoded.
   SetApplyConversion(false);
 
   // Errors and redirects may not have a body.  Synthesize an empty string stream
   // here so later code can be simpler.
+  mBodyReader = aBody;
   if (!mBodyReader) {
-    nsresult rv = NS_NewCStringInputStream(getter_AddRefs(mBodyReader),
-                                           EmptyCString());
+    rv = NS_NewCStringInputStream(getter_AddRefs(mBodyReader), EmptyCString());
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   nsCOMPtr<nsIURI> responseURI;
   if (!aFinalURLSpec.IsEmpty()) {
-    nsresult rv = NS_NewURI(getter_AddRefs(responseURI), aFinalURLSpec);
+    rv = NS_NewURI(getter_AddRefs(responseURI), aFinalURLSpec);
     NS_ENSURE_SUCCESS(rv, rv);
   } else {
     responseURI = mURI;
   }
 
   bool equal = false;
   Unused << mURI->Equals(responseURI, &equal);
   if (!equal) {
-    return RedirectForOpaqueResponse(responseURI);
+    rv = RedirectForOpaqueResponse(responseURI);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
   }
 
-  return StartPump();
+  rv = StartPump();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+InterceptedHttpChannel::FinishSynthesizedResponse()
+{
+  if (mCanceled) {
+    // Return NS_OK.  The channel should fire callbacks with an error code
+    // if it was cancelled before this point.
+    return NS_OK;
+  }
+
+  // TODO: Remove this API after interception moves to the parent process in
+  //       e10s mode.
+
+  return NS_OK;
 }
 
 NS_IMETHODIMP
 InterceptedHttpChannel::CancelInterception(nsresult aStatus)
 {
   return Cancel(aStatus);
 }
 
 NS_IMETHODIMP
-InterceptedHttpChannel::GetResponseBody(nsIOutputStream** aResponseBody)
-{
-  if (!mBodyWriter) {
-    nsresult rv = NS_NewPipe(getter_AddRefs(mBodyReader),
-                             getter_AddRefs(mBodyWriter),
-                             0,          // default segment size
-                             UINT32_MAX, // infinite pipe length
-                             true,       // non-blocking reader
-                             true);      // non-blocking writer
-    NS_ENSURE_SUCCESS(rv, rv);
-  }
-  nsCOMPtr<nsIOutputStream> ref(mBodyWriter);
-  ref.forget(aResponseBody);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
 InterceptedHttpChannel::GetChannel(nsIChannel** aChannel)
 {
   nsCOMPtr<nsIChannel> ref(this);
   ref.forget(aChannel);
   return NS_OK;
 }
 
 NS_IMETHODIMP
@@ -926,32 +979,33 @@ InterceptedHttpChannel::OnRedirectVerify
   if (hook) {
     hook->OnRedirectResult(NS_SUCCEEDED(rv));
   }
 
   if (NS_FAILED(rv)) {
     Cancel(rv);
   }
 
+  MaybeCallBodyCallback();
+
   mIsPending = false;
   ReleaseListeners();
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 InterceptedHttpChannel::OnStartRequest(nsIRequest* aRequest,
                                        nsISupports* aContext)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (!mProgressSink) {
     GetCallback(mProgressSink);
   }
-  mTransactionTimings.responseStart = TimeStamp::Now();
   if (mListener) {
     mListener->OnStartRequest(this, mListenerContext);
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 InterceptedHttpChannel::OnStopRequest(nsIRequest* aRequest,
@@ -959,24 +1013,24 @@ InterceptedHttpChannel::OnStopRequest(ns
                                       nsresult aStatus)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (NS_SUCCEEDED(mStatus)) {
     mStatus = aStatus;
   }
 
+  MaybeCallBodyCallback();
+
   // Its possible that we have any async runnable queued to report some
   // progress when OnStopRequest() is triggered.  Report any left over
   // progress immediately.  The extra runnable will then do nothing thanks
   // to the ReleaseListeners() call below.
   MaybeCallStatusAndProgress();
 
-  mTransactionTimings.responseEnd = TimeStamp::Now();
-
   mIsPending = false;
 
   // Register entry to the Performance resource timing
   mozilla::dom::Performance* documentPerformance = GetPerformance();
   if (documentPerformance) {
     documentPerformance->AddEntry(this, this);
   }
 
--- a/netwerk/protocol/http/InterceptedHttpChannel.h
+++ b/netwerk/protocol/http/InterceptedHttpChannel.h
@@ -66,19 +66,19 @@ class InterceptedHttpChannel final : pub
   NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
 
 private:
   friend class HttpAsyncAborter<InterceptedHttpChannel>;
 
   UniquePtr<nsHttpResponseHead> mSynthesizedResponseHead;
   nsCOMPtr<nsIChannel> mRedirectChannel;
   nsCOMPtr<nsIInputStream> mBodyReader;
-  nsCOMPtr<nsIOutputStream> mBodyWriter;
   nsCOMPtr<nsISupports> mReleaseHandle;
   nsCOMPtr<nsIProgressEventSink> mProgressSink;
+  nsCOMPtr<nsIInterceptedBodyCallback> mBodyCallback;
   RefPtr<nsInputStreamPump> mPump;
   RefPtr<ADivertableParentChannel> mParentChannel;
   TimeStamp mFinishResponseStart;
   TimeStamp mFinishResponseEnd;
   Atomic<int64_t> mProgress;
   int64_t mProgressReported;
   int64_t mSynthesizedStreamLength;
   uint64_t mResumeStartPos;
@@ -120,23 +120,27 @@ private:
   StartPump();
 
   nsresult
   OpenRedirectChannel();
 
   void
   MaybeCallStatusAndProgress();
 
+  void
+  MaybeCallBodyCallback();
+
 public:
   static already_AddRefed<InterceptedHttpChannel>
   CreateForInterception(PRTime aCreationTime, const TimeStamp& aCreationTimestamp,
                         const TimeStamp& aAsyncOpenTimestamp);
 
   static already_AddRefed<InterceptedHttpChannel>
   CreateForSynthesis(const nsHttpResponseHead* aHead, nsIInputStream* aBody,
+                     nsIInterceptedBodyCallback* aBodyCallback,
                      PRTime aCreationTime,
                      const TimeStamp& aCreationTimestamp,
                      const TimeStamp& aAsyncOpenTimestamp);
 
   NS_IMETHOD
   Cancel(nsresult aStatus) override;
 
   NS_IMETHOD
--- a/netwerk/test/unit/test_synthesized_response.js
+++ b/netwerk/test/unit/test_synthesized_response.js
@@ -49,19 +49,18 @@ function make_channel(url, body, cb) {
     },
     channelIntercepted: function(channel) {
       channel.QueryInterface(Ci.nsIInterceptedChannel);
       if (body) {
         var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
                             .createInstance(Ci.nsIStringInputStream);
         synthesized.data = body;
 
-        NetUtil.asyncCopy(synthesized, channel.responseBody, function() {
-          channel.finishSynthesizedResponse('');
-        });
+        channel.startSynthesizedResponse(synthesized, null, '');
+        channel.finishSynthesizedResponse();
       }
       if (cb) {
         cb(channel);
       }
       return {
         dispatch: function() { }
       };
     },
@@ -138,20 +137,19 @@ add_test(function() {
 
 // ensure that the channel waits for a decision and synthesizes headers correctly
 add_test(function() {
   var chan = make_channel(URL + '/body', null, function(channel) {
     do_timeout(100, function() {
       var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
                           .createInstance(Ci.nsIStringInputStream);
       synthesized.data = NON_REMOTE_BODY;
-      NetUtil.asyncCopy(synthesized, channel.responseBody, function() {
-        channel.synthesizeHeader("Content-Length", NON_REMOTE_BODY.length);
-        channel.finishSynthesizedResponse('');
-      });
+      channel.synthesizeHeader("Content-Length", NON_REMOTE_BODY.length);
+      channel.startSynthesizedResponse(synthesized, null, '');
+      channel.finishSynthesizedResponse();
     });
   });
   chan.asyncOpen2(new ChannelListener(handle_synthesized_response, null));
 });
 
 // ensure that the channel waits for a decision
 add_test(function() {
   var chan = make_channel(URL + '/body', null, function(chan) {
@@ -164,22 +162,21 @@ add_test(function() {
 
 // ensure that the intercepted channel supports suspend/resume
 add_test(function() {
   var chan = make_channel(URL + '/body', null, function(intercepted) {
     var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
                         .createInstance(Ci.nsIStringInputStream);
     synthesized.data = NON_REMOTE_BODY;
 
-    NetUtil.asyncCopy(synthesized, intercepted.responseBody, function() {
-      // set the content-type to ensure that the stream converter doesn't hold up notifications
-      // and cause the test to fail
-      intercepted.synthesizeHeader("Content-Type", "text/plain");
-      intercepted.finishSynthesizedResponse('');
-    });
+    // set the content-type to ensure that the stream converter doesn't hold up notifications
+    // and cause the test to fail
+    intercepted.synthesizeHeader("Content-Type", "text/plain");
+    intercepted.startSynthesizedResponse(synthesized, null, '');
+    intercepted.finishSynthesizedResponse();
   });
   chan.asyncOpen2(new ChannelListener(handle_synthesized_response, null,
 				     CL_ALLOW_UNKNOWN_CL | CL_SUSPEND | CL_EXPECT_3S_DELAY));
 });
 
 // ensure that the intercepted channel can be cancelled
 add_test(function() {
   var chan = make_channel(URL + '/body', null, function(intercepted) {
@@ -207,37 +204,38 @@ add_test(function() {
 
 // ensure that the intercepted channel can be canceled during the response
 add_test(function() {
   var chan = make_channel(URL + '/body', null, function(intercepted) {
     var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
                         .createInstance(Ci.nsIStringInputStream);
     synthesized.data = NON_REMOTE_BODY;
 
-    NetUtil.asyncCopy(synthesized, intercepted.responseBody, function() {
-      let channel = intercepted.channel;
-      intercepted.finishSynthesizedResponse('');
-      channel.cancel(Cr.NS_BINDING_ABORTED);
-    });
+    let channel = intercepted.channel;
+    intercepted.startSynthesizedResponse(synthesized, null, '');
+    intercepted.finishSynthesizedResponse();
+    channel.cancel(Cr.NS_BINDING_ABORTED);
   });
   chan.asyncOpen2(new ChannelListener(run_next_test, null,
                                      CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL));
 });
 
 // ensure that the intercepted channel can be canceled before the response
 add_test(function() {
   var chan = make_channel(URL + '/body', null, function(intercepted) {
     var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
                         .createInstance(Ci.nsIStringInputStream);
     synthesized.data = NON_REMOTE_BODY;
 
-    NetUtil.asyncCopy(synthesized, intercepted.responseBody, function() {
-      intercepted.channel.cancel(Cr.NS_BINDING_ABORTED);
-      intercepted.finishSynthesizedResponse('');
-    });
+    intercepted.channel.cancel(Cr.NS_BINDING_ABORTED);
+
+    // This should not throw, but result in the channel firing callbacks
+    // with an error status.
+    intercepted.startSynthesizedResponse(synthesized, null, '');
+    intercepted.finishSynthesizedResponse();
   });
   chan.asyncOpen2(new ChannelListener(run_next_test, null,
                                      CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL));
 });
 
 // Ensure that nsIInterceptedChannel.channelIntercepted() can return an error.
 // In this case we should automatically ResetInterception() and complete the
 // network request.
--- a/taskcluster/ci/test/test-sets.yml
+++ b/taskcluster/ci/test/test-sets.yml
@@ -310,29 +310,31 @@ android-common-tests:
     - crashtest
     - jsreftest
     - mochitest
     - mochitest-chrome
     - mochitest-clipboard
     - mochitest-gpu
     - mochitest-media
     - reftest
+    - test-verify
     - xpcshell
 
 android-debug-tests:
     # Marionette only available on Fennec debug builds as a security precaution
     - marionette
 
 android-opt-tests:
     - robocop
 
 android-gradle-tests:
     - mochitest-chrome
     - robocop
     - geckoview
+    - test-verify
 
 android-x86-tests:
     - mochitest-chrome
     - xpcshell
 
 devtools-tests:
     - mochitest-devtools-chrome
 
--- a/taskcluster/ci/test/tests.yml
+++ b/taskcluster/ci/test/tests.yml
@@ -2022,35 +2022,43 @@ test-verify:
     allow-software-gl-layers: false
     run-on-projects:
         by-test-platform:
             # do not run on ccov; see also the enable_code_coverage transform
             linux64-ccov/.*: []
             default: built-projects
     tier:
         by-test-platform:
-            macosx.*: 3
-            windows.*: 3
-            # windows10-64-asan.*: 3
+            android.*: 3
+            windows10-64-asan.*: 3
             default: 2
     mozharness:
-        script: desktop_unittest.py
-        no-read-buildbot-config: true
-        config:
-            by-test-platform:
-                windows.*:
-                    - unittests/win_taskcluster_unittest.py
-                macosx.*:
-                    - remove_executables.py
-                    - unittests/mac_unittest.py
-                linux.*:
-                    - unittests/linux_unittest.py
-                    - remove_executables.py
-        extra-options:
-            - --verify
+        by-test-platform:
+            android.*:
+                script: android_emulator_unittest.py
+                config:
+                    - android/androidarm_4_3.py
+                no-read-buildbot-config: true
+                extra-options:
+                    - --verify
+            default:
+                script: desktop_unittest.py
+                config:
+                    by-test-platform:
+                        windows.*:
+                            - unittests/win_taskcluster_unittest.py
+                        macosx.*:
+                            - remove_executables.py
+                            - unittests/mac_unittest.py
+                        linux.*:
+                            - unittests/linux_unittest.py
+                            - remove_executables.py
+                no-read-buildbot-config: true
+                extra-options:
+                    - --verify
 
 web-platform-tests:
     description: "Web platform test run"
     suite: web-platform-tests
     treeherder-symbol: tc-W(wpt)
     chunks:
       by-test-platform:
         macosx64/opt: 5
--- a/testing/mochitest/mochitest_options.py
+++ b/testing/mochitest/mochitest_options.py
@@ -826,17 +826,17 @@ class MochitestArguments(ArgumentContain
                         'Missing binary %s required for '
                         '--use-test-media-devices' % f)
 
         if options.nested_oop:
             options.e10s = True
 
         options.leakThresholds = {
             "default": options.defaultLeakThreshold,
-            "tab": 10000,  # See dependencies of bug 1051230.
+            "tab": 3000,  # See dependencies of bug 1401764.
             # GMP rarely gets a log, but when it does, it leaks a little.
             "geckomediaplugin": 20000,
         }
 
         # XXX We can't normalize test_paths in the non build_obj case here,
         # because testRoot depends on the flavor, which is determined by the
         # mach command and therefore not finalized yet. Conversely, test paths
         # need to be normalized here for the mach case.
--- a/testing/mozharness/scripts/android_emulator_unittest.py
+++ b/testing/mozharness/scripts/android_emulator_unittest.py
@@ -32,16 +32,17 @@ from mozharness.mozilla.testing.testbase
 from mozharness.mozilla.testing.unittest import EmulatorMixin
 
 
 class AndroidEmulatorTest(BlobUploadMixin, TestingMixin, EmulatorMixin, VCSMixin, BaseScript, MozbaseMixin):
     config_options = [[
         ["--test-suite"],
         {"action": "store",
          "dest": "test_suite",
+         "default": None
         }
     ], [
         ["--adb-path"],
         {"action": "store",
          "dest": "adb_path",
          "default": None,
          "help": "Path to adb",
         }
@@ -114,17 +115,17 @@ class AndroidEmulatorTest(BlobUploadMixi
         self.test_packages_url = c.get('test_packages_url')
         self.test_manifest = c.get('test_manifest')
         self.robocop_path = os.path.join(abs_dirs['abs_work_dir'], "robocop.apk")
         self.minidump_stackwalk_path = c.get("minidump_stackwalk_path")
         self.emulator = c.get('emulator')
         self.test_suite = c.get('test_suite')
         self.this_chunk = c.get('this_chunk')
         self.total_chunks = c.get('total_chunks')
-        if self.test_suite not in self.config["suite_definitions"]:
+        if self.test_suite and self.test_suite not in self.config["suite_definitions"]:
             # accept old-style test suite name like "mochitest-3"
             m = re.match("(.*)-(\d*)", self.test_suite)
             if m:
                 self.test_suite = m.group(1)
                 if self.this_chunk is None:
                     self.this_chunk = m.group(2)
         self.sdk_level = None
         self.xre_path = None
@@ -148,16 +149,20 @@ class AndroidEmulatorTest(BlobUploadMixi
             abs_dirs['abs_work_dir'], 'hostutils')
         dirs['abs_modules_dir'] = os.path.join(
             dirs['abs_test_install_dir'], 'modules')
         dirs['abs_blob_upload_dir'] = os.path.join(
             abs_dirs['abs_work_dir'], 'blobber_upload_dir')
         dirs['abs_emulator_dir'] = abs_dirs['abs_work_dir']
         dirs['abs_mochitest_dir'] = os.path.join(
             dirs['abs_test_install_dir'], 'mochitest')
+        dirs['abs_reftest_dir'] = os.path.join(
+            dirs['abs_test_install_dir'], 'reftest')
+        dirs['abs_xpcshell_dir'] = os.path.join(
+            dirs['abs_test_install_dir'], 'xpcshell')
         dirs['abs_marionette_dir'] = os.path.join(
             dirs['abs_test_install_dir'], 'marionette', 'harness', 'marionette_harness')
         dirs['abs_marionette_tests_dir'] = os.path.join(
             dirs['abs_test_install_dir'], 'marionette', 'tests', 'testing',
             'marionette', 'harness', 'marionette_harness', 'tests')
         dirs['abs_avds_dir'] = self.config.get("avds_dir", "/home/cltbld/.android")
 
         for key in dirs.keys():
@@ -456,20 +461,21 @@ class AndroidEmulatorTest(BlobUploadMixi
 
         if self.this_chunk is not None:
             cmd.extend(['--this-chunk', self.this_chunk])
         if self.total_chunks is not None:
             cmd.extend(['--total-chunks', self.total_chunks])
 
         try_options, try_tests = self.try_args(self.test_suite)
         cmd.extend(try_options)
-        cmd.extend(self.query_tests_args(
-            self.config["suite_definitions"][self.test_suite].get("tests"),
-            None,
-            try_tests))
+        if self.config.get('verify') != True:
+            cmd.extend(self.query_tests_args(
+                self.config["suite_definitions"][self.test_suite].get("tests"),
+                None,
+                try_tests))
 
         return cmd
 
     def _get_repo_url(self, path):
         """
            Return a url for a file (typically a tooltool manifest) in this hg repo
            and using this revision (or mozilla-central/default if repo/rev cannot
            be determined).
@@ -653,19 +659,19 @@ class AndroidEmulatorTest(BlobUploadMixi
         # Get a post-boot emulator process list for diagnostics
         ps_cmd = [self.adb_path, '-s', self.emulator["device_id"], 'shell', 'ps']
         self._run_with_timeout(30, ps_cmd)
 
     def download_and_extract(self):
         """
         Download and extract fennec APK, tests.zip, host utils, and robocop (if required).
         """
-        super(AndroidEmulatorTest, self).download_and_extract(suite_categories=[self.test_suite])
+        super(AndroidEmulatorTest, self).download_and_extract(suite_categories=self._query_suite_categories())
         dirs = self.query_abs_dirs()
-        if self.test_suite.startswith('robocop'):
+        if self.test_suite and self.test_suite.startswith('robocop'):
             robocop_url = self.installer_url[:self.installer_url.rfind('/')] + '/robocop.apk'
             self.info("Downloading robocop...")
             self.download_file(robocop_url, 'robocop.apk', dirs['abs_work_dir'], error_level=FATAL)
         self.rmtree(dirs['abs_xre_dir'])
         self.mkdir_p(dirs['abs_xre_dir'])
         if self.config["hostutils_manifest_path"]:
             url = self._get_repo_url(self.config["hostutils_manifest_path"])
             self._tooltool_fetch(url, dirs['abs_xre_dir'])
@@ -676,17 +682,17 @@ class AndroidEmulatorTest(BlobUploadMixi
                 self.fatal("xre path not found in %s" % dirs['abs_xre_dir'])
         else:
             self.fatal("configure hostutils_manifest_path!")
 
     def install(self):
         """
         Install APKs on the emulator
         """
-        install_needed = self.config["suite_definitions"][self.test_suite].get("install")
+        install_needed = (not self.test_suite) or self.config["suite_definitions"][self.test_suite].get("install")
         if install_needed == False:
             self.info("Skipping apk installation for %s" % self.test_suite)
             return
 
         assert self.installer_path is not None, \
             "Either add installer_path to the config or use --installer-path."
 
         self.sdk_level = self._run_with_timeout(30, [self.adb_path, '-s', self.emulator['device_id'],
@@ -694,63 +700,126 @@ class AndroidEmulatorTest(BlobUploadMixi
 
         # Install Fennec
         install_ok = self._retry(3, 30, self._install_fennec_apk, "Install app APK")
         if not install_ok:
             self.fatal('INFRA-ERROR: Failed to install %s on %s' %
                 (self.installer_path, self.emulator["name"]), EXIT_STATUS_DICT[TBPL_RETRY])
 
         # Install Robocop if required
-        if self.test_suite.startswith('robocop'):
+        if self.test_suite and self.test_suite.startswith('robocop'):
             install_ok = self._retry(3, 30, self._install_robocop_apk, "Install Robocop APK")
             if not install_ok:
                 self.fatal('INFRA-ERROR: Failed to install %s on %s' %
                     (self.robocop_path, self.emulator["name"]), EXIT_STATUS_DICT[TBPL_RETRY])
 
         self.info("Finished installing apps for %s" % self.emulator["name"])
 
+    def _query_suites(self):
+        if self.test_suite:
+            return [(self.test_suite, self.test_suite)]
+        # test-verification: determine test suites to be verified
+        all = [('mochitest', {'plain': 'mochitest',
+                              'chrome': 'mochitest-chrome',
+                              'plain-clipboard': 'mochitest-plain-clipboard',
+                              'plain-gpu': 'mochitest-plain-gpu'}),
+               ('reftest', {'reftest': 'reftest', 'crashtest': 'crashtest'}),
+               ('xpcshell', {'xpcshell': 'xpcshell'})]
+        suites = []
+        for (category, all_suites) in all:
+            cat_suites = self.query_verify_category_suites(category, all_suites)
+            for k in cat_suites.keys():
+                suites.append((k, cat_suites[k]))
+        return suites
+
+    def _query_suite_categories(self):
+        if self.test_suite:
+            categories = [self.test_suite]
+        else:
+            # test-verification
+            categories = ['mochitest', 'reftest', 'xpcshell']
+        return categories
+
     def run_tests(self):
         """
         Run the tests
         """
-        cmd = self._build_command()
-
-        try:
-            cwd = self._query_tests_dir()
-        except:
-            self.fatal("Don't know how to run --test-suite '%s'!" % self.test_suite)
-        env = self.query_env()
-        if self.query_minidump_stackwalk():
-            env['MINIDUMP_STACKWALK'] = self.minidump_stackwalk_path
-        env['MOZ_UPLOAD_DIR'] = self.query_abs_dirs()['abs_blob_upload_dir']
-        env['MINIDUMP_SAVE_PATH'] = self.query_abs_dirs()['abs_blob_upload_dir']
-        env['RUST_BACKTRACE'] = 'full'
-
-        self.info("Running on %s the command %s" % (self.emulator["name"], subprocess.list2cmdline(cmd)))
-        self.info("##### %s log begins" % self.test_suite)
-
-        # TinderBoxPrintRe does not know about the '-debug' categories
+        self.start_time = datetime.datetime.now()
+        max_verify_time = datetime.timedelta(minutes=60)
         aliases = {
             'reftest-debug': 'reftest',
             'jsreftest-debug': 'jsreftest',
             'crashtest-debug': 'crashtest',
         }
-        suite_category = aliases.get(self.test_suite, self.test_suite)
-        parser = self.get_test_output_parser(
-            suite_category,
-            config=self.config,
-            log_obj=self.log_obj,
-            error_list=self.error_list)
-        self.run_command(cmd, cwd=cwd, env=env, output_parser=parser)
-        tbpl_status, log_level = parser.evaluate_parser(0)
-        parser.append_tinderboxprint_line(self.test_suite)
+
+        verify_args = []
+        suites = self._query_suites()
+        for (verify_suite, suite) in suites:
+            self.test_suite = suite
+
+            cmd = self._build_command()
+
+            try:
+                cwd = self._query_tests_dir()
+            except:
+                self.fatal("Don't know how to run --test-suite '%s'!" % self.test_suite)
+            env = self.query_env()
+            if self.query_minidump_stackwalk():
+                env['MINIDUMP_STACKWALK'] = self.minidump_stackwalk_path
+            env['MOZ_UPLOAD_DIR'] = self.query_abs_dirs()['abs_blob_upload_dir']
+            env['MINIDUMP_SAVE_PATH'] = self.query_abs_dirs()['abs_blob_upload_dir']
+            env['RUST_BACKTRACE'] = 'full'
+
+            for verify_args in self.query_ve