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 682254 a29052590fc6538b89641e690d11731ca8e78120
parent 682253 8b57edba9837c1f18d48470b8e2a940f5d0111ce (current diff)
parent 682068 f605961878cc80936b242290ef1674e82dac97fe (diff)
child 682255 3ae649e23cdcbe9b70a2031870d4cd02634921a8
child 682256 aa32e796a01291c71474474794aef49ef76fe33c
child 682259 97c4e4a524ca160d618f81d1480d4babee0040f3
child 682262 76c8eb7f568a034ac4c025b327eb3e5a0a0f9862
child 682267 a8a1e8cc1980498ea030bd9285570236c4d95dc2
child 682272 27aae4f8e0dabf703f915296281be9cbbf11f462
child 682275 486e142e0b3a649985d6bfbf09138f569b58f53f
child 682285 b3149fb2423b9e6a48007a54374f171e31f0c971
child 682286 9ced634c9ce8395798e26682eeef074bf0398307
child 682290 706a663a625795f00e17f0c1578cc7d9cf91e85e
child 682317 8fad090dd5ef1e309ac4714f6c7e3c236455bafd
child 682329 80d01e93b38f58608d8087598c1fa5f8f748532f
child 682332 c6003c712437f28a2b442e9f8130f57fbf4bcd0f
child 682334 835239788497126e3be33a0dbbb0543e94fc9f18
child 682338 9f73bd06b52ce15e56ba983fa5f62c2557ee26a6
child 682366 f63de6d97bdf373021e5b7b04b499d2978eae9ff
child 682377 a8ad86d0ceebaba07ff18f43510814b2948f6145
child 682394 c674c5bbe1d34421301e0f2f160e26ef4e6a1f5b
child 682395 ba6e1aa39a32b07740cdeecc47129cbc759f7e7c
child 682398 4aea2ace2d5aca090f24e1ddb128d71171394e1b
child 682617 414412a0f01566b84f28d12df4985369d2a8110b
child 682619 ee31ba2e32860b0cf84769931e16fc0f32ea19c8
child 682633 1b554879b2d297bae7db32cd99d95a963421d5e5
child 682640 2b26783aa1753a8d766999c3d248209b28d9ce2a
child 682641 d4cd287ecb4c670f062a3be0784f843c730f1e8d
child 682645 b14f5c9d8c86130cba1629a5d772e2970b148106
child 682683 0b8e3bba27dfc350fb911e68febd670dc1c2dd93
child 682718 d5f72d7d2dc5c618d3df43db8143350b8192a378
child 682724 032d96f2c3069cdb728eff35e18ed4b611c3803d
child 682726 0fe07ad344299da805c3e822ad46271f38e40b5e
child 682728 f6b8f9d6279feb005348393c09e2355d817c25fe
child 682729 15efe1c2f45971041acc727a6935d497f2a696fd
child 682731 942c31d8e24ddd23a5da9bcbe6dcd3ba85f54340
child 682733 b77c9c5c9753f3f2fe2d8fc8a3c51dc58aa971a2
child 682743 47eb04a16c932e51989c3495806b396936edfdbd
child 682752 467738712f75b6f7daa87003eacac2ffb8d6f47d
child 682756 4bbaf442fd3b4865fb22ec11091410495d518a56
child 682758 370f1858ff14efbc49ff0b65c927332d9d679385
child 682764 8b8e537d5cfa4e200416bf1a8553284981683586
child 682776 8017018ef97ec7d2b4338f0d1c87795937cb0342
child 682777 d3af7a8818dae4f8e29d13b1d90e9abd6cd2e0f4
child 682780 b7e210501c0c14369a4878d9ae78df39d57dbb99
child 682789 fe9c42a30a7becf6b3257d1face42087ae9b068d
child 682806 400cfb21ee094ad33e15681f213af6dbe20ebbf5
child 682817 c0f2816ca7e86fa16e340bb334f469d1b44564c0
child 682823 052e29d1ae1a89db041cce6e05a729f5303246c2
child 682824 9003c6fd9f1fc316d05df913085bfc459639df74
child 682830 cda895c3d1ddb30b8baafbf2002d09c100f7db3b
child 682834 e16e383d89293d09252468b958d5f4ebc133c3bb
child 682835 3aebceafaedfd0fc58f549761c8dab04d1a8fa04
child 682845 853e36f1ddcb065c2e0af9d2cf015f04b40983e1
child 682848 4d4bb045ae4ceb71f22269085e31d14987615703
child 682849 c2673317b0306e9e0c51ca82a5b0421971e7f47e
child 682853 aeae5e965a165cb78c4175c716445f14af79f3cb
child 682857 5800ddf47a73779722af73a0bc3cbba3496e9764
child 682903 8d58d31c25ae7fdaba968c958014639d61a2b718
child 683319 c539cfc5ecddcb94b892c23c0dbabb58fe7eadb5
child 683363 8e72794bd0d00bc7ee84bac7eea064e4c97b3161
child 684044 77b59655b0f2af680c885329de0acf1666dff02c
child 684146 17b9f923589b007299a4386e8c5979075a4afa64
child 684781 aa3054216753857f73676ca8da0a2d444f4c46e2
child 684919 f38d2a8c3db40cc12ad951f2124568be68772aef
child 685079 a36697c4d1f0d9500fa43c9b43df762f8118c77d
child 686089 41724b0d32a5d10ae27483ac14c64b9d45fac02f
child 688859 28c32f6b8c2cff683a7e0d14e4ac63b0d8bfc673
child 695730 7573d155290b202e79d8dc25150847f2167d5947
child 710920 46ae197b8c13d45d58b8c4b8c6c3485a5dbbc24d
child 710931 67dc8bc18cfbb2c747d59b319dea60c8cae4a02f
push id85046
push userjdescottes@mozilla.com
push dateWed, 18 Oct 2017 10:12:20 +0000
reviewersmerge, merge
milestone58.0a1
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_